diff --git a/.gitignore b/.gitignore index abcfaf4..4e1fbad 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -elements.html \ No newline at end of file +elements.html +notified_entries.json +data/ diff --git a/README.md b/README.md index b32387f..954737a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,26 @@ Spitfire Browser is a fast, secure, and elegant web browser built on Firefox. Th - [ ] Add working downloads -- [ ] Add blog/updates +- [X] Add blog/updates + +- [ ] Add config file + +### Blog entries should be fromated this way: + +```md +[HEADER] +t: TITLE +d: SHORT-DESC +p: 2024-08-16 15:04 +a: AUTHOR +[END] + +# lorem ipsum + + Vestibulum fermentum tortor id mi. Nullam at arcu a est sollicitudin euismod. Nullam faucibus mi quis velit. Mauris dictum facilisis augue. Nullam sapien sem, ornare ac, nonummy non, lobortis a enim. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Etiam commodo dui eget wisi. Mauris dictum facilisis augue. Etiam posuere lacus quis dolor. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. + +Vivamus luctus egestas leo. Phasellus faucibus molestie nisl. Etiam commodo dui eget wisi. Donec vitae arcu. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam neque. Suspendisse sagittis ultrices augue. Suspendisse nisl. Etiam sapien elit, consequat eget, tristique non, venenatis quis, ante. Phasellus rhoncus. Maecenas libero. +``` ### Based on: diff --git a/discord.go b/discord.go index 28daaf6..831a1ce 100644 --- a/discord.go +++ b/discord.go @@ -7,8 +7,6 @@ import ( "net/http" ) -const discordWebhookURL = "YOUR_DISCORD_WEBHOOK_URL" // Replace with your Discord webhook URL - type DiscordWebhookPayload struct { Content string `json:"content"` } diff --git a/main.go b/main.go index 0505633..be607fa 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "flag" "fmt" "html/template" @@ -21,12 +22,15 @@ import ( ) 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 + 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 { @@ -35,26 +39,32 @@ type Blog struct { } type BlogEntry struct { - Content string - Date time.Time - Number int + 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 - Description string - BlogLinks []string - Content template.HTML - PrevLink string - NextLink string + 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 + blogs []Blog + bot *tgbotapi.BotAPI + port int + creationTimes = make(map[string]time.Time) + creationTimesM sync.Mutex + notifiedEntries = make(map[int]bool) ) func init() { @@ -66,6 +76,9 @@ 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 == "" { @@ -100,43 +113,17 @@ func main() { 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) + renderBlogEntry(w, r, blogName, entryNumber) return } } @@ -178,7 +165,7 @@ func renderIndex(w http.ResponseWriter) { renderTemplate(w, "index.html", nil) } -func renderBlogEntry(w http.ResponseWriter, blogName string, entryNumber int) { +func renderBlogEntry(w http.ResponseWriter, r *http.Request, blogName string, entryNumber int) { var blog Blog for _, b := range blogs { if b.Name == blogName { @@ -191,11 +178,20 @@ func renderBlogEntry(w http.ResponseWriter, blogName string, entryNumber int) { 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 - if i > 0 { + + // 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) } - if i < len(blog.Entries)-1 { + + // 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 @@ -203,69 +199,25 @@ func renderBlogEntry(w http.ResponseWriter, blogName string, entryNumber int) { } 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, + 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) { @@ -289,14 +241,6 @@ func getBlogs(dir string) ([]Blog, error) { 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 @@ -307,28 +251,12 @@ func getBlogEntries(dir string) (Blog, error) { for _, file := range files { if filepath.Ext(file.Name()) == ".md" { - content, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) + entry, err := parseMarkdownFile(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)) } } @@ -344,6 +272,74 @@ func getBlogEntries(dir string) (Blog, error) { 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 { @@ -412,27 +408,15 @@ func handleFileChange(path string, isNew bool) { func updateBlogEntries(blogName, path string) { for i, blog := range blogs { if blog.Name == blogName { - content, err := ioutil.ReadFile(path) + entry, err := parseMarkdownFile(path) if err != nil { - log.Printf("Error reading file: %v", err) + log.Printf("Error parsing markdown 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 { + if e.Number == entry.Number { blogs[i].Entries[j] = entry updated = true break @@ -447,37 +431,25 @@ func updateBlogEntries(blogName, path string) { return blogs[i].Entries[a].Number > blogs[i].Entries[b].Number }) - log.Printf("Updated blog %s with entry %d", blogName, number) + log.Printf("Updated blog %s with entry %d", blogName, entry.Number) return } } // If blog not found, create new one - content, err := ioutil.ReadFile(path) + entry, err := parseMarkdownFile(path) if err != nil { - log.Printf("Error reading file: %v", err) + log.Printf("Error parsing markdown 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) + log.Printf("Created new blog %s with entry %d", blogName, entry.Number) } func getFileModTime(path string) (time.Time, error) { @@ -487,3 +459,16 @@ func getFileModTime(path string) (time.Time, error) { } return info.ModTime(), nil } + +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) +} diff --git a/run.sh b/run.sh index d26f4e9..42630ba 100755 --- a/run.sh +++ b/run.sh @@ -13,4 +13,4 @@ while [[ "$#" -gt 0 ]]; do done # Run the Go application with the parsed flags -go run discord.go rss.go telegram.go main.go -p=$PORT +go run discord.go rss.go telegram.go save.go main.go -p=$PORT diff --git a/save.go b/save.go new file mode 100644 index 0000000..02533cc --- /dev/null +++ b/save.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" +) + +func loadNotifiedEntries() { + data, err := ioutil.ReadFile(notifiedFilePath) + if err != nil { + if os.IsNotExist(err) { + return // No file, no entries notified yet + } + log.Fatalf("Error reading notified entries file: %v", err) + } + + err = json.Unmarshal(data, ¬ifiedEntries) + if err != nil { + log.Fatalf("Error parsing notified entries file: %v", err) + } +} + +func saveNotifiedEntry(entryNumber int) { + notifiedEntries[entryNumber] = true + data, err := json.Marshal(notifiedEntries) + if err != nil { + log.Fatalf("Error serializing notified entries: %v", err) + } + + err = ioutil.WriteFile(notifiedFilePath, data, 0644) + if err != nil { + log.Fatalf("Error writing notified entries file: %v", err) + } +} diff --git a/static/css/fancy-gallery.css b/static/css/fancy-gallery.css index b325fea..c309ea9 100644 --- a/static/css/fancy-gallery.css +++ b/static/css/fancy-gallery.css @@ -47,7 +47,7 @@ .gallery-item:hover::before { opacity: 1; } - +/* @keyframes pulse { 0%, 100% { transform: scale(1); @@ -61,4 +61,4 @@ .gallery-item { animation: pulse 5s infinite; -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/telegram.go b/telegram.go index efc036c..9bd0026 100644 --- a/telegram.go +++ b/telegram.go @@ -24,3 +24,19 @@ func startTelegramBot() { } } } + +func sendTelegramNotification(message string) { + if bot == nil { + log.Println("Telegram bot is not initialized") + return + } + + // Replace with the actual chat ID you want to send messages to + chatID := int64(YOUR_TELEGRAM_CHAT_ID) + + msg := tgbotapi.NewMessage(chatID, message) + _, err := bot.Send(msg) + if err != nil { + log.Printf("Error sending Telegram notification: %v", err) + } +} diff --git a/templates/news.html b/templates/news.html index 98eeaef..1a6d99c 100644 --- a/templates/news.html +++ b/templates/news.html @@ -55,6 +55,8 @@

{{.Title}}

+

{{.Author}}

+

{{.Date}}

{{.Content}}