Unverified Commit 17583418 authored by Kevin Lyda's avatar Kevin Lyda
Browse files

Implement SEARCH and finish PRINT

parent 99381ba8
Loading
Loading
Loading
Loading
+6 −3
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@ 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.
  * 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]
  * Missing [RESPOND] [MAIL] [SET PROMPT_EXPIRE] [SET NOREADNEW] [SET NOSHOWNEW] [SET EXPIRE_LIMIT] [SET NOPROMPT_EXPIRE] [SET READNEW] [SET SHOWNEW] [SET DEFAULT_EXPIRE] [SHOW NEW]
  * Run this.Skew.Safe() before... each command?  each write?
  * Handle broadcast messages - create a broadcast table and add an expiration column.
  * Database
@@ -30,6 +30,9 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
    * Commands for a local mail system?
    * Commands to connect to Mattermost or mastodon?
  * Make a spreadsheet for signups.
  * Pager:
    * Make / work for search.
  * Run [VACUUM](https://www.sqlite.org/lang_vacuum.html) on the expire run.

Polishing.  Review each command and put a + next to each as it is
fully done.
@@ -40,8 +43,8 @@ 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
  MAIL      +MARK       MODIFY     MOVE      +NEXT      +PRINT
 +QUIT      +READ       REMOVE    +REPLY      RESPOND   +SEARCH
  SEEN       SELECT    +SET       +SHOW      +SSH       +UNMARK
  UNSEEN    +USER

+4 −0
Original line number Diff line number Diff line
@@ -90,6 +90,10 @@ func FindFolder(name string) storage.Folder {
// IsFolderAccess checks if a user can access a folder.
func IsFolderAccess(name, login string) bool {
	ctx := storage.Context()
	admin, _ := this.Q.IsUserAdmin(ctx, login)
	if admin == 1 {
		return true
	}
	found, _ := this.Q.IsFolderAccess(ctx, storage.IsFolderAccessParams{
		Name:  name,
		Login: login,
+2 −1
Original line number Diff line number Diff line
@@ -916,8 +916,9 @@ header. If a "search-string" is not specified, a search is made using
the previously specified string, starting with the message following the
one  you are  currently reading  (or have  just read).  Once started,  a
search can be aborted by typing a CTRL-C.`,
		MinArgs: 1,
		MinArgs: 0,
		MaxArgs: 1,
		Action:  ActionSearch,
		Flags: dclish.Flags{
			"/FOLDER": {
				Description: `/FOLDER=(folder,[...])
+169 −3
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import (
	"git.lyda.ie/kevin/bulletin/editor"
	"git.lyda.ie/kevin/bulletin/folders"
	"git.lyda.ie/kevin/bulletin/pager"
	"git.lyda.ie/kevin/bulletin/storage"
	"git.lyda.ie/kevin/bulletin/this"
)

@@ -101,7 +102,6 @@ func ActionAdd(cmd *dclish.Command) error {
		optExtract = 1
	}
	if cmd.Flags["/FOLDER"].Value != "" {
		fmt.Printf("/FOLDER = %s\n", cmd.Flags["/FOLDER"].Value)
		optFolder = strings.Split(cmd.Flags["/FOLDER"].Value, ",")
	}
	if cmd.Flags["/INDENT"].Value == "true" {
@@ -253,14 +253,24 @@ func ActionNext(_ *dclish.Command) error {

// ActionPrint handles the `PRINT` command.
func ActionPrint(cmd *dclish.Command) error {
	// TODO: handle flags.
	all := false
	if cmd.Flags["/ALL"].Value == "true" {
		all = true
	}

	ctx := storage.Context()
	msgids := []int64{this.MsgID}
	if len(cmd.Args) == 1 {
	var err error
	if len(cmd.Args) == 1 {
		if all {
			return errors.New("Can't provide a message list and /ALL")
		}
		msgids, err = ParseNumberList(cmd.Args[0])
		if err != nil {
			return err
		}
	} else if all {
		msgids, err = this.Q.ListMessageIDs(ctx, this.Folder.Name)
	}
	print("\033[5i")
	for _, msgid := range msgids {
@@ -453,3 +463,159 @@ func ActionUnmark(cmd *dclish.Command) error {
	}
	return nil
}

// ActionSearch handles the `SEARCH` command.  This will show all messages
// matching a search term.
//
// See subtoutines SEARCH and GET_SEARCH in bulletin2.for for the
// original implementation.
func ActionSearch(cmd *dclish.Command) error {
	ctx := storage.Context()
	var err error
	optFolders := []string{this.Folder.Name}
	if cmd.Flags["/FOLDER"].Value != "" {
		optFolders = strings.Split(strings.ToUpper(cmd.Flags["/FOLDER"].Value), ",")
		for i := range optFolders {
			folder, _ := this.Q.FindFolderExact(ctx, optFolders[i])
			if folder.Name != "" {
				return fmt.Errorf("Folder '%s' not found", optFolders[i])
			}
			if folders.IsFolderAccess(optFolders[i], this.User.Login) {
				return fmt.Errorf("Folder '%s' is not accessible", optFolders[i])
			}
		}
	}

	optReply := false
	if cmd.Flags["/REPLY"].Value == "true" {
		optReply = true
	}

	optReverse := false
	if cmd.Flags["/REVERSE"].Value == "true" {
		optReverse = true
	}

	optStart := int64(-1) // -1 means first message.
	if optReverse {
		optStart = 0 // 0 means last message.
	}
	if cmd.Flags["/START"].Set {
		optStart, err = strconv.ParseInt(cmd.Flags["/START"].Value, 10, 64)
		if err != nil {
			return err
		}
		if optStart < 1 {
			return errors.New("/START must be 1 or larger")
		}
	}

	optSubject := false
	if cmd.Flags["/SUBJECT"].Value == "true" {
		optSubject = true
	}

	var searchTerm string
	if optReply {
		if optSubject || len(cmd.Args) == 1 {
			return errors.New("Can't specify /REPLY and a search term or /SUBJECT")
		}
		msg, err := this.Q.ReadMessage(ctx, storage.ReadMessageParams{
			Folder: this.Folder.Name,
			ID:     this.MsgID,
		})
		if err != nil {
			return err
		}
		searchTerm = "Re: " + msg.Subject
	} else {
		searchTerm = cmd.Args[0]
	}

	allMsgs := []storage.Message{}
	msgs := []storage.Message{}
	var start int64
	for i := range optFolders {
		switch optStart {
		case -1:
			start = 1
		case 0:
			start, err := this.Q.LastMsgidIgnoringSeen(ctx, optFolders[i])
			if err != nil || start == 0 {
				continue
			}
		default:
			start = optStart
		}
		if optReply {
			if optReverse {
				msgs, err = this.Q.SearchReplyReverse(ctx,
					storage.SearchReplyReverseParams{
						Subject: searchTerm,
						ID:      start,
						Folder:  optFolders[i],
					})
			} else {
				msgs, err = this.Q.SearchReply(ctx,
					storage.SearchReplyParams{
						Subject: searchTerm,
						ID:      start,
						Folder:  optFolders[i],
					})
			}
		} else if optSubject {
			if optReverse {
				msgs, err = this.Q.SearchSubjectReverse(ctx,
					storage.SearchSubjectReverseParams{
						Column1: nullStr(searchTerm),
						ID:      start,
						Folder:  optFolders[i],
					})
			} else {
				msgs, err = this.Q.SearchSubject(ctx,
					storage.SearchSubjectParams{
						Column1: nullStr(searchTerm),
						ID:      start,
						Folder:  optFolders[i],
					})
			}
		} else {
			if optReverse {
				msgs, err = this.Q.SearchReverse(ctx,
					storage.SearchReverseParams{
						Column1: nullStr(searchTerm),
						ID:      start,
						Folder:  optFolders[i],
					})
			} else {
				msgs, err = this.Q.Search(ctx,
					storage.SearchParams{
						Column1: nullStr(searchTerm),
						ID:      start,
						Folder:  optFolders[i],
					})
			}
		}
		if err != nil {
			continue
		}
		if len(allMsgs)+len(msgs) > 100 {
			fmt.Println("Too many messages match; narrow search term.")
			return nil
		}
		allMsgs = append(allMsgs, msgs...)
	}

	if len(allMsgs) == 0 {
		fmt.Println("No messages found.")
		return nil
	}
	buf := strings.Builder{}
	for _, msg := range allMsgs {
		buf.WriteString(msg.String())
		buf.WriteString("\n\n")
	}
	pager.Pager(buf.String())

	return nil
}

repl/storage.go

0 → 100644
+10 −0
Original line number Diff line number Diff line
package repl

import "database/sql"

func nullStr(s string) sql.NullString {
	return sql.NullString{
		String: s,
		Valid:  true,
	}
}
Loading