shithub: hell

ref: ec15ce45df576a510c469c83157986b42142a067
dir: /main.go/

View raw version
package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"time"

	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
	"github.com/k3a/html2text"
)

func main() {
	hc, err := NewHellclient()
	if err != nil {
		fmt.Printf("Error starting account: %v\n", err)
		return
	}

	rl := hc.rl
	client := *hc.client

	//Horrible io pipe hack
	//Replaces system stdout with the readline one
	r, w, _ := os.Pipe()
	os.Stdout = w

	go func() {
		io.Copy(rl.Stdout(), r)
	}()

	homeMap := hc.homeMap
	debugMap := hc.debugMap
	lastindex := ""
	recentpost := &hc.recentpost

	go StreamHomeTimeline(&client, homeMap, hc)

	for {
		func() {
			line, err := rl.Readline()
			//If we get an interupt error, we'll return to read the next line
			//If the next line is empty, exit the program
			//If it isn't empty, we were just clearing the line

			if err != nil {
				fmt.Println("Error:", err)
				return
			}
			command, arguments, found := processInput(line)

			//empty line
			if command == "" && arguments == "" {
				hc.togglepause()
				return
			}

			//if we didn't get a slash command then the user is just posting
			if command == "" && arguments != "" {
				hc.dispatchStatus(line, "public")
				return
			}

			if !found {
				fmt.Printf("Command not found: \"%s\"\n", command)
				return
			}

			hc.lock()
			defer hc.unlock()

			index, content, _ := strings.Cut(arguments, " ")

			postItem, postOK := homeMap[index]
			debugItem := debugMap[index]

			//Wether we got a post index or not
			var foundindex bool
			//If there's no index selected load the last post we operated on
			if postOK {
				foundindex = true
				lastindex = index
			} else {
				postItem, postOK = homeMap[lastindex]
			}

			// "." refers to most recently acted on status
			if !foundindex && index == "." {
				index = lastindex
			}

			//Okay now see if the post we end up with is a reblog
			if postOK {
				if postItem.Reblog != nil {
					postItem = postItem.Reblog
				}
			}

			//Contextual commands that need to handle their own requirements
			switch command {
			case "detach":
				hc.attacher.clearAttachments()
				hc.prompt.UpdatePrompt()
				return
			case "attach":
				if arguments == "" {
					arguments, err = pickFilename(hc.preferences.FilePicker)
				}
				if err != nil {
					fmt.Printf("File picking error: %s\n", err)
				}
				err := hc.attacher.uploadAttachment(arguments)
				if err != nil {
					fmt.Printf("Upload error: %s\n", err)
				}
				hc.prompt.UpdatePrompt()
				return
			case "help":
				fmt.Println(hyphenate(helpString(hc.configPath)))
				return
			case "reload":
				account, _, err := loadConfig()
				if err != nil {
					fmt.Printf("Error reloading config: %s\n", err)
					return
				}
				fmt.Println("Successfully reloaded preferences")
				hc.preferences = &account.Preferences
				return
			case "stats":
				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()))
				fmt.Print(sb.String())
				hc.stats.slock.Unlock()
				return
			case "prev":
				if hc.page != nil {
					hc.page.Prev()
					fmt.Print(hc.page.String())
				}
				return
			case "next":
				if hc.page != nil {
					hc.page.Next()
					fmt.Print(hc.page.String())
				}
				return
			case "bookmarks":
				hc.pause(true)
				hc.page = &Page{}
				getter := &BasicStatusGetter{getter: hc.client.GetBookmarks}
				hc.page.loader = &StatusPages{hc: hc, getter: getter}
				fmt.Print(hc.page.String())
				return
			case "likes":
				hc.pause(true)
				hc.page = &Page{}
				getter := &BasicStatusGetter{getter: hc.client.GetFavourites}
				hc.page.loader = &StatusPages{hc: hc, getter: getter}
				fmt.Print(hc.page.String())
				return
			case "notice":
				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 {
						fmt.Print(err)
					}
					return
				}
				hc.page = &Page{}
				hc.page.loader = &NotificationPages{hc: hc}
				fmt.Print(hc.page.String())
				if err != nil {
					fmt.Print(err)
				}
				return
			case "pause":
				hc.togglepause()
				return
			case "resume":
				hc.pause(false)
				return
			case "rm":
				if !foundindex && *recentpost != nil {
					deletefunc := func(job *GenericJob) {
						err = client.DeleteStatus(context.Background(), (*recentpost).ID)
					}
					deleteJob := hc.dispatchFunc(deletefunc)
					deleteJob.Wait()
					if err != nil {
						fmt.Println(err)
					}
					*recentpost = nil
					return
				}
				if !postOK {
					fmt.Println("No recent status to delete or post index not valid")
					return
				}
				deletefunc := func(job *GenericJob) {
					err = client.DeleteStatus(context.Background(), (*postItem).ID)
				}
				deleteJob := hc.dispatchFunc(deletefunc)
				deleteJob.Wait()
				if err != nil {
					fmt.Println(err)
				}
				return
			}

			if arguments == "" && !postOK {
				fmt.Printf("%v requires an argument\n", command)
				return
			}

			//Commands that don't take an index
			switch command {
			case "dm":
				hc.dispatchStatus(arguments, "direct")
				return
			}

			//Commands that accept debug indexes
			switch command {
			case "examine":
				if foundindex {
					hc.PrintObjectProperties(postItem)
					return
				}
				hc.PrintObjectProperties(debugItem)
				return
			}
			formatter := &StatusFormatter{prefs: hc.preferences, status: postItem, postContext: hc.ctxref, localindex: index}
			templater := newStatusTemplateRenderer(formatter)

			//Commands require status indexes
			switch command {
			case "cat":
				if !postOK {
					fmt.Println("cat: no valid status")
					return
				}
				if index == "" {
					index = lastindex
				}
				line, _ := templater.render("$standard_status\n$detail_line")
				fmt.Print(line)
				return
			case "like":
				_, err := client.Favourite(context.Background(), postItem.ID)
				if err != nil {
					printMastodonErr(err)
				} else {
					line, _ := templater.render("Favourited: $standard_status")
					fmt.Print(line)
				}
				return
			case "mark":
				_, err := client.Bookmark(context.Background(), postItem.ID)
				if err != nil {
					printMastodonErr(err)
				} else {
					line, _ := templater.render("Bookmarked: $index $username $content $media_descriptions\n")
					fmt.Print(line)
				}
				return
			case "unmark":
				postCopy, err := client.GetStatus(context.Background(), postItem.ID)
				if !postCopy.Bookmarked.(bool) && err != nil {
					fmt.Printf("Post not bookmarked.\n")
					return
				}
				_, err = client.Unbookmark(context.Background(), postItem.ID)
				if err != nil {
					printMastodonErr(err)
				} else {
					line, _ := templater.render("Unbookmarked: $index $username $content $media_descriptions\n")
					fmt.Print(line)
				}
				return
			case "open":
				url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)
				openItemInOS(hc.preferences.Browser, url)
				return
			case "url":
				_, indexstr, _ := strings.Cut(arguments, " ")
				urlindex, err := strconv.Atoi(indexstr)
				if err != nil {
					urlindex = 1
				}
				if urlindex > len(hc.homeref.urlmap[index]) {
					fmt.Printf("Bad url index\n")
					return
				}
				openItemInOS(hc.preferences.Browser, hc.homeref.urlmap[index][urlindex-1])
				return
			case "play":
				_, indexstr, _ := strings.Cut(arguments, " ")
				urlindex, err := strconv.Atoi(indexstr)
				if err != nil {
					urlindex = 1
				}
				if urlindex > len(hc.homeref.urlmap[index]) {
					fmt.Printf("Bad url index\n")
					return
				}
				openItemInOS(hc.preferences.MediaPlayer, hc.homeref.urlmap[index][urlindex-1])
				return
			case "view":
				err := hc.previewPostImages(postItem, hc.preferences.ImageViewer)
				if err != nil {
					fmt.Printf("Image preview failed: %v\n", err)
				}
				return
			case "import":
				err := hc.previewPostImages(postItem, hc.preferences.MediaImport)
				if err != nil {
					fmt.Printf("Image preview failed: %v\n", err)
				}
				return
			case "download":
				savePostImages(postItem, hc.preferences.Save_Location)
				return
			case "rt":
				rtStatus, err := client.Reblog(context.Background(), postItem.ID)
				if err != nil {
					fmt.Println(err)
					return
				}
				*recentpost = rtStatus
				hc.printAndIncrement(hc.ctxref, rtStatus)
				return
			case "parent":
				if postItem.InReplyToID == nil {
					fmt.Printf("%v doesn't have a parent\n", index)
					return
				}
				parentStatus, _ := client.GetStatus(context.Background(), mastodon.ID(postItem.InReplyToID.(string)))
				hc.printAndIncrement(hc.ctxref, parentStatus)
				return
			case "children":
				context, err := client.GetStatusContext(context.Background(), postItem.ID)
				if err != nil {
					fmt.Println(err)
					return
				}
				if len(context.Descendants) == 0 {
					fmt.Printf("\"%s\" has no children\n", index)
				}
				for post := range context.Descendants {
					hc.printAndIncrement(hc.ctxref, context.Descendants[post])
				}
				return
			case "edit":
				if content == "" || content == " " {
					fixedHTML, err := prepareForEdit(postItem)
					if err != nil {
						fmt.Printf("Error loading post HTML: %s\n", err)
						return
					}
					rl.SetDefault(fmt.Sprintf("/edit %v %v", index, html2text.HTML2Text(fixedHTML)))
					return
				}
				var MediaIDs []mastodon.ID
				for _, media := range postItem.MediaAttachments {
					MediaIDs = append(MediaIDs, media.ID)
				}
				toot := &mastodon.Toot{
					Status:      content,
					MediaIDs:    MediaIDs,
					Sensitive:   postItem.Sensitive,
					SpoilerText: postItem.SpoilerText,
					Visibility:  postItem.Visibility,
					Language:    postItem.Language,
				}
				if postItem.InReplyToID != nil {
					id := mastodon.ID(postItem.InReplyToID.(string))
					toot.InReplyToID = id
				}
				_, err = client.UpdateStatus(context.Background(), toot, postItem.ID)
				if err != nil {
					fmt.Println(err)
					return
				}
			case "thread":
				hc.pause(true)
				hc.page = &Page{disablereverse: true}
				getter := &ThreadStatusGetter{target: postItem, client: hc.client}
				hc.page.loader = &StatusPages{hc: hc, getter: getter}
				fmt.Print(hc.page.String())
				return
			case "pinned":
				var account *mastodon.Account
				if foundindex || index == "" {
					account = &postItem.Account
				} else {
					account = hc.resolveAccount(index)
					if account == nil {
						return
					}
				}

				hc.pause(true)
				hc.page = &Page{}
				hc.page.itembuffer = new([]PageItem)
				getter := &PinnedStatusGetter{client: hc.client, ID: account.ID}
				hc.page.loader = &StatusPages{hc: hc, getter: getter}
				fmt.Print(hc.page.String())
				return
			case "account":
				var account *mastodon.Account
				if foundindex || index == "" {
					account = &postItem.Account
				} else {
					account = hc.resolveAccount(index)
					if account == nil {
						return
					}
				}

				hc.pause(true)
				hc.page = &Page{}
				hc.page.itembuffer = new([]PageItem)
				getter := &AccountStatusGetter{client: hc.client, ID: account.ID}
				hc.page.loader = &StatusPages{hc: hc, getter: getter}
				*hc.page.itembuffer = append(*hc.page.itembuffer, makePageItem(hc.formatAccount(account)))
				(*hc.page.itembuffer)[0].lines = -1
				fmt.Print(hc.page.String())
				return
			case "unfollow":
				var account *mastodon.Account
				if foundindex || index == "" {
					account = &postItem.Account
				} else {
					account = hc.resolveAccount(index)
					if account == nil {
						return
					}
				}
				var relationship *mastodon.Relationship
				unfollowfunc := func(job *GenericJob) { relationship, err = hc.client.AccountUnfollow(context.Background(), account.ID) }
				unfollowjob := hc.dispatchFunc(unfollowfunc)
				unfollowjob.Wait()
				if err != nil {
					fmt.Printf("Error unfollowing account: %s\n", err)
					return
				}
				if !relationship.Following {
					fmt.Printf("Successfully unfollowed %s\n", index)
					return
				}
				fmt.Printf("Unknown failure, account is still followed\n")
				return

			case "follow":
				var account *mastodon.Account
				if foundindex || index == "" {
					account = &postItem.Account
				} else {
					account = hc.resolveAccount(index)
					if account == nil {
						return
					}
				}
				var relationship *mastodon.Relationship
				followfunc := func(job *GenericJob) { relationship, err = hc.client.AccountFollow(context.Background(), account.ID) }
				followjob := hc.dispatchFunc(followfunc)
				followjob.Wait()
				if err != nil {
					fmt.Printf("Error requesting follow: %s\n", err)
					return
				}
				if relationship.Following {
					fmt.Printf("Successfully followed %s\n", account.Acct)
					return
				}
				if relationship.Requested {
					fmt.Printf("Follow request sent to %s\n", account.Acct)
					return
				}
			case "fpost":
				_, err := hc.filterStatus(postItem)
				if err != nil {
					fmt.Printf("Error filtering post: %v\n", err)
					return
				}
				url := fmt.Sprintf("%v/statuses/%v", client.Config.Server, postItem.ID)
				fmt.Printf("Filtered %v\n", url)
				return
			case "ufpost":
				_, err := hc.unfilterStatus(postItem)
				if err != nil {
					fmt.Printf("Error unfiltering post: %v\n", err)
					return
				}
				line, _ := templater.render("Unfiltered: %s> $username $content $media_descriptions\n")
				fmt.Print(line)

				return
			}

			//Posts that need an index and an argument
			if content == "" {
				fmt.Printf("\"%v\" requires an argument\n", command)
				return
			}

			switch command {
			case "reply":
				hc.dispatchReply(content, postItem.ID, postItem)
				return
			}

		}()
	}
}