a lot
This commit is contained in:
2
go.mod
2
go.mod
@@ -3,8 +3,10 @@ module miningtcup.me/notes-cli
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
|
7
go.sum
7
go.sum
@@ -1,7 +1,13 @@
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
@@ -10,6 +16,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
|
282
main.go
282
main.go
@@ -2,23 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/pbkdf2"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -29,6 +20,8 @@ import (
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -51,15 +44,73 @@ type Note struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func check(err error, format string, a ...any) bool {
|
||||
if err != nil {
|
||||
if writeErr := os.WriteFile("error.log", []byte(fmt.Sprintf(format+": %s", a, err)), 0600); writeErr != nil {
|
||||
log.Fatalf("Error writing error '%s' to error.log: %s\n", err, writeErr)
|
||||
func scanInt(promptFormat string, maxNum int64, minNum int64, promptA ...any) (choice int64, err error) {
|
||||
validateInt := func(input string) error {
|
||||
parsedInt, err := strconv.ParseInt(input, 10, 64)
|
||||
if parsedInt > maxNum || parsedInt < minNum {
|
||||
return fmt.Errorf("Out of bounds (must be between [inclusive] %d and %d)", minNum, maxNum)
|
||||
}
|
||||
log.Fatalf("\033[31m"+format+"\033[0m: %s\n", a, err)
|
||||
return false
|
||||
return err
|
||||
}
|
||||
return true
|
||||
|
||||
prompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||
Validate: validateInt,
|
||||
}
|
||||
|
||||
choiceString, err := prompt.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
choice, err = strconv.ParseInt(choiceString, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func scanString(template string, promptFormat string, promptA ...any) (choice string, err error) {
|
||||
prompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||
Default: template,
|
||||
}
|
||||
|
||||
choice, err = prompt.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func scanConfirm(promptFormat string, promptA ...any) (choice string, err error) {
|
||||
prompt := promptui.Prompt{
|
||||
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
choice, err = prompt.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func scanSelect(selection []string, template string, promptFormat string, promptA ...any) (choice int, err error) {
|
||||
prompt := promptui.Select{
|
||||
Label: fmt.Sprintf(promptFormat, promptA...),
|
||||
Items: selection,
|
||||
}
|
||||
|
||||
choice, _, err = prompt.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func readPassword() (string, error) {
|
||||
@@ -87,22 +138,6 @@ func readPassword() (string, error) {
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("pkcs7: Data is empty")
|
||||
}
|
||||
if length%blockSize != 0 {
|
||||
return nil, errors.New("pkcs7: Data is not block-aligned")
|
||||
}
|
||||
padLen := int(data[length-1])
|
||||
ref := bytes.Repeat([]byte{byte(padLen)}, padLen)
|
||||
if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
|
||||
return nil, errors.New("pkcs7: Invalid padding")
|
||||
}
|
||||
return data[:length-padLen], nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
scanner = *bufio.NewScanner(os.Stdin)
|
||||
home := os.Getenv("HOME")
|
||||
@@ -153,23 +188,25 @@ CREATE TABLE notes (
|
||||
|
||||
serverAddress, username, password = userData[0], userData[1], userData[2]
|
||||
var notes []Note
|
||||
fzf := exec.Command("fzf", "--ansi")
|
||||
pipe, err := fzf.StdinPipe()
|
||||
check(err, "Error getting stdin pipe for fzf")
|
||||
check(fzf.Start(), "Error starting fzf")
|
||||
// fzf := exec.Command("fzf", "--ansi")
|
||||
// pipe, err := fzf.StdinPipe()
|
||||
// check(err, "Error getting stdin pipe for fzf")
|
||||
// check(fzf.Start(), "Error starting fzf")
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cacheJsonBody := load(db)
|
||||
cacheJsonBody, err := load(db)
|
||||
check(err, "Error loading data from cache")
|
||||
for i := range cacheJsonBody {
|
||||
go list(cacheJsonBody[i][1], ¬es, &wg)
|
||||
go list(cacheJsonBody[i], ¬es, &wg)
|
||||
wg.Add(1)
|
||||
}
|
||||
fetchJsonBody := fetch()
|
||||
|
||||
for i := range fetchJsonBody {
|
||||
if !slices.Contains(cacheJsonBody, fetchJsonBody[i]) {
|
||||
go list(fetchJsonBody[i][1], ¬es, &wg)
|
||||
wg.Add(1)
|
||||
fetchJsonBody, err := fetch()
|
||||
if err == nil {
|
||||
for i := range fetchJsonBody {
|
||||
if !slices.Contains(cacheJsonBody, fetchJsonBody[i]) {
|
||||
go list(fetchJsonBody[i], ¬es, &wg)
|
||||
wg.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,11 +222,73 @@ CREATE TABLE notes (
|
||||
continue
|
||||
}
|
||||
date := time.Unix(timestamp/1000, 0).Format("Mon, Jan 01, 06")
|
||||
fmt.Fprintf(pipe, "%s | \033[1m%s\033[0m | %s\n", date, notes[i].Title, strings.ReplaceAll(notes[i].Body, "\n", " / "))
|
||||
r, g, b, _ := decodeColorInt(notes[i].Color)
|
||||
check(err, "Error decoding color int %d", notes[i].Color)
|
||||
formattedNote := strings.TrimSpace(
|
||||
fmt.Sprintf(
|
||||
"[%02d] %s | \033[1;38;2;%d;%d;%dm%s\033[0m | %s\n",
|
||||
i+1,
|
||||
date,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
strings.TrimSpace(notes[i].Title),
|
||||
strings.ReplaceAll(
|
||||
notes[i].Body,
|
||||
"\n",
|
||||
"/",
|
||||
),
|
||||
),
|
||||
)
|
||||
terminalWidth, _, err := term.GetSize(int(syscall.Stdin))
|
||||
check(err, "Error getting terminal size for stdin")
|
||||
fmt.Printf("%.*s\n", terminalWidth, formattedNote)
|
||||
}
|
||||
pipe.Close()
|
||||
// pipe.Close()
|
||||
cache(db, fetchJsonBody)
|
||||
fzf.Wait()
|
||||
// fzf.Wait()
|
||||
|
||||
fmt.Println()
|
||||
if fetchJsonBody == nil {
|
||||
fmt.Println("\033[1;31mNotes couldn't be loaded from the server.\033[0m")
|
||||
fmt.Println("A connection to the server is required to create, edit, or delete notes.")
|
||||
fmt.Println("\033[1mTroubleshooting steps:\033[0m")
|
||||
fmt.Println(` * Check your internet or network connection.
|
||||
* Make sure the server address is correct.
|
||||
* Make sure the server is running the latest version of noteserver.
|
||||
* Check the server's internet or network connection.
|
||||
`)
|
||||
return
|
||||
}
|
||||
chosenOperationIndex, err := scanSelect([]string{"\033[32mCreate\033[0m", "\033[31mDelete\033[0m", "\033[33mEdit\033[0m"}, "", "Select operation")
|
||||
check(err, "Error scanning selection")
|
||||
switch chosenOperationIndex {
|
||||
case 0:
|
||||
create(Note{})
|
||||
fmt.Println("Success!")
|
||||
case 1:
|
||||
chosenNoteIndex, err := scanInt(
|
||||
"Select note to \033[31mdelete\033[37m by index (1-%d)",
|
||||
int64(len(notes)),
|
||||
1,
|
||||
len(notes),
|
||||
)
|
||||
check(err, "Error scanning integer")
|
||||
chosenNoteIndex -= 1
|
||||
scanConfirm("Really delete '%s'", notes[chosenNoteIndex].Title)
|
||||
del(notes[chosenNoteIndex].ID, db)
|
||||
case 2:
|
||||
chosenNoteIndex, err := scanInt(
|
||||
"Select note to \033[33medit\033[37m by index (1-%d)",
|
||||
int64(len(notes)),
|
||||
1,
|
||||
len(notes),
|
||||
)
|
||||
check(err, "Error scanning integer")
|
||||
chosenNoteIndex -= 1
|
||||
create(notes[chosenNoteIndex])
|
||||
del(notes[chosenNoteIndex].ID, db)
|
||||
}
|
||||
default:
|
||||
login()
|
||||
}
|
||||
@@ -242,88 +341,13 @@ func login() {
|
||||
fmt.Println("\nAwesome! To get started, run 'notes add' or 'notes list'")
|
||||
}
|
||||
|
||||
func decrypt(data string, password string) string {
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(data)
|
||||
check(err, "Error decoding base64 data")
|
||||
|
||||
if len(dataBytes) < 32 {
|
||||
return ""
|
||||
}
|
||||
|
||||
iv := dataBytes[0:16]
|
||||
salt := dataBytes[16:32]
|
||||
ciphertext := dataBytes[32:]
|
||||
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
fmt.Printf("Ciphertext length %d is not a multiple of block size %d\n", len(ciphertext), aes.BlockSize)
|
||||
return ""
|
||||
}
|
||||
|
||||
key, err := pbkdf2.Key(sha256.New, password, salt, 65536, 32)
|
||||
check(err, "Error deriving pbkdf2-sha256 key from password")
|
||||
block, err := aes.NewCipher(key)
|
||||
check(err, "Error creating AES key")
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
strippedPlaintext, err := pkcs7strip(plaintext, 16)
|
||||
func check(err error, format string, a ...any) bool {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(strippedPlaintext)
|
||||
}
|
||||
|
||||
func fetch() [][2]string {
|
||||
response, err := http.Get(fmt.Sprintf("https://%s/get?user=%s", serverAddress, url.QueryEscape(username)))
|
||||
check(err, "Error getting notes from '%s' as '%s'", serverAddress, username)
|
||||
defer response.Body.Close()
|
||||
body, err := io.ReadAll(response.Body)
|
||||
check(err, "Error reading response body while getting notes")
|
||||
|
||||
var jsonBody [][2]string
|
||||
err = json.Unmarshal(body, &jsonBody)
|
||||
check(err, "Error unmarshalling response body")
|
||||
|
||||
return jsonBody
|
||||
}
|
||||
|
||||
func load(db *sql.DB) [][2]string {
|
||||
var rowCount int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM notes").Scan(&rowCount)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
const limit = 100
|
||||
if rowCount > limit { // change this if there's ever an option to increase limit
|
||||
rowCount = limit // change this if there's ever an option to increase limit
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT * FROM notes LIMIT ?", rowCount)
|
||||
check(err, "Error querying cache database", err)
|
||||
defer rows.Close()
|
||||
|
||||
notes := make([][2]string, rowCount)
|
||||
|
||||
for i := 0; rows.Next(); i++ {
|
||||
var currentRow [2]string
|
||||
check(rows.Scan(¤tRow[0], ¤tRow[1]), "Error scanning cache database rows")
|
||||
notes[i] = currentRow
|
||||
}
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
func list(jsonBody string, notes *[]Note, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
var decryptedBody Note
|
||||
err := json.Unmarshal([]byte(decrypt(jsonBody, password)), &decryptedBody)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(decryptedBody.Title) > 0 && len(decryptedBody.Body) > 0 {
|
||||
*notes = append(*notes, decryptedBody) // ew
|
||||
return
|
||||
if writeErr := os.WriteFile("error.log", fmt.Appendf([]byte{}, format+": %s", a, err), 0600); writeErr != nil {
|
||||
log.Fatalf("\033[31mError writing error '%s' to error.log:\033[0m %s\n", err, writeErr)
|
||||
}
|
||||
log.Fatalf("\033[31m"+format+":\033[0m %s\n", a, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
Reference in New Issue
Block a user