Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 1 | import {waitFor} from 'poll-until-promise'; |
| 2 | |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 3 | import {CCApi} from '../../common/api.js'; |
| 4 | import {parseUrl} from '../../common/commonUtils.js'; |
| 5 | |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 6 | import AvatarsDB from './utils/AvatarsDB.js' |
| 7 | |
| 8 | export default class AvatarsHandler { |
| 9 | constructor() { |
| 10 | this.isFilterSetUp = false; |
| 11 | this.privateForums = []; |
| 12 | this.db = new AvatarsDB(); |
| 13 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 14 | |
| 15 | // Gets a list of private forums. If it is already cached, the cached list is |
| 16 | // returned; otherwise it is also computed and cached. |
| 17 | getPrivateForums() { |
| 18 | return new Promise((resolve, reject) => { |
| 19 | if (this.isFilterSetUp) return resolve(this.privateForums); |
| 20 | |
| 21 | if (!document.documentElement.hasAttribute('data-startup')) |
| 22 | return reject('[threadListAvatars] Couldn\'t get startup data.'); |
| 23 | |
| 24 | var startupData = |
| 25 | JSON.parse(document.documentElement.getAttribute('data-startup')); |
| 26 | var forums = startupData?.['1']?.['2']; |
| 27 | if (forums === undefined) |
| 28 | return reject( |
| 29 | '[threadListAvatars] Couldn\'t retrieve forums from startup data.'); |
| 30 | |
| 31 | for (var f of forums) { |
| 32 | var forumId = f?.['2']?.['1']?.['1']; |
| 33 | var forumVisibility = f?.['2']?.['18']; |
| 34 | if (forumId === undefined || forumVisibility === undefined) { |
| 35 | console.warn( |
| 36 | '[threadListAvatars] Coudln\'t retrieve forum id and/or forum visibility for the following forum:', |
| 37 | f); |
| 38 | continue; |
| 39 | } |
| 40 | |
| 41 | // forumVisibility's value 1 means "PUBLIC". |
| 42 | if (forumVisibility != 1) this.privateForums.push(forumId); |
| 43 | } |
| 44 | |
| 45 | // Forum 51488989 is marked as public but it is in fact private. |
| 46 | this.privateForums.push('51488989'); |
| 47 | |
| 48 | this.isFilterSetUp = true; |
| 49 | return resolve(this.privateForums); |
| 50 | }); |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 51 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 52 | |
| 53 | // Some threads belong to private forums, and this feature will not be able to |
| 54 | // get its avatars since it makes an anonymomus call to get the contents of |
| 55 | // the thread. |
| 56 | // |
| 57 | // This function returns whether avatars should be retrieved depending on if |
| 58 | // the thread belongs to a known private forum. |
| 59 | shouldRetrieveAvatars(thread) { |
| 60 | return this.getPrivateForums().then(privateForums => { |
| 61 | return !privateForums.includes(thread.forum); |
| 62 | }); |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 63 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 64 | |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 65 | // Get an object with the author of the thread, an array of the first |num| |
| 66 | // replies from the thread |thread|, and additional information about the |
| 67 | // thread. |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 68 | getFirstMessages(thread, num = 15) { |
| 69 | return CCApi( |
| 70 | 'ViewThread', { |
| 71 | 1: thread.forum, |
| 72 | 2: thread.thread, |
| 73 | // options |
| 74 | 3: { |
| 75 | // pagination |
| 76 | 1: { |
| 77 | 2: num, // maxNum |
| 78 | }, |
| 79 | 3: true, // withMessages |
| 80 | 5: true, // withUserProfile |
| 81 | 10: false, // withPromotedMessages |
| 82 | 16: false, // withThreadNotes |
| 83 | 18: true, // sendNewThreadIfMoved |
| 84 | } |
| 85 | }, |
| 86 | // |authentication| is false because otherwise this would mark |
| 87 | // the thread as read as a side effect, and that would mark all |
| 88 | // threads in the list as read. |
| 89 | // |
| 90 | // Due to the fact that we have to call this endpoint |
| 91 | // anonymously, this means we can't retrieve information about |
| 92 | // threads in private forums. |
| 93 | /* authentication = */ false) |
| 94 | .then(data => { |
| 95 | var numMessages = data?.['1']?.['8']; |
| 96 | if (numMessages === undefined) |
| 97 | throw new Error( |
| 98 | 'Request to view thread doesn\'t include the number of messages'); |
| 99 | |
| 100 | var messages = numMessages == 0 ? [] : data?.['1']['3']; |
| 101 | if (messages === undefined) |
| 102 | throw new Error( |
| 103 | 'numMessages was ' + numMessages + |
| 104 | ' but the response didn\'t include any message.'); |
| 105 | |
| 106 | var author = data?.['1']?.['4']; |
| 107 | if (author === undefined) |
| 108 | throw new Error( |
| 109 | 'Author isn\'t included in the ViewThread response.'); |
| 110 | |
| 111 | return { |
| 112 | messages, |
| 113 | author, |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 114 | |
| 115 | // The following fields are useful for the cache and can be |
| 116 | // undefined, but this is checked before adding an entry to the |
| 117 | // cache. |
| 118 | updatedTimestamp: data?.['1']?.['2']?.['1']?.['4'], |
| 119 | lastMessageId: data?.['1']?.['2']?.['10'], |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 120 | }; |
| 121 | }); |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 122 | } |
| 123 | |
| 124 | // Get a list of at most |num| avatars for thread |thread| by calling the API |
| 125 | getVisibleAvatarsFromServer(thread, num) { |
| 126 | return this.getFirstMessages(thread).then(result => { |
| 127 | var messages = result.messages; |
| 128 | var author = result.author; |
| 129 | var updatedTimestamp = |
| 130 | Math.floor(Number.parseInt(result.updatedTimestamp) / 1000000); |
| 131 | var lastMessageId = result.lastMessageId; |
| 132 | |
| 133 | var avatarUrls = []; |
| 134 | |
| 135 | var authorUrl = author?.['1']?.['2']; |
| 136 | if (authorUrl !== undefined) avatarUrls.push(authorUrl); |
| 137 | |
| 138 | for (var m of messages) { |
| 139 | var url = m?.['3']?.['1']?.['2']; |
| 140 | |
| 141 | if (url === undefined) continue; |
| 142 | if (!avatarUrls.includes(url)) avatarUrls.push(url); |
| 143 | if (avatarUrls.length == 3) break; |
| 144 | } |
| 145 | |
| 146 | // Add entry to cache if all the extra metadata could be retrieved. |
| 147 | if (updatedTimestamp !== undefined && lastMessageId !== undefined) |
| 148 | this.db.putCacheEntry({ |
| 149 | threadId: thread.thread, |
| 150 | updatedTimestamp, |
| 151 | lastMessageId, |
| 152 | avatarUrls, |
| 153 | num, |
| 154 | lastUsedTimestamp: Math.floor(Date.now() / 1000), |
| 155 | }); |
| 156 | |
| 157 | return avatarUrls; |
| 158 | }); |
| 159 | } |
| 160 | |
| 161 | // Returns an object with a cache entry that matches the request if found (via |
| 162 | // the |entry| property). The property |found| indicates whether the cache |
| 163 | // entry was found. |
| 164 | getVisibleAvatarsFromCache(thread, num) { |
| 165 | return waitFor( |
| 166 | () => this.db.getCacheEntry(thread.thread).then(entry => { |
| 167 | if (entry === undefined || entry.num < num) |
| 168 | return { |
| 169 | found: false, |
| 170 | }; |
| 171 | |
| 172 | // Only use cache entry if lastUsedTimestamp is within the last 30 |
| 173 | // seconds (which means it has been checked for invalidations): |
| 174 | var now = Math.floor(Date.now() / 1000); |
| 175 | var diff = now - entry.lastUsedTimestamp; |
| 176 | if (diff > 30) |
| 177 | throw new Error( |
| 178 | 'lastUsedTimestamp isn\'t within the last 30 seconds (the difference is:', |
| 179 | diff, ').'); |
| 180 | return { |
| 181 | found: true, |
| 182 | entry, |
| 183 | }; |
| 184 | }), |
| 185 | { |
| 186 | interval: 400, |
| 187 | timeout: 10 * 1000, |
| 188 | }); |
| 189 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 190 | |
| 191 | // Get a list of at most |num| avatars for thread |thread| |
| 192 | getVisibleAvatars(thread, num = 3) { |
| 193 | return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => { |
| 194 | if (!shouldRetrieve) { |
| 195 | console.debug('[threadListAvatars] Skipping thread', thread); |
| 196 | return []; |
| 197 | } |
| 198 | |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 199 | return this.getVisibleAvatarsFromCache(thread, num) |
| 200 | .then(res => { |
| 201 | if (res.found) return res.entry.avatarUrls; |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 202 | |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 203 | return this.getVisibleAvatarsFromServer(thread, num); |
| 204 | }) |
| 205 | .catch(err => { |
| 206 | console.error( |
| 207 | '[threadListAvatars] Error while retrieving avatars:', err); |
| 208 | return this.getVisibleAvatarsFromServer(thread, num); |
| 209 | }); |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 210 | }); |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 211 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 212 | |
| 213 | // Inject avatars for thread summary (thread item) |node| in a thread list. |
| 214 | inject(node) { |
| 215 | var header = node.querySelector( |
| 216 | 'ec-thread-summary .main-header .panel-description a.header'); |
| 217 | if (header === null) { |
| 218 | console.error( |
| 219 | '[threadListAvatars] Header is not present in the thread item\'s DOM.'); |
| 220 | return; |
| 221 | } |
| 222 | |
| 223 | var thread = parseUrl(header.href); |
| 224 | if (thread === false) { |
| 225 | console.error('[threadListAvatars] Thread\'s link cannot be parsed.'); |
| 226 | return; |
| 227 | } |
| 228 | |
| 229 | this.getVisibleAvatars(thread) |
| 230 | .then(avatarUrls => { |
| 231 | var avatarsContainer = document.createElement('div'); |
| 232 | avatarsContainer.classList.add('TWPT-avatars'); |
| 233 | |
| 234 | var count = Math.floor(Math.random() * 4); |
| 235 | |
| 236 | for (var i = 0; i < avatarUrls.length; ++i) { |
| 237 | var avatar = document.createElement('div'); |
| 238 | avatar.classList.add('TWPT-avatar'); |
| 239 | avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')'; |
| 240 | avatarsContainer.appendChild(avatar); |
| 241 | } |
| 242 | |
| 243 | header.appendChild(avatarsContainer); |
| 244 | }) |
| 245 | .catch(err => { |
| 246 | console.error( |
| 247 | '[threadListAvatars] Could not retrieve avatars for thread', |
| 248 | thread, err); |
| 249 | }); |
Adrià Vilanova Martínez | 27c6996 | 2021-07-17 23:32:51 +0200 | [diff] [blame^] | 250 | } |
Adrià Vilanova Martínez | 3465e77 | 2021-07-11 19:18:41 +0200 | [diff] [blame] | 251 | }; |