Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

Get project ready for deployment

+179 -288
.dockerignore

This is a binary file and will not be displayed.

+1
.gitignore
··· 15 15 # production 16 16 /build 17 17 */dist 18 + **/dist 18 19 19 20 # misc 20 21 .DS_Store
+54
apps/aqua/Dockerfile
··· 1 + # Use the official Bun image as base 2 + FROM oven/bun:1.0 AS builder 3 + 4 + # Set working directory 5 + WORKDIR /app 6 + 7 + # Copy root workspace files 8 + COPY package.json bun.lockb ./ 9 + 10 + # Copy turbo.json 11 + COPY turbo.json ./ 12 + 13 + # Copy workspace packages 14 + COPY packages/db/ ./packages/db/ 15 + COPY packages/lexicons/ ./packages/lexicons/ 16 + COPY packages/tsconfig/ ./packages/tsconfig/ 17 + 18 + # Copy the aqua app 19 + COPY apps/aqua/ ./apps/aqua/ 20 + 21 + # Install dependencies 22 + RUN bun install 23 + 24 + # Build workspace packages (if needed) 25 + RUN bun run build --filter=@teal/db 26 + RUN bun run build --filter=@teal/lexicons 27 + 28 + # Build the aqua app 29 + WORKDIR /app/apps/aqua 30 + RUN bun run build 31 + 32 + # Start production image (node lts ideally) 33 + FROM node:bookworm-slim 34 + 35 + WORKDIR /app 36 + 37 + # Copy built assets from builder 38 + COPY --from=builder /app/apps/aqua/dist ./dist 39 + # copy base node modules 40 + COPY --from=builder /app/node_modules ./node_modules 41 + COPY --from=builder /app/apps/aqua/node_modules ./node_modules 42 + COPY --from=builder /app/apps/aqua/package.json ./ 43 + 44 + # move public to cwd 45 + RUN mv ./dist/public ./public 46 + 47 + # Set environment variables 48 + ENV NODE_ENV=production 49 + 50 + # Expose the port your app runs on 51 + EXPOSE 3000 52 + 53 + # Start the application 54 + CMD ["npm", "run", "start"]
+5 -14
apps/aqua/package.json
··· 7 7 "scripts": { 8 8 "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", 9 9 "bun:dev": "bun run --hot src/index.ts | pino-pretty", 10 - "build": "tsup", 11 - "start": "node dist/index.js", 10 + "deno:dev": "deno run --allow-net --allow-read --allow-env --allow-run --unstable --watch src/index.ts | pino-pretty", 11 + "build": "tsup && cp -r public dist/", 12 + "start": "node dist/index.cjs | pino-pretty", 12 13 "lexgen": "lex gen-server ./src/lexicon ./lexicons/*", 13 14 "clean": "rimraf dist coverage", 14 15 "check-types": "tsc --noEmit", ··· 21 22 "@atproto/common": "^0.4.4", 22 23 "@atproto/identity": "^0.4.3", 23 24 "@atproto/lexicon": "^0.4.2", 24 - "@atproto/oauth-client-node": "^0.2.0", 25 + "@atproto/oauth-client-node": "^0.2.1", 25 26 "@atproto/sync": "^0.1.5", 26 27 "@atproto/syntax": "^0.3.0", 27 28 "@atproto/xrpc-server": "^0.6.4", ··· 49 50 "tsup": "^8.3.5", 50 51 "tsx": "^4.19.2", 51 52 "typescript": "^5.6.3" 52 - }, 53 - "tsup": { 54 - "entry": [ 55 - "src", 56 - "!src/**/__tests__/**", 57 - "!src/**/*.test.*" 58 - ], 59 - "splitting": false, 60 - "sourcemap": true, 61 - "clean": true 62 53 } 63 - } 54 + }
+15 -13
apps/aqua/src/auth/router.ts
··· 1 1 import { atclient } from "./client"; 2 2 import { db } from "@teal/db/connect"; 3 - import {atProtoSession} from "@teal/db/schema" 4 - import { eq } from "drizzle-orm" 3 + import { atProtoSession } from "@teal/db/schema"; 4 + import { eq } from "drizzle-orm"; 5 5 import { EnvWithCtx, TealContext } from "@/ctx"; 6 6 import { Hono } from "hono"; 7 7 import { tealSession } from "@teal/db/schema"; ··· 42 42 maxAge: 60 * 60 * 24 * 365, 43 43 }); 44 44 45 - if(params.get("spa")) { 45 + if (params.get("spa")) { 46 46 return c.json({ 47 47 provider: "atproto", 48 48 jwt: did, 49 49 accessToken: did, 50 - }) 50 + }); 51 51 } 52 52 53 53 return c.redirect("/"); ··· 56 56 return Response.json({ error: "Could not authorize user" }); 57 57 } 58 58 } 59 - 60 59 61 60 // Refresh an access token from a refresh token. Should be only used in SPAs. 62 61 // Pass in 'key' and 'refresh_token' query params. ··· 67 66 const params = new URLSearchParams(honoParams); 68 67 let key = params.get("key"); 69 68 let refresh_token = params.get("refresh_token"); 70 - if(!key || !refresh_token) { 71 - return Response.json({error: "Missing key or refresh_token"}); 69 + if (!key || !refresh_token) { 70 + return Response.json({ error: "Missing key or refresh_token" }); 72 71 } 73 72 // check if refresh token is valid 74 - let r_tk_check = await db.select().from(atProtoSession).where(eq(atProtoSession.key, key)).execute() as any; 73 + let r_tk_check = (await db 74 + .select() 75 + .from(atProtoSession) 76 + .where(eq(atProtoSession.key, key)) 77 + .execute()) as any; 75 78 76 - if(r_tk_check.tokenSet.refresh_token !== refresh_token) { 77 - return Response.json({error: "Invalid refresh token"}); 79 + if (r_tk_check.tokenSet.refresh_token !== refresh_token) { 80 + return Response.json({ error: "Invalid refresh token" }); 78 81 } 79 - 80 82 81 83 const session = await atclient.restore(key); 82 84 ··· 108 110 }); 109 111 110 112 return c.json({ 111 - provider:"atproto", 113 + provider: "atproto", 112 114 jwt: did, 113 115 accessToken: did, 114 - }) 116 + }); 115 117 } catch (e) { 116 118 console.error(e); 117 119 return Response.json({ error: "Could not authorize user" });
+14 -11
apps/aqua/src/index.ts
··· 1 1 import { serve } from "@hono/node-server"; 2 - import { serveStatic } from '@hono/node-server/serve-static' 2 + import { serveStatic } from "@hono/node-server/serve-static"; 3 3 import { Hono } from "hono"; 4 4 import { db } from "@teal/db/connect"; 5 5 import { getAuthRouter } from "./auth/router"; ··· 14 14 15 15 const HEAD = `<head> 16 16 <link rel="stylesheet" href="/latex.css"> 17 - </head>` 17 + </head>`; 18 18 19 19 const logger = pino({ name: "server start" }); 20 20 ··· 48 48 <a href="/">home</a> 49 49 <a href="/stamp">stamp</a> 50 50 </div> 51 - <a href="/logout" style="background-color: #cc0000; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;">logout</a> 51 + <form action="/logout" method="post" class="session-form"> 52 + <button type="submit" style="background-color: #cc0000; color: white; border: none; padding: 0rem 0.5rem; border-radius: 0.5rem;">logout</button> 53 + </form> 52 54 </div> 53 55 </div> 54 56 <div class="container"> 55 - 57 + 56 58 </div> 57 59 </div>`, 58 60 ); ··· 210 212 const body = await c.req.parseBody(); 211 213 let { artist, track, link } = body; 212 214 // shouldn't get a File, escape now 213 - if (artist instanceof File || track instanceof File || link instanceof File) return c.redirect("/stamp"); 214 - 215 + if (artist instanceof File || track instanceof File || link instanceof File) 216 + return c.redirect("/stamp"); 217 + 215 218 artist = sanitizeUrl(artist); 216 219 track = sanitizeUrl(track); 217 220 link = sanitizeUrl(link); 218 - 221 + 219 222 const agent = await getSessionAgent(c); 220 223 221 224 if (agent) { ··· 245 248 embed: embed, 246 249 }); 247 250 248 - console.log(`post: ${post}`); 249 - 250 251 return c.html( 251 252 ` 252 253 ${HEAD} ··· 272 273 </div>`, 273 274 ); 274 275 } 275 - return c.html(`<h1>doesn't look like you're logged in... try <a href="/login">logging in?</a></h1>`); 276 + return c.html( 277 + `<h1>doesn't look like you're logged in... try <a href="/login">logging in?</a></h1>`, 278 + ); 276 279 }); 277 280 278 - app.use('/*', serveStatic({ root: '/src/public' })); 281 + app.use("/*", serveStatic({ root: "/public" })); 279 282 280 283 const run = async () => { 281 284 logger.info("Running in " + navigator.userAgent);
apps/aqua/src/public/fonts/LM-bold-italic.ttf apps/aqua/public/fonts/LM-bold-italic.ttf
apps/aqua/src/public/fonts/LM-bold-italic.woff apps/aqua/public/fonts/LM-bold-italic.woff
apps/aqua/src/public/fonts/LM-bold-italic.woff2 apps/aqua/public/fonts/LM-bold-italic.woff2
apps/aqua/src/public/fonts/LM-bold.ttf apps/aqua/public/fonts/LM-bold.ttf
apps/aqua/src/public/fonts/LM-bold.woff apps/aqua/public/fonts/LM-bold.woff
apps/aqua/src/public/fonts/LM-bold.woff2 apps/aqua/public/fonts/LM-bold.woff2
apps/aqua/src/public/fonts/LM-italic.ttf apps/aqua/public/fonts/LM-italic.ttf
apps/aqua/src/public/fonts/LM-italic.woff apps/aqua/public/fonts/LM-italic.woff
apps/aqua/src/public/fonts/LM-italic.woff2 apps/aqua/public/fonts/LM-italic.woff2
apps/aqua/src/public/fonts/LM-regular.ttf apps/aqua/public/fonts/LM-regular.ttf
apps/aqua/src/public/fonts/LM-regular.woff apps/aqua/public/fonts/LM-regular.woff
apps/aqua/src/public/fonts/LM-regular.woff2 apps/aqua/public/fonts/LM-regular.woff2
apps/aqua/src/public/fonts/Libertinus-bold-italic.woff2 apps/aqua/public/fonts/Libertinus-bold-italic.woff2
apps/aqua/src/public/fonts/Libertinus-bold.woff2 apps/aqua/public/fonts/Libertinus-bold.woff2
apps/aqua/src/public/fonts/Libertinus-italic.woff2 apps/aqua/public/fonts/Libertinus-italic.woff2
apps/aqua/src/public/fonts/Libertinus-regular.woff2 apps/aqua/public/fonts/Libertinus-regular.woff2
apps/aqua/src/public/fonts/Libertinus-semibold-italic.woff2 apps/aqua/public/fonts/Libertinus-semibold-italic.woff2
apps/aqua/src/public/fonts/Libertinus-semibold.woff2 apps/aqua/public/fonts/Libertinus-semibold.woff2
apps/aqua/src/public/latex.css apps/aqua/public/latex.css
+4 -2
apps/aqua/tsconfig.json
··· 1 1 { 2 2 "extends": "@teal/tsconfig/base.json", 3 + "include": ["src/**/*"], 4 + "exclude": ["node_modules", "dist"], 3 5 "compilerOptions": { 6 + "baseUrl": ".", 4 7 "paths": { 5 - "@/*": ["./src/*"], 6 - "+lexicons/*": ["../../lexicons/*"] 8 + "@/*": ["src/*"] 7 9 } 8 10 } 9 11 }
+15
apps/aqua/tsup.config.ts
··· 1 + import { defineConfig } from "tsup"; 2 + 3 + export default defineConfig({ 4 + format: ["cjs"], 5 + entry: ["./src/index.ts"], 6 + dts: false, 7 + shims: true, 8 + skipNodeModulesBundle: false, 9 + clean: true, 10 + minify: false, 11 + bundle: true, 12 + // https://github.com/egoist/tsup/issues/619 13 + noExternal: [/(.*)/, "@teal/db", "@teal/lexicons"], 14 + splitting: false, 15 + });
-5
apps/viridian/go.mod
··· 1 - module viridian 2 - 3 - go 1.21.5 4 - 5 - require github.com/pebbe/zmq4 v1.2.11
-2
apps/viridian/go.sum
··· 1 - github.com/pebbe/zmq4 v1.2.11 h1:Ua5mgIaZeabUGnH7tqswkUcjkL7JYGai5e8v4hpEU9Q= 2 - github.com/pebbe/zmq4 v1.2.11/go.mod h1:nqnPueOapVhE2wItZ0uOErngczsJdLOGkebMxaO8r48=
-29
apps/viridian/main.go
··· 1 - package main 2 - 3 - import ( 4 - "fmt" 5 - "sync" 6 - 7 - server "viridian/server" 8 - worker "viridian/worker" 9 - ) 10 - 11 - func main() { 12 - var wg sync.WaitGroup 13 - 14 - // Run the server in a goroutine 15 - go server.RunServer() 16 - 17 - // Simulate workers, each running in a separate goroutine 18 - workerCount := worker.GetCoreCount() 19 - fmt.Printf("Starting %d workers\n", workerCount) 20 - for i := 1; i <= workerCount; i++ { 21 - fmt.Printf("Starting worker %d\n", i) 22 - workerID := fmt.Sprintf("worker-%d", i) 23 - wg.Add(1) 24 - go worker.RunWorker(workerID, &wg) 25 - } 26 - 27 - // Wait for all workers to complete (this won't happen in this example since workers run indefinitely) 28 - wg.Wait() 29 - }
-11
apps/viridian/package.json
··· 1 - { 2 - "name": "@teal/viridian", 3 - "version": "0.0.0", 4 - "private": true, 5 - "scripts": { 6 - "install": "go mod download", 7 - "build": "go build main.go", 8 - "dev": "go run main.go", 9 - "start": "go run main.go" 10 - } 11 - }
-62
apps/viridian/server/server.go
··· 1 - package server 2 - 3 - import ( 4 - "fmt" 5 - "time" 6 - 7 - "viridian/types" 8 - 9 - zmq "github.com/pebbe/zmq4" 10 - ) 11 - 12 - // RunServer launches the server that distributes jobs to workers 13 - func RunServer() { 14 - server := &types.Server{ 15 - Jobs: []types.Job{{"job1", "pending"}, {"job2", "pending"}, {"job3", "pending"}}, 16 - WorkerJobs: make(map[string]string), 17 - JobStatus: make(map[string]string), 18 - } 19 - 20 - // Initialize job status 21 - for _, job := range server.Jobs { 22 - server.JobStatus[job.ID] = "pending" 23 - } 24 - 25 - // Create a ZeroMQ ROUTER socket for distributing jobs 26 - socket, _ := zmq.NewSocket(zmq.ROUTER) 27 - defer socket.Close() 28 - socket.Bind("tcp://*:5555") 29 - 30 - for len(server.Jobs) > 0 { 31 - // Wait for a worker to send a ready message 32 - workerAddr, _ := socket.RecvMessage(0) 33 - 34 - fmt.Printf("Received ready message from worker %s\n", workerAddr) 35 - 36 - fmt.Print(workerAddr[2]) 37 - 38 - // Lock jobs to pick the next one safely 39 - server.Mu.Lock() 40 - if len(server.Jobs) == 0 { 41 - server.Mu.Unlock() 42 - continue 43 - } 44 - job := server.Jobs[0] 45 - server.Jobs = server.Jobs[1:] 46 - server.Mu.Unlock() 47 - 48 - // Track the job for this worker 49 - server.TrackJob(workerAddr[0], job.ID) 50 - 51 - // Send job to the worker 52 - fmt.Printf("Sending job %s to worker %s\n", job.ID, workerAddr[0]) 53 - socket.SendMessage(workerAddr[0], "", job.ID) 54 - 55 - time.Sleep(1 * time.Second) // Simulate a delay 56 - 57 - // In a real-world scenario, this would be based on a worker response 58 - server.MarkJobCompleted(workerAddr[0]) 59 - } 60 - 61 - fmt.Println("All jobs processed") 62 - }
-46
apps/viridian/types/types.go
··· 1 - package types 2 - 3 - import ( 4 - "strconv" 5 - "sync" 6 - ) 7 - 8 - // Job represents a job with an ID and status 9 - type Job struct { 10 - ID string 11 - Status string 12 - } 13 - 14 - // Server represents the state of the server, including jobs, worker-job associations, and job status 15 - type Server struct { 16 - Jobs []Job // Job queue 17 - WorkerJobs map[string]string // Mapping worker ID -> job ID 18 - JobStatus map[string]string // Mapping job ID -> status 19 - Mu sync.Mutex // Mutex to handle concurrency 20 - } 21 - 22 - func (s *Server) AddJob() Job { 23 - s.Mu.Lock() 24 - defer s.Mu.Unlock() 25 - job := Job{"job" + strconv.Itoa(len(s.Jobs)), "pending"} 26 - s.Jobs = append(s.Jobs, job) 27 - 28 - return job 29 - } 30 - 31 - // TrackJob assigns a job to a worker and marks it as in-progress 32 - func (s *Server) TrackJob(workerID, jobID string) { 33 - s.Mu.Lock() 34 - defer s.Mu.Unlock() 35 - s.WorkerJobs[workerID] = jobID 36 - s.JobStatus[jobID] = "in-progress" 37 - } 38 - 39 - // MarkJobCompleted marks a job as completed and removes the worker-job association 40 - func (s *Server) MarkJobCompleted(workerID string) { 41 - s.Mu.Lock() 42 - defer s.Mu.Unlock() 43 - jobID := s.WorkerJobs[workerID] 44 - s.JobStatus[jobID] = "completed" 45 - delete(s.WorkerJobs, workerID) // Remove worker-job association 46 - }
-1
apps/viridian/worker/ffmpeg.go
··· 1 - package worker
-47
apps/viridian/worker/worker.go
··· 1 - package worker 2 - 3 - import ( 4 - "fmt" 5 - "runtime" 6 - "sync" 7 - "time" 8 - 9 - zmq "github.com/pebbe/zmq4" 10 - ) 11 - 12 - // RunWorker simulates a worker that connects to the server and processes jobs 13 - func RunWorker(id string, wg *sync.WaitGroup) { 14 - defer wg.Done() 15 - 16 - // Create a ZeroMQ REQ socket for receiving jobs 17 - socket, _ := zmq.NewSocket(zmq.REQ) 18 - defer socket.Close() 19 - socket.Connect("tcp://localhost:5555") 20 - 21 - for { 22 - // Send a ready signal to the server 23 - socket.SendMessage(fmt.Sprintf("READY %s", id), 0) 24 - 25 - // Receive the job 26 - msg, err := socket.RecvMessage(0) 27 - if err != nil { 28 - fmt.Println("Error receiving job:", err) 29 - continue 30 - } 31 - job := msg[0] 32 - 33 - // Process the job 34 - fmt.Printf("Worker %s received job: %s\n", id, job) 35 - socket.SendMessage(fmt.Sprintf("RECEIVED %s", job), 0) 36 - 37 - // Simulate processing the job 38 - fmt.Printf("Worker %s processing job: %s\n", id, job) 39 - socket.SendMessage(fmt.Sprintf("PROCESSING %s", job), 0) 40 - time.Sleep(2 * time.Second) // Simulate job processing delay 41 - } 42 - } 43 - 44 - // GetCoreCount returns the number of CPU cores on the current machine 45 - func GetCoreCount() int { 46 - return runtime.NumCPU() 47 - }
bun.lockb

This is a binary file and will not be displayed.

+51 -37
compose.dev.yml
··· 1 1 services: 2 - traefik: 3 - image: traefik:v2.10 4 - container_name: traefik 5 - command: 6 - - "--api.insecure=true" 7 - - "--providers.file.directory=/etc/traefik/dynamic" 8 - - "--providers.file.watch=true" 9 - - "--entrypoints.web.address=:80" 2 + aqua-api: 3 + build: 4 + context: . 5 + dockerfile: apps/aqua/Dockerfile 6 + container_name: aqua-app 10 7 ports: 11 - - "80:80" # HTTP 12 - - "8080:8080" # Dashboard 13 - volumes: 14 - - ./traefik/dynamic:/etc/traefik/dynamic:ro 8 + - "3000:3000" 15 9 networks: 16 10 - app_network 17 - extra_hosts: 18 - - "host.docker.internal:host-gateway" # This allows reaching host machine 11 + depends_on: 12 + - postgres 13 + - redis 14 + env_file: 15 + - .env 16 + # traefik: 17 + # image: traefik:v2.10 18 + # container_name: traefik 19 + # command: 20 + # - "--api.insecure=true" 21 + # - "--providers.file.directory=/etc/traefik/dynamic" 22 + # - "--providers.file.watch=true" 23 + # - "--entrypoints.web.address=:80" 24 + # ports: 25 + # - "80:80" # HTTP 26 + # - "8080:8080" # Dashboard 27 + # volumes: 28 + # - ./traefik/dynamic:/etc/traefik/dynamic:ro 29 + # networks: 30 + # - app_network 31 + # extra_hosts: 32 + # - "host.docker.internal:host-gateway" # This allows reaching host machine 19 33 20 - postgres: 21 - image: postgres:latest 22 - container_name: postgres_db 23 - environment: 24 - POSTGRES_USER: postgres 25 - POSTGRES_PASSWORD: yourpassword 26 - POSTGRES_DB: yourdatabase 27 - ports: 28 - - "5432:5432" 29 - volumes: 30 - - postgres_data:/var/lib/postgresql/data 31 - networks: 32 - - app_network 34 + # postgres: 35 + # image: postgres:latest 36 + # container_name: postgres_db 37 + # environment: 38 + # POSTGRES_USER: postgres 39 + # POSTGRES_PASSWORD: yourpassword 40 + # POSTGRES_DB: yourdatabase 41 + # ports: 42 + # - "5432:5432" 43 + # volumes: 44 + # - postgres_data:/var/lib/postgresql/data 45 + # networks: 46 + # - app_network 33 47 34 - redis: 35 - image: redis:latest 36 - container_name: redis_cache 37 - ports: 38 - - "6379:6379" 39 - volumes: 40 - - redis_data:/data 41 - command: redis-server --appendonly yes 42 - networks: 43 - - app_network 48 + # redis: 49 + # image: redis:latest 50 + # container_name: redis_cache 51 + # ports: 52 + # - "6379:6379" 53 + # volumes: 54 + # - redis_data:/data 55 + # command: redis-server --appendonly yes 56 + # networks: 57 + # - app_network 44 58 45 59 networks: 46 60 app_network:
+17
compose.yaml
··· 1 + services: 2 + aqua-api: 3 + build: 4 + context: . 5 + dockerfile: apps/aqua/Dockerfile 6 + container_name: aqua-api 7 + # pass through db.sqlite 8 + volumes: 9 + - ./db.sqlite:/app/db.sqlite 10 + ports: 11 + - "3000:3000" 12 + env_file: 13 + - .env 14 + 15 + volumes: 16 + postgres_data: 17 + redis_data:
+2 -5
packages/db/connect.ts
··· 3 3 import process from "node:process"; 4 4 import path from "node:path"; 5 5 6 - console.log( 7 - "Loading SQLite file at", 8 - path.join(process.cwd(), "../../db.sqlite"), 9 - ); 6 + console.log("Loading SQLite file at", path.join(process.cwd(), "./db.sqlite")); 10 7 11 8 export const db = drizzle({ 12 9 connection: 13 10 // default is in project root / db.sqlite 14 11 process.env.DATABASE_URL ?? 15 - "file:" + path.join(process.cwd(), "../../db.sqlite"), 12 + "file:" + path.join(process.cwd(), "./db.sqlite"), 16 13 // doesn't seem to work? 17 14 //casing: "snake_case", 18 15 schema: schema,
-2
packages/jetstring/src/index.ts
··· 2 2 import { db } from "@teal/db/connect"; 3 3 import { status } from "@teal/db/schema"; 4 4 import { CommitCreateEvent, Jetstream } from "@skyware/jetstream"; 5 - import { server } from "@teal/lexicons/generated/server/types"; 6 - import ws from "ws"; 7 5 8 6 class Handler { 9 7 private static instance: Handler;
+1 -1
packages/tsconfig/base.json
··· 1 1 { 2 2 "$schema": "https://json.schemastore.org/tsconfig", 3 3 "compilerOptions": { 4 - "target": "ES2021", 4 + "target": "ESNext", 5 5 "module": "ESNext", 6 6 "moduleResolution": "node", 7 7 "esModuleInterop": true,