(() => { const self = reg_ns("me"); 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) { 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("react", async (_, element, asset, asset_type, is_like) => { 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) => { 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) { setTimeout(() => { window.location.href = `/post/${res.payload}`; }, 100); } }); }); 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, ]); }); }); // 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 += ``; } }); 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) => { return await ( await fetch("/api/v1/auth/user/connections/_shown", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ connection, shown, }), }) ).json(); }); })(); (() => { 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 (res) => { // 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.open( `https://accounts.spotify.com/authorize?${params.toString()}`, ); window.location.reload(); }); }); 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) => { // 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") => { 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)} (${Math.floor((progress_ms / duration_ms) * 100)}%)`; } if (display === "left") { return $.ms_time_text(progress_ms); } return $.ms_time_text(duration_ms); }, ); })();