Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
276 lines (255 sloc) 7.18 KB
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))
}
}
}