ref: ec63ec1b6056822aace571109c568921ff239ee3
dir: /main.go/
package main
import (
"context"
"fmt"
"io"
"os"
"runtime/debug"
"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 "version":
if buildInfo, ok := debug.ReadBuildInfo(); ok {
fmt.Printf("%+v\n", buildInfo)
return
}
fmt.Printf("No version information available.")
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 "read":
defer hc.prompt.UpdatePrompt()
notifications, err := hc.GetUnreadNotifications()
if err != nil {
fmt.Print(err)
}
if len(notifications) > 0 {
err = hc.SetNotificationsRead(notifications[len(notifications)-1].ID)
if err != nil {
fmt.Print(err)
}
}
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 "translate":
if !postOK {
fmt.Println("translate: no valid status")
return
}
if index == "" {
index = lastindex
}
translated, err := client.TranslateStatus(context.Background(), postItem.ID)
if err != nil {
fmt.Printf("Translation error: %s\n", err)
return
}
hc.PrintObjectProperties(translated)
case "like":
likefunc := func() {
_, err = client.Favourite(context.Background(), postItem.ID)
}
hc.dispatchAnon(likefunc).Wait()
if err != nil {
printMastodonErr(err)
} else {
line, _ := templater.render("Favourited: $standard_status")
fmt.Print(line)
}
return
case "mark":
markfunc := func() {_, err = client.Bookmark(context.Background(), postItem.ID)}
hc.dispatchAnon(markfunc).Wait()
if err != nil {
printMastodonErr(err)
} else {
line, _ := templater.render("Bookmarked: $index $username $content $media_descriptions\n")
fmt.Print(line)
}
return
case "unmark":
var postCopy *mastodon.Status
unmarkfunc := func() {
postCopy, err = client.GetStatus(context.Background(), postItem.ID)
}
hc.dispatchAnon(unmarkfunc).Wait()
if err != nil {
fmt.Printf("Error removing bookmark: %s\n", err)
return
}
if !postCopy.Bookmarked.(bool) {
fmt.Printf("Post not bookmarked.\n")
return
}
hc.dispatchAnon(func() {_, err = client.Unbookmark(context.Background(), postItem.ID)}).Wait()
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":
rtfunc := func() {
rtStatus, err := client.Reblog(context.Background(), postItem.ID)
if err != nil {
fmt.Println(err)
return
}
*recentpost = rtStatus
hc.printAndIncrement(hc.ctxref, rtStatus)
return
}
hc.dispatchAnon(rtfunc).Wait()
return
case "parent":
parentfunc := func() {
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
}
hc.dispatchAnon(parentfunc).Wait()
return
case "children":
childfunc := func() {
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
}
hc.dispatchAnon(childfunc).Wait()
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
}
editfunc := func() {
_, err = client.UpdateStatus(context.Background(), toot, postItem.ID)
if err != nil {
fmt.Println(err)
return
}
}
hc.dispatchAnon(editfunc).Wait()
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
}
}()
}
}