Init
This commit is contained in:
commit
310fcc6eb0
86 changed files with 10611 additions and 0 deletions
608
main.go
Normal file
608
main.go
Normal file
|
@ -0,0 +1,608 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"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"
|
||||
)
|
||||
|
||||
/*
|
||||
Spitfire Browser by Internet Addict (https://weforge.xyz/Spitfire/Website)
|
||||
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||
*/
|
||||
|
||||
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 the periodic notification checker
|
||||
go startNotificationChecker(10 * time.Minute)
|
||||
|
||||
// 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))))
|
||||
|
||||
// Custom handler to serve only directories and their contents under /news-assets/
|
||||
http.Handle("/news-assets/", http.StripPrefix("/news-assets/", http.HandlerFunc(serveDirectoriesOnly)))
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
// 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 startNotificationChecker(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
checkAndSendNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Helper function to add loading="lazy" to all img tags
|
||||
func injectLazyLoading(htmlContent string) string {
|
||||
// Split the content by <img tags to process them individually
|
||||
parts := strings.Split(htmlContent, "<img")
|
||||
|
||||
// Start with the first part which is before any <img tag
|
||||
modifiedContent := parts[0]
|
||||
|
||||
// Iterate over the remaining parts
|
||||
for _, part := range parts[1:] {
|
||||
// Find the closing bracket of the img tag
|
||||
endOfTag := strings.Index(part, ">")
|
||||
if endOfTag == -1 {
|
||||
// If no closing bracket is found, add the remaining part as is
|
||||
modifiedContent += "<img" + part
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the actual <img tag
|
||||
imgTag := part[:endOfTag]
|
||||
|
||||
// Check if loading="lazy" is already present
|
||||
if !strings.Contains(imgTag, "loading=") {
|
||||
// Insert loading="lazy" before the closing of the img tag
|
||||
imgTag = " loading=\"lazy\"" + imgTag
|
||||
}
|
||||
|
||||
// Rebuild the full content with the modified img tag
|
||||
modifiedContent += "<img" + imgTag + part[endOfTag:]
|
||||
}
|
||||
|
||||
return modifiedContent
|
||||
}
|
||||
|
||||
// For redering HTML Blogs
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Convert .md to HTML
|
||||
htmlContent := blackfriday.Run([]byte(entry.Content))
|
||||
|
||||
// Double check "/news-assets/" in URL of images
|
||||
htmlContent = bytes.ReplaceAll(htmlContent, []byte("src=\"./"), []byte(fmt.Sprintf("src=\"/news-assets/%d/", entryNumber)))
|
||||
|
||||
// Apply lazy loading to the generated HTML content
|
||||
htmlContentWithLazyLoading := injectLazyLoading(string(htmlContent))
|
||||
|
||||
pageData := PageData{
|
||||
Title: entry.Title,
|
||||
Date: entry.Date.Format("2006-01-02 15:04"),
|
||||
Desc: entry.Description,
|
||||
Author: entry.Author,
|
||||
Content: template.HTML(htmlContentWithLazyLoading),
|
||||
PrevLink: prevLink,
|
||||
NextLink: nextLink,
|
||||
}
|
||||
|
||||
renderTemplate(w, "news.html", pageData)
|
||||
}
|
||||
|
||||
func getBlogs(dir string) ([]Blog, error) {
|
||||
var blogs []Blog
|
||||
|
||||
files, err := os.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 := os.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 := os.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 {}
|
||||
}
|
||||
|
||||
// serveDirectoriesOnly handles requests and only serves directories and their contents.
|
||||
func serveDirectoriesOnly(w http.ResponseWriter, r *http.Request) {
|
||||
requestedPath := filepath.Join(dataDir, "news", r.URL.Path)
|
||||
|
||||
// Check if the requested path is a directory
|
||||
fileInfo, err := os.Stat(requestedPath)
|
||||
if err != nil {
|
||||
http.NotFound(w, r) // If the path doesn't exist, return a 404
|
||||
return
|
||||
}
|
||||
|
||||
// Block access to any files at the root level or the .git directory
|
||||
if isRootLevel(requestedPath) || isRestrictedDirectory(requestedPath) {
|
||||
http.NotFound(w, r) // Block access to root-level files like .md and the .git directory
|
||||
return
|
||||
}
|
||||
|
||||
// Block access to any .md files at the root level
|
||||
if isRootLevel(requestedPath) && strings.HasSuffix(requestedPath, ".md") {
|
||||
http.NotFound(w, r) // Block access to root-level .md files
|
||||
return
|
||||
}
|
||||
|
||||
// If the path is a directory, serve its contents
|
||||
if fileInfo.IsDir() {
|
||||
http.FileServer(http.Dir(requestedPath)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve the file within the subdirectory
|
||||
http.FileServer(http.Dir(filepath.Join(dataDir, "news"))).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// isRootLevel checks if the file is at the root level of /news-assets/
|
||||
func isRootLevel(path string) bool {
|
||||
relPath, err := filepath.Rel(filepath.Join(dataDir, "news"), path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// The relative path should not contain any slashes if it's at the root level
|
||||
return !strings.Contains(relPath, string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// isRestrictedDirectory checks if the path is within a restricted directory like .git
|
||||
func isRestrictedDirectory(path string) bool {
|
||||
// Normalize the path
|
||||
cleanPath := filepath.Clean(path)
|
||||
|
||||
// Check if the path contains .git directory or other restricted directories
|
||||
return strings.Contains(cleanPath, string(os.PathSeparator)+".git") || strings.HasSuffix(cleanPath, ".git")
|
||||
}
|
||||
|
||||
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)
|
||||
checkAndSendNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
log.Printf("Sent notifications for entry %d: %s", entry.Number, entry.Title)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue