blob: 5e5eeeecedaadbb7df3de979b62e04ad59c75257 [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 => {
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020061 if (privateForums.includes(thread.forum)) return false;
62
63 return this.db.isForumUnauthorized(thread.forum).then(res => !res);
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020064 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020065 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020066
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020067 // Get an object with the author of the thread, an array of the first |num|
68 // replies from the thread |thread|, and additional information about the
69 // thread.
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(() => {
100 throw new Error('Permission denied to load thread.');
101 });
102
103 var data = response.body;
104
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200105 var numMessages = data?.['1']?.['8'];
106 if (numMessages === undefined)
107 throw new Error(
108 'Request to view thread doesn\'t include the number of messages');
109
110 var messages = numMessages == 0 ? [] : data?.['1']['3'];
111 if (messages === undefined)
112 throw new Error(
113 'numMessages was ' + numMessages +
114 ' but the response didn\'t include any message.');
115
116 var author = data?.['1']?.['4'];
117 if (author === undefined)
118 throw new Error(
119 'Author isn\'t included in the ViewThread response.');
120
121 return {
122 messages,
123 author,
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200124
125 // The following fields are useful for the cache and can be
126 // undefined, but this is checked before adding an entry to the
127 // cache.
128 updatedTimestamp: data?.['1']?.['2']?.['1']?.['4'],
129 lastMessageId: data?.['1']?.['2']?.['10'],
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200130 };
131 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200132 }
133
134 // Get a list of at most |num| avatars for thread |thread| by calling the API
135 getVisibleAvatarsFromServer(thread, num) {
136 return this.getFirstMessages(thread).then(result => {
137 var messages = result.messages;
138 var author = result.author;
139 var updatedTimestamp =
140 Math.floor(Number.parseInt(result.updatedTimestamp) / 1000000);
141 var lastMessageId = result.lastMessageId;
142
143 var avatarUrls = [];
144
145 var authorUrl = author?.['1']?.['2'];
146 if (authorUrl !== undefined) avatarUrls.push(authorUrl);
147
148 for (var m of messages) {
149 var url = m?.['3']?.['1']?.['2'];
150
151 if (url === undefined) continue;
152 if (!avatarUrls.includes(url)) avatarUrls.push(url);
153 if (avatarUrls.length == 3) break;
154 }
155
156 // Add entry to cache if all the extra metadata could be retrieved.
157 if (updatedTimestamp !== undefined && lastMessageId !== undefined)
158 this.db.putCacheEntry({
159 threadId: thread.thread,
160 updatedTimestamp,
161 lastMessageId,
162 avatarUrls,
163 num,
164 lastUsedTimestamp: Math.floor(Date.now() / 1000),
165 });
166
167 return avatarUrls;
168 });
169 }
170
171 // Returns an object with a cache entry that matches the request if found (via
172 // the |entry| property). The property |found| indicates whether the cache
173 // entry was found.
174 getVisibleAvatarsFromCache(thread, num) {
175 return waitFor(
176 () => this.db.getCacheEntry(thread.thread).then(entry => {
177 if (entry === undefined || entry.num < num)
178 return {
179 found: false,
180 };
181
182 // Only use cache entry if lastUsedTimestamp is within the last 30
183 // seconds (which means it has been checked for invalidations):
184 var now = Math.floor(Date.now() / 1000);
185 var diff = now - entry.lastUsedTimestamp;
186 if (diff > 30)
187 throw new Error(
188 'lastUsedTimestamp isn\'t within the last 30 seconds (the difference is:',
189 diff, ').');
190 return {
191 found: true,
192 entry,
193 };
194 }),
195 {
196 interval: 400,
197 timeout: 10 * 1000,
198 });
199 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200200
201 // Get a list of at most |num| avatars for thread |thread|
202 getVisibleAvatars(thread, num = 3) {
203 return this.shouldRetrieveAvatars(thread).then(shouldRetrieve => {
204 if (!shouldRetrieve) {
205 console.debug('[threadListAvatars] Skipping thread', thread);
206 return [];
207 }
208
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200209 return this.getVisibleAvatarsFromCache(thread, num)
210 .then(res => {
211 if (res.found) return res.entry.avatarUrls;
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200212
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200213 return this.getVisibleAvatarsFromServer(thread, num);
214 })
215 .catch(err => {
216 console.error(
217 '[threadListAvatars] Error while retrieving avatars:', err);
218 return this.getVisibleAvatarsFromServer(thread, num);
219 });
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200220 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200221 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200222
223 // Inject avatars for thread summary (thread item) |node| in a thread list.
224 inject(node) {
225 var header = node.querySelector(
226 'ec-thread-summary .main-header .panel-description a.header');
227 if (header === null) {
228 console.error(
229 '[threadListAvatars] Header is not present in the thread item\'s DOM.');
230 return;
231 }
232
233 var thread = parseUrl(header.href);
234 if (thread === false) {
235 console.error('[threadListAvatars] Thread\'s link cannot be parsed.');
236 return;
237 }
238
239 this.getVisibleAvatars(thread)
240 .then(avatarUrls => {
241 var avatarsContainer = document.createElement('div');
242 avatarsContainer.classList.add('TWPT-avatars');
243
244 var count = Math.floor(Math.random() * 4);
245
246 for (var i = 0; i < avatarUrls.length; ++i) {
247 var avatar = document.createElement('div');
248 avatar.classList.add('TWPT-avatar');
249 avatar.style.backgroundImage = 'url(\'' + avatarUrls[i] + '\')';
250 avatarsContainer.appendChild(avatar);
251 }
252
253 header.appendChild(avatarsContainer);
254 })
255 .catch(err => {
256 console.error(
257 '[threadListAvatars] Could not retrieve avatars for thread',
258 thread, err);
259 });
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200260 }
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +0200261};