glo/static/js/01-api.js
2026-10-30 00:58:51 +00:00

464 lines
13 KiB
JavaScript

// static/js/01-api.js
"use strict";
window.Glow = window.Glow || {};
Glow.refreshCsrfToken = async () => {
const response = await fetch("/api/csrf", {
credentials: "same-origin",
headers: {
Accept: "application/json"
}
});
if (!response.ok) {
throw new Error("Could not refresh security token.");
}
const data = await response.json();
Glow.state.csrfToken = data.csrf_token || "";
};
Glow.handleUnauthorized = () => {
Glow.state.currentUser = null;
Glow.state.csrfToken = "";
Glow.state.rooms = [];
Glow.state.activeRoom = null;
Glow.state.teams = [];
Glow.state.activeTeam = null;
Glow.state.channels = [];
Glow.state.activeChannel = null;
Glow.state.dms = [];
Glow.state.activeDm = null;
Glow.state.activeThread = null;
Glow.state.threadReplies = [];
Glow.state.pages = [];
Glow.state.activePage = null;
Glow.state.pageTypeFilter = "";
Glow.state.activeProfile = null;
Glow.state.settings = null;
Glow.state.selectedAttachment = [];
Glow.state.selectedThreadAttachment = [];
Glow.state.typingUsers = [];
Glow.state.dmTypingUsers = [];
Glow.state.pollInFlight = false;
Glow.state.channelPollInFlight = false;
Glow.state.dmPollInFlight = false;
Glow.state.feedPollInFlight = false;
Glow.state.threadPollInFlight = false;
Glow.state.messageRequestId += 1;
Glow.state.channelRequestId += 1;
Glow.state.dmRequestId += 1;
Glow.state.feedRequestId += 1;
Glow.state.threadRequestId += 1;
clearTimeout(Glow.state.pollTimer);
clearTimeout(Glow.state.typingTimer);
clearTimeout(Glow.state.calls.reconnectTimer);
clearTimeout(Glow.state.calls.invitePollTimer);
clearTimeout(Glow.state.calls.participantPollTimer);
clearTimeout(Glow.state.calls.heartbeatTimer);
Glow.state.calls.activeCall = null;
Glow.state.calls.ws = null;
Glow.state.calls.session = null;
Glow.state.calls.localStream = null;
Glow.state.calls.muted = false;
Glow.state.calls.selfPeerId = null;
Glow.state.calls.peerConnections = new Map();
Glow.state.calls.remoteStreams = new Map();
Glow.state.calls.participantAudioEls = new Map();
Glow.state.calls.peers = [];
Glow.state.calls.reconnecting = false;
Glow.state.calls.joining = false;
Glow.state.calls.leaving = false;
Glow.state.calls.invites = [];
Glow.state.calls.backendParticipants = [];
Glow.state.calls.scope = null;
Glow.state.calls.statusText = "Idle";
Glow.state.calls.permissions = {
can_manage: false,
can_end: false,
can_invite: false
};
Glow.clearMessages?.();
Glow.clearFeed?.();
Glow.clearDms?.();
Glow.closeHeaderMenu?.();
Glow.closeReactionPopup?.();
Glow.closeMediaModal?.();
Glow.endLocalCallUi?.();
Glow.setSelectedAttachment?.([]);
Glow.setSelectedThreadAttachment?.([]);
Glow.els.threadBox?.replaceChildren();
Glow.els.pagesBox?.replaceChildren();
Glow.els.pageDetailBox?.replaceChildren();
Glow.els.profileBox?.replaceChildren();
if (Glow.els.teamSelect) {
Glow.els.teamSelect.replaceChildren();
Glow.els.teamSelect.disabled = true;
}
if (Glow.els.roomSelect) {
Glow.els.roomSelect.replaceChildren();
Glow.els.roomSelect.disabled = true;
}
if (Glow.els.typingBar) {
Glow.els.typingBar.textContent = "";
Glow.els.typingBar.classList.add("hidden");
}
if (Glow.els.presenceLabel) {
Glow.els.presenceLabel.textContent = "";
Glow.els.presenceLabel.classList.add("hidden");
}
Glow.state.activeView = "feed";
Glow.updateAuthUI?.();
Glow.showAuthNotice?.("Your session has expired. Please sign in again.");
Glow.els.authOverlay.classList.remove("hidden");
};
Glow.apiRequest = async (url, options = {}, retryCsrf = true) => {
const method = (options.method || "GET").toUpperCase();
const headers = new Headers(options.headers || {});
headers.set("Accept", "application/json");
if (!["GET", "HEAD"].includes(method)) {
headers.set("X-CSRF-Token", Glow.state.csrfToken || "");
}
let response;
try {
response = await fetch(url, {
credentials: "same-origin",
...options,
method,
headers
});
} catch (_) {
const error = new Error("Network error. Check your connection and try again.");
error.code = "network";
throw error;
}
let data = {};
try {
data = await response.json();
} catch (_) {
data = {};
}
if (response.status === 403 && data.code === "blacklisted") {
Glow.showBlocked(data.error);
throw new Error(data.error || "Access blocked.");
}
if (response.status === 403 && data.code === "csrf" && retryCsrf) {
await Glow.refreshCsrfToken();
return Glow.apiRequest(url, options, false);
}
if (response.status === 401) {
Glow.handleUnauthorized();
}
if (!response.ok) {
const error = new Error(data.error || "Request failed.");
error.status = response.status;
error.code = data.code;
error.retryAfter = data.retry_after;
error.restrictedUntil = data.restricted_until;
error.data = data;
throw error;
}
if (data.csrf_token) {
Glow.state.csrfToken = data.csrf_token;
}
return data;
};
/* profile/settings */
Glow.apiGetMyProfile = () => Glow.apiRequest("/api/profile");
Glow.apiUpdateMyProfile = payload =>
Glow.apiRequest("/api/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiGetPublicProfile = username =>
Glow.apiRequest(`/api/profiles/${encodeURIComponent(username)}`);
Glow.apiGetMySettings = () => Glow.apiRequest("/api/settings");
Glow.apiUpdateMySettings = payload =>
Glow.apiRequest("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
/* teams/channels */
Glow.apiCreateTeam = payload =>
Glow.apiRequest("/api/teams", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiUpdateTeam = (teamSlug, payload) =>
Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiCreateChannel = (teamSlug, payload) =>
Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/channels`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiUpdateChannel = (teamSlug, channelSlug, payload) =>
Glow.apiRequest(
`/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}
);
Glow.apiDeleteChannel = (teamSlug, channelSlug) =>
Glow.apiRequest(
`/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}`,
{ method: "DELETE" }
);
Glow.apiListTeamMembers = teamSlug =>
Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/members`);
Glow.apiAddTeamMember = (teamSlug, payload) =>
Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/members`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiUpdateTeamMember = (teamSlug, userId, payload) =>
Glow.apiRequest(
`/api/teams/${encodeURIComponent(teamSlug)}/members/${encodeURIComponent(userId)}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}
);
Glow.apiRemoveTeamMember = (teamSlug, userId) =>
Glow.apiRequest(
`/api/teams/${encodeURIComponent(teamSlug)}/members/${encodeURIComponent(userId)}`,
{ method: "DELETE" }
);
Glow.apiCreateTeamInvite = (teamSlug, payload = {}) =>
Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/invites`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiAcceptTeamInvite = token =>
Glow.apiRequest(`/api/team-invites/${encodeURIComponent(token)}/accept`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
/* rooms */
Glow.apiCreateRoom = payload =>
Glow.apiRequest("/api/rooms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiUpdateRoom = (roomSlug, payload) =>
Glow.apiRequest(`/api/rooms/${encodeURIComponent(roomSlug)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
/* reactions */
Glow.apiGetMessageReactions = messageId =>
Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/reactions`);
Glow.apiAddReaction = (messageId, emoji) =>
Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/reactions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ emoji })
});
Glow.apiRemoveReaction = (messageId, emoji) =>
Glow.apiRequest(
`/api/messages/${encodeURIComponent(messageId)}/reactions/${encodeURIComponent(emoji)}`,
{ method: "DELETE" }
);
/* message editing / receipts / search / presence */
Glow.apiEditMessage = (messageId, payload) =>
Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiSearch = query =>
Glow.apiRequest(`/api/search?q=${encodeURIComponent(query)}`);
Glow.apiMarkRead = messageIds =>
Glow.apiRequest("/api/read-receipts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message_ids: messageIds })
});
Glow.apiGetMessageReceipts = messageId =>
Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/receipts`);
Glow.apiPingPresence = payload =>
Glow.apiRequest("/api/presence", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiGetRoomPresence = roomSlug =>
Glow.apiRequest(`/api/rooms/${encodeURIComponent(roomSlug)}/presence`);
Glow.apiGetChannelPresence = (teamSlug, channelSlug) =>
Glow.apiRequest(
`/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}/presence`
);
Glow.apiGetDmPresence = dmId =>
Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/presence`);
Glow.apiLoadHistory = params => {
const qs = new URLSearchParams(params);
return Glow.apiRequest(`/api/messages/history?${qs.toString()}`);
};
/* DMs */
Glow.apiListDms = () => Glow.apiRequest("/api/dms");
Glow.apiCreateDm = payload =>
Glow.apiRequest("/api/dms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiGetDmMessages = (dmId, afterId = null) => {
const base = `/api/dms/${encodeURIComponent(dmId)}/messages`;
return Glow.apiRequest(
afterId == null ? base : `${base}?after_id=${encodeURIComponent(afterId)}`
);
};
Glow.apiSendDmMessage = (dmId, payload) =>
Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/messages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiGetDmTyping = dmId =>
Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/typing`);
Glow.apiSetDmTyping = (dmId, typing) =>
Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/typing`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ typing })
});
/* group calls */
Glow.apiGetGroupCall = sessionId =>
Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}`);
Glow.apiJoinGroupCall = sessionId =>
Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/join`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
Glow.apiHeartbeatGroupCall = sessionId =>
Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/heartbeat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
Glow.apiLeaveGroupCall = sessionId =>
Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/leave`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
Glow.apiEndGroupCall = sessionId =>
Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/end`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
Glow.apiGetGroupCallParticipants = sessionId =>
Glow.apiRequest(
`/api/group-calls/${encodeURIComponent(sessionId)}/participants`
);
Glow.apiInviteToGroupCall = payload =>
Glow.apiRequest("/api/group-calls/invite", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
Glow.apiListGroupCallInvites = () =>
Glow.apiRequest("/api/group-calls/invites");
Glow.apiAcceptGroupCallInvite = inviteId =>
Glow.apiRequest(
`/api/group-calls/invites/${encodeURIComponent(inviteId)}/accept`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
}
);
Glow.apiDismissGroupCallInvite = inviteId =>
Glow.apiRequest(
`/api/group-calls/invites/${encodeURIComponent(inviteId)}/dismiss`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
}
);