Unverified Commit 008acd2f authored by Kevin Lyda's avatar Kevin Lyda
Browse files

A bunch of improvements

  * Better onboarding - can pull ssh keys from github-like sites.
  * Cleaned up help.
  * Reviewed command status.  42 need work.
parent 70424038
Loading
Loading
Loading
Loading
+37 −42
Original line number Diff line number Diff line
@@ -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

+39 −8
Original line number Diff line number Diff line
@@ -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"
)

@@ -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
}
+34 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"strings"
@@ -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)
	}
}
+2 −0
Original line number Diff line number Diff line
@@ -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
+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"
@@ -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
}

@@ -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)
@@ -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