Website/main.go

511 lines
12 KiB
Go
Raw Permalink Normal View History

2024-01-09 14:08:56 +00:00
package main
import (
2024-08-16 12:46:31 +00:00
"bufio"
2024-08-24 17:07:36 +00:00
"bytes"
2024-08-14 10:43:07 +00:00
"flag"
2024-01-09 14:08:56 +00:00
"fmt"
2024-08-14 10:43:07 +00:00
"html/template"
"io/ioutil"
"log"
2024-01-09 14:08:56 +00:00
"net/http"
2024-08-14 10:43:07 +00:00
"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 (
2024-08-16 12:46:31 +00:00
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"
2024-08-14 10:43:07 +00:00
)
type Blog struct {
Name string
Entries []BlogEntry
}
type BlogEntry struct {
2024-08-16 12:46:31 +00:00
Title string
Description string
Author string
Content string
Date time.Time
Number int
Notified bool // To track if the notification was sent
2024-08-14 10:43:07 +00:00
}
type PageData struct {
2024-08-16 12:46:31 +00:00
Title string
Date string
Desc string
Author string
Content template.HTML
PrevLink string
NextLink string
2024-08-14 10:43:07 +00:00
}
var (
2024-08-16 12:46:31 +00:00
blogs []Blog
bot *tgbotapi.BotAPI
port int
creationTimes = make(map[string]time.Time)
creationTimesM sync.Mutex
notifiedEntries = make(map[int]bool)
2024-01-09 14:08:56 +00:00
)
2024-08-14 10:43:07 +00:00
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")
}
2024-01-09 14:08:56 +00:00
func main() {
2024-08-14 12:18:58 +00:00
// Parse the flags
2024-08-14 10:43:07 +00:00
flag.Parse()
2024-08-16 12:46:31 +00:00
// Load the notified entries from the file
loadNotifiedEntries()
2024-08-14 12:18:58 +00:00
// Retrieve the Telegram bot token from the environment variable
2024-08-14 10:43:07 +00:00
botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
if botToken == "" {
botToken = botTokenEnv
}
2024-08-14 12:18:58 +00:00
// Initialize the Telegram bot
2024-08-14 10:43:07 +00:00
var err error
bot, err = tgbotapi.NewBotAPI(botToken)
if err != nil {
log.Printf("Warning: Error creating Telegram bot: %v", err)
} else {
go startTelegramBot()
}
2024-08-14 12:18:58 +00:00
// Retrieve blog entries from the data directory
2024-08-14 10:43:07 +00:00
blogs, err = getBlogs(dataDir)
if err != nil {
log.Fatalf("Error getting blogs: %v", err)
}
2024-08-14 12:18:58 +00:00
// Start watching for changes in the data directory
2024-08-14 10:43:07 +00:00
go watchForChanges(dataDir)
2024-08-14 12:18:58 +00:00
// Serve static files (CSS, JS, etc.) from the /static directory
2024-08-14 10:43:07 +00:00
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
2024-08-24 17:07:36 +00:00
// Serve files in the /data/news/ directory under /news-assets/
http.Handle("/news-assets/", http.StripPrefix("/news-assets/", http.FileServer(http.Dir(dataDir+"/news/"))))
2024-08-22 17:10:14 +00:00
// 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)
})
2024-08-25 19:28:18 +00:00
http.HandleFunc("/suggestions", handleSuggestions)
2024-08-22 20:10:47 +00:00
// 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)
})
2024-08-14 12:18:58 +00:00
// Define route handlers
2024-08-14 10:43:07 +00:00
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
2024-08-14 10:55:34 +00:00
renderIndex(w)
2024-08-14 10:43:07 +00:00
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 {
2024-08-16 12:46:31 +00:00
renderBlogEntry(w, r, blogName, entryNumber)
2024-08-14 10:43:07 +00:00
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)
})
2024-08-14 12:18:58 +00:00
// Start the HTTP server on the specified port
2024-08-14 10:43:07 +00:00
serverURL := fmt.Sprintf("http://localhost:%d", port)
log.Printf("Starting server on %s", serverURL)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
2024-08-14 11:55:29 +00:00
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)
}
2024-08-16 12:46:31 +00:00
func renderBlogEntry(w http.ResponseWriter, r *http.Request, blogName string, entryNumber int) {
2024-08-14 11:55:29 +00:00
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 {
2024-08-16 12:46:31 +00:00
// 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
}
2024-08-14 11:55:29 +00:00
entry = e
2024-08-16 12:46:31 +00:00
// Check if the previous entry is visible
if i > 0 && !time.Now().Before(blog.Entries[i-1].Date) {
2024-08-14 11:55:29 +00:00
prevLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i-1].Number)
}
2024-08-16 12:46:31 +00:00
// Check if the next entry is visible
if i < len(blog.Entries)-1 && !time.Now().Before(blog.Entries[i+1].Date) {
2024-08-14 11:55:29 +00:00
nextLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i+1].Number)
}
break
}
}
htmlContent := blackfriday.Run([]byte(entry.Content))
2024-08-24 17:07:36 +00:00
htmlContent = bytes.ReplaceAll(htmlContent, []byte("src=\"./"), []byte(fmt.Sprintf("src=\"/news-assets/%d/", entryNumber)))
2024-08-14 11:55:29 +00:00
pageData := PageData{
2024-08-16 12:46:31 +00:00
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,
2024-08-14 11:55:29 +00:00
}
renderTemplate(w, "news.html", pageData)
2024-08-16 12:46:31 +00:00
// Handle notifications if they haven't been sent yet
if !entry.Notified {
sendNotifications(entry)
entry.Notified = true
saveNotifiedEntry(entry.Number)
2024-08-14 11:55:29 +00:00
}
}
2024-08-14 10:43:07 +00:00
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" {
2024-08-16 12:46:31 +00:00
entry, err := parseMarkdownFile(filepath.Join(dir, file.Name()))
2024-08-14 10:43:07 +00:00
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
}
2024-08-16 12:46:31 +00:00
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
}
2024-08-14 10:43:07 +00:00
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 {
2024-08-16 12:46:31 +00:00
entry, err := parseMarkdownFile(path)
2024-08-14 10:43:07 +00:00
if err != nil {
2024-08-16 12:46:31 +00:00
log.Printf("Error parsing markdown file: %v", err)
2024-08-14 10:43:07 +00:00
return
}
updated := false
for j, e := range blogs[i].Entries {
2024-08-16 12:46:31 +00:00
if e.Number == entry.Number {
2024-08-14 10:43:07 +00:00
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
})
2024-08-16 12:46:31 +00:00
log.Printf("Updated blog %s with entry %d", blogName, entry.Number)
2024-08-14 10:43:07 +00:00
return
}
}
// If blog not found, create new one
2024-08-16 12:46:31 +00:00
entry, err := parseMarkdownFile(path)
2024-08-14 10:43:07 +00:00
if err != nil {
2024-08-16 12:46:31 +00:00
log.Printf("Error parsing markdown file: %v", err)
2024-08-14 10:43:07 +00:00
return
}
newBlog := Blog{
Name: blogName,
Entries: []BlogEntry{entry},
}
blogs = append(blogs, newBlog)
2024-08-16 12:46:31 +00:00
log.Printf("Created new blog %s with entry %d", blogName, entry.Number)
2024-08-14 10:43:07 +00:00
}
2024-08-16 12:46:31 +00:00
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)
}