blob: 093e21743ad0a89ed39e654ae21c7a792284ed1f [file] [log] [blame]
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02001import {waitFor} from 'poll-until-promise';
2
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02003import {CCApi} from '../../common/api.js';
4import {parseUrl} from '../../common/commonUtils.js';
5
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02006import AvatarsDB from './utils/AvatarsDB.js'
7
8export default class AvatarsHandler {
9 constructor() {
10 this.isFilterSetUp = false;
11 this.privateForums = [];
12 this.db = new AvatarsDB();
13 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020014
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ínez27c69962021-07-17 23:32:51 +020051 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020052
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 //
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +020057 // This function returns whether the thread belongs to a known private forum.
58 isPrivateThread(thread) {
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020059 return this.getPrivateForums().then(privateForums => {
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +020060 if (privateForums.includes(thread.forum)) return true;
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020061
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +020062 return this.db.isForumUnauthorized(thread.forum);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020063 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020064 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020065
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020066 // Get an object with the author of the thread, an array of the first |num|
67 // replies from the thread |thread|, and additional information about the
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +020068 // thread. It also returns whether the thread is private, in which case the
69 // previous properties will be missing.
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020070 getFirstMessages(thread, num = 15) {
71 return CCApi(
72 'ViewThread', {
73 1: thread.forum,
74 2: thread.thread,
75 // options
76 3: {
77 // pagination
78 1: {
79 2: num, // maxNum
80 },
81 3: true, // withMessages
82 5: true, // withUserProfile
83 10: false, // withPromotedMessages
84 16: false, // withThreadNotes
85 18: true, // sendNewThreadIfMoved
86 }
87 },
88 // |authentication| is false because otherwise this would mark
89 // the thread as read as a side effect, and that would mark all
90 // threads in the list as read.
91 //
92 // Due to the fact that we have to call this endpoint
93 // anonymously, this means we can't retrieve information about
94 // threads in private forums.
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020095 /* authentication = */ false, /* authuser = */ 0,
96 /* returnUnauthorizedStatus = */ true)
97 .then(response => {
98 if (response.unauthorized)
99 return this.db.putUnauthorizedForum(thread.forum).then(() => {
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200100 return {
101 isPrivate: true,
102 };
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200103 });
104
105 var data = response.body;
106
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200107 var numMessages = data?.['1']?.['8'];
108 if (numMessages === undefined)
109 throw new Error(
110 'Request to view thread doesn\'t include the number of messages');
111
112 var messages = numMessages == 0 ? [] : data?.['1']['3'];
113 if (messages === undefined)
114 throw new Error(
115 'numMessages was ' + numMessages +
116 ' but the response didn\'t include any message.');
117
118 var author = data?.['1']?.['4'];
119 if (author === undefined)
120 throw new Error(
121 'Author isn\'t included in the ViewThread response.');
122
123 return {
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200124 isPrivate: false,
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200125 messages,
126 author,
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200127
128 // The following fields are useful for the cache and can be
129 // undefined, but this is checked before adding an entry to the
130 // cache.
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200131 lastMessageId: data?.['1']?.['2']?.['10'],
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200132 };
133 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200134 }
135
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200136 // Get the following data:
137 // - |isPrivate|: whether the thread is private.
138 // - |avatars|: a list of at most |num| avatars for thread |thread| by calling
139 // the API, if |isPrivate| is false.
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200140 getVisibleAvatarsFromServer(thread, num) {
141 return this.getFirstMessages(thread).then(result => {
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200142 if (result.isPrivate)
143 return {
144 isPrivate: true,
145 };
146
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200147 var messages = result.messages;
148 var author = result.author;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200149 var lastMessageId = result.lastMessageId;
150
151 var avatarUrls = [];
152
153 var authorUrl = author?.['1']?.['2'];
154 if (authorUrl !== undefined) avatarUrls.push(authorUrl);
155
156 for (var m of messages) {
157 var url = m?.['3']?.['1']?.['2'];
158
159 if (url === undefined) continue;
160 if (!avatarUrls.includes(url)) avatarUrls.push(url);
161 if (avatarUrls.length == 3) break;
162 }
163
164 // Add entry to cache if all the extra metadata could be retrieved.
Adrià Vilanova Martínezac9fc9e2021-07-22 12:45:32 +0200165 if (lastMessageId !== undefined)
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200166 this.db.putCacheEntry({
167 threadId: thread.thread,
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200168 lastMessageId,
169 avatarUrls,
170 num,
171 lastUsedTimestamp: Math.floor(Date.now() / 1000),
172 });
173
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200174 return {
175 isPrivate: false,
176 avatars: avatarUrls,
177 };
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200178 });
179 }
180
181 // Returns an object with a cache entry that matches the request if found (via
182 // the |entry| property). The property |found| indicates whether the cache
183 // entry was found.
Adrià Vilanova Martínez4cfa32f2021-07-22 17:29:03 +0200184 //
185 // The |checkRecent| parameter is used to indicate whether lastUsedTimestamp
186 // must be within the last 30 seconds (which means that the thread has been
187 // checked for a potential invalidation).
188 getVisibleAvatarsFromCache(thread, num, checkRecent) {
189 return this.db.getCacheEntry(thread.thread).then(entry => {
190 if (entry === undefined || entry.num < num)
191 return {
192 found: false,
193 };
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200194
Adrià Vilanova Martínez4cfa32f2021-07-22 17:29:03 +0200195 if (checkRecent) {
196 var now = Math.floor(Date.now() / 1000);
197 var diff = now - entry.lastUsedTimestamp;
198 if (diff > 30)
199 throw new Error(
200 'lastUsedTimestamp isn\'t within the last 30 seconds (id: ' +
201 thread.thread + ' the difference is: ' + diff + ').');
202 }
203
204 return {
205 found: true,
206 entry,
207 };
208 });
209 }
210
211 // Waits for the XHR interceptor to invalidate any outdated threads and
212 // returns what getVisibleAvatarsFromCache returns. If this times out, it
213 // returns the current cache entry anyways if it exists.
214 getVisibleAvatarsFromCacheAfterInvalidations(thread, num) {
215 return waitFor(
216 () => this.getVisibleAvatarsFromCache(
217 thread, num, /* checkRecent = */ true),
218 {
219 interval: 450,
220 timeout: 2 * 1000,
221 })
222 .catch(err => {
223 console.debug(
224 '[threadListAvatars] Error while retrieving avatars from cache ' +
225 '(probably timed out waiting for lastUsedTimestamp to change):',
226 err);
227
228 // Sometimes when going back to a thread list, the API call to load
229 // the thread list is not made, and so the previous piece of code
230 // times out waiting to intercept that API call and handle thread
231 // invalidations.
232 //
233 // If this is the case, this point will be reached. We'll assume we
234 // intercept all API calls, so reaching this point means that an API
235 // call wasn't made. Therefore, try again to get visible avatars from
236 // the cache without checking whether the entry has been checked for
237 // potential invalidation.
238 //
239 // See https://bugs.avm99963.com/p/twpowertools/issues/detail?id=10.
240 return this.getVisibleAvatarsFromCache(
241 thread, num, /* checkRecent = */ false);
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200242 });
243 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200244
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200245 // Get an object with the following data:
246 // - |state|: 'ok' (the avatars list could be retrieved) or 'private' (the
247 // thread is private, so the avatars list could not be retrieved).
248 // - |avatars|: list of at most |num| avatars for thread |thread|
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200249 getVisibleAvatars(thread, num = 3) {
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200250 return this.isPrivateThread(thread).then(isPrivate => {
251 if (isPrivate)
252 return {
253 state: 'private',
254 avatars: [],
255 };
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200256
Adrià Vilanova Martínez4cfa32f2021-07-22 17:29:03 +0200257 return this.getVisibleAvatarsFromCacheAfterInvalidations(thread, num)
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200258 .then(res => {
Adrià Vilanova Martínez4cfa32f2021-07-22 17:29:03 +0200259 if (!res.found) {
260 var err = new Error('Cache entry doesn\'t exist.');
261 err.name = 'notCached';
262 throw err;
263 }
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200264 return {
265 state: 'ok',
266 avatars: res.entry.avatarUrls,
267 };
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200268 })
269 .catch(err => {
Adrià Vilanova Martínez4cfa32f2021-07-22 17:29:03 +0200270 // If the name is "notCached", then this is not an actual error so
271 // don't log an error, but still get avatars from the server.
272 if (err?.name !== 'notCached')
273 console.error(
274 '[threadListAvatars] Error while accessing avatars cache:',
275 err);
276
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200277 return this.getVisibleAvatarsFromServer(thread, num).then(res => {
278 if (res.isPrivate)
279 return {
280 state: 'private',
281 avatars: [],
282 };
283
284 return {
285 state: 'ok',
286 avatars: res.avatars,
287 };
288 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200289 });
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200290 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200291 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200292
293 // Inject avatars for thread summary (thread item) |node| in a thread list.
294 inject(node) {
295 var header = node.querySelector(
296 'ec-thread-summary .main-header .panel-description a.header');
297 if (header === null) {
298 console.error(
299 '[threadListAvatars] Header is not present in the thread item\'s DOM.');
300 return;
301 }
302
303 var thread = parseUrl(header.href);
304 if (thread === false) {
305 console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
306 return;
307 }
308
309 this.getVisibleAvatars(thread)
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200310 .then(res => {
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200311 var avatarsContainer = document.createElement('div');
312 avatarsContainer.classList.add('TWPT-avatars');
313
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200314 var avatarUrls = res.avatars;
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200315
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200316 if (res.state == 'private') {
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200317 var avatar = document.createElement('div');
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200318 avatar.classList.add('TWPT-avatar-private-placeholder');
319 avatar.textContent = 'vpn_key';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200320 avatarsContainer.appendChild(avatar);
Adrià Vilanova Martínez87110e92021-08-11 19:23:16 +0200321 } else {
322 for (var i = 0; i < avatarUrls.length; ++i) {
323 var avatar = document.createElement('div');
324 avatar.classList.add('TWPT-avatar');
325 avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
326 avatarsContainer.appendChild(avatar);
327 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200328 }
329
330 header.appendChild(avatarsContainer);
331 })
332 .catch(err => {
333 console.error(
334 '[threadListAvatars] Could not retrieve avatars for thread',
335 thread, err);
336 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200337 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200338};