package main
import (
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/gorilla/sessions"
)
const (
sessionName = "twitter-2006-session"
)
//go:embed templates/*
var templatesFS embed.FS
//go:embed static/*
var staticFS embed.FS
var (
oauthApp *oauth.ClientApp
store *sessions.CookieStore
tpl *template.Template
)
// Run initializes global state and starts the HTTP server.
func Run() {
config := oauth.NewPublicConfig(
os.Getenv("BSKY_CLIENT_ID"),
os.Getenv("BSKY_REDIRECT_URI"),
[]string{"atproto", "transition:generic"},
)
oauthApp = oauth.NewClientApp(&config, oauth.NewMemStore())
key := os.Getenv("SESSION_DB_KEY")
if key == "" {
log.Fatal("SESSION_DB_KEY environment variable not set")
}
derivedKey, err := deriveKeyFromEnv(key)
if err != nil {
log.Fatalf("failed to derive key from SESSION_DB_KEY: %v", err)
}
store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
store.Options = &sessions.Options{HttpOnly: true, Secure: false, Path: "/"}
var dbPath string
if v := os.Getenv("SESSION_DB_PATH"); v != "" {
dbPath = v
}
sqliteStore, err := NewSQLiteStore(dbPath, derivedKey)
if err != nil {
log.Fatalf("failed to initialize SQLite store: %v", err)
}
oauthApp.Store = sqliteStore
funcMap := template.FuncMap{
"getPostText": getPostText,
"getProfileURL": getProfileURL,
"getPostURL": getPostURL,
"getFollowingCount": getFollowingCount,
"getFollowersCount": getFollowersCount,
"getPostsCount": getPostsCount,
"getDisplayName": getDisplayNameFromProfile,
"getCursor": func(t interface{}) string { return getCursorFromAny(t) },
"getPostPrefix": GetPostPrefix,
"getEmbedRecord": GetEmbedRecord,
"embedContext": embedContext,
"getPostMedia": GetPostMedia,
"getMediaForTemplate": GetMediaForTemplate,
"makeElementID": MakeElementID,
"wrapThread": wrapThread,
// newly added helpers
"avatarURL": AvatarURL,
"AvatarURL": AvatarURL,
"hasAvatar": HasAvatar,
"bannerURL": BannerURL,
"postBoxInitial": PostBoxInitial,
"postBoxPlaceholder": PostBoxPlaceholder,
"isPostRetweet": IsPostRetweet,
"isPostQuote": IsPostQuote,
"buildPostVM": buildPostVMForTemplate,
"hasItems": HasItems,
// reply helpers
"isPostReply": IsPostReply,
"replyParentURI": ReplyParentURI,
"shortURI": ShortURI,
"getParentInfo": GetParentInfo,
"getReplyChainInfos": GetReplyChainInfos,
"getEmbeddedParentInfo": GetEmbeddedParentInfo,
"hasEmbedRecord": HasEmbedRecord,
"isReply": IsReply,
// helper to build small maps in templates
"dict": func(vals ...interface{}) map[string]interface{} {
m := make(map[string]interface{})
for i := 0; i < len(vals); i += 2 {
k, _ := vals[i].(string)
if i+1 < len(vals) {
m[k] = vals[i+1]
}
}
return m
},
"getIsFav": getIsFav,
// expose like counts to templates
"getLikeCount": getLikeCount,
}
tpl = template.Must(template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/*.html"))
subStaticFS, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatalf("failed to prepare static filesystem: %v", err)
}
http.HandleFunc("/", handleIndex)
http.HandleFunc("/signin", handleSignin)
http.HandleFunc("/post-status", handlePostStatus)
http.HandleFunc("/login", handleLogin)
http.HandleFunc("/logout", handleLogout)
http.HandleFunc("/oauth-callback", handleOAuthCallback)
http.HandleFunc("/oauth-client-metadata.json", handleClientMetadata)
http.HandleFunc("/timeline", handleTimeline)
http.HandleFunc("/timeline/post", handleTimelinePost)
http.HandleFunc("/post/", handlePost)
http.HandleFunc("/profile/", handleProfile)
http.HandleFunc("/reply", handleReply)
http.HandleFunc("/htmx/timeline", htmxTimelineFeed)
http.HandleFunc("/htmx/profile", htmxProfileFeed)
http.HandleFunc("/video/", handleVideo)
http.HandleFunc("/about", handleAbout)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subStaticFS))))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Println("Listening on http://localhost:" + port)
log.Fatal(http.ListenAndServe(":"+port, loggingMiddleware(http.DefaultServeMux)))
}