-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathbackground.js
252 lines (216 loc) · 9.75 KB
/
background.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// A static import is required in b/g scripts because they are executed in their own env
// not connected to the content scripts where wasm is loaded automatically
import initWasmModule, { hello_wasm, add_random_tracks } from './wasm/wasm_mod.js';
console.log("Background script started");
// console.log(await chrome.permissions.getAll());
// values extracted from headers and spotify responses
// for passing onto WASM
let authHeaderValue = ""; // creds
let tokenHeaderValue = ""; // creds
let userUri = ""; // user ID
let userDetailsRequestHeaders = new Headers(); // a copy of Spotify headers to impersonate the user
// a temp flag to stop multiple fetches
let fetching = false;
// run the wasm initializer before calling wasm methods
// the initializer is generated by wasm_pack
(async () => {
await initWasmModule();
hello_wasm(); // this call logs a hello message from WASM for demo purposes
})();
// A placeholder for OnSuccess in .then
function onSuccess(message) {
// console.log(`Send OK: ${JSON.stringify(message)}`);
}
// A placeholder for OnError in .then
function onError(error) {
// console.error(`Promise error: ${error}`);
}
// A placeholder for OnError in .then
function onErrorWithLog(error) {
console.error(`Promise error: ${error}`);
}
// Popup button handler
// Fetches the data from Spotify using the creds extracted earlier
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
// console.log(`Popup message received: ${JSON.stringify(request)}, ${JSON.stringify(sender)}`);
// check what kind of message it is - act on it or log it if the msg cannot be understood
let numberOfTracksToAdd = 500; // default value
if (request?.action == "btn_add") {
numberOfTracksToAdd = Number(request?.qty); // Number() cast is required because WASM wrapper asserts types and expects a number for Rust's u32
// this is a check for the main action - let the code run its course after the completion of this if-block
console.log(`User clicked btn_add / tracks to add: ${numberOfTracksToAdd}`);
// this check is probably redundant, but since there is no automated testing we'd better tell users what's happening
if (!numberOfTracksToAdd) {
chrome.runtime.sendMessage("Missing how many tracks to add param. It's a bug.").then(onSuccess, onError);
return;
}
}
else {
// this is an unexpected option - something is off or there is a bug
console.error(`Unexpected popup.html message - it's a bug`);
console.error(JSON.stringify(request));
return;
}
// only one wasm should be running at a time
// TODO: disable the button
if (fetching) {
chrome.runtime.sendMessage("Already running. Restart the browser if stuck on this message.").then(onSuccess, onError);
return;
}
const playlistId = await getPlaylistIdFromCurrentTabUrl();
// user ID is loaded first time the extension is invoked
if (!userUri) {
try {
await fetchUserDetails()
}
catch (e) {
console.error(e);
chrome.runtime.sendMessage("Error while fetching user details from Spotify. Reload the page and try again.").then(onSuccess, onError);
return;
}
};
// cannot proceed without userUri
if (!userUri) {
chrome.runtime.sendMessage("Missing user details. Reload the page and try again.").then(onSuccess, onError);
return;
}
// call the WASM code
if (authHeaderValue && tokenHeaderValue && !fetching) {
// indicate an active WASM process
fetching = true;
toggleToolbarBadge();
// remove the listener because there will be a lot of requests from WASM
// it makes no sense to intercept them for token extraction
// there is a small chance the token changes while WASM is running
chrome.webRequest.onBeforeSendHeaders.removeListener(captureSessionToken);
// call WASM
add_random_tracks(authHeaderValue, tokenHeaderValue, playlistId, userUri, numberOfTracksToAdd)
.catch((e) => {
console.error(e);
chrome.runtime.sendMessage(JSON.stringify(e)).then(onSuccess, onError);
})
.finally(() => {
// reset WASM, log to inactive and drop toolbar icon badge
fetching = false;
// restore the listener to capture any token changes in between WASM runs
chrome.webRequest.onBeforeSendHeaders.addListener(captureSessionToken, { urls: ['https://api-partner.spotify.com/pathfinder/v1/query*'] }, ["requestHeaders"])
// restore the toolbar badge that signals to the popup that WASM is no longer running
toggleToolbarBadge();
})
}
});
/// Sets the badge as per fetching var and notifies the popup about the status change
/// When the popup window is loaded, it checks if the badge is set and presumes that the WASM script is running
function toggleToolbarBadge() {
chrome.action.setBadgeText(
{ text: (fetching) ? "..." : "" }
).then(onSuccess, onErrorWithLog)
chrome.runtime.sendMessage(fetching).then(onSuccess, onError);
}
// Gets Spotify request headers from request details to extract creds
async function captureSessionToken(requestDetails) {
console.log("captureSessionToken fired")
// console.log(details)
// console.log(details.tabId)
// local copies in case the headers are empty or incomplete
// which is unlikely to happen - why would Spotify send an invalid request to itself?
const headers = new Headers();
let auth = "";
let token = "";
// loop through all headers and grab the two we need
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
for (const header of requestDetails.requestHeaders) {
headers.set(header.name, header.value)
if (header.name == 'authorization') {
auth = header.value
// console.log(authHeaderValue)
}
if (header.name == 'client-token') {
token = header.value
// console.log(tokenHeaderValue)
}
}
if (auth && token) {
authHeaderValue = auth;
tokenHeaderValue = token
userDetailsRequestHeaders = headers;
// console.log(`Tokens captured: ${authHeaderValue} / ${tokenHeaderValue}`)
// console.log(requestDetails.requestHeaders)
}
else {
// console.log(`Tokens missing: ${auth} / ${token} / ${requestDetails.requestHeaders}`)
}
// for (const header of userDetailsRequestHeaders) {
// console.log(`${header[0]}: ${header[1]}`)
// }
}
// A Spotify request interceptor to capture user creds
chrome.webRequest.onBeforeSendHeaders.addListener(captureSessionToken, { urls: ['https://api-partner.spotify.com/pathfinder/v1/query*'] }, ["requestHeaders"])
// Requests user details from Spotify to extract user ID
async function fetchUserDetails() {
console.log("fetchUserDetails fired")
if (!authHeaderValue) {
// this happens in FF when the permission was just granted and the processing kicked off
// but it takes some time to get the tokens
// there is probably a more elegant solution than asking the user to reload
console.log("Missing auth token for user details request");
return;
}
// https://javascript.plainenglish.io/fetch-data-in-chrome-extension-v3-2b73719ffc0e
const resp = await fetch('https://api-partner.spotify.com/pathfinder/v1/query?operationName=profileAndAccountAttributes&variables=%7B%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22d6989ae82c66a7fa0b3e40775552e08fa6856b77af1b71863fdd7f2cffb5d4d4%22%7D%7D', {
method: 'GET',
headers: userDetailsRequestHeaders,
});
// store the creds in the session vars
const respJson = await resp.json();
// console.log(JSON.stringify(respJson));
/*
{
"data": {
"me": {
"profile": {
"uri": "spotify:user:onebro.me",
"username": "onebro.me",
"name": "rimutaka",
"avatar": {
"sources": [
{
"url": "https://i.scdn.co/image/ab67757000003b82cfd0d586af121bdac41f2c7b"
},
{
"url": "https://i.scdn.co/image/ab6775700000ee85cfd0d586af121bdac41f2c7b"
}
]
}
},
"account": {
"attributes": {
"dsaModeEnabled": false,
"dsaModeAvailable": true,
"optInTrialPremiumOnlyMarket": false
}
}
}
},
"extensions": {}
}
*/
userUri = respJson?.data?.me?.profile?.uri;
console.log(`User URI: ${userUri}`)
}
// Returns the playlist ID or undefined.
// The playlist is at the end of the URL
// https://open.spotify.com/playlist/3h9rkMXa434AeAIDdA5Dd2
async function getPlaylistIdFromCurrentTabUrl() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
// console.log(JSON.stringify(tab));
if (!tab || !tab.url) {
chrome.runtime.sendMessage("Cannot get playlist tab URL. Reload the page and try again.").then(onSuccess, onError);
console.log("Empty active tab URL")
return undefined
}
const playlistId = tab.url.substring(34)
console.log(`Playlist ID: ${playlistId}`)
return playlistId
}