(() => {
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);
},
);
})();