// 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.settingsSaved = null; Glow.state.settingsDraft = null; Glow.state.settingsDirty = false; 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.metrics = { packetLoss: null, uptime: null, latency: null, elapsedSeconds: 0, userCount: 0 }; 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.profileResetCropState?.(); 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({}) } ); /* optional / not yet guaranteed on all backends */ Glow.apiBookmarkMessage = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/bookmark`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); Glow.apiUnbookmarkMessage = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/bookmark`, { method: "DELETE" }); Glow.apiPinMessage = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); Glow.apiUnpinMessage = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/pin`, { method: "DELETE" }); Glow.apiListPins = (scopeKind, scopeId) => Glow.apiRequest( `/api/pins?scope_kind=${encodeURIComponent(scopeKind)}&scope_id=${encodeURIComponent(scopeId)}` ); Glow.apiListBookmarks = () => Glow.apiRequest("/api/bookmarks"); Glow.apiCreateMessageShare = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/share`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); Glow.apiCreateAttachmentShare = attachmentId => Glow.apiRequest(`/api/attachments/${encodeURIComponent(attachmentId)}/share`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); Glow.apiCreateScheduledMessage = payload => Glow.apiRequest("/api/scheduled-messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });