tuiter 2006
1package main
2
3import (
4 "embed"
5 "html/template"
6 "io/fs"
7 "log"
8 "net/http"
9 "os"
10
11 "github.com/bluesky-social/indigo/atproto/auth/oauth"
12 "github.com/gorilla/sessions"
13)
14
15const (
16 sessionName = "twitter-2006-session"
17)
18
19//go:embed templates/*
20var templatesFS embed.FS
21
22//go:embed static/*
23var staticFS embed.FS
24
25var (
26 oauthApp *oauth.ClientApp
27 store *sessions.CookieStore
28 tpl *template.Template
29)
30
31// Run initializes global state and starts the HTTP server.
32func Run() {
33 config := oauth.NewPublicConfig(
34 os.Getenv("BSKY_CLIENT_ID"),
35 os.Getenv("BSKY_REDIRECT_URI"),
36 []string{"atproto", "transition:generic"},
37 )
38 oauthApp = oauth.NewClientApp(&config, oauth.NewMemStore())
39
40 key := os.Getenv("SESSION_DB_KEY")
41 if key == "" {
42 log.Fatal("SESSION_DB_KEY environment variable not set")
43 }
44
45 derivedKey, err := deriveKeyFromEnv(key)
46 if err != nil {
47 log.Fatalf("failed to derive key from SESSION_DB_KEY: %v", err)
48 }
49
50 store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
51 store.Options = &sessions.Options{HttpOnly: true, Secure: false, Path: "/"}
52
53 var dbPath string
54 if v := os.Getenv("SESSION_DB_PATH"); v != "" {
55 dbPath = v
56 }
57 sqliteStore, err := NewSQLiteStore(dbPath, derivedKey)
58 if err != nil {
59 log.Fatalf("failed to initialize SQLite store: %v", err)
60 }
61 oauthApp.Store = sqliteStore
62
63 funcMap := template.FuncMap{
64 "getPostText": getPostText,
65 "getProfileURL": getProfileURL,
66 "getPostURL": getPostURL,
67 "getFollowingCount": getFollowingCount,
68 "getFollowersCount": getFollowersCount,
69 "getPostsCount": getPostsCount,
70 "getDisplayName": getDisplayNameFromProfile,
71 "getCursor": func(t interface{}) string { return getCursorFromAny(t) },
72 "getPostPrefix": GetPostPrefix,
73 "getEmbedRecord": GetEmbedRecord,
74 "embedContext": embedContext,
75 "getPostMedia": GetPostMedia,
76 "getMediaForTemplate": GetMediaForTemplate,
77 "makeElementID": MakeElementID,
78 "wrapThread": wrapThread,
79 // newly added helpers
80 "avatarURL": AvatarURL,
81 "AvatarURL": AvatarURL,
82 "hasAvatar": HasAvatar,
83 "bannerURL": BannerURL,
84 "postBoxInitial": PostBoxInitial,
85 "postBoxPlaceholder": PostBoxPlaceholder,
86 "isPostRetweet": IsPostRetweet,
87 "isPostQuote": IsPostQuote,
88 "buildPostVM": buildPostVMForTemplate,
89 "hasItems": HasItems,
90 // reply helpers
91 "isPostReply": IsPostReply,
92 "replyParentURI": ReplyParentURI,
93 "shortURI": ShortURI,
94 "getParentInfo": GetParentInfo,
95 "getReplyChainInfos": GetReplyChainInfos,
96 "getEmbeddedParentInfo": GetEmbeddedParentInfo,
97 "hasEmbedRecord": HasEmbedRecord,
98 "isReply": IsReply,
99 // helper to build small maps in templates
100 "dict": func(vals ...interface{}) map[string]interface{} {
101 m := make(map[string]interface{})
102 for i := 0; i < len(vals); i += 2 {
103 k, _ := vals[i].(string)
104 if i+1 < len(vals) {
105 m[k] = vals[i+1]
106 }
107 }
108 return m
109 },
110 "getIsFav": getIsFav,
111 // expose like counts to templates
112 "getLikeCount": getLikeCount,
113 }
114
115 tpl = template.Must(template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/*.html"))
116
117 subStaticFS, err := fs.Sub(staticFS, "static")
118 if err != nil {
119 log.Fatalf("failed to prepare static filesystem: %v", err)
120 }
121
122 http.HandleFunc("/", handleIndex)
123 http.HandleFunc("/signin", handleSignin)
124 http.HandleFunc("/post-status", handlePostStatus)
125 http.HandleFunc("/login", handleLogin)
126 http.HandleFunc("/logout", handleLogout)
127 http.HandleFunc("/oauth-callback", handleOAuthCallback)
128 http.HandleFunc("/oauth-client-metadata.json", handleClientMetadata)
129 http.HandleFunc("/timeline", handleTimeline)
130 http.HandleFunc("/timeline/post", handleTimelinePost)
131 http.HandleFunc("/post/", handlePost)
132 http.HandleFunc("/profile/", handleProfile)
133 http.HandleFunc("/reply", handleReply)
134 http.HandleFunc("/htmx/timeline", htmxTimelineFeed)
135 http.HandleFunc("/htmx/profile", htmxProfileFeed)
136 http.HandleFunc("/video/", handleVideo)
137 http.HandleFunc("/about", handleAbout)
138 http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subStaticFS))))
139
140 port := os.Getenv("PORT")
141 if port == "" {
142 port = "8080"
143 }
144 log.Println("Listening on http://localhost:" + port)
145 log.Fatal(http.ListenAndServe(":"+port, loggingMiddleware(http.DefaultServeMux)))
146}