glow/client/client.go
2021-10-22 13:16:49 -04:00

221 lines
5.5 KiB
Go

package client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"sort"
"time"
"github.com/charmbracelet/charm/kv"
"github.com/dgraph-io/badger/v3"
"github.com/google/uuid"
)
// Client provides the Glow interface to the Charm Cloud
type Client struct {
kv *kv.KV
}
var stashPrefix = []byte("stash_")
// ErrorPageOutOfBounds is an error for an invalid page number.
var ErrorPageOutOfBounds = errors.New("page must be a value of 1 or greater")
// MarkdownsByCreatedAtDesc sorts markdown documents by date in descending
// order. It implements sort.Interface for []Markdown based on the CreatedAt
// field.
type MarkdownsByCreatedAtDesc []*Markdown
// Sort implementation for MarkdownByCreatedAt.
func (m MarkdownsByCreatedAtDesc) Len() int { return len(m) }
func (m MarkdownsByCreatedAtDesc) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m MarkdownsByCreatedAtDesc) Less(i, j int) bool { return m[i].CreatedAt.After(m[j].CreatedAt) }
// Markdown is the struct that contains the markdown and note data. If
// EncryptKeyID is not blank, the content should be assumed to be encrypted.
// Once decrypted, that field will be blanked.
type Markdown struct {
ID string `json:"id"`
Note string `json:"note"`
Body string `json:"body,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// NewClient creates a new Client with the default settings
func NewClient() (*Client, error) {
kv, err := kv.OpenWithDefaults("charm.sh.glow")
if err != nil {
return nil, err
}
err = kv.Sync()
if err != nil {
return nil, err
}
return &Client{kv: kv}, nil
}
// GetNews returns the Glow paginated news results.
func (cc *Client) GetNews(page int) ([]*Markdown, error) {
if page < 1 {
return nil, ErrorPageOutOfBounds
}
var news []*Markdown
// err := cc.makeAPIRequest("GET", fmt.Sprintf("news?page=%d", page), nil, &news)
// if err != nil {
// return nil, err
// }
return news, nil
}
// GetNewsMarkdown returns the Markdown struct for the given news markdown ID.
func (cc *Client) GetNewsMarkdown(markdownID string) (*Markdown, error) {
var md Markdown
// err := cc.makeAPIRequest("GET", fmt.Sprintf("news/%d", markdownID), nil, &md)
// if err != nil {
// return nil, err
// }
return &md, nil
}
// GetStash returns the paginated user stash for the authenticated Charm user.
func (cc *Client) GetStash(page int) ([]*Markdown, error) {
if page < 1 {
return nil, ErrorPageOutOfBounds
}
limit := 50
startOffset := (page * limit) - limit
endOffset := page * limit
var stash MarkdownsByCreatedAtDesc
err := cc.kv.View(func(txn *badger.Txn) error {
opt := badger.DefaultIteratorOptions
opt.Prefix = stashPrefix
it := txn.NewIterator(opt)
defer it.Close()
for it.Seek(stashPrefix); it.ValidForPrefix(stashPrefix); it.Next() {
item := it.Item()
err := item.Value(func(v []byte) error {
md := &Markdown{}
err := json.Unmarshal(v, md)
if err != nil {
return err
}
stash = append(stash, md)
return nil
})
if err != nil {
return err
}
if len(stash) > endOffset {
break
}
}
return nil
})
if err != nil {
return nil, err
}
sort.Sort(stash)
if startOffset >= len(stash) {
return []*Markdown{}, nil
}
if endOffset > len(stash) {
return stash[startOffset:], nil
}
return stash[startOffset:endOffset], nil
}
// GetStashMarkdown returns the Markdown struct for the given stash markdown ID.
func (cc *Client) GetStashMarkdown(markdownID string) (*Markdown, error) {
var md Markdown
d, err := cc.kv.Get([]byte(markdownID))
if err != nil {
return nil, err
}
err = json.Unmarshal(d, &md)
if err != nil {
return nil, err
}
return &md, nil
}
// StashMarkdown encrypts and stashes a new markdown file with note.
func (cc *Client) StashMarkdown(note string, body string) (*Markdown, error) {
gid := uuid.New().String()
md := &Markdown{Note: note, Body: body, ID: gid, CreatedAt: time.Now()}
err := cc.saveMarkdown(md)
if err != nil {
return nil, err
}
return md, nil
}
// DeleteMarkdown deletes the stash markdown for the given ID.
func (cc *Client) DeleteMarkdown(markdownID string) error {
txn, err := cc.kv.NewTransaction(true)
if err != nil {
return err
}
mid, sid := markdownKeys(markdownID)
err = txn.Delete(mid)
if err != nil {
return err
}
err = txn.Delete(sid)
if err != nil {
return err
}
return cc.kv.Commit(txn, func(err error) {
if err != nil {
log.Printf("Badger commit error: %s", err)
}
})
}
// SetMarkdownNote updates the note for a given stash markdown ID.
func (cc *Client) SetMarkdownNote(markdownID string, note string) error {
md, err := cc.GetStashMarkdown(markdownID)
if err != nil {
return err
}
md.Note = note
return cc.saveMarkdown(md)
}
func (cc *Client) saveMarkdown(md *Markdown) error {
mid, sid := markdownKeys(md.ID)
txn, err := cc.kv.NewTransaction(true)
if err != nil {
return err
}
buf := bytes.NewBuffer(nil)
err = json.NewEncoder(buf).Encode(md)
if err != nil {
return err
}
err = txn.Set(mid, buf.Bytes())
if err != nil {
return err
}
buf = bytes.NewBuffer(nil)
smd := &Markdown{ID: md.ID, Note: md.Note, CreatedAt: md.CreatedAt}
err = json.NewEncoder(buf).Encode(smd)
if err != nil {
return err
}
err = txn.Set(sid, buf.Bytes())
if err != nil {
return err
}
return cc.kv.Commit(txn, func(err error) {
if err != nil {
log.Printf("Badger commit error: %s", err)
}
})
}
func markdownKeys(markdownID string) ([]byte, []byte) {
return []byte(markdownID), []byte(fmt.Sprintf("%s%s", stashPrefix, markdownID))
}