at main 4.4 kB view raw
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}