Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
1package main
2
3import (
4 "html/template"
5 "log"
6 "net/http"
7 "os"
8 "strings"
9 "time"
10
11 "github.com/limeleaf/diffdown/internal/auth"
12 "github.com/limeleaf/diffdown/internal/collaboration"
13 "github.com/limeleaf/diffdown/internal/db"
14 "github.com/limeleaf/diffdown/internal/handler"
15 "github.com/limeleaf/diffdown/internal/middleware"
16)
17
18func main() {
19 // Config
20 port := os.Getenv("PORT")
21 if port == "" {
22 port = "8080"
23 }
24 dbPath := os.Getenv("DB_PATH")
25 if dbPath == "" {
26 dbPath = "./diffdown.db"
27 }
28 sessionSecret := os.Getenv("DIFFDOWN_SESSION_SECRET")
29 if sessionSecret == "" {
30 sessionSecret = "dev-secret-change-in-production"
31 }
32 baseURL := os.Getenv("BASE_URL")
33 if baseURL == "" {
34 baseURL = "http://127.0.0.1:8080"
35 }
36
37 // Init
38 auth.InitStore(sessionSecret)
39
40 database, err := db.Open(dbPath)
41 if err != nil {
42 log.Fatalf("Failed to open database: %v", err)
43 }
44 defer database.Close()
45
46 if err := database.Migrate(); err != nil {
47 log.Fatalf("Failed to run migrations: %v", err)
48 }
49
50 funcMap := template.FuncMap{
51 "fmtdate": func(s string) string {
52 for _, layout := range []string{time.RFC3339Nano, time.RFC3339} {
53 if t, err := time.Parse(layout, s); err == nil {
54 return t.Format("January 2, 2006 at 15:04")
55 }
56 }
57 return s
58 },
59 }
60
61 pages := []string{
62 "about", "atproto_login", "documents", "document_view", "document_edit",
63 "landing", "login", "new_document", "register",
64 }
65 tmpls := make(map[string]*template.Template, len(pages))
66 for _, page := range pages {
67 name := page + ".html"
68 tmpls[name] = template.Must(template.New(name).Funcs(funcMap).ParseFiles(
69 "templates/base.html",
70 "templates/"+name,
71 ))
72 }
73
74 collabHub := collaboration.NewHub()
75 h := handler.New(database, tmpls, baseURL, collabHub)
76
77 // Routes
78 mux := http.NewServeMux()
79
80 // Static files are handled outside the mux to avoid conflicts with
81 // wildcard patterns.
82 staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static")))
83 faviconHandler := http.FileServer(http.Dir("static"))
84
85 // Auth routes
86
87 mux.HandleFunc("POST /auth/logout", h.Logout)
88 mux.HandleFunc("GET /client-metadata.json", h.ClientMetadata)
89 mux.HandleFunc("GET /auth/atproto", h.ATProtoLoginPage)
90 mux.HandleFunc("POST /auth/atproto", h.ATProtoLoginSubmit)
91 mux.HandleFunc("GET /auth/atproto/callback", h.ATProtoCallback)
92
93 // Page routes
94 mux.HandleFunc("GET /about", h.AboutPage)
95
96 // Dashboard
97 mux.HandleFunc("GET /", h.Dashboard)
98
99 // Document routes
100 mux.HandleFunc("GET /docs/new", h.NewDocumentPage)
101 mux.HandleFunc("POST /docs/new", h.NewDocumentSubmit)
102 mux.HandleFunc("GET /docs/{rkey}", h.DocumentView)
103 mux.HandleFunc("GET /docs/{rkey}/edit", h.DocumentEdit)
104 // Collaborator-scoped routes (owner DID in path)
105 mux.HandleFunc("GET /docs/{did}/{rkey}", h.CollaboratorDocumentView)
106 mux.HandleFunc("GET /docs/{did}/{rkey}/edit", h.CollaboratorDocumentEdit)
107
108 // API
109 mux.HandleFunc("POST /api/render", h.APIRender)
110 mux.HandleFunc("POST /api/docs/{rkey}/save", h.APIDocumentSave)
111 mux.HandleFunc("PUT /api/docs/{rkey}/autosave", h.APIDocumentAutoSave)
112 mux.HandleFunc("DELETE /api/docs/{rkey}", h.APIDocumentDelete)
113 mux.HandleFunc("POST /api/docs/{rkey}/invite", h.DocumentInvite)
114 mux.HandleFunc("GET /docs/{rkey}/accept", h.AcceptInvite)
115 mux.HandleFunc("POST /api/docs/{rkey}/comments", h.CommentCreate)
116 mux.HandleFunc("GET /api/docs/{rkey}/comments", h.CommentList)
117 mux.HandleFunc("PATCH /api/docs/{rkey}/comments/{commentId}", h.CommentUpdate)
118 mux.HandleFunc("POST /api/docs/{rkey}/steps", h.SubmitSteps)
119 mux.HandleFunc("GET /api/docs/{rkey}/steps", h.GetSteps)
120
121 // WebSocket
122 mux.HandleFunc("GET /ws/docs/{rkey}", h.CollaboratorWebSocket)
123
124 // Middleware stack
125 stack := middleware.Logger(
126 middleware.InjectUser(
127 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128 if strings.HasPrefix(r.URL.Path, "/static/") {
129 staticHandler.ServeHTTP(w, r)
130 return
131 }
132 if r.URL.Path == "/favicon.ico" || r.URL.Path == "/favicon.svg" ||
133 strings.HasPrefix(r.URL.Path, "/favicon/") {
134 faviconHandler.ServeHTTP(w, r)
135 return
136 }
137 mux.ServeHTTP(w, r)
138 }),
139 ),
140 )
141
142 log.Printf("Starting server on :%s", port)
143 log.Fatal(http.ListenAndServe(":"+port, stack))
144}