TODO: add grant creation api, grant tab in sessions tab of settings, grant api endpoints
902 lines
28 KiB
JavaScript
902 lines
28 KiB
JavaScript
(() => {
|
|
const self = reg_ns("me", ["streams"]);
|
|
|
|
self.LOGIN_ACCOUNT_TOKENS = JSON.parse(
|
|
window.localStorage.getItem("atto:login_account_tokens") || "{}",
|
|
);
|
|
|
|
self.define("logout", async () => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you would like to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch("/api/v1/auth/logout", {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
delete self.LOGIN_ACCOUNT_TOKENS[res.payload];
|
|
self.set_login_account_tokens(self.LOGIN_ACCOUNT_TOKENS);
|
|
|
|
const next = Object.entries(self.LOGIN_ACCOUNT_TOKENS)[0];
|
|
|
|
if (next) {
|
|
self.login(next[0]);
|
|
} else {
|
|
setTimeout(() => {
|
|
window.location.href = "/";
|
|
}, 150);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
self.define("remove_post", async (_, id) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/posts/${id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("purge_post", async (_, id) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this? This cannot be undone.",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/posts/${id}/purge`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("restore_post", async (_, id) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/posts/${id}/restore`, {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("react", async (_, element, asset, asset_type, is_like) => {
|
|
await trigger("atto::debounce", ["reactions::toggle"]);
|
|
fetch("/api/v1/reactions", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
asset,
|
|
asset_type,
|
|
is_like,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
const like = element.parentElement.querySelector(
|
|
'[hook_element="reaction.like"]',
|
|
);
|
|
|
|
const dislike = element.parentElement.querySelector(
|
|
'[hook_element="reaction.dislike"]',
|
|
);
|
|
|
|
if (is_like) {
|
|
like.classList.add("green");
|
|
like.querySelector("svg").classList.add("filled");
|
|
|
|
dislike.classList.remove("red");
|
|
} else {
|
|
dislike.classList.add("red");
|
|
|
|
like.classList.remove("green");
|
|
like.querySelector("svg").classList.remove("filled");
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
self.define("remove_notification", (_, id) => {
|
|
fetch(`/api/v1/notifications/${id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("update_notification_read_status", (_, id, read) => {
|
|
fetch(`/api/v1/notifications/${id}/read_status`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
read,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("clear_notifs", async () => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch("/api/v1/notifications/my", {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define(
|
|
"repost",
|
|
(_, id, content, community, do_not_redirect = false) => {
|
|
return new Promise((resolve, _) => {
|
|
fetch(`/api/v1/posts/${id}/repost`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
content,
|
|
community,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
if (!do_not_redirect) {
|
|
setTimeout(() => {
|
|
window.location.href = `/post/${res.payload}`;
|
|
}, 100);
|
|
}
|
|
|
|
resolve(res.payload);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
);
|
|
|
|
self.define("report", (_, asset, asset_type) => {
|
|
window.open(
|
|
`/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,
|
|
);
|
|
});
|
|
|
|
self.define("seen", () => {
|
|
fetch("/api/v1/auth/user/me/seen", {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
if (!res.ok) {
|
|
trigger("atto::toast", ["error", res.message]);
|
|
}
|
|
});
|
|
});
|
|
|
|
self.define("remove_question", async (_, id) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/questions/${id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("ip_block_question", async (_, id) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/questions/${id}/block_ip`, {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
|
|
self.define("notifications_stream", ({ _, streams }) => {
|
|
const element = document.getElementById("notifications_span");
|
|
|
|
streams.subscribe("notifs");
|
|
streams.event("notifs", "message", (data) => {
|
|
if (!data.method.Packet) {
|
|
console.warn("notifications stream cannot read this message");
|
|
return;
|
|
}
|
|
|
|
const inner_data = JSON.parse(data.data);
|
|
if (data.method.Packet.Crud === "Create") {
|
|
const current = Number.parseInt(element.innerText || "0");
|
|
|
|
if (current <= 0) {
|
|
element.classList.remove("hidden");
|
|
}
|
|
|
|
element.innerText = current + 1;
|
|
|
|
// check if we're already connected
|
|
const connected =
|
|
window.sessionStorage.getItem("atto:connected/notifs") ===
|
|
"true";
|
|
|
|
if (connected) {
|
|
return;
|
|
}
|
|
|
|
window.sessionStorage.setItem("atto:connected/notifs", "true");
|
|
|
|
// send notification
|
|
const enabled =
|
|
window.localStorage.getItem("atto:notifs_enabled") ===
|
|
"true";
|
|
|
|
if (Notification.permission === "granted" && enabled) {
|
|
// try to pull notification user
|
|
const matches = /\/api\/v1\/auth\/user\/find\/(\d*)/.exec(
|
|
inner_data.content,
|
|
);
|
|
|
|
// ...
|
|
new Notification(inner_data.title, {
|
|
body: inner_data.content,
|
|
icon: matches[1]
|
|
? `/api/v1/auth/user/${matches[1]}/avatar?selector_type=id`
|
|
: "/public/favicon.svg",
|
|
lang: "en-US",
|
|
});
|
|
|
|
console.info("notification created");
|
|
}
|
|
} else if (data.method.Packet.Crud === "Delete") {
|
|
const current = Number.parseInt(element.innerText || "0");
|
|
|
|
if (current - 1 <= 0) {
|
|
element.classList.add("hidden");
|
|
}
|
|
|
|
element.innerText = current - 1;
|
|
} else {
|
|
console.warn("correct packet type but with wrong data");
|
|
}
|
|
});
|
|
});
|
|
|
|
self.define("notifications_button", (_, element) => {
|
|
if (Notification.permission === "granted") {
|
|
let enabled =
|
|
window.localStorage.getItem("atto:notifs_enabled") === "true";
|
|
|
|
function text() {
|
|
if (!enabled) {
|
|
element.innerText = "Enable notifications";
|
|
} else {
|
|
element.innerText = "Disable notifications";
|
|
}
|
|
}
|
|
|
|
element.addEventListener("click", () => {
|
|
enabled = !enabled;
|
|
window.localStorage.setItem("atto:notifs_enabled", enabled);
|
|
|
|
text();
|
|
});
|
|
|
|
text();
|
|
} else if (Notification.permission !== "denied") {
|
|
element.innerText = "Enable notifications";
|
|
element.addEventListener("click", () => {
|
|
Notification.requestPermission().then((permission) => {
|
|
if (permission === "granted") {
|
|
window.localStorage.setItem(
|
|
"atto:notifs_enabled",
|
|
"true",
|
|
);
|
|
|
|
window.location.reload();
|
|
} else {
|
|
alert(
|
|
"Permission denied! You must allow this permission for browser notifications.",
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
self.define("emojis", async () => {
|
|
const payload = (await (await fetch("/api/v1/my_emojis")).json())
|
|
.payload;
|
|
|
|
const out = [];
|
|
|
|
for (const [community, [category, emojis]] of Object.entries(payload)) {
|
|
for (const emoji of emojis) {
|
|
out.push({
|
|
category,
|
|
name: emoji.name,
|
|
shortcodes: [`${community}.${emoji.name}`],
|
|
url: `/api/v1/communities/${community}/emojis/${emoji.name}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
return out;
|
|
});
|
|
|
|
// token switcher
|
|
self.define(
|
|
"set_login_account_tokens",
|
|
({ $ }, value) => {
|
|
$.LOGIN_ACCOUNT_TOKENS = value;
|
|
window.localStorage.setItem(
|
|
"atto:login_account_tokens",
|
|
JSON.stringify(value),
|
|
);
|
|
},
|
|
["object"],
|
|
);
|
|
|
|
self.define("login", ({ $ }, username) => {
|
|
const token = self.LOGIN_ACCOUNT_TOKENS[username];
|
|
|
|
if (!token) {
|
|
return;
|
|
}
|
|
|
|
window.location.href = `/api/v1/auth/token?token=${token}`;
|
|
});
|
|
|
|
self.define("render_token_picker", ({ $ }, element) => {
|
|
element.innerHTML = "";
|
|
for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) {
|
|
element.innerHTML += `<button class="quaternary w-full justify-start" onclick="trigger('me::login', ['${token[0]}'])">
|
|
<img
|
|
title="${token[0]}'s avatar"
|
|
src="/api/v1/auth/user/${token[0]}/avatar?selector_type=username"
|
|
alt="Avatar image"
|
|
class="avatar"
|
|
style="--size: 24px"
|
|
/>
|
|
|
|
<span>${token[0]}</span>
|
|
</button>`;
|
|
}
|
|
});
|
|
|
|
self.define("switch_account", () => {
|
|
document.getElementById("tokens_dialog").showModal();
|
|
});
|
|
})();
|
|
|
|
(() => {
|
|
const self = reg_ns("connections");
|
|
|
|
self.define("pkce_verifier", async (_, length) => {
|
|
let text = "";
|
|
const possible =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
text += possible.charAt(
|
|
Math.floor(Math.random() * possible.length),
|
|
);
|
|
}
|
|
|
|
return text;
|
|
});
|
|
|
|
self.define("pkce_challenge", async (_, verifier) => {
|
|
const data = new TextEncoder().encode(verifier);
|
|
const digest = await window.crypto.subtle.digest("SHA-256", data);
|
|
return btoa(
|
|
String.fromCharCode.apply(null, [...new Uint8Array(digest)]),
|
|
)
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "");
|
|
});
|
|
|
|
self.define("delete", async (_, connection) => {
|
|
if (
|
|
!(await trigger("atto::confirm", [
|
|
"Are you sure you want to do this?",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/v1/auth/user/connections/${connection}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
|
|
self.define("push_con_data", async (_, connection, data) => {
|
|
return await (
|
|
await fetch("/api/v1/auth/user/connections/_data", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
connection,
|
|
data,
|
|
}),
|
|
})
|
|
).json();
|
|
});
|
|
|
|
self.define("push_con_state", async (_, connection, data) => {
|
|
return await (
|
|
await fetch("/api/v1/auth/user/connections/_state", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
connection,
|
|
data,
|
|
}),
|
|
})
|
|
).json();
|
|
});
|
|
|
|
self.define("push_con_shown", async (_, connection, shown) => {
|
|
fetch("/api/v1/auth/user/connections/_shown", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
connection,
|
|
shown,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger("atto::toast", [
|
|
res.ok ? "success" : "error",
|
|
res.message,
|
|
]);
|
|
});
|
|
});
|
|
})();
|
|
|
|
(() => {
|
|
const self = reg_ns("spotify");
|
|
|
|
self.define("create_connection", (_, client_id) => {
|
|
fetch("/api/v1/auth/user/connections/spotify", {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then(async (_) => {
|
|
// create challenge and store
|
|
const verifier = await trigger("connections::pkce_verifier", [
|
|
128,
|
|
]);
|
|
|
|
const challenge = await trigger("connections::pkce_challenge", [
|
|
verifier,
|
|
]);
|
|
|
|
await trigger("connections::push_con_data", [
|
|
"Spotify",
|
|
{
|
|
verifier,
|
|
challenge,
|
|
},
|
|
]);
|
|
|
|
// ...
|
|
const params = new URLSearchParams();
|
|
params.append("client_id", client_id);
|
|
params.append("response_type", "code");
|
|
params.append(
|
|
"redirect_uri",
|
|
`${window.location.origin}/auth/connections_link/Spotify`,
|
|
);
|
|
params.append(
|
|
"scope",
|
|
"user-read-recently-played user-read-playback-state user-read-currently-playing",
|
|
);
|
|
params.append("code_challenge_method", "S256");
|
|
params.append("code_challenge", challenge);
|
|
|
|
window.location.href = `https://accounts.spotify.com/authorize?${params.toString()}`;
|
|
});
|
|
});
|
|
|
|
self.define("get_token", async (_, client_id, verifier, code) => {
|
|
const params = new URLSearchParams();
|
|
params.append("client_id", client_id);
|
|
params.append("grant_type", "authorization_code");
|
|
params.append("code", code);
|
|
params.append(
|
|
"redirect_uri",
|
|
`${window.location.origin}/auth/connections_link/Spotify`,
|
|
);
|
|
params.append("code_verifier", verifier);
|
|
|
|
const result = await fetch("https://accounts.spotify.com/api/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: params,
|
|
});
|
|
|
|
const { access_token, refresh_token, expires_in } = await result.json();
|
|
return [access_token, refresh_token, expires_in];
|
|
});
|
|
|
|
self.define("refresh_token", async (_, client_id, token) => {
|
|
const params = new URLSearchParams();
|
|
params.append("client_id", client_id);
|
|
params.append("grant_type", "refresh_token");
|
|
params.append("refresh_token", token);
|
|
|
|
const result = await fetch("https://accounts.spotify.com/api/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: params,
|
|
});
|
|
|
|
const { access_token, refresh_token, expires_in } = await result.json();
|
|
return [access_token, refresh_token, expires_in];
|
|
});
|
|
|
|
self.define("profile", async (_, token) => {
|
|
return await (
|
|
await fetch("https://api.spotify.com/v1/me", {
|
|
method: "GET",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
).json();
|
|
});
|
|
|
|
self.define("get_playing", async (_, token) => {
|
|
// <https://developer.spotify.com/documentation/web-api/reference/get-information-about-the-users-current-playback>
|
|
return await (
|
|
await fetch("https://api.spotify.com/v1/me/player", {
|
|
method: "GET",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
).json();
|
|
});
|
|
|
|
self.define("publish_playing", async (_, playing) => {
|
|
if (!playing.is_playing) {
|
|
return await trigger("connections::push_con_state", [
|
|
"Spotify",
|
|
{
|
|
external_urls: {},
|
|
data: {},
|
|
},
|
|
]);
|
|
}
|
|
|
|
if (playing.item.is_local) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
window.localStorage.getItem("atto:connections.spotify/id") ===
|
|
playing.item.id
|
|
) {
|
|
// item already pushed to connection, no need right now
|
|
return;
|
|
}
|
|
|
|
window.localStorage.setItem(
|
|
"atto:connections.spotify/id",
|
|
playing.item.id,
|
|
);
|
|
|
|
return await trigger("connections::push_con_state", [
|
|
"Spotify",
|
|
{
|
|
external_urls: {
|
|
track: playing.item.external_urls.spotify,
|
|
artist: playing.item.artists[0].external_urls.spotify,
|
|
album: playing.item.album.external_urls.spotify,
|
|
album_img: playing.item.album.images[0].url,
|
|
},
|
|
data: {
|
|
id: playing.item.id,
|
|
// track
|
|
track: playing.item.name,
|
|
artist: playing.item.artists[0].name,
|
|
album: playing.item.album.name,
|
|
// image
|
|
// img_w: playing.item.album.images[0].width.toString(),
|
|
// img_h: playing.item.album.images[0].width.toString(),
|
|
// times
|
|
timestamp: playing.timestamp.toString(),
|
|
progress_ms: playing.progress_ms.toString(),
|
|
duration_ms: playing.item.duration_ms.toString(),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
self.define("ms_time_text", (_, ms) => {
|
|
const minutes = Math.floor(ms / 60000);
|
|
const seconds = ((ms % 60000) / 1000).toFixed(0);
|
|
return `${minutes}:${(seconds < 10 ? "0" : "") + seconds}`;
|
|
});
|
|
|
|
self.define(
|
|
"timestamp",
|
|
({ $ }, updated_, progress_ms_, duration_ms_, display = "full") => {
|
|
if (duration_ms_ === "0") {
|
|
return;
|
|
}
|
|
|
|
const now = new Date().getTime();
|
|
const updated = Number.parseInt(updated_) + 8000;
|
|
|
|
let elapsed_since_update = now - updated;
|
|
if (elapsed_since_update < 0) {
|
|
elapsed_since_update = 0;
|
|
}
|
|
|
|
const progress_ms =
|
|
Number.parseInt(progress_ms_) + elapsed_since_update;
|
|
|
|
const duration_ms = Number.parseInt(duration_ms_);
|
|
|
|
if (progress_ms > duration_ms) {
|
|
// song is over
|
|
return "";
|
|
}
|
|
|
|
if (display === "full") {
|
|
return `${$.ms_time_text(progress_ms)}/${$.ms_time_text(duration_ms)} <span class="fade">(${Math.floor((progress_ms / duration_ms) * 100)}%)</span>`;
|
|
}
|
|
|
|
if (display === "left") {
|
|
return $.ms_time_text(progress_ms);
|
|
}
|
|
|
|
return $.ms_time_text(duration_ms);
|
|
},
|
|
);
|
|
})();
|
|
|
|
(() => {
|
|
const self = reg_ns("last_fm");
|
|
|
|
self.define("api", async (_, method, data) => {
|
|
return JSON.parse(
|
|
(
|
|
await (
|
|
await fetch(
|
|
"/api/v1/auth/user/connections/last_fm/api_proxy",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
method,
|
|
data,
|
|
}),
|
|
},
|
|
)
|
|
).json()
|
|
).payload,
|
|
);
|
|
});
|
|
|
|
self.define("create_connection", (_, client_id) => {
|
|
fetch("/api/v1/auth/user/connections/last_fm", {
|
|
method: "POST",
|
|
})
|
|
.then((res) => res.json())
|
|
.then(async (_) => {
|
|
const params = new URLSearchParams();
|
|
params.append("api_key", client_id);
|
|
params.append(
|
|
"cb",
|
|
`${window.location.origin}/auth/connections_link/LastFm`,
|
|
);
|
|
|
|
window.location.href = `https://last.fm/api/auth?${params.toString()}`;
|
|
});
|
|
});
|
|
|
|
self.define("pull_track_info", async (_, artist, name) => {
|
|
const params = new URLSearchParams();
|
|
params.append("query", `query=artist:"${artist}" and track:"${name}"`);
|
|
params.append("limit", "1");
|
|
params.append("fmt", "json");
|
|
|
|
return (
|
|
await (
|
|
await fetch(
|
|
`https://musicbrainz.org/ws/2/recording?${params.toString()}`,
|
|
)
|
|
).json()
|
|
).recordings[0];
|
|
});
|
|
|
|
self.define("get_session", async ({ $ }, token) => {
|
|
return await $.api("auth.getSession", {
|
|
token,
|
|
});
|
|
});
|
|
|
|
self.define("get_playing", async ({ $ }, user, session_token) => {
|
|
// <https://lastfm-docs.github.io/api-docs/user/getRecentTracks/>
|
|
return (
|
|
await $.api("user.getRecentTracks", {
|
|
user,
|
|
sk: session_token,
|
|
limit: "1",
|
|
extended: "1",
|
|
})
|
|
).recenttracks.track[0];
|
|
});
|
|
|
|
self.define("publish_playing", async ({ $ }, playing) => {
|
|
if (!playing || !playing["@attr"] || !playing["@attr"].nowplaying) {
|
|
return await trigger("connections::push_con_state", [
|
|
"LastFm",
|
|
{
|
|
external_urls: {},
|
|
data: {},
|
|
},
|
|
]);
|
|
}
|
|
|
|
if (
|
|
window.localStorage.getItem("atto:connections.last_fm/name") ===
|
|
playing.name
|
|
) {
|
|
// item already pushed to connection, no need right now
|
|
return;
|
|
}
|
|
|
|
window.localStorage.setItem(
|
|
"atto:connections.last_fm/name",
|
|
playing.name,
|
|
);
|
|
|
|
const mb_info = await $.pull_track_info(
|
|
playing.artist.name,
|
|
playing.name,
|
|
);
|
|
|
|
return await trigger("connections::push_con_state", [
|
|
"LastFm",
|
|
{
|
|
external_urls: {
|
|
track: playing.url,
|
|
artist: playing.artist.url,
|
|
track_img: playing.image[2]["#text"],
|
|
},
|
|
data: {
|
|
id: mb_info.id,
|
|
// track
|
|
track: playing.name,
|
|
artist: playing.artist.name,
|
|
album: playing.album["#text"],
|
|
// times
|
|
timestamp: new Date().getTime().toString(),
|
|
duration_ms: (mb_info.length || 0).toString(),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
})();
|