Loading NOTES.md +37 −42 Original line number Diff line number Diff line Loading @@ -21,55 +21,50 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented ## Things to do * Run [godoc](http://localhost:6060/) and then review where the help text is lacking. * Implement each command. * Next: folder commands - MODIFY * Messages edit: CHANGE, REPLY * Moving messages: COPY, MOVE * Mark messages: MARK, UNMARK * ~~Compound commands: SET and SHOW - make HELP work for them.~~ * Mail: MAIL, FORWARD, RESPOND * Review each command and fully implement it. * Missing [MAIL] [RESPOND] [SET DEFAULT_EXPIRE] [SET READNEW] [SET NOREADNEW] [SET NOSHOWNEW] [SET NOPROMPT_EXPIRE] [SET EXPIRE_LIMIT] [SET PROMPT_EXPIRE] [SET SHOWNEW] [SHOW NEW] [SEARCH] * Run this.Skew.Safe() before... each command? each write? * Handle broadcast messages - have bulletin watch a directory and display files from it. Then have them delete the file if it's older than 5 minutes (allow for failure) * Handle broadcast messages - create a broadcast table and add an expiration column. * Database * trigger to limit values for 'visibility'? * Add commands: * ~~A way to add / delete ssh keys.~~ * Commands for a local mail system? * Commands to connect to Mattermost or mastodon? * ~~Commands to manage users.~~ * Make a spreadsheet for signups. * Make sure ssh key is fully unique. Done: * ~~Editor - need an embedded editor~~ Implemented using tview; good enough * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/) * [gkilo](https://github.com/vcnovaes/gkilo) * This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no) * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning * Another option is tview - [simpler](https://github.com/rivo/tview). * ~~Implement batch jobs~~ * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * ~~Add a pager~~ * ~~SHOW VERSION~~ * ~~Check db version; notify user if it changes; refuse to write to db if it has.~~ * ~~this.Folder should be a storage.Folder.~~ * ~~Add some of the early announcements from the sources - see the conversion branch - to the GENERAL folder.~~ * ~~Move to a storage layer.~~ * ~~Cleanup help output.~~ * ~~Remove the node/cluster/newsgroup/mailing-list related flags.~~ * ~~Remove BBOARD references.~~ * ~~format with `par w72j1`~~ * ~~Handle MARK for SELECT and DIRECTORY.~~ * ~~Remove all file related things. Which means no need for most (all?) /EDIT flags~~ * ~~Stop the seeded messages from being deleted by the expire batch command.~~ Polishing. Review each command and put a + next to each as it is fully done. Top level: ADD +BACK BULLETIN CHANGE COPY CREATE +Ctrl-C +CURRENT DELETE DIRECTORY +EXIT +FIRST +Folders FORWARD +HELP INDEX +Keypad +LAST MAIL +MARK MODIFY MOVE +NEXT PRINT +QUIT READ REMOVE REPLY RESPOND SEARCH SEEN SELECT +SET +SHOW +SSH +UNMARK UNSEEN +USER SET: ACCESS +ALWAYS BRIEF DEFAULT_EXPIRE EXPIRE_LIMIT FOLDER +NOALWAYS NOBRIEF NONOTIFY NOPROMPT_EXPIRE NOREADNEW NOSHOWNEW NOSYSTEM NOTIFY PROMPT_EXPIRE READNEW SHOWNEW SYSTEM SHOW: +FLAGS FOLDER -NEW +PRIVILEGES USER +VERSION SSH: +ADD +DELETE +FETCH +LIST USER: +ADD +ADMIN +DELETE +DISABLE +ENABLE LIST +MOD +NAME +NOADMIN +NOMOD ## Module links Loading batch/batch.go +39 −8 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import ( "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/key" "git.lyda.ie/kevin/bulletin/storage" "git.lyda.ie/kevin/bulletin/users" "github.com/adrg/xdg" ) Loading Loading @@ -125,23 +126,53 @@ the first user.`) // Install crontab. bulletin, err := os.Executable() if err != nil { panic(err) // TODO: cleanup error handling. } ask.CheckErr(err) crontab := &strings.Builder{} template.Must(template.New("crontab").Parse(crontabTemplate)). Execute(crontab, map[string]string{"Bulletin": bulletin}) fmt.Printf("Adding this to crontab:\n\n%s\n", crontab.String()) err = installCrontab(crontab.String()) if err != nil { panic(err) // TODO: cleanup error handling. } ask.CheckErr(err) // Mark that install has happened. err = touch(touchfile) if err != nil { panic(err) // TODO: cleanup error handling. ask.CheckErr(err) return 0 } // NewUser creates a new user based on command line arguments. func NewUser(args []string) int { // Make sure we have enough args. if len(args) != 3 { fmt.Println("ERROR: Must supply login, site nickname and site username.") return 1 } // Create a user if missing. login := strings.ToUpper(args[0]) err := users.ValidLogin(login) ask.CheckErr(err) store, err := storage.Open() ask.CheckErr(err) q := storage.New(store.DB) ctx := storage.Context() u, err := q.GetUser(ctx, login) if u.Login == "" { u, err = q.AddUser(ctx, storage.AddUserParams{ Login: login, }) ask.CheckErr(err) } if u.Login == "" { fmt.Println("ERROR: Failed to make user.") return 1 } response := key.Fetch(u.Login, args[1], args[2]) fmt.Println(response) if strings.HasPrefix(response, "ERROR") { return 1 } return 0 } key/key.go +34 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" "net/http" "os" "path" "strings" Loading Loading @@ -190,3 +191,36 @@ func Delete(public string) error { return nil } // Fetch fetches keys and adds them. func Fetch(login, nickname, username string) string { sites := map[string]string{ "codeberg": "https://codeberg.org/%s.keys", "gitlab": "https://gitlab.com/%s.keys", "github": "https://github.com/%s.keys", } site := sites[strings.ToLower(nickname)] if site == "" { return fmt.Sprintln("ERROR: site nickname unknown.") } url := fmt.Sprintf(site, username) resp, err := http.Get(url) if err != nil { return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err) } scanner := bufio.NewScanner(resp.Body) keys := 0 for scanner.Scan() { keyline := string(bytes.TrimSpace(scanner.Bytes())) Add(login, keyline) keys++ } switch keys { case 0: return fmt.Sprintln("No keys added.") case 1: return fmt.Sprintln("Key is added.") default: return fmt.Sprintf("%d keys added.\n", keys) } } main.go +2 −0 Original line number Diff line number Diff line Loading @@ -49,6 +49,8 @@ func main() { exitcode = batch.Expire() case "install": exitcode = batch.Install() case "new-user": exitcode = batch.NewUser(cmd.Args().Slice()) default: fmt.Println("ERROR: can only run batch commands as SYSTEM.") exitcode = 1 Loading repl/accounts.go +57 −32 Original line number Diff line number Diff line package repl import ( "bufio" "bytes" "fmt" "net/http" "strings" "git.lyda.ie/kevin/bulletin/ask" Loading @@ -23,12 +20,29 @@ func ActionUser(cmd *dclish.Command) error { // ActionUserAdd handles the `USER ADD` command. func ActionUserAdd(cmd *dclish.Command) error { ctx := storage.Context() login := strings.ToUpper(cmd.Args[0]) err := users.ValidLogin(login) u, err := users.ValidExistingLogin(this.Q, login) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } if u.Login != "" { fmt.Println("ERROR: User already exists.") return nil } u, err = this.Q.AddUser(ctx, storage.AddUserParams{ Login: login, Name: cmd.Args[1], }) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } if u.Login == "" { fmt.Println("ERROR: Failed to make user; unknown reason.") return nil } return nil } Loading Loading @@ -183,6 +197,35 @@ func ActionUserNomod(cmd *dclish.Command) error { return actionUserMod(cmd, 0, "not a moderator") } // ActionUserName handles the `USER LIST` command. func ActionUserName(cmd *dclish.Command) error { if len(cmd.Args) == 2 && this.User.Admin == 0 { fmt.Println("ERROR: You are not an admin.") return nil } login := this.User.Login name := cmd.Args[0] if len(cmd.Args) == 2 { login = strings.ToUpper(cmd.Args[0]) name = cmd.Args[1] _, err := users.ValidExistingLogin(this.Q, login) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } } ctx := storage.Context() err := this.Q.UpdateUserName(ctx, storage.UpdateUserNameParams{ Login: login, Name: name, }) if err != nil { fmt.Printf("ERROR: Failed to update user name (%s).\n", err) } return nil } // ActionSSH handles the `SSH` command. func ActionSSH(cmd *dclish.Command) error { fmt.Println(cmd.Description) Loading Loading @@ -282,36 +325,18 @@ func ActionSSHDelete(cmd *dclish.Command) error { // ActionSSHFetch handles the `SSH FETCH` command. func ActionSSHFetch(cmd *dclish.Command) error { sites := map[string]string{ "codeberg": "https://codeberg.org/%s.keys", "gitlab": "https://gitlab.com/%s.keys", "github": "https://github.com/%s.keys", } siteTemplate := sites[strings.ToLower(cmd.Args[0])] if siteTemplate == "" { fmt.Println("ERROR: site nickname unknown.") return nil } site := fmt.Sprintf(siteTemplate, cmd.Args[1]) resp, err := http.Get(site) if err != nil { fmt.Printf("ERROR: Failed to fetch ssh keys (%s).\n", err) login := this.User.Login sitename := cmd.Args[0] username := cmd.Args[1] if len(cmd.Args) == 3 && this.User.Admin == 0 { fmt.Println("ERROR: You are not an admin.") return nil } scanner := bufio.NewScanner(resp.Body) keys := 0 for scanner.Scan() { keyline := string(bytes.TrimSpace(scanner.Bytes())) key.Add(this.User.Login, keyline) keys++ } switch keys { case 0: fmt.Println("No keys added.") case 1: fmt.Println("Key is added.") default: fmt.Printf("%d keys added.\n", keys) if len(cmd.Args) == 3 { login = cmd.Args[0] sitename = cmd.Args[1] username = cmd.Args[2] } fmt.Print(key.Fetch(login, sitename, username)) return nil } Loading
NOTES.md +37 −42 Original line number Diff line number Diff line Loading @@ -21,55 +21,50 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented ## Things to do * Run [godoc](http://localhost:6060/) and then review where the help text is lacking. * Implement each command. * Next: folder commands - MODIFY * Messages edit: CHANGE, REPLY * Moving messages: COPY, MOVE * Mark messages: MARK, UNMARK * ~~Compound commands: SET and SHOW - make HELP work for them.~~ * Mail: MAIL, FORWARD, RESPOND * Review each command and fully implement it. * Missing [MAIL] [RESPOND] [SET DEFAULT_EXPIRE] [SET READNEW] [SET NOREADNEW] [SET NOSHOWNEW] [SET NOPROMPT_EXPIRE] [SET EXPIRE_LIMIT] [SET PROMPT_EXPIRE] [SET SHOWNEW] [SHOW NEW] [SEARCH] * Run this.Skew.Safe() before... each command? each write? * Handle broadcast messages - have bulletin watch a directory and display files from it. Then have them delete the file if it's older than 5 minutes (allow for failure) * Handle broadcast messages - create a broadcast table and add an expiration column. * Database * trigger to limit values for 'visibility'? * Add commands: * ~~A way to add / delete ssh keys.~~ * Commands for a local mail system? * Commands to connect to Mattermost or mastodon? * ~~Commands to manage users.~~ * Make a spreadsheet for signups. * Make sure ssh key is fully unique. Done: * ~~Editor - need an embedded editor~~ Implemented using tview; good enough * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/) * [gkilo](https://github.com/vcnovaes/gkilo) * This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no) * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning * Another option is tview - [simpler](https://github.com/rivo/tview). * ~~Implement batch jobs~~ * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * ~~Add a pager~~ * ~~SHOW VERSION~~ * ~~Check db version; notify user if it changes; refuse to write to db if it has.~~ * ~~this.Folder should be a storage.Folder.~~ * ~~Add some of the early announcements from the sources - see the conversion branch - to the GENERAL folder.~~ * ~~Move to a storage layer.~~ * ~~Cleanup help output.~~ * ~~Remove the node/cluster/newsgroup/mailing-list related flags.~~ * ~~Remove BBOARD references.~~ * ~~format with `par w72j1`~~ * ~~Handle MARK for SELECT and DIRECTORY.~~ * ~~Remove all file related things. Which means no need for most (all?) /EDIT flags~~ * ~~Stop the seeded messages from being deleted by the expire batch command.~~ Polishing. Review each command and put a + next to each as it is fully done. Top level: ADD +BACK BULLETIN CHANGE COPY CREATE +Ctrl-C +CURRENT DELETE DIRECTORY +EXIT +FIRST +Folders FORWARD +HELP INDEX +Keypad +LAST MAIL +MARK MODIFY MOVE +NEXT PRINT +QUIT READ REMOVE REPLY RESPOND SEARCH SEEN SELECT +SET +SHOW +SSH +UNMARK UNSEEN +USER SET: ACCESS +ALWAYS BRIEF DEFAULT_EXPIRE EXPIRE_LIMIT FOLDER +NOALWAYS NOBRIEF NONOTIFY NOPROMPT_EXPIRE NOREADNEW NOSHOWNEW NOSYSTEM NOTIFY PROMPT_EXPIRE READNEW SHOWNEW SYSTEM SHOW: +FLAGS FOLDER -NEW +PRIVILEGES USER +VERSION SSH: +ADD +DELETE +FETCH +LIST USER: +ADD +ADMIN +DELETE +DISABLE +ENABLE LIST +MOD +NAME +NOADMIN +NOMOD ## Module links Loading
batch/batch.go +39 −8 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import ( "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/key" "git.lyda.ie/kevin/bulletin/storage" "git.lyda.ie/kevin/bulletin/users" "github.com/adrg/xdg" ) Loading Loading @@ -125,23 +126,53 @@ the first user.`) // Install crontab. bulletin, err := os.Executable() if err != nil { panic(err) // TODO: cleanup error handling. } ask.CheckErr(err) crontab := &strings.Builder{} template.Must(template.New("crontab").Parse(crontabTemplate)). Execute(crontab, map[string]string{"Bulletin": bulletin}) fmt.Printf("Adding this to crontab:\n\n%s\n", crontab.String()) err = installCrontab(crontab.String()) if err != nil { panic(err) // TODO: cleanup error handling. } ask.CheckErr(err) // Mark that install has happened. err = touch(touchfile) if err != nil { panic(err) // TODO: cleanup error handling. ask.CheckErr(err) return 0 } // NewUser creates a new user based on command line arguments. func NewUser(args []string) int { // Make sure we have enough args. if len(args) != 3 { fmt.Println("ERROR: Must supply login, site nickname and site username.") return 1 } // Create a user if missing. login := strings.ToUpper(args[0]) err := users.ValidLogin(login) ask.CheckErr(err) store, err := storage.Open() ask.CheckErr(err) q := storage.New(store.DB) ctx := storage.Context() u, err := q.GetUser(ctx, login) if u.Login == "" { u, err = q.AddUser(ctx, storage.AddUserParams{ Login: login, }) ask.CheckErr(err) } if u.Login == "" { fmt.Println("ERROR: Failed to make user.") return 1 } response := key.Fetch(u.Login, args[1], args[2]) fmt.Println(response) if strings.HasPrefix(response, "ERROR") { return 1 } return 0 }
key/key.go +34 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" "net/http" "os" "path" "strings" Loading Loading @@ -190,3 +191,36 @@ func Delete(public string) error { return nil } // Fetch fetches keys and adds them. func Fetch(login, nickname, username string) string { sites := map[string]string{ "codeberg": "https://codeberg.org/%s.keys", "gitlab": "https://gitlab.com/%s.keys", "github": "https://github.com/%s.keys", } site := sites[strings.ToLower(nickname)] if site == "" { return fmt.Sprintln("ERROR: site nickname unknown.") } url := fmt.Sprintf(site, username) resp, err := http.Get(url) if err != nil { return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err) } scanner := bufio.NewScanner(resp.Body) keys := 0 for scanner.Scan() { keyline := string(bytes.TrimSpace(scanner.Bytes())) Add(login, keyline) keys++ } switch keys { case 0: return fmt.Sprintln("No keys added.") case 1: return fmt.Sprintln("Key is added.") default: return fmt.Sprintf("%d keys added.\n", keys) } }
main.go +2 −0 Original line number Diff line number Diff line Loading @@ -49,6 +49,8 @@ func main() { exitcode = batch.Expire() case "install": exitcode = batch.Install() case "new-user": exitcode = batch.NewUser(cmd.Args().Slice()) default: fmt.Println("ERROR: can only run batch commands as SYSTEM.") exitcode = 1 Loading
repl/accounts.go +57 −32 Original line number Diff line number Diff line package repl import ( "bufio" "bytes" "fmt" "net/http" "strings" "git.lyda.ie/kevin/bulletin/ask" Loading @@ -23,12 +20,29 @@ func ActionUser(cmd *dclish.Command) error { // ActionUserAdd handles the `USER ADD` command. func ActionUserAdd(cmd *dclish.Command) error { ctx := storage.Context() login := strings.ToUpper(cmd.Args[0]) err := users.ValidLogin(login) u, err := users.ValidExistingLogin(this.Q, login) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } if u.Login != "" { fmt.Println("ERROR: User already exists.") return nil } u, err = this.Q.AddUser(ctx, storage.AddUserParams{ Login: login, Name: cmd.Args[1], }) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } if u.Login == "" { fmt.Println("ERROR: Failed to make user; unknown reason.") return nil } return nil } Loading Loading @@ -183,6 +197,35 @@ func ActionUserNomod(cmd *dclish.Command) error { return actionUserMod(cmd, 0, "not a moderator") } // ActionUserName handles the `USER LIST` command. func ActionUserName(cmd *dclish.Command) error { if len(cmd.Args) == 2 && this.User.Admin == 0 { fmt.Println("ERROR: You are not an admin.") return nil } login := this.User.Login name := cmd.Args[0] if len(cmd.Args) == 2 { login = strings.ToUpper(cmd.Args[0]) name = cmd.Args[1] _, err := users.ValidExistingLogin(this.Q, login) if err != nil { fmt.Printf("ERROR: %s.\n", err) return nil } } ctx := storage.Context() err := this.Q.UpdateUserName(ctx, storage.UpdateUserNameParams{ Login: login, Name: name, }) if err != nil { fmt.Printf("ERROR: Failed to update user name (%s).\n", err) } return nil } // ActionSSH handles the `SSH` command. func ActionSSH(cmd *dclish.Command) error { fmt.Println(cmd.Description) Loading Loading @@ -282,36 +325,18 @@ func ActionSSHDelete(cmd *dclish.Command) error { // ActionSSHFetch handles the `SSH FETCH` command. func ActionSSHFetch(cmd *dclish.Command) error { sites := map[string]string{ "codeberg": "https://codeberg.org/%s.keys", "gitlab": "https://gitlab.com/%s.keys", "github": "https://github.com/%s.keys", } siteTemplate := sites[strings.ToLower(cmd.Args[0])] if siteTemplate == "" { fmt.Println("ERROR: site nickname unknown.") return nil } site := fmt.Sprintf(siteTemplate, cmd.Args[1]) resp, err := http.Get(site) if err != nil { fmt.Printf("ERROR: Failed to fetch ssh keys (%s).\n", err) login := this.User.Login sitename := cmd.Args[0] username := cmd.Args[1] if len(cmd.Args) == 3 && this.User.Admin == 0 { fmt.Println("ERROR: You are not an admin.") return nil } scanner := bufio.NewScanner(resp.Body) keys := 0 for scanner.Scan() { keyline := string(bytes.TrimSpace(scanner.Bytes())) key.Add(this.User.Login, keyline) keys++ } switch keys { case 0: fmt.Println("No keys added.") case 1: fmt.Println("Key is added.") default: fmt.Printf("%d keys added.\n", keys) if len(cmd.Args) == 3 { login = cmd.Args[0] sitename = cmd.Args[1] username = cmd.Args[2] } fmt.Print(key.Fetch(login, sitename, username)) return nil }