Statusphere running on a slice 馃崟
1import { render } from "preact-render-to-string";
2import { App } from "./components/App.tsx";
3import { StatusTimeline } from "./components/HomePage.tsx";
4import { PORT, sessionStore, oauthSessions, atprotoClient } from "./config.ts";
5import { AuthenticatedUser } from "./types.ts";
6import { fetchStatusesWithAuthors } from "./api.ts";
7import { getUserTimezone } from "./utils.ts";
8
9async function handler(req: Request): Promise<Response> {
10 const url = new URL(req.url);
11 const pathname = url.pathname;
12
13 // Get current user session
14 const currentUser = await sessionStore.getCurrentUser(req);
15
16 // Routing
17 if (pathname === "/") {
18 return await handleHome(req, currentUser);
19 }
20
21 if (pathname === "/login") {
22 return handleLogin(req, currentUser);
23 }
24
25 if (pathname === "/oauth/authorize" && req.method === "POST") {
26 return await handleOAuthAuthorize(req);
27 }
28
29 if (pathname === "/oauth/callback") {
30 return await handleOAuthCallback(req);
31 }
32
33 if (pathname === "/logout" && req.method === "POST") {
34 return await handleLogout(req);
35 }
36
37 if (pathname === "/status" && req.method === "POST") {
38 return await handleSetStatus(req, currentUser);
39 }
40
41 return new Response("Not Found", { status: 404 });
42}
43
44async function handleHome(
45 req: Request,
46 currentUser: AuthenticatedUser
47): Promise<Response> {
48 const statuses = await fetchStatusesWithAuthors();
49 const userTimezone = getUserTimezone(req);
50 const html = render(App({ currentUser, statuses, userTimezone }));
51
52 return new Response(`<!DOCTYPE html>${html}`, {
53 headers: { "Content-Type": "text/html" },
54 });
55}
56
57function handleLogin(req: Request, currentUser: AuthenticatedUser): Response {
58 if (currentUser.isAuthenticated) {
59 return Response.redirect(new URL("/", req.url), 302);
60 }
61
62 const html = render(App({ currentUser, page: "login" }));
63
64 return new Response(`<!DOCTYPE html>${html}`, {
65 headers: { "Content-Type": "text/html" },
66 });
67}
68
69async function handleOAuthAuthorize(req: Request): Promise<Response> {
70 await atprotoClient.oauth?.logout();
71
72 const formData = await req.formData();
73 const loginHint = formData.get("loginHint") as string;
74
75 const authResult = await atprotoClient.oauth!.authorize({ loginHint });
76
77 return Response.redirect(authResult.authorizationUrl, 302);
78}
79
80async function handleOAuthCallback(req: Request): Promise<Response> {
81 const url = new URL(req.url);
82 const code = url.searchParams.get("code");
83 const state = url.searchParams.get("state");
84
85 if (!code || !state) {
86 return new Response("Missing OAuth parameters", { status: 400 });
87 }
88
89 await atprotoClient.oauth!.handleCallback({ code, state });
90
91 const sessionId = await oauthSessions.createOAuthSession();
92
93 if (!sessionId) {
94 return new Response("Failed to create session", { status: 500 });
95 }
96
97 const sessionCookie = sessionStore.createSessionCookie(sessionId);
98
99 return new Response(null, {
100 status: 302,
101 headers: {
102 Location: new URL("/", req.url).toString(),
103 "Set-Cookie": sessionCookie,
104 },
105 });
106}
107
108async function handleLogout(req: Request): Promise<Response> {
109 const session = await sessionStore.getSessionFromRequest(req);
110
111 if (session) {
112 await oauthSessions.logout(session.sessionId);
113 }
114
115 const clearCookie = sessionStore.createLogoutCookie();
116
117 return new Response(null, {
118 status: 302,
119 headers: {
120 Location: new URL("/login", req.url).toString(),
121 "Set-Cookie": clearCookie,
122 },
123 });
124}
125
126async function handleSetStatus(
127 req: Request,
128 currentUser: AuthenticatedUser
129): Promise<Response> {
130 if (!currentUser.isAuthenticated) {
131 // For HTMX requests, send redirect header
132 const isHtmxRequest = req.headers.get("HX-Request") === "true";
133 if (isHtmxRequest) {
134 return new Response(null, {
135 status: 200,
136 headers: {
137 "HX-Redirect": "/login",
138 },
139 });
140 }
141 return Response.redirect(new URL("/login", req.url), 302);
142 }
143
144 const isHtmxRequest = req.headers.get("HX-Request") === "true";
145
146 const formData = await req.formData();
147 const status = formData.get("status") as string;
148
149 try {
150 await atprotoClient.xyz.statusphere.status.createRecord({
151 status,
152 createdAt: new Date().toISOString(),
153 });
154 } catch (error) {
155 console.error("Error setting status:", error);
156 return new Response("Error setting status", { status: 500 });
157 }
158
159 if (isHtmxRequest) {
160 // Return updated timeline for HTMX
161 try {
162 const statuses = await fetchStatusesWithAuthors();
163 const userTimezone = getUserTimezone(req);
164 const html = render(StatusTimeline({ statuses, userTimezone }));
165
166 return new Response(html, {
167 headers: { "Content-Type": "text/html" },
168 });
169 } catch (error) {
170 console.error("Error fetching updated statuses:", error);
171 return new Response("Error loading statuses", { status: 500 });
172 }
173 }
174
175 return Response.redirect(new URL("/", req.url), 302);
176}
177
178Deno.serve({ port: PORT, hostname: "0.0.0.0" }, handler);