Spaces:
Running
Running
| import { serve } from "bun"; | |
| import { Expo } from "expo-server-sdk"; | |
| const expo = new Expo(); | |
| const events = []; | |
| const html = ` | |
| <!DOCTYPE html><html><head><title>Timed Event Scheduler</title></head><body> | |
| <h1>Schedule Timed Event</h1> | |
| <form onsubmit="submitEvent(event)"> | |
| <label>ID: <input type="text" id="id" required /></label><br /> | |
| <label>Delay (sec): <input type="number" id="delay" required /></label><br /> | |
| <label>Message: <input type="text" id="message" /></label><br /> | |
| <label>Event Type: | |
| <select id="type"> | |
| <option value="timeout">Timeout</option> | |
| <option value="interval">Interval</option> | |
| <option value="webhook">Webhook</option> | |
| </select> | |
| </label><br /> | |
| <label>Repeat Every (sec): <input type="number" id="repeat" /></label><br /> | |
| <label>Expo Token(s) (comma-separated): <input type="text" id="tokens" /></label><br /> | |
| <label>Webhook URL: <input type="text" id="webhookUrl" /></label><br /> | |
| <label>Webhook Payload (JSON): <textarea id="payload"></textarea></label><br /> | |
| <button type="submit">Submit</button> | |
| </form> | |
| <script> | |
| async function submitEvent(e) { | |
| e.preventDefault(); | |
| const id = document.getElementById('id').value; | |
| const delay = parseInt(document.getElementById('delay').value); | |
| const message = document.getElementById('message').value; | |
| const type = document.getElementById('type').value; | |
| const repeat = parseInt(document.getElementById('repeat').value || 0); | |
| const tokens = document.getElementById('tokens').value.split(',').map(t => t.trim()).filter(Boolean); | |
| const webhookUrl = document.getElementById('webhookUrl').value; | |
| const payloadText = document.getElementById('payload').value; | |
| let payload = {}; | |
| try { | |
| if (payloadText) payload = JSON.parse(payloadText); | |
| } catch { | |
| alert('Invalid JSON payload'); | |
| return; | |
| } | |
| const res = await fetch('/events', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ id, delay, message, type, repeat, expoTokens: tokens, webhookUrl, payload }) | |
| }); | |
| alert(await res.text()); | |
| } | |
| </script> | |
| </body></html> | |
| `; | |
| serve({ | |
| port: 7860, | |
| fetch: async (req) => { | |
| const url = new URL(req.url); | |
| if (req.method === "GET" && url.pathname === "/") { | |
| return new Response(html, { headers: { "Content-Type": "text/html" } }); | |
| } | |
| // POST /events - Create Event | |
| if (req.method === "POST" && url.pathname === "/events") { | |
| try { | |
| const body = await req.json(); | |
| const { id, delay, message, type, repeat, expoTokens, webhookUrl, payload } = body; | |
| if (!id || !delay || !type) { | |
| return new Response("Missing 'id', 'delay', or 'type'", { status: 400 }); | |
| } | |
| if (events.find(e => e.id === id)) { | |
| return new Response("Event ID already exists", { status: 409 }); | |
| } | |
| events.push({ | |
| id, | |
| timestamp: Date.now() + delay * 1000, | |
| type, | |
| repeat: type === "interval" && repeat ? repeat * 1000 : undefined, | |
| data: { | |
| message, | |
| expoTokens: Array.isArray(expoTokens) ? expoTokens : [], | |
| webhookUrl, | |
| payload | |
| } | |
| }); | |
| return new Response("Event scheduled", { status: 200 }); | |
| } catch (err) { | |
| return new Response("Invalid JSON", { status: 400 }); | |
| } | |
| } | |
| // DELETE /events/:id - Remove Event | |
| if (req.method === "DELETE" && url.pathname.startsWith("/events/")) { | |
| const id = url.pathname.split("/")[2]; | |
| const index = events.findIndex(e => e.id === id); | |
| if (index === -1) return new Response("Event not found", { status: 404 }); | |
| events.splice(index, 1); | |
| return new Response("Event deleted", { status: 200 }); | |
| } | |
| return new Response("Not Found", { status: 404 }); | |
| } | |
| }); | |
| // Event Loop | |
| setInterval(async () => { | |
| const now = Date.now(); | |
| const due = events.filter(e => e.timestamp <= now); | |
| for (const e of due) { | |
| console.log(`⏰ Executing event: ${e.id} (${e.type})`); | |
| // Expo Notifications | |
| if (e.data.expoTokens?.length && e.data.message) { | |
| console.log("found a notifications events") | |
| const messages = e.data.expoTokens | |
| .filter(t => Expo.isExpoPushToken(t)) | |
| .map(token => ({ | |
| to: token, | |
| sound: 'default', | |
| title: '⏰ Scheduled Event', | |
| body: e.data.message, | |
| data: {} | |
| })); | |
| for (const chunk of expo.chunkPushNotifications(messages)) { | |
| try { | |
| await expo.sendPushNotificationsAsync(chunk); | |
| } catch (err) { | |
| console.error("🔥 Expo error:", err); | |
| } | |
| } | |
| } | |
| // Webhook Trigger | |
| if (e.type === "webhook" && e.data.webhookUrl) { | |
| console.log("found a webhook event") | |
| try { | |
| await fetch(e.data.webhookUrl, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(e.data.payload || {}) | |
| }); | |
| console.log(`📡 Webhook fired for ${e.id}`); | |
| } catch (err) { | |
| console.error(`❌ Webhook failed for ${e.id}`, err); | |
| } | |
| } | |
| // Repeat or remove | |
| if (e.type === "interval" && e.repeat) { | |
| e.timestamp = now + e.repeat; | |
| } else { | |
| const i = events.findIndex(ev => ev.id === e.id); | |
| if (i !== -1) events.splice(i, 1); | |
| } | |
| } | |
| }, 1000); |