blob: 3b273337fc4b821f69cfc00a1fac90ceb7136811 [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 //
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ínez27c69962021-07-17 23:32:51 +020063 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020064
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020065 // 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ínez3465e772021-07-11 19:18:41 +020068 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ínez27c69962021-07-17 23:32:51 +0200114
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ínez3465e772021-07-11 19:18:41 +0200120 };
121 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200122 }
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ínez3465e772021-07-11 19:18:41 +0200190
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ínez27c69962021-07-17 23:32:51 +0200199 return this.getVisibleAvatarsFromCache(thread, num)
200 .then(res => {
201 if (res.found) return res.entry.avatarUrls;
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200202
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200203 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ínez3465e772021-07-11 19:18:41 +0200210 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200211 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200212
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ínez27c69962021-07-17 23:32:51 +0200250 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200251};