ref: aad6a455b839e261efb64881838e4bd9c4efc562
dir: /main.go/
package main
import (
"context"
"fmt"
"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
enablePipeHack(rl)
homeMap := hc.homeMap
debugMap := hc.debugMap
lastindex := ""
recentpost := &hc.recentpost
go StreamHomeTimeline(&client, homeMap, hc)
for {
func() {
line, err := rl.Readline()
command, arguments, found := processInput(line)
//empty line
if command == "" && arguments == "" && err == nil {
hc.togglepause()
return
}
if command == "" && arguments == "" && err != nil {
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, " ")
// "." refers to most recently acted on status
if index == "." {
index = lastindex
}
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]
}
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
}
}
accByNameOrRef := func() (account *mastodon.Account) {
if foundindex || index == "" {
account = &postItem.Account
} else {
account = hc.resolveAccount(index)
if account == nil {
return
}
}
return
}
//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)
return
}
err := hc.attacher.uploadAttachment(arguments)
if err != nil {
fmt.Printf("Upload error: %s\n", err)
return
}
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 "page":
if hc.page != nil {
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
case "dm":
if arguments != "" {
hc.dispatchStatus(arguments, "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
case "home":
hc.page = &Page{}
getter := &BasicStatusGetter{getter: hc.client.GetTimelineHome}
hc.page.loader = &StatusPages{hc: hc, getter: getter}
fmt.Print(hc.page.String())
hc.pause(true)
return
case "local":
hc.page = &Page{}
localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
return hc.client.GetTimelinePublic(ctx, true, pg)
}
getter := &BasicStatusGetter{getter: localAPI}
hc.page.loader = &StatusPages{hc: hc, getter: getter}
fmt.Print(hc.page.String())
hc.pause(true)
return
case "public":
hc.page = &Page{}
localAPI := func(ctx context.Context, pg *mastodon.Pagination) ([]*mastodon.Status, error) {
return hc.client.GetTimelinePublic(ctx, false, pg)
}
getter := &BasicStatusGetter{getter: localAPI}
hc.page.loader = &StatusPages{hc: hc, getter: getter}
fmt.Print(hc.page.String())
hc.pause(true)
return
}
if arguments == "" && !postOK {
fmt.Printf("%v requires an argument\n", command)
return
}
//Commands that don't take an index
switch command {
}
//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("$index $display_name $username_full $content $media_descriptions\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 "unlike":
likefunc := func() {
_, err = client.Unfavourite(context.Background(), postItem.ID)
}
hc.dispatchAnon(likefunc).Wait()
if err != nil {
printMastodonErr(err)
} else {
line, _ := templater.render("Unfavourited: $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 "hrt":
// We want to filter RTs from the RTer
if reblogger != nil {
postItem = reblogger
}
account := accByNameOrRef()
if account == nil {
fmt.Printf("Account lookup failed.\n")
return
}
relationships, err := hc.client.GetAccountRelationships(context.Background(), []string{string(account.ID)})
if err != nil {
fmt.Printf("Error loading relationship.\n")
return
}
relationship := relationships[0]
if !relationship.Following {
fmt.Printf("can't filter rts from user you don't follow!\n")
return
}
if relationship.ShowingReblogs {
_, err := hc.client.AccountFollowDetailed(context.Background(), account.ID, true, relationship.Notifying)
if err != nil {
fmt.Printf("Error updating settings\n")
return
}
fmt.Printf("No longer showing RTs from <%s>\n", account.Acct)
return
}
//Turn them back on if they were off!
_, err = hc.client.AccountFollowDetailed(context.Background(), account.ID, false, relationship.Notifying)
if err != nil {
fmt.Printf("Error updating settings\n")
return
}
fmt.Printf("Now showing RTs from <%s>\n", account.Acct)
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 == " " {
if (postItem == nil) || postItem.Account.ID != hc.currentuser.ID {
fmt.Printf("cannot edit other's statuses!\n")
return
}
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.HTML2TextWithOptions(fixedHTML, html2text.WithUnixLineBreaks())))
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":
account := accByNameOrRef()
if account == nil {
fmt.Printf("Account lookup failed.\n")
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":
account := accByNameOrRef()
if account == nil {
fmt.Printf("Account lookup failed.")
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":
account := accByNameOrRef()
if account == nil {
fmt.Printf("Account lookup failed.\n")
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> $standard_or_subject\n")
fmt.Print(line)
return
case "block":
var account *mastodon.Account
if foundindex || index == "" {
account = &postItem.Account
} else {
account = hc.resolveAccount(index)
if account == nil {
fmt.Printf("Account or index not found.\n")
return
}
}
blockfunc := func() {
_, err := client.AccountBlock(context.Background(), account.ID)
if err != nil {
fmt.Printf("Error blocking account: %s.\n")
return
}
fmt.Printf("Account blocked: %s\n", account.Acct)
}
hc.dispatchAnon(blockfunc).Wait()
return
case "unblock":
var account *mastodon.Account
if foundindex || index == "" {
account = &postItem.Account
} else {
account = hc.resolveAccount(index)
if account == nil {
fmt.Printf("Account or index not found.\n")
return
}
}
unblockfunc := func() {
_, err := client.AccountBlock(context.Background(), account.ID)
if err != nil {
fmt.Printf("Error blocking account: %s.\n")
return
}
fmt.Printf("Account unblocked: %s\n", account.Acct)
}
hc.dispatchAnon(unblockfunc).Wait()
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.SpoilerText, postItem.ID, postItem)
return
}
}()
}
}