580 lines
17 KiB
JavaScript
580 lines
17 KiB
JavaScript
(() => {
|
|
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 += `<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) => {
|
|
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) => {
|
|
// <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") => {
|
|
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);
|
|
},
|
|
);
|
|
})();
|