shithub: hell

ref: 4a4ffec0868b56ce4dcea82999d7ba6fd9f01e91
dir: /commands.go/

View raw version
package main

import (
	"fmt"
	"runtime/debug"
	"strings"
	"time"

	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
)

var commands = []string{"examine", "reply", "like", "thread", "open", "prev", "download", "dm", "rt", "hrt", "parent", "children", "rm", "mark", "unmark", "account", "import", "pause", "resume", "url", "fpost", "ufpost", "edit", "notice", "stats", "next", "view", "bookmarks", "follow", "unfollow", "likes", "help", "reload", "attach", "detach", "pinned", "cat", "play", "translate", "read", "version", "local", "public", "block", "unblock", "unlike", "home", "page", "profile"}

func processInput(input string, commands []string) (command string, arguments string, found bool) {

	if input == "" {
		command = ""
		arguments = ""
		return
	}

	if input[0] != '/' {
		command = ""
		arguments = input
		return
	}

	inputcommand, _, hasargument := strings.Cut(input[1:], " ")

	if !hasargument {
		inputcommand = input[1:]
	}

	for _, choice := range commands {
		if strings.HasPrefix(choice, inputcommand) {
			command = choice
			_, arguments, _ = strings.Cut(input, " ")
			found = true
			break
		}
	}

	if command == "" {
		command = inputcommand
	}

	return
}

func pullParameter(input string) (parameter string, remainder string) {
	if strings.HasPrefix(input, "?") {
		parameter, _, _ = strings.Cut(input, " ")
		if len(parameter) == len(input) {
			remainder = ""
			parameter = input
		} else {
			remainder = input[len(parameter)+1:]
		}
	} else {
		return "", input
	}

	prefix, _, cut := strings.Cut(parameter, "\"")

	if cut {
		quote, _, _ := strings.Cut(input[len(prefix)+1:], "\"")
		parameter = prefix + quote
		if len(parameter)+3 <= len(input) {
			remainder = input[len(parameter)+3:]
		} else {
			remainder = input[len(parameter)+2:]
		}
	}
	return
}

func splitParameter(input string) (parameter []string) {
	if len(input) < 2 {
		//nothing here
		return nil
	}
	input = input[1:]
	key, value, _ := strings.Cut(input, "=")
	return []string{key, value}
}

func extractInputParameters(input string) (remainder string, parameters [][]string) {
	parameter := " "
	remainder = input
	for parameter != "" {
		parameter, remainder = pullParameter(remainder)
		if parameter != " " && parameter != "" {
			parameters = append(parameters, splitParameter(parameter))
		}
	}
	return
}

type cmdflag uint8

const (
	free cmdflag = 1 << iota
	status
	account
	argument
)

type cmdloader struct {
	hc          *Hellclient
	lastindex   string
	lastaccount *mastodon.Account
	commands    []string
	cmdmap      map[string]cmder
}

func (loader *cmdloader) init(cmds []cmder) {
	loader.cmdmap = make(map[string]cmder)
	for i, _ := range cmds {
		name := cmds[i].name()
		loader.commands = append(loader.commands, name)
		loader.cmdmap[name] = cmds[i]
	}
}

// Cases
// Return loaded account if tag was found or was empty but loaded
// Return loaded account if index was empty
// Load lastaccount if it exists or try to look one up, return account or nil
func (data *cmddata) lookupAccount(loader *cmdloader) *mastodon.Account {

	if data.found_index {
		return data.account
	}
	if data.index == "" && data.account != nil {
		return data.account
	}

	account := loader.lastaccount
	if account == nil {
		account = loader.hc.resolveAccount(data.index)
		if account == nil {
			return nil
		}
	}

	loader.lastaccount = account
	loader.lastindex = ""
	return account
}

func (loader *cmdloader) run(line string) (string, error) {
	data, err := loader.processLine(line)
	if err != nil {
		return "", nil
	}
	if !data.cmd_found {
		return "", fmt.Errorf("Command %s not found.\n", data.command)
	}
	return loader.cmdmap[data.command].result(data), nil
}

func (loader *cmdloader) processLine(line string) (*cmddata, error) {
	command, arguments, found := processInput(line, loader.commands)
	index, content, _ := strings.Cut(arguments, " ")
	// "." refers to most recently acted on status
	// but if we have an account set, we don't want to set an index!
	var dot_index bool
	if index == "." {
		dot_index = true
	}
	if index == "." && loader.lastaccount == nil {
		index = loader.lastindex
	}

	postItem, postOK := loader.hc.homeMap[index]
	foundindex := false
	//If there's no index selected load the last post we operated on
	if postOK {
		foundindex = true
		loader.lastindex = index
		loader.lastaccount = nil
	} else {
		postItem, postOK = loader.hc.homeMap[loader.lastindex]
	}
	var reblogger *mastodon.Status
	//Okay now see if the post we end up with is a reblog
	if postOK {
		if postItem.Reblog != nil {
			reblogger = postItem
			postItem = postItem.Reblog
		}
	}

	cmdctx := &cmddata{
		command:      command,
		content:      content,
		raw_argument: arguments,
		dot_index:    dot_index,
		found_index:  foundindex,
		index:        index,
		cmd_found:    found,
	}
	if loader.lastaccount != nil {
		cmdctx.account = loader.lastaccount
	}
	if postOK {
		cmdctx.status = postItem
		cmdctx.account = &postItem.Account
		cmdctx.reblogger = reblogger

	}

	return cmdctx, nil
}

type cmddata struct {
	status       *mastodon.Status
	account      *mastodon.Account
	reblogger    *mastodon.Status
	raw_argument string
	command      string
	cmd_found    bool
	content      string
	found_index  bool
	dot_index    bool
	index        string
}

// Cmder interface and post data
type cmd struct {
	data  *cmddata
	cmder cmder
}

type cmder interface {
	name() string
	flags() cmdflag
	result(data *cmddata) string
}

func (cmd *cmd) String() string {
	return cmd.cmder.result(cmd.data)
}

// return an error message if cmddata matches cmders flags
func (cmd *cmd) checkReqs() (err error) {
	if cmd.cmder.flags()&free != 0 {
		return nil
	}
	if (cmd.cmder.flags()&status != 0) && (cmd.cmder.flags()&account != 0) {
		if cmd.data.status == nil && cmd.data.account == nil {
			return fmt.Errorf("%s requires a status or an account", cmd.cmder.name())
		}
	} else {
		if (cmd.cmder.flags()&status != 0) && (cmd.data.status == nil) {
			return fmt.Errorf("%s requires a status", cmd.cmder.name())
		}
		if (cmd.cmder.flags()&account != 0) && (cmd.data.account == nil) {
			return fmt.Errorf("%s requires an account", cmd.cmder.name())
		}
	}

	if (cmd.cmder.flags()&argument != 0) && (cmd.data.raw_argument == "") {
		return fmt.Errorf("%s requires an argument", cmd.cmder.name())
	}

	return nil
}

type dmcmd struct {
	*Hellclient
}

func (cmd *dmcmd) name() string {
	return "dm"
}

func (cmd *dmcmd) flags() cmdflag {
	return argument
}

func (cmd *dmcmd) result(data *cmddata) string {
	hc := cmd.Hellclient
	if data.raw_argument != "" {
		hc.dispatchStatus(data.raw_argument, "direct")
		return ""
	}
	hc.page = &Page{}
	getter := &BasicStatusGetter{getter: hc.client.GetTimelineDirect}
	hc.page.loader = &StatusPages{hc: hc, getter: getter}
	fmt.Print(hc.page.String())
	hc.pause(true)
	return ""
}

type profilecmd struct {
	*Hellclient
	*cmdloader
}

func (cmd *profilecmd) name() string {
	return "profile"
}

func (cmd *profilecmd) flags() cmdflag {
	return free
}

func (cmd *profilecmd) result(data *cmddata) string {
	hc := cmd.Hellclient
	account := data.lookupAccount(cmd.cmdloader)
	if account == nil {
		return fmt.Sprintf("Account lookup failed.\n")
	}
	return fmt.Sprint(hc.formatAccount(account))
}

type helpcmd struct {
	*Hellclient
}

func (cmd *helpcmd) name() string {
	return "help"
}

func (cmd *helpcmd) flags() cmdflag {
	return free
}

func (cmd *helpcmd) result(data *cmddata) string {
	return fmt.Sprintln(hyphenate(helpString(cmd.configPath)))
}

type reloadcmd struct {
	*Hellclient
}

func (cmd *reloadcmd) flags() cmdflag {
	return free
}

func (cmd *reloadcmd) name() string {
	return "reload"
}

func (cmd *reloadcmd) result(data *cmddata) string {
	account, _, err := loadConfig()
	if err != nil {
		return fmt.Sprintf("Error reloading config: %s\n", err)
	}
	cmd.preferences = &account.Preferences
	return fmt.Sprintln("Successfully reloaded preferences")

}

type detachcmd struct {
	*Hellclient
}

func (cmd *detachcmd) flags() cmdflag {
	return free
}

func (cmd *detachcmd) name() string {
	return "detach"
}

func (cmd *detachcmd) result(data *cmddata) string {
	cmd.attacher.clearAttachments()
	cmd.prompt.UpdatePrompt()
	return ""
}

type attachcmd struct {
	*Hellclient
}

func (cmd *attachcmd) flags() cmdflag {
	return free
}

func (cmd *attachcmd) name() string {
	return "attach"
}

func (cmd *attachcmd) result(data *cmddata) string {
	hc := cmd.Hellclient
	var err error
	filename := data.content
	if filename == "" {
		filename, err = pickFilename(hc.preferences.FilePicker)
	}
	if err != nil {
		return fmt.Sprintf("File picking error: %s\n", err)
	}
	err = hc.attacher.uploadAttachment(filename)
	if err != nil {
		return fmt.Sprintf("Upload error: %s\n", err)
	}
	hc.prompt.UpdatePrompt()
	return ""
}

type statscmd struct {
	*Hellclient
}

func (cmd *statscmd) flags() cmdflag {
	return free
}

func (cmd *statscmd) name() string {
	return "stats"
}

func (cmd *statscmd) result(data *cmddata) string {
	hc := cmd.Hellclient
	hc.stats.slock.Lock()
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("API Calls: %d\n", hc.stats.APICalls))
	sb.WriteString(fmt.Sprintf("Statuses Received: %d\n", hc.stats.IncomingStatuses))
	sb.WriteString("Statuses per hour:")
	sb.WriteString(fmt.Sprintf("%.2f\n", float32(hc.stats.IncomingStatuses)/(float32(time.Since(hc.stats.StartedTime))/float32(time.Hour))))
	sb.WriteString(fmt.Sprintf("Started At: %s\n", hc.stats.StartedTime))
	timeSince := time.Since(hc.stats.StartedTime)
	timeSince = timeSince.Round(time.Second)
	sb.WriteString(fmt.Sprintf("Runtime: %s\n", timeSince.String()))
	hc.stats.slock.Unlock()
	return sb.String()
}

type basiccmd struct {
	hc     *Hellclient
	bname  string
	bflags cmdflag
	doer   func(*cmddata) string
}

func (cmd *basiccmd) flags() cmdflag {
	return cmd.bflags
}

func (cmd *basiccmd) name() string {
	return cmd.bname
}

func (cmd *basiccmd) result(data *cmddata) string {
	return cmd.doer(data)
}

func (hc *Hellclient) versioncmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "version"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		if buildInfo, ok := debug.ReadBuildInfo(); ok {
			return fmt.Sprintf("%+v\n", buildInfo)
		}
		return fmt.Sprintf("No version information available.")
	}
	return cmd
}

func (hc *Hellclient) prevcmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "prev"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		if hc.page != nil {
			hc.page.Prev()
			return fmt.Sprint(hc.page.String())
		}
		return fmt.Sprintf("No page loaded")
	}
	return cmd
}

func (hc *Hellclient) nextcmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "next"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		if hc.page != nil {
			hc.page.Next()
			return hc.page.String()
		}
		return fmt.Sprintf("No page loaded")
	}
	return cmd
}

func (hc *Hellclient) pagecmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "page"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		if hc.page != nil {
			return hc.page.String()
		}
		return fmt.Sprintf("No page loaded")
	}
	return cmd
}

func (hc *Hellclient) bookmarkscmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "bookmarks"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		hc.pause(true)
		hc.page = &Page{}
		getter := &BasicStatusGetter{getter: hc.client.GetBookmarks}
		hc.page.loader = &StatusPages{hc: hc, getter: getter}
		return hc.page.String()
	}
	return cmd
}

func (hc *Hellclient) readcmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "read"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		defer hc.prompt.UpdatePrompt()
		notifications, err := hc.GetUnreadNotifications()
		if err != nil {
			return fmt.Sprintf("%s\n", err)
		}
		if len(notifications) > 0 {
			err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
			if err != nil {
				return fmt.Sprintf("%s\n", err)
			}
		}
		return ""
	}
	return cmd
}

func (hc *Hellclient) noticecmd() cmder {
	cmd := &basiccmd{}
	cmd.hc = hc
	cmd.bname = "notice"
	cmd.bflags = free
	cmd.doer = func(data *cmddata) string {
		defer hc.prompt.UpdatePrompt()
		defer hc.pause(true)
		notifications, err := hc.GetUnreadNotifications()
		if len(notifications) > 0 {
			hc.PrintNotifications(notifications)
			err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
			if err != nil {
				return fmt.Sprintf("%s\n", err)
			}
			return ""
		}
		hc.page = &Page{}
		hc.page.loader = &NotificationPages{hc: hc}
		return hc.page.String()
	}
	return cmd
}

func (hc *Hellclient) newCmdArray() []cmder {
	cmdarray := []cmder{
		&profilecmd{hc, hc.cmdload},
		&dmcmd{hc},
		&helpcmd{hc},
		&reloadcmd{hc},
		&detachcmd{hc},
		&attachcmd{hc},
		&statscmd{hc},
		hc.versioncmd(),
		hc.prevcmd(),
		hc.nextcmd(),
		hc.pagecmd(),
		hc.bookmarkscmd(),
		hc.readcmd(),
		hc.noticecmd(),
	}
	return cmdarray
}