package main import ( "flag" "fmt" "html/template" "io/ioutil" "log" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" "github.com/fsnotify/fsnotify" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/russross/blackfriday/v2" ) const ( dataDir = "./data" templateDir = "./templates" staticDir = "./static" defaultPort = 8080 botTokenEnv = "YOUR_TELEGRAM_BOT_TOKEN" // Replace with your bot's token or set via environment variable pageSize = 5 // Number of blog entries per page ) type Blog struct { Name string Entries []BlogEntry } type BlogEntry struct { Content string Date time.Time Number int } type PageData struct { Title string Description string BlogLinks []string Content template.HTML PrevLink string NextLink string } var ( blogs []Blog bot *tgbotapi.BotAPI port int creationTimes = make(map[string]time.Time) creationTimesM sync.Mutex ) func init() { flag.IntVar(&port, "p", defaultPort, "Specify the port to run the server on") flag.IntVar(&port, "port", defaultPort, "Specify the port to run the server on") } func main() { // Parse the flags flag.Parse() // Retrieve the Telegram bot token from the environment variable botToken := os.Getenv("TELEGRAM_BOT_TOKEN") if botToken == "" { botToken = botTokenEnv } // Initialize the Telegram bot var err error bot, err = tgbotapi.NewBotAPI(botToken) if err != nil { log.Printf("Warning: Error creating Telegram bot: %v", err) } else { go startTelegramBot() } // Retrieve blog entries from the data directory blogs, err = getBlogs(dataDir) if err != nil { log.Fatalf("Error getting blogs: %v", err) } // Start watching for changes in the data directory go watchForChanges(dataDir) // Serve static files (CSS, JS, etc.) from the /static directory http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) // Define route handlers http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { renderIndex(w) return } if r.URL.Path == "/download" { renderTemplate(w, "download.html", nil) return } if r.URL.Path == "/download-linux" { renderTemplate(w, "download-linux.html", nil) return } pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) < 2 { http.NotFound(w, r) return } siteURL := fmt.Sprintf("http://localhost:%d", port) // Define siteURL here if pathParts[1] == "rss" { generateRSSFeed(w, blogs, siteURL) return } if pathParts[1] == "all" { page, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil || page < 1 { page = 1 } renderAllBlogs(w, r, page) return } if len(pathParts) == 3 { blogName := pathParts[1] entryNumber, err := strconv.Atoi(pathParts[2]) if err == nil { renderBlogEntry(w, blogName, entryNumber) return } } blogName := pathParts[1] for _, blog := range blogs { if blog.Name == blogName { http.Redirect(w, r, fmt.Sprintf("/%s/%d", blogName, blog.Entries[0].Number), http.StatusFound) return } } http.NotFound(w, r) }) // Start the HTTP server on the specified port serverURL := fmt.Sprintf("http://localhost:%d", port) log.Printf("Starting server on %s", serverURL) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) } func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { tmplPath := filepath.Join(templateDir, tmpl) t, err := template.ParseFiles(tmplPath) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) log.Printf("Error parsing template %s: %v", tmpl, err) return } err = t.Execute(w, data) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) log.Printf("Error executing template %s: %v", tmpl, err) } } func renderIndex(w http.ResponseWriter) { renderTemplate(w, "index.html", nil) } func renderBlogEntry(w http.ResponseWriter, blogName string, entryNumber int) { var blog Blog for _, b := range blogs { if b.Name == blogName { blog = b break } } var entry BlogEntry var prevLink, nextLink string for i, e := range blog.Entries { if e.Number == entryNumber { entry = e if i > 0 { prevLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i-1].Number) } if i < len(blog.Entries)-1 { nextLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i+1].Number) } break } } htmlContent := blackfriday.Run([]byte(entry.Content)) content := fmt.Sprintf("
%s
", htmlContent) pageData := PageData{ Title: "", Description: "", BlogLinks: getBlogLinks(), Content: template.HTML(content), PrevLink: prevLink, NextLink: nextLink, } // Use the existing renderTemplate to render with a specific PageData type. renderTemplate(w, "news.html", pageData) } func renderAllBlogs(w http.ResponseWriter, r *http.Request, page int) { var allEntries []BlogEntry for _, blog := range blogs { allEntries = append(allEntries, blog.Entries...) } sort.Slice(allEntries, func(i, j int) bool { return allEntries[i].Date.After(allEntries[j].Date) }) totalEntries := len(allEntries) startIndex := (page - 1) * pageSize endIndex := startIndex + pageSize if startIndex >= totalEntries { http.NotFound(w, r) return } if endIndex > totalEntries { endIndex = totalEntries } pagedEntries := allEntries[startIndex:endIndex] var content strings.Builder for _, entry := range pagedEntries { htmlContent := blackfriday.Run([]byte(entry.Content)) content.WriteString(fmt.Sprintf("
%s
", htmlContent)) } var prevLink, nextLink string if startIndex > 0 { prevLink = fmt.Sprintf("/all?page=%d", page-1) } if endIndex < totalEntries { nextLink = fmt.Sprintf("/all?page=%d", page+1) } pageData := PageData{ Title: "All Blogs", Description: "Combined blog entries from all blogs.", BlogLinks: getBlogLinks(), Content: template.HTML(content.String()), PrevLink: prevLink, NextLink: nextLink, } renderTemplate(w, "news.html", pageData) } func getBlogs(dir string) ([]Blog, error) { var blogs []Blog files, err := ioutil.ReadDir(dir) if err != nil { return nil, err } for _, file := range files { if file.IsDir() { blog, err := getBlogEntries(filepath.Join(dir, file.Name())) if err != nil { return nil, err } blogs = append(blogs, blog) } } return blogs, nil } func getBlogLinks() []string { var links []string for _, blog := range blogs { links = append(links, blog.Name) } return links } func getBlogEntries(dir string) (Blog, error) { var entries []BlogEntry files, err := ioutil.ReadDir(dir) if err != nil { return Blog{}, err } for _, file := range files { if filepath.Ext(file.Name()) == ".md" { content, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) if err != nil { return Blog{}, err } date, err := getFileModTime(filepath.Join(dir, file.Name())) if err != nil { return Blog{}, err } number, err := strconv.Atoi(strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))) if err != nil { return Blog{}, err } entry := BlogEntry{ Content: string(content), Date: date, Number: number, } entries = append(entries, entry) sendDiscordNotification(fmt.Sprintf("New blog entry: %d", number)) } } sort.Slice(entries, func(i, j int) bool { return entries[i].Number > entries[j].Number }) blog := Blog{ Name: filepath.Base(dir), Entries: entries, } return blog, nil } func watchForChanges(dir string) { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatalf("Error creating file watcher: %v", err) } defer watcher.Close() go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Op&fsnotify.Create == fsnotify.Create { handleFileChange(event.Name, true) } else if event.Op&fsnotify.Write == fsnotify.Write { handleFileChange(event.Name, false) } case err, ok := <-watcher.Errors: if !ok { return } log.Printf("File watcher error: %v", err) } } }() err = watcher.Add(dir) if err != nil { log.Fatalf("Error adding directory to watcher: %v", err) } // Add subdirectories err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() && path != dir { return watcher.Add(path) } return nil }) if err != nil { log.Fatalf("Error adding subdirectories to watcher: %v", err) } // Block forever select {} } func handleFileChange(path string, isNew bool) { if filepath.Ext(path) == ".md" { creationTimesM.Lock() defer creationTimesM.Unlock() if isNew { creationTimes[path] = time.Now() } dir := filepath.Dir(path) blogName := filepath.Base(dir) updateBlogEntries(blogName, path) } } func updateBlogEntries(blogName, path string) { for i, blog := range blogs { if blog.Name == blogName { content, err := ioutil.ReadFile(path) if err != nil { log.Printf("Error reading file: %v", err) return } number, err := strconv.Atoi(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) if err != nil { log.Printf("Error extracting number from file: %v", err) return } entry := BlogEntry{ Content: string(content), Date: creationTimes[path], Number: number, } updated := false for j, e := range blogs[i].Entries { if e.Number == number { blogs[i].Entries[j] = entry updated = true break } } if !updated { blogs[i].Entries = append(blogs[i].Entries, entry) } sort.Slice(blogs[i].Entries, func(a, b int) bool { return blogs[i].Entries[a].Number > blogs[i].Entries[b].Number }) log.Printf("Updated blog %s with entry %d", blogName, number) return } } // If blog not found, create new one content, err := ioutil.ReadFile(path) if err != nil { log.Printf("Error reading file: %v", err) return } number, err := strconv.Atoi(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) if err != nil { log.Printf("Error extracting number from file: %v", err) return } entry := BlogEntry{ Content: string(content), Date: creationTimes[path], Number: number, } newBlog := Blog{ Name: blogName, Entries: []BlogEntry{entry}, } blogs = append(blogs, newBlog) log.Printf("Created new blog %s with entry %d", blogName, number) } func getFileModTime(path string) (time.Time, error) { info, err := os.Stat(path) if err != nil { return time.Time{}, err } return info.ModTime(), nil }