package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html"
"html/template"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/blevesearch/bleve"
"p83.nl/go/ekster/pkg/util"
"p83.nl/go/indieauth"
)
func init() {
log.SetFlags(log.Lshortfile)
}
var (
mp PagesRepository
port = flag.Int("port", 8080, "listen port")
baseurl = flag.String("baseurl", "", "baseurl")
redirectURI string = ""
)
type Backref struct {
Name string
Title string
LineHTML template.HTML
Line string
}
// Page
type Page struct {
// Name is the filename of the page
Name string
// Title is the human-readable title of the page
Title string
Content string
Refs map[string][]Backref
}
type DiffPage struct {
Content string
Diff template.HTML
}
type Revision struct {
Version string
Page DiffPage
Summary string
}
type Change struct {
Page string
Date time.Time
EndDate time.Time
Body string
Count int
DateSummary string
TimeSummary string
}
type PagesRepository interface {
Get(p string) Page
Save(p string, page Page, summary, author string) error
Exist(p string) bool
PageHistory(p string) ([]Revision, error)
RecentChanges() ([]Change, error)
AllPages() ([]Page, error)
}
type pageBaseInfo struct {
BaseURL string
RedirectURI string
}
type indexPage struct {
pageBaseInfo
Session *Session
Title string
Name string
Content template.HTML
Backrefs map[string][]Backref
}
type Node struct {
Id int `json:"id"`
Label string `json:"label"`
}
type Edge struct {
From int `json:"from"`
To int `json:"to"`
}
type graphPage struct {
pageBaseInfo
Session *Session
Title string
Name string
References Refs
Nodes template.JS
Edges template.JS
}
type editPage struct {
pageBaseInfo
Session *Session
Title string
Content string
Name string
Editor template.HTML
Backrefs map[string][]Backref
}
type historyPage struct {
pageBaseInfo
Session *Session
Title string
Name string
History []Revision
}
type recentPage struct {
pageBaseInfo
Session *Session
Title string
Name string
Recent []Change
}
type indexHandler struct{}
type graphHandler struct{}
type saveHandler struct {
SearchIndex bleve.Index
}
type editHandler struct{}
type historyHandler struct{}
type recentHandler struct{}
type authHandler struct{}
func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if r.Method == http.MethodGet {
if r.URL.Path == "/auth/login" {
t, err := template.ParseFiles("templates/layout.html", "templates/login.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
} else if r.URL.Path == "/auth/callback" {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if state != sess.State {
log.Printf("mismatched state: %s != %s", state, sess.State)
http.Error(w, "mismatched state", 500)
return
}
authURL := sess.AuthorizationEndpoint
verified, response, err := indieauth.VerifyAuthCode(*baseurl, code, redirectURI, authURL)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if !verified {
http.Redirect(w, r, "/auth/login", 302)
return
}
sess.Me = response.Me
sess.LoggedIn = true
sess.State = ""
http.Redirect(w, r, sess.NextURI, 302)
return
} else if r.URL.Path == "/auth/logout" {
t, err := template.ParseFiles("templates/layout.html", "templates/logout.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
} else if r.Method == http.MethodPost {
if r.URL.Path == "/auth/login" {
r.ParseForm()
urlString := r.PostFormValue("url")
u, err := url.Parse(urlString)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
endpoints, err := indieauth.GetEndpoints(u)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
authURL, err := url.Parse(endpoints.AuthorizationEndpoint)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
state := util.RandStringBytes(32)
sess.AuthorizationEndpoint = authURL.String()
sess.Me = urlString
sess.LoggedIn = false
sess.RedirectURI = redirectURI
sess.NextURI = "/"
sess.State = state
newURL := indieauth.CreateAuthenticationURL(*authURL, urlString, *baseurl, redirectURI, state)
http.Redirect(w, r, newURL, 302)
return
} else if r.URL.Path == "/auth/logout" {
sess.LoggedIn = false
sess.Me = ""
sess.AuthorizationEndpoint = ""
http.Redirect(w, r, "/", 302)
return
}
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *historyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
r.ParseForm()
page := r.URL.Path[9:]
if page == "" {
page = "Home"
}
history, err := mp.PageHistory(page)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
funcs := template.FuncMap{
"historyIndex": func(x int, history []Revision) int { return len(history) - x },
}
t, err := template.New("layout.html").Funcs(funcs).ParseFiles("templates/layout.html", "templates/history.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
pageBase := getPageBase()
pageData := historyPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "History of " + cleanTitle(page),
Name: page,
History: history,
}
err = t.Execute(w, pageData)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func getPageBase() pageBaseInfo {
clientID := *baseurl
pageBase := pageBaseInfo{
BaseURL: clientID,
RedirectURI: redirectURI,
}
return pageBase
}
func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
log.Println(err)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
err = r.ParseForm()
if err != nil {
http.Error(w, err.Error(), 500)
log.Println(err)
return
}
isJson := r.PostForm.Get("json") == "1"
page := r.PostForm.Get("p")
summary := r.PostForm.Get("summary")
pageData := Page{Content: r.PostForm.Get("content")}
err = mp.Save(page, pageData, summary, sess.Me)
if err != nil {
log.Println(err)
}
if isJson {
fmt.Print(w, "{}")
} else {
http.Redirect(w, r, "/"+page, http.StatusFound)
}
}
func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
page := r.URL.Path[6:]
if page == "" {
page = "Home"
}
mpPage := mp.Get(page)
pageText := mpPage.Content
if pageText == "" {
pageText = "[]"
}
var rawMsg json.RawMessage
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg)
jsonEditor := pageText != "" && err == nil
var editor template.HTML
if jsonEditor {
editor, err = renderEditor(page, pageText, "json", *baseurl)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
} else {
editor = template.HTML(fmt.Sprintf(`
`, html.EscapeString(pageText)))
}
pageBase := getPageBase()
data := editPage{
pageBaseInfo: pageBase,
Session: sess,
Title: cleanTitle(page),
Content: pageText,
Editor: editor,
Name: page,
Backrefs: mpPage.Refs,
}
t, err := template.ParseFiles("templates/layout.html", "templates/edit.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
refs := make(Refs)
f, err := os.Open(filepath.Join(mp.(*FilePages).dirname, "backrefs.json"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer f.Close()
err = json.NewDecoder(f).Decode(&refs)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var nodesBuf bytes.Buffer
var edgesBuf bytes.Buffer
var nodes []Node
var edges []Edge
nodeCount := 1
nodeMap := make(map[string]int)
for key, references := range refs {
if strings.HasSuffix(key, "_2020") {
continue
}
if _, e := nodeMap[key]; !e {
nodeMap[key] = nodeCount
nodeCount += 1
}
for _, item := range references {
if strings.HasSuffix(item.Name, "_2020") {
continue
}
if _, e := nodeMap[item.Name]; !e {
nodeMap[item.Name] = nodeCount
nodeCount += 1
}
}
}
delete(nodeMap, "DONE")
delete(nodeMap, "TODO")
delete(nodeMap, "Home")
delete(nodeMap, "Projects")
delete(nodeMap, "Notes")
delete(nodeMap, "Books")
delete(nodeMap, "Daily_Notes")
delete(nodeMap, "NewJsonTest")
for name, id := range nodeMap {
nodes = append(nodes, Node{
Id: id,
Label: name,
})
}
edgeSet := make(map[Edge]bool)
for key, references := range refs {
if key == "DONE" {
continue
}
if strings.HasSuffix(key, "_2020") {
continue
}
if toID, e := nodeMap[key]; e {
for _, item := range references {
if strings.HasSuffix(item.Name, "_2020") {
continue
}
if fromID, e := nodeMap[item.Name]; e {
if fromID == toID {
continue
}
edge := Edge{
From: fromID,
To: toID,
}
if _, e := edgeSet[edge]; !e {
edgeSet[edge] = true
edges = append(edges, edge)
}
}
}
}
}
err = json.NewEncoder(&nodesBuf).Encode(&nodes)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = json.NewEncoder(&edgesBuf).Encode(&edges)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
pageBase := getPageBase()
data := graphPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "Graph",
Name: "Graph",
References: refs,
Nodes: template.JS(nodesBuf.String()),
Edges: template.JS(edgesBuf.String()),
}
t, err := template.ParseFiles("templates/layout.html", "templates/graph.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
format := r.URL.Query().Get("format")
if format == "" {
format = "html"
}
if !(format == "html" || format == "markdown") {
http.Error(w, "unknown format", http.StatusBadRequest)
}
page := r.URL.Path[1:]
if page == "" {
page = "Home"
}
mpPage := mp.Get(page)
pageText := mpPage.Content
if pageText == "" {
http.Redirect(w, r, "/edit/"+page, 302)
return
}
if pageText == "" {
pageText = "[]"
}
var rawMsg json.RawMessage
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&rawMsg)
jsonPage := pageText != "" && err == nil
if jsonPage {
var listItems []struct {
Indented int
Text string
}
err = json.NewDecoder(strings.NewReader(pageText)).Decode(&listItems)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
builder := strings.Builder{}
for _, item := range listItems {
lines := strings.Split(item.Text, "\n")
if len(lines) > 1 {
first := true
for _, line := range lines {
if first {
builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ")
builder.WriteString(line)
builder.WriteByte('\n')
first = false
} else {
builder.WriteString(strings.Repeat(" ", item.Indented+1))
builder.WriteString(line)
builder.WriteByte('\n')
}
}
} else {
builder.WriteString(strings.Repeat(" ", item.Indented))
builder.WriteString("* ")
builder.WriteString(item.Text)
builder.WriteByte('\n')
}
}
pageText = builder.String()
}
if format == "html" {
metaKV, err := regexp.Compile(`(\w+)::\s+(.*)`)
if err != nil {
log.Fatal(err)
}
pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2")
pageText = renderLinks(pageText)
pageText = renderMarkdown2(pageText)
pageBase := getPageBase()
data := indexPage{
pageBaseInfo: pageBase,
Session: sess,
Title: cleanTitle(page),
Content: template.HTML(pageText),
Name: page,
Backrefs: mpPage.Refs,
}
t, err := template.ParseFiles("templates/layout.html", "templates/view.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
log.Println(err)
// http.Error(w, err.Error(), 500)
return
}
} else if format == "markdown" {
_, err = io.WriteString(w, "# ")
_, err = io.WriteString(w, cleanTitle(page))
_, err = io.WriteString(w, "\n\n")
_, err = io.WriteString(w, pageText)
}
}
func renderLinks(pageText string) string {
hrefRE, err := regexp.Compile(`#?\[\[\s*([^\]]+)\s*\]\]`)
if err != nil {
log.Fatal(err)
}
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
tag := false
if s[0] == '#' {
s = strings.TrimPrefix(s, "#[[")
tag = true
} else {
s = strings.TrimPrefix(s, "[[")
}
s = strings.TrimSuffix(s, "]]")
s = strings.TrimSpace(s)
if tag {
return fmt.Sprintf(`%s`, cleanNameURL(s), s)
}
return fmt.Sprintf("[%s](/%s)", s, cleanNameURL(s))
})
return pageText
}
func (h *recentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
if !sess.LoggedIn {
http.Redirect(w, r, "/", http.StatusFound)
return
}
r.ParseForm()
changes, err := mp.RecentChanges()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// group recentchanges on Page
if len(changes) > 0 {
f := groupRecentChanges(changes)
changes = changes[f:]
}
for i, c := range changes {
if c.Count <= 1 {
changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006")
changes[i].TimeSummary = c.Date.Format("15:04")
} else {
changes[i].DateSummary = c.Date.Format("Mon, 2 Jan 2006")
changes[i].TimeSummary = fmt.Sprintf("%s - %s", c.Date.Format("15:04"), c.EndDate.Format("15:04"))
}
}
t, err := template.New("layout.html").ParseFiles("templates/layout.html", "templates/recent.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
pageBase := getPageBase()
err = t.Execute(w, recentPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "Recent changes",
Name: "Recent changes",
Recent: changes,
})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
type LinkResponseMetaImage struct {
Url string `json:"url"`
}
type LinkResponseMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image LinkResponseMetaImage `json:"image"`
}
type LinkResponse struct {
Success int `json:"success"`
Link string `json:"link"`
Meta LinkResponseMeta `json:"meta"`
}
func main() {
flag.Parse()
*baseurl = strings.TrimRight(*baseurl, "/") + "/"
redirectURI = fmt.Sprintf("%sauth/callback", *baseurl)
dataDir := "data"
searchIndex, err := createSearchIndex(dataDir, "_page-index")
if err != nil {
log.Fatal(err)
}
defer searchIndex.Close()
sh, err := NewSearchHandler(searchIndex)
if err != nil {
log.Fatal(err)
}
mp = NewFilePages(dataDir, searchIndex)
http.Handle("/auth/", &authHandler{})
http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
http.ServeFile(w, r, filepath.Join(dataDir, LinksFile))
})
http.Handle("/search/", sh)
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist"))))
http.Handle("/save/", &saveHandler{})
http.Handle("/edit/", &editHandler{})
http.Handle("/history/", &historyHandler{})
http.Handle("/recent/", &recentHandler{})
http.Handle("/graph/", &graphHandler{})
http.Handle("/", &indexHandler{})
fmt.Printf("Running on port %d\n", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
indexDir := filepath.Join(dataDir, indexName)
if _, err := os.Stat(indexDir); os.IsNotExist(err) {
indexMapping := bleve.NewIndexMapping()
searchIndex, err := bleve.New(indexDir, indexMapping)
if err != nil {
return nil, err
}
fp := NewFilePages(dataDir, nil)
pages, err := fp.AllPages()
if err != nil {
return nil, err
}
for _, page := range pages {
so, err := createSearchObject(page)
if err != nil {
log.Println(err)
continue
}
err = searchIndex.Index(page.Name, so)
if err != nil {
return nil, err
}
}
return searchIndex, nil
} else {
searchIndex, err := bleve.Open(indexDir)
if err != nil {
return nil, err
}
return searchIndex, nil
}
}