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 LineEditHTML 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 ShowGraph bool TodayPage string } type Node struct { Id int `json:"id"` Label string `json:"label"` Color *string `json:"color"` Opacity float64 `json:"opacity"` } type Edge struct { From int `json:"from"` To int `json:"to"` } type graphPage struct { pageBaseInfo Session *Session Title string Name string 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 ShowGraph bool TodayPage string } 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() title := cleanTitle(page) if newTitle, err := PageTitle(pageText); err == nil { title = newTitle } data := editPage{ pageBaseInfo: pageBase, Session: sess, Title: title, Content: pageText, Editor: editor, Name: page, Backrefs: mpPage.Refs, TodayPage: todayPage(), ShowGraph: page != "Daily_Notes", } 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 { log.Println(err) 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 } gb, err := NewGraphBuilder(mp) if err != nil { http.Error(w, err.Error(), 500) return } gb.prepareNodeMap() gb.RemoveNode("DONE") gb.RemoveNode("TODO") gb.RemoveNode("Home") gb.RemoveNode("Projects") gb.RemoveNode("Notes") gb.RemoveNode("Books") gb.RemoveNode("Daily_Notes") gb.RemoveNode("NewJsonTest") gb.RemoveNodeWithSuffix("_2020") err = gb.prepareGraph() if err != nil { http.Error(w, err.Error(), 500) return } var nodesBuf bytes.Buffer var edgesBuf bytes.Buffer err = json.NewEncoder(&nodesBuf).Encode(&gb.nodes) if err != nil { http.Error(w, err.Error(), 500) return } err = json.NewEncoder(&edgesBuf).Encode(&gb.edges) if err != nil { http.Error(w, err.Error(), 500) return } pageBase := getPageBase() data := graphPage{ pageBaseInfo: pageBase, Session: sess, Title: "Graph", Name: "Graph", 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() metaKV, err := regexp.Compile(`(\w+)::\s+(.*)`) if err != nil { log.Fatal(err) } 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) title := cleanTitle(page) 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 { if matches := metaKV.FindStringSubmatch(item.Text); matches != nil { if matches[1] == "Title" { title = matches[2] } } builder.WriteString(strings.Repeat(" ", item.Indented)) builder.WriteString("* ") builder.WriteString(item.Text) builder.WriteByte('\n') } } pageText = builder.String() } if format == "html" { pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2") pageText = renderLinks(pageText, false) pageText = renderMarkdown2(pageText) pageBase := getPageBase() data := indexPage{ pageBaseInfo: pageBase, Session: sess, Title: title, Content: template.HTML(pageText), Name: page, Backrefs: mpPage.Refs, ShowGraph: page != "Daily_Notes", TodayPage: todayPage(), } 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, title) _, err = io.WriteString(w, "\n\n") _, err = io.WriteString(w, pageText) } } func renderLinks(pageText string, edit bool) 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) } editPart := "" if edit { editPart = "edit/" } return fmt.Sprintf("[%s](/%s%s)", s, editPart, 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.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) { gb, err := NewGraphBuilder(mp) if err != nil { http.Error(w, err.Error(), 500) return } for _, name := range r.URL.Query()["name"] { err = gb.buildFromCenter(name) if err != nil { http.Error(w, err.Error(), 500) return } } // Keep a copy of the nodes, buildFromCenter appends to the nodeMap var nodes []string for k, _ := range gb.nodeMap { nodes = append(nodes, k) } for _, node := range nodes { err = gb.buildFromCenter(node) } err = gb.prepareGraph() if err != nil { http.Error(w, err.Error(), 500) return } type data struct { Nodes []Node `json:"nodes"` Edges []Edge `json:"edges"` } err = json.NewEncoder(w).Encode(&data{gb.nodes, gb.edges}) if err != nil { http.Error(w, err.Error(), 500) return } }) 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 } }