Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| package main | |
| import ( | |
| "path" | |
| _ "github.com/mattn/go-sqlite3" | |
| "strk.kbt.io/projects/go/libravatar" | |
| "bufio" | |
| "database/sql" | |
| "fmt" | |
| "html/template" | |
| "log" | |
| "net/http" | |
| "net/http/cgi" | |
| "net/http/fcgi" | |
| "net/url" | |
| "os" | |
| "os/signal" | |
| "strings" | |
| "syscall" | |
| "time" | |
| ) | |
| const ( | |
| ITEMS = 24 | |
| UITEMS = 6 | |
| DBNAME = "llist.sqlite" | |
| TMPLFILE = "llist.gtml" | |
| CR_LINKS = `CREATE TABLE IF NOT EXISTS links ( | |
| id INTEGER PRIMARY KEY, | |
| title TEXT, url TEXT, | |
| name TEXT, posted DATETIME)` | |
| CR_COMM = `CREATE TABLE IF NOT EXISTS comments ( | |
| id INTEGER PRIMARY KEY, | |
| link INTEGER NOT NULL, reply INTEGER, | |
| name TEXT, content TEXT, posted DATETIME, | |
| FOREIGN KEY (link) REFERENCES links (id) ON DELETE CASCADE, | |
| FOREIGN KEY (reply) REFERENCES comments (id) ON DELETE CASCADE)` | |
| CR_TAGS = `CREATE TABLE IF NOT EXISTS tags ( | |
| id INTEGER PRIMARY KEY, | |
| name TEXT, link INTEGER, | |
| FOREIGN KEY (link) REFERENCES links (id) ON DELETE CASCADE)` | |
| SE_LINK = `SELECT id, title, url, posted, name, | |
| (SELECT count(1) FROM comments WHERE comments.link = links.id) | |
| FROM links WHERE links.id = ?` | |
| SE_LINKS = `SELECT id, title, url, posted, name, | |
| (SELECT count(1) FROM comments WHERE comments.link = links.id) | |
| FROM links | |
| ORDER BY posted DESC LIMIT ? OFFSET ?` | |
| SE_COMM = `SELECT id, posted, name, content | |
| FROM comments | |
| WHERE link = ? AND reply = ? | |
| ORDER BY posted` | |
| SE_BY_TAG = `SELECT links.id, title, url, posted, links.name, | |
| (SELECT count(1) FROM comments WHERE comments.link = links.id) | |
| FROM links | |
| LEFT JOIN tags ON tags.link = links.id | |
| WHERE tags.name = ? | |
| ORDER BY posted DESC LIMIT ? OFFSET ?` | |
| SE_LINK_BY_USER = `SELECT id, title, url, posted, | |
| (SELECT count(1) FROM comments WHERE comments.link = links.id) | |
| FROM links | |
| WHERE name = ? | |
| ORDER BY posted DESC LIMIT ? OFFSET ?` | |
| SE_COMM_BY_USER = `SELECT id, link, content, posted | |
| FROM comments | |
| WHERE name = ? | |
| ORDER BY posted DESC LIMIT ? OFFSET ?` | |
| SE_TAG_BY_OCC = `SELECT a.name FROM tags a | |
| INNER JOIN tags b ON (a.name = b.name) | |
| GROUP BY a.name | |
| ORDER BY count(1) DESC | |
| LIMIT 10;` | |
| SE_TAGS = `SELECT name FROM tags WHERE link = ?` | |
| SE_TITLE = `SELECT title FROM links WHERE id = ?` | |
| IN_LINK = `INSERT INTO links (title, url, posted, name) | |
| values (?, ?, datetime("now"), ?)` | |
| IN_COMM = `INSERT INTO comments (link, reply, posted, name, content) | |
| values (?, ?, datetime("now"), ?, ?)` | |
| IN_TAG = `INSERT INTO tags (name, link) values (?, ?)` | |
| DE_LINK = `DELETE FROM links WHERE id = ?` | |
| DE_COMM = `DELETE FROM comments WHERE id = ?` | |
| ) | |
| var ( | |
| tmpl *template.Template | |
| db *sql.DB | |
| users map[string]User | |
| titles map[int]string | |
| ) | |
| type User struct { | |
| passwd string | |
| Email string | |
| Descr string | |
| } | |
| type Comment struct { | |
| Id int | |
| Posted time.Time | |
| Name string | |
| Text string | |
| Replies []Comment | |
| Response int | |
| } | |
| type Link struct { | |
| Id int | |
| Posted time.Time | |
| Name string | |
| Title string | |
| Url string | |
| Comments []Comment | |
| CCount int | |
| Tags []string | |
| } | |
| func init() { | |
| var err error | |
| if db, err = sql.Open("sqlite3", DBNAME); err != nil { | |
| log.Fatal(err) | |
| } | |
| if _, err = db.Exec(CR_LINKS); err != nil { | |
| log.Fatal(err) | |
| } | |
| if _, err = db.Exec(CR_TAGS); err != nil { | |
| log.Fatal(err) | |
| } | |
| if _, err = db.Exec(CR_COMM); err != nil { | |
| log.Fatal(err) | |
| } | |
| var dir = "gtml" | |
| if os.Getenv("GTML") != "" { | |
| dir = os.Getenv("GTML") | |
| } | |
| tmpl = template.Must(template.New("").Funcs(template.FuncMap{ | |
| "inc": func(i int) int { | |
| return i + 1 | |
| }, "dec": func(i int) int { | |
| return i - 1 | |
| }, "domain": func(rawurl string) string { | |
| url, err := url.Parse(rawurl) | |
| if err != nil { | |
| return "" | |
| } | |
| return url.Host | |
| }, "calcStart": func(page int) int { | |
| return (page-1)*ITEMS + 1 | |
| }, "isType": func(tag string) bool { | |
| switch tag { | |
| case "video", "pdf", "audio", "podcast", "slides": | |
| return true | |
| } | |
| return false | |
| }, "libr": func(author string) string { | |
| r, _ := libravatar.FromEmail(author) | |
| return r | |
| }, "since": func(p time.Time) string { | |
| d := time.Since(p) | |
| switch { | |
| case d.Hours() > 24*365: | |
| return fmt.Sprintf("%d years ago", | |
| int(d.Hours()/(24*365))) | |
| case d.Hours() > 24: | |
| return fmt.Sprintf("%d days ago", | |
| int(d.Hours()/24)) | |
| case d.Hours() > 1: | |
| return fmt.Sprintf("%d hours ago", | |
| int(d.Hours())) | |
| case d.Minutes() > 1: | |
| return fmt.Sprintf("%d minutes ago", | |
| int(d.Minutes())) | |
| default: // if posted less than 1m ago | |
| return "just now" | |
| } | |
| }, "cGetTitle": func(id int) string { | |
| title, err := queryTitle(id) | |
| if err != nil { | |
| return "N/A" | |
| } | |
| return title | |
| }, | |
| }).ParseGlob(dir + "/*.gtml")) | |
| } | |
| func forceLoadUsers() { | |
| users = make(map[string]User) | |
| var userf string | |
| if userf = os.Getenv("USERF"); userf == "" { | |
| userf = "/etc/llist_users" | |
| } | |
| unf, err := os.Open(userf) | |
| defer unf.Close() | |
| if err != nil && !os.IsNotExist(err) { | |
| log.Fatal(err) | |
| } | |
| sca := bufio.NewScanner(unf) | |
| if sca.Err() != nil { | |
| log.Fatal(sca.Err()) | |
| } | |
| lc := 1 // line counter | |
| for sca.Scan() { // name:email:passwd | |
| line := sca.Text() | |
| if len(strings.TrimSpace(line)) == 0 { | |
| lc++ | |
| continue // ignore empty lines | |
| } | |
| parts := strings.SplitN(line, ":", 4) | |
| if len(parts) < 4 { | |
| log.Printf("Malformed line %d to be ignored: %s\n", lc, line) | |
| continue | |
| } | |
| users[parts[0]] = User{ | |
| passwd: parts[1], | |
| Email: parts[2], | |
| Descr: parts[3], | |
| } | |
| lc++ | |
| } | |
| } | |
| func loadUsers() { | |
| if len(users) == 0 { // find better/more efficient solution | |
| forceLoadUsers() | |
| } | |
| } | |
| func main() { | |
| defer db.Close() | |
| // reaload users when SIGUSER1 is caught | |
| c := make(chan os.Signal) | |
| signal.Notify(c, syscall.SIGUSR1) | |
| go func() { | |
| for { | |
| _ = <-c | |
| forceLoadUsers() | |
| } | |
| }() | |
| http.HandleFunc("/subm", submit) | |
| http.HandleFunc("/user", genuser) | |
| http.HandleFunc("/post", genpost) | |
| http.HandleFunc("/delete", delete) | |
| http.HandleFunc("/rss", genrss) | |
| http.HandleFunc("/", genlist) | |
| mux := http.NewServeMux() | |
| mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { | |
| rw.Header().Set("Content-Type", "text/html") | |
| path := path.Dir(req.URL.Path) | |
| if path == "" || path == "/" { | |
| genlist(rw, req) | |
| return | |
| } | |
| http.StripPrefix(path, http.DefaultServeMux).ServeHTTP(rw, req) | |
| }) | |
| if strings.HasSuffix(os.Args[0], ".cgi") { | |
| log.Println(cgi.Serve(mux)) | |
| } else if strings.HasSuffix(os.Args[0], ".fcgi") { | |
| log.Println(fcgi.Serve(nil, mux)) | |
| } else { | |
| if port := os.Getenv("PORT"); port == "" { | |
| log.Fatal("$PORT not defined") | |
| } else { | |
| log.Fatal(http.ListenAndServe(":"+port, nil)) | |
| } | |
| } | |
| } |