Compare commits

...

2 commits

4 changed files with 153 additions and 69 deletions

View file

@ -24,10 +24,30 @@
href="/static/css/tiny-mde.min.css" href="/static/css/tiny-mde.min.css"
/> />
<script type="text/javascript"> <script type="text/javascript">
var tinyMDE = new TinyMDE.Editor({ textarea: "noteBodyInput" }); var tinyMDE = new TinyMDE.Editor({ textarea: "noteBodyInput" });
var commandBar = new TinyMDE.CommandBar({ var commandBar = new TinyMDE.CommandBar({
element: "toolbar", element: "toolbar",
editor: tinyMDE, editor: tinyMDE,
commands: [
'bold', 'italic', 'strikethrough',
'|',
'code',
'|',
'h1', 'h2',
'|',
'ul', 'ol', {name: "checklist", title: "Check List", action: makeChecklist},
'|',
'blockquote', 'hr',
'|',
'insertLink', 'insertImage'
],
}); });
function makeChecklist(editor) {
editor.wrapSelection("* [ ] ", "")
};
</script> </script>
{{end}} {{end}}

View file

@ -7,6 +7,26 @@
<a class="btn btn-primary" href="{{.urlEdit}}">Edit</a> <a class="btn btn-primary" href="{{.urlEdit}}">Edit</a>
<a class="btn btn-danger" href="{{.urlDelete}}">Delete</a> <a class="btn btn-danger" href="{{.urlDelete}}">Delete</a>
</div> </div>
<div class="mt-2">
<h3>Ownership</h3>
{{if .note.Viewers}}
<p>This note is owned by <em>{{.note.Owner}}</em> and is further visible to</p>
<ul>
{{range .viewers}}
<li>{{.}}</li>
{{end}}
</ul>
{{else}}
<p>This note is owned by <em>{{.note.Owner}}</em>.</p>
{{end}}
<form action="{{.urlShare}}" method="POST">
<div class="mb-3">
<input type="text" class="form-control" id="viewerInput" name="viewer" aria-described-by="viewerHelp" />
<div id="viewerHelp" class="form-text">Share with other user</div>
</div>
<button class="btn btn-primary" type="submit">Share</button>
</div>
<script> <script>
let checkBoxes = document.querySelectorAll('input[type=checkbox]') let checkBoxes = document.querySelectorAll('input[type=checkbox]')

View file

@ -5,7 +5,7 @@ package notes
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/base64" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@ -34,7 +34,8 @@ type Note struct {
BodyRendered template.HTML BodyRendered template.HTML
Body []byte Body []byte
Owner string Owner string
Viewers []string Viewers map[string]struct{}
Uid string
} }
type NoteStore struct { type NoteStore struct {
@ -71,20 +72,20 @@ func Init() error {
log.Printf("Looking in %s", notesDir) log.Printf("Looking in %s", notesDir)
for _, f := range files { for _, f := range files {
if !f.IsDir() { if !f.IsDir() {
encodedTitle := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) uid := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
note, err := LoadNote(encodedTitle) note, err := loadNote(uid)
if err != nil { if err != nil {
return err return err
} }
title := note.Title title := note.Title
log.Printf("Found note %s (title '%s', owner %s)", encodedTitle, title, note.Owner) log.Printf("Found note %s (title '%s', owner %s)", uid, title, note.Owner)
Notes.notes[note.Owner] = append(Notes.notes[note.Owner], note) Notes.Add(note, note.Owner)
for _, viewer := range note.Viewers { for viewer := range note.Viewers {
Notes.notes[viewer] = append(Notes.notes[viewer], note) Notes.Add(note, viewer)
} }
} }
} }
@ -96,11 +97,11 @@ func (ns *NoteStore) Get(owner string) []*Note {
return ns.notes[owner] return ns.notes[owner]
} }
func (ns *NoteStore) GetOne(owner string, encodedTitle string) (*Note, bool) { func (ns *NoteStore) GetOne(owner string, uid string) (*Note, bool) {
notes := ns.Get(owner) notes := ns.Get(owner)
for _, note := range notes { for _, note := range notes {
if note.EncodedTitle() == encodedTitle { if note.Uid == uid {
log.Printf("Found single note during GetOne %+v", note) log.Printf("Found single note during GetOne %+v", note)
return note, true return note, true
} }
@ -108,8 +109,8 @@ func (ns *NoteStore) GetOne(owner string, encodedTitle string) (*Note, bool) {
return nil, false return nil, false
} }
func (ns *NoteStore) Add(note *Note) { func (ns *NoteStore) Add(note *Note, user string) {
ns.notes[note.Owner] = append(ns.notes[note.Owner], note) ns.notes[user] = append(ns.notes[user], note)
} }
func fmtPath(path string) string { func fmtPath(path string) string {
@ -117,14 +118,42 @@ func fmtPath(path string) string {
} }
func (n *Note) marshalFrontmatter() ([]byte, error) { func (n *Note) marshalFrontmatter() ([]byte, error) {
viewers := make([]string, 0, len(n.Viewers))
for viewer := range n.Viewers {
viewers = append(viewers, viewer)
}
frontmatter := make(map[string]interface{}) frontmatter := make(map[string]interface{})
frontmatter["owner"] = n.Owner frontmatter["owner"] = n.Owner
frontmatter["viewers"] = n.Viewers frontmatter["viewers"] = viewers
frontmatter["title"] = n.Title
marshaled, err := yaml.Marshal(&frontmatter) marshaled, err := yaml.Marshal(&frontmatter)
return marshaled, err return marshaled, err
} }
func (n *Note) ViewersAsList() []string {
keys := make([]string, 0, len(n.Viewers))
for key := range n.Viewers {
keys = append(keys, key)
}
return keys
}
func NewNoteNoSave(title string, owner string) *Note {
note := &Note{Title: title, Owner: owner}
note.Viewers = make(map[string]struct{})
return note
}
func NewNote(title string, owner string) *Note {
note := NewNoteNoSave(title, owner)
note.Uid = rand.Text()
note.Save()
Notes.Add(note, note.Owner)
return note
}
// Save a note to a path derived from the title // Save a note to a path derived from the title
func (n *Note) Save() error { func (n *Note) Save() error {
frontmatter, err := n.marshalFrontmatter() frontmatter, err := n.marshalFrontmatter()
@ -132,7 +161,7 @@ func (n *Note) Save() error {
return err return err
} }
filename := filepath.Join(conf.Conf.NotesDir, fmtPath(n.EncodedTitle())) filename := filepath.Join(conf.Conf.NotesDir, fmtPath(n.Uid))
return os.WriteFile( return os.WriteFile(
filename, filename,
[]byte(fmt.Sprintf("---\n%s---\n%s", frontmatter, n.Body)), []byte(fmt.Sprintf("---\n%s---\n%s", frontmatter, n.Body)),
@ -152,9 +181,9 @@ func (n *Note) Render() {
n.BodyRendered = template.HTML(buf.String()) n.BodyRendered = template.HTML(buf.String())
} }
// LoadNote from the disk. The path is derived from the title // loadNote from the disk. The path is derived from the uid
func LoadNote(encodedTitle string) (*Note, error) { func loadNote(uid string) (*Note, error) {
filename := filepath.Join(conf.Conf.NotesDir, fmtPath(encodedTitle)) filename := filepath.Join(conf.Conf.NotesDir, fmtPath(uid))
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -186,8 +215,6 @@ func LoadNote(encodedTitle string) (*Note, error) {
body = append(body, '\n') body = append(body, '\n')
} }
title := DecodeTitle(encodedTitle)
var buf bytes.Buffer var buf bytes.Buffer
context := parser.NewContext() context := parser.NewContext()
if err := md.Convert([]byte(fullBody), &buf, parser.WithContext(context)); err != nil { if err := md.Convert([]byte(fullBody), &buf, parser.WithContext(context)); err != nil {
@ -201,7 +228,14 @@ func LoadNote(encodedTitle string) (*Note, error) {
return nil, errors.New("invalid note, missing 'owner' in frontmatter") return nil, errors.New("invalid note, missing 'owner' in frontmatter")
} }
note := &Note{Title: title, Body: body, Owner: owner} title, ok := metaData["title"].(string)
if !ok {
return nil, errors.New("invalid note, missing 'title' in frontmatter")
}
note := NewNoteNoSave(title, owner)
note.Uid = uid
note.Body = body
viewers := metaData["viewers"].([]interface{}) viewers := metaData["viewers"].([]interface{})
@ -210,20 +244,25 @@ func LoadNote(encodedTitle string) (*Note, error) {
if !ok { if !ok {
return nil, errors.New("invalid note, non string type in 'viewers' in frontmatter") return nil, errors.New("invalid note, non string type in 'viewers' in frontmatter")
} }
note.Viewers = append(note.Viewers, v)
if v == "" {
continue
}
note.AddViewer(v)
} }
log.Printf("Note %s shared with %v", note.Title, note.Viewers) log.Printf("Note %s shared with %v", note.Title, note.Viewers)
return note, nil return note, nil
} }
func DeleteNote(title string) error { func (n *Note) AddViewer(viewer string) {
filename := filepath.Join(conf.Conf.NotesDir, fmtPath(title)) n.Viewers[viewer] = struct{}{}
return os.Remove(filename)
} }
func (n *Note) EncodedTitle() string { func DeleteNote(uid string) error {
return base64.StdEncoding.EncodeToString([]byte(n.Title)) filename := filepath.Join(conf.Conf.NotesDir, fmtPath(uid))
return os.Remove(filename)
} }
type toggleCheckboxTransformer struct{ nthBox int } type toggleCheckboxTransformer struct{ nthBox int }
@ -288,12 +327,3 @@ func (n *Note) ToggleBox(nthBox int) {
n.Body = buf.Bytes() n.Body = buf.Bytes()
n.Save() n.Save()
} }
func DecodeTitle(encodedTitle string) string {
title, err := base64.StdEncoding.DecodeString(encodedTitle)
if err != nil {
log.Printf("Couldn't decode base64 string '%s': %s", encodedTitle, err)
}
return string(title)
}

View file

@ -27,6 +27,7 @@ func GetRoutes(prefix string) *http.ServeMux {
"view": {Path: "/{note}/", Protocol: "GET", Handler: view}, "view": {Path: "/{note}/", Protocol: "GET", Handler: view},
"delete": {Path: "/{note}/delete/", Protocol: "GET", Handler: delete}, "delete": {Path: "/{note}/delete/", Protocol: "GET", Handler: delete},
"edit": {Path: "/{note}/edit/", Protocol: "GET", Handler: edit}, "edit": {Path: "/{note}/edit/", Protocol: "GET", Handler: edit},
"share": {Path: "/{note}/share/", Protocol: "POST", Handler: share},
"save": {Path: "/{note}/edit/save/", Protocol: "POST", Handler: save}, "save": {Path: "/{note}/edit/save/", Protocol: "POST", Handler: save},
"togglebox": {Path: "/{note}/togglebox/", Protocol: "POST", Handler: togglebox}, "togglebox": {Path: "/{note}/togglebox/", Protocol: "POST", Handler: togglebox},
}, },
@ -35,21 +36,25 @@ func GetRoutes(prefix string) *http.ServeMux {
} }
func view(w http.ResponseWriter, r *http.Request) { func view(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value(middleware.ContextKey("user")).(string) user := r.Context().Value(middleware.ContextKey("user")).(string)
title := r.PathValue("note") uid := r.PathValue("note")
note, err := notes.LoadNote(title) note, ok := notes.Notes.GetOne(user, uid)
urlEdit := myurls.Reverse("edit", urls.Repl{"note": title})
urlNew := myurls.Reverse("new", urls.Repl{}) if !ok {
urlDelete := myurls.Reverse("delete", urls.Repl{"note": title}) http.NotFound(w, r)
if err != nil {
http.Redirect(w, r, urlEdit, http.StatusFound)
return return
} }
viewers := note.ViewersAsList()
context := templ.Ctx{"note": note, "urlEdit": urlEdit, "urlDelete": urlDelete, "urlNew": urlNew} urlEdit := myurls.Reverse("edit", urls.Repl{"note": uid})
urlNew := myurls.Reverse("new", urls.Repl{})
urlDelete := myurls.Reverse("delete", urls.Repl{"note": uid})
urlShare := myurls.Reverse("share", urls.Repl{"note": uid})
context := templ.Ctx{"note": note, "urlEdit": urlEdit, "urlDelete": urlDelete, "urlNew": urlNew, "urlShare": urlShare, "viewers": viewers}
note.Render() note.Render()
err = templ.RenderTemplate(w, r, "view.tmpl.html", context) err := templ.RenderTemplate(w, r, "view.tmpl.html", context)
if err != nil { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
http.Error(w, "Couldn't load template", http.StatusInternalServerError) http.Error(w, "Couldn't load template", http.StatusInternalServerError)
@ -58,18 +63,17 @@ func view(w http.ResponseWriter, r *http.Request) {
} }
func edit(w http.ResponseWriter, r *http.Request) { func edit(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value(middleware.ContextKey("user")).(string) user := r.Context().Value(middleware.ContextKey("user")).(string)
encodedTitle := r.PathValue("note") uid := r.PathValue("note")
note, err := notes.LoadNote(encodedTitle) note, ok := notes.Notes.GetOne(user, uid)
if err != nil { if !ok {
title := notes.DecodeTitle(encodedTitle) note = notes.NewNote("", user)
note = &notes.Note{Title: title}
} }
urlSave := myurls.Reverse("save", urls.Repl{"note": encodedTitle}) urlSave := myurls.Reverse("save", urls.Repl{"note": uid})
context := templ.Ctx{"note": note, "urlSave": urlSave, "text": string(note.Body)} context := templ.Ctx{"note": note, "urlSave": urlSave, "text": string(note.Body)}
err = templ.RenderTemplate(w, r, "edit.tmpl.html", context) err := templ.RenderTemplate(w, r, "edit.tmpl.html", context)
if err != nil { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
http.Error(w, "Couldn't load template", http.StatusInternalServerError) http.Error(w, "Couldn't load template", http.StatusInternalServerError)
@ -85,11 +89,9 @@ func new(w http.ResponseWriter, r *http.Request) {
title = "<New Note>" title = "<New Note>"
} }
note := &notes.Note{Title: title, Owner: user} note := notes.NewNote(title, user)
note.Save()
notes.Notes.Add(note)
urlEdit := myurls.Reverse("edit", urls.Repl{"note": note.EncodedTitle()}) urlEdit := myurls.Reverse("edit", urls.Repl{"note": note.Uid})
http.Redirect(w, r, urlEdit, http.StatusFound) http.Redirect(w, r, urlEdit, http.StatusFound)
} }
@ -110,8 +112,8 @@ func delete(w http.ResponseWriter, r *http.Request) {
func save(w http.ResponseWriter, r *http.Request) { func save(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(middleware.ContextKey("user")).(string) user := r.Context().Value(middleware.ContextKey("user")).(string)
oldTitle := r.PathValue("note") uid := r.PathValue("note")
note, ok := notes.Notes.GetOne(user, oldTitle) note, ok := notes.Notes.GetOne(user, uid)
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
@ -125,32 +127,44 @@ func save(w http.ResponseWriter, r *http.Request) {
note.Body = []byte(body) note.Body = []byte(body)
note.Save() note.Save()
if oldTitle != note.EncodedTitle() { http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.Uid}), http.StatusFound)
notes.DeleteNote(oldTitle) }
func share(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(middleware.ContextKey("user")).(string)
uid := r.PathValue("note")
note, ok := notes.Notes.GetOne(user, uid)
if !ok {
http.NotFound(w, r)
} }
http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()}), http.StatusFound) viewer := r.FormValue("viewer")
note.AddViewer(viewer)
note.Save()
http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.Uid}), http.StatusFound)
} }
func togglebox(w http.ResponseWriter, r *http.Request) { func togglebox(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value(middleware.ContextKey("user")).(string) user := r.Context().Value(middleware.ContextKey("user")).(string)
title := r.PathValue("note") uid := r.PathValue("note")
nthBox, err := strconv.Atoi(r.FormValue("box")) nthBox, err := strconv.Atoi(r.FormValue("box"))
if err != nil { if err != nil {
log.Fatal("You fucked up boy") log.Fatal("You fucked up boy")
return return
} }
note, err := notes.LoadNote(title) note, ok := notes.Notes.GetOne(user, uid)
if err != nil { if !ok {
http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": title}), http.StatusFound) http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": uid}), http.StatusFound)
return return
} }
note.ToggleBox(nthBox) note.ToggleBox(nthBox)
http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()}), http.StatusFound) http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.Uid}), http.StatusFound)
} }
type titleAndURL struct { type titleAndURL struct {
@ -170,7 +184,7 @@ func list(w http.ResponseWriter, r *http.Request) {
for _, note := range ns { for _, note := range ns {
titlesAndUrls = append( titlesAndUrls = append(
titlesAndUrls, titlesAndUrls,
titleAndURL{Title: note.Title, URL: myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()})}, titleAndURL{Title: note.Title, URL: myurls.Reverse("view", urls.Repl{"note": note.Uid})},
) )
} }