package main import ( "bufio" "bytes" "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" notifiedFilePath = "./notified_entries.json" defaultPort = 8080 pageSize = 5 // Number of blog entries per page botTokenEnv = "YOUR_TELEGRAM_BOT_TOKEN" // Replace with your bot's token or set via environment variable YOUR_TELEGRAM_CHAT_ID = 0 discordWebhookURL = "YOUR_DISCORD_WEBHOOK_URL" ) type Blog struct { Name string Entries []BlogEntry } type BlogEntry struct { Title string Description string Author string Content string Date time.Time Number int Notified bool // To track if the notification was sent } type PageData struct { Title string Date string Desc string Author 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 notifiedEntries = make(map[int]bool) ) 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() // Load the notified entries from the file loadNotifiedEntries() // 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)))) // Serve files in the /data/news/ directory under /news-assets/ http.Handle("/news-assets/", http.StripPrefix("/news-assets/", http.FileServer(http.Dir(dataDir+"/news/")))) // Serve downloads.html at /downloads http.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) { renderTemplate(w, "download.html", nil) }) // Serve download-linux.html at /download-linux http.HandleFunc("/download-linux", func(w http.ResponseWriter, r *http.Request) { renderTemplate(w, "download-linux.html", nil) }) http.HandleFunc("/suggestions", handleSuggestions) // Route for generating the RSS feed for all blogs http.HandleFunc("/rss", func(w http.ResponseWriter, r *http.Request) { siteURL := fmt.Sprintf("http://%s", r.Host) // or you can use a fixed base URL generateAtomFeed(w, blogs, siteURL) }) // Route for generating the RSS feed for a specific blog http.HandleFunc("/rss/", func(w http.ResponseWriter, r *http.Request) { pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) < 3 { http.NotFound(w, r) return } blogName := pathParts[2] for _, blog := range blogs { if blog.Name == blogName { siteURL := fmt.Sprintf("http://%s", r.Host) // or you can use a fixed base URL generateBlogAtomFeed(w, blog, siteURL) return } } http.NotFound(w, r) }) // Define route handlers http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { renderIndex(w) return } pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) < 2 { http.NotFound(w, r) return } if len(pathParts) == 3 { blogName := pathParts[1] entryNumber, err := strconv.Atoi(pathParts[2]) if err == nil { renderBlogEntry(w, r, 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, r *http.Request, 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 { // Check if the entry date is in the future if time.Now().Before(e.Date) { http.NotFound(w, r) // If the post date is in the future, do not show the entry return } entry = e // Check if the previous entry is visible if i > 0 && !time.Now().Before(blog.Entries[i-1].Date) { prevLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i-1].Number) } // Check if the next entry is visible if i < len(blog.Entries)-1 && !time.Now().Before(blog.Entries[i+1].Date) { nextLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i+1].Number) } break } } htmlContent := blackfriday.Run([]byte(entry.Content)) htmlContent = bytes.ReplaceAll(htmlContent, []byte("src=\"./"), []byte(fmt.Sprintf("src=\"/news-assets/%d/", entryNumber))) pageData := PageData{ Title: entry.Title, Date: entry.Date.Format("2006-01-02 15:04"), Desc: entry.Description, Author: entry.Author, Content: template.HTML(htmlContent), PrevLink: prevLink, NextLink: nextLink, } renderTemplate(w, "news.html", pageData) // Handle notifications if they haven't been sent yet if !entry.Notified { sendNotifications(entry) entry.Notified = true saveNotifiedEntry(entry.Number) } } 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 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" { entry, err := parseMarkdownFile(filepath.Join(dir, file.Name())) if err != nil { return Blog{}, err } entries = append(entries, entry) } } 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 parseMarkdownFile(path string) (BlogEntry, error) { content, err := ioutil.ReadFile(path) if err != nil { return BlogEntry{}, err } scanner := bufio.NewScanner(strings.NewReader(string(content))) var title, description, postTime, author string var articleContent strings.Builder var insideHeader bool for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "[HEADER]" { insideHeader = true continue } else if line == "[END]" { insideHeader = false continue } if insideHeader { if strings.HasPrefix(line, "t:") { title = strings.TrimSpace(line[2:]) } else if strings.HasPrefix(line, "d:") { description = strings.TrimSpace(line[2:]) } else if strings.HasPrefix(line, "p:") { postTime = strings.TrimSpace(line[2:]) } else if strings.HasPrefix(line, "a:") { author = strings.TrimSpace(line[2:]) } } else { articleContent.WriteString(line + "\n") } } date := time.Now() if postTime != "" { date, err = time.Parse("2006-01-02 15:04", postTime) if err != nil { log.Printf("Error parsing date in file %s: %v", path, err) } } number, err := strconv.Atoi(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) if err != nil { log.Printf("Error extracting number from file %s: %v", path, err) return BlogEntry{}, err } // Check if the entry has already been notified notified := notifiedEntries[number] entry := BlogEntry{ Title: title, Description: description, Author: author, Content: articleContent.String(), Date: date, Number: number, Notified: notified, } return entry, 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 { entry, err := parseMarkdownFile(path) if err != nil { log.Printf("Error parsing markdown file: %v", err) return } updated := false for j, e := range blogs[i].Entries { if e.Number == entry.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, entry.Number) return } } // If blog not found, create new one entry, err := parseMarkdownFile(path) if err != nil { log.Printf("Error parsing markdown file: %v", err) return } newBlog := Blog{ Name: blogName, Entries: []BlogEntry{entry}, } blogs = append(blogs, newBlog) log.Printf("Created new blog %s with entry %d", blogName, entry.Number) } func sendNotifications(entry BlogEntry) { message := fmt.Sprintf("New blog post published!\nTitle: %s\nDescription: %s\nAuthor: %s\nDate: %s", entry.Title, entry.Description, entry.Author, entry.Date.Format("2006-01-02 15:04")) // Send notification to Telegram sendTelegramNotification(message) // Send notification to Discord sendDiscordNotification(message) log.Printf("Sent notifications for entry %d: %s", entry.Number, entry.Title) }