shithub: hell

ref: 1389447da976e939392467abbf3420fc14851f15
dir: /mastodon.go/

View raw version
package main

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"net/url"
	"strings"

	"github.com/k3a/html2text"
	"github.com/mattn/go-mastodon"
	"golang.org/x/net/html"
)

func ConfigureClient() *mastodon.Client {
	appConfig := &mastodon.AppConfig{
		Server:       "https://eldritch.cafe",
		ClientName:   "hellclient",
		Scopes:       "read write follow",
		Website:      "https://github.com/mattn/go-mastodon",
		RedirectURIs: "urn:ietf:wg:oauth:2.0:oob",
	}

	app, err := mastodon.RegisterApp(context.Background(), appConfig)
	if err != nil {
		log.Fatal(err)
	}

	// Have the user manually get the token and send it back to us
	u, err := url.Parse(app.AuthURI)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Open your browser to \n%s\n and copy/paste the given authroization code\n", u)
	var userAuthorizationCode string
	fmt.Print("Paste the code here:")
	fmt.Scanln(&userAuthorizationCode)

	config := &mastodon.Config{
		Server:       "https://eldritch.cafe",
		ClientID:     app.ClientID,
		ClientSecret: app.ClientSecret,
	}

	// Create the client
	c := mastodon.NewClient(config)

	// Exchange the User authentication code with an access token, that can be used to interact with the api on behalf of the user
	err = c.GetUserAccessToken(context.Background(), userAuthorizationCode, app.RedirectURI)
	if err != nil {
		log.Fatal(err)
	}

	return c
}

func initClient(account *account) *mastodon.Client {

	clientID := account.MASTODON_CLIENT_ID
	clientSecret := account.MASTODON_CLIENT_ID
	accessToken := account.MASTODON_ACCESS_TOKEN
	url := account.URL

	config := &mastodon.Config{
		Server:       url,
		ClientID:     clientID,
		ClientSecret: clientSecret,
		AccessToken:  accessToken,
	}

	c := mastodon.NewClient(config)
	return c
}

func (hc *Hellclient) renderStatus(content string, index string) string {
	doc, err := html.Parse(strings.NewReader(content))
	if err != nil {
		log.Fatal(err)
	}

	//clear out the url map
	hc.urlMap[index] = []string{}

	for node := range doc.Descendants() {
		if node.Data == "a" && node.Type == html.ElementNode {
			ismention := false
			href := ""
			for attr := range node.Attr {
				if node.Attr[attr].Key == "class" && strings.Contains(node.Attr[attr].Val, "mention") {
					node.Data = "div"
					ismention = true
					continue
				}
				if node.Attr[attr].Key == "href" {
					href = node.Attr[attr].Val
				}
			}
			if !ismention {
				hc.urlMap[index] = append(hc.urlMap[index], href)
				refnode := &html.Node{
					Type: html.TextNode,
					Data: fmt.Sprintf(" [%v]", len(hc.urlMap[index]))}
				if node.Parent != nil {
					node.Parent.InsertBefore(refnode, node.NextSibling)
				}
			}
		}
	}

	//Rip off the HTML body the parser made for us
	for node := range doc.Descendants() {
		if node.Data == "body" {
			node.Type = html.DocumentNode
			doc = node
		}
	}

	var rendered bytes.Buffer
	err = html.Render(&rendered, doc)

	if err != nil {
		log.Fatal(err)
	}
	return rendered.String()
}

func processStatusHints(toot *mastodon.Toot, postpointer *string) {
	posttext := *postpointer
	posttext, hints := extractInputParameters(posttext)
	toot.Status = posttext

	for _, arg := range hints {
		key := arg[0]
		val := arg[1]
		switch key {
		case "unlisted":
			toot.Visibility = "unlisted"
		case "public":
			toot.Visibility = "public"
		case "followers":
			toot.Visibility = "private"
		case "direct":
			toot.Visibility = "direct"
		case "subject":
			if len(val) > 0 {
				toot.SpoilerText = val
			}
		case "sensitive":
			toot.Sensitive = true
		}
	}
	*postpointer = posttext
}

func postReply(posttext string, client mastodon.Client, replyto mastodon.ID, currentuser mastodon.ID, postItem *mastodon.Status) (status *mastodon.Status, err error) {
	toot := mastodon.Toot{
		Status:      posttext,
		InReplyToID: replyto,
	}

	toot.Visibility = postItem.Visibility

	processStatusHints(&toot, &posttext)

	if currentuser == postItem.Account.ID {
		status, err = postStatusDetailed(client, toot)
		return
	}
	var sb strings.Builder
	sb.WriteString("@")
	sb.WriteString(getUserString(postItem))
	sb.WriteString(" ")
	sb.WriteString(posttext)

	toot.Status = sb.String()
	status, err = postStatusDetailed(client, toot)
	return
}

func postStatus(posttext string, client mastodon.Client, visibility string) (status *mastodon.Status, err error) {
	// Post a toot
	toot := mastodon.Toot{
		Status:     posttext,
		Visibility: visibility,
	}

	processStatusHints(&toot, &posttext)

	status, err = postStatusDetailed(client, toot)
	return
}

func postStatusDetailed(client mastodon.Client, toot mastodon.Toot) (status *mastodon.Status, err error) {

	status, err = client.PostStatus(context.Background(), &toot)
	return
}

func getUserString(post *mastodon.Status) string {
	return post.Account.Acct
}

func formatAccount(account *mastodon.Account) string {
	var sb strings.Builder

	sb.WriteString(fmt.Sprintf("%v\n", account.DisplayName))
	sb.WriteString(fmt.Sprintf("%v <%v>\n", account.Username, account.Acct))
	sb.WriteString(fmt.Sprintf("Posts: %v Followers: %v Following: %v\n", account.StatusesCount, account.FollowersCount, account.FollowingCount))
	sb.WriteString(fmt.Sprintf("Created %v\n", account.CreatedAt))
	sb.WriteString(fmt.Sprintf("Locked: %v\n", account.Locked))
	sb.WriteString(fmt.Sprintf("%v\n\n", html2text.HTML2Text(account.Note)))

	return sb.String()
}
// Spaces before prefixes (no space if you're not passing a prefix)
func (hc *Hellclient) formatReblog(post *mastodon.Status, index string) string {
	reblogString := fmt.Sprintf(" <%v> Reblogged", post.Account.Username)
	return hyphenate(hc.formatStatusDetailed(post.Reblog, index, reblogString))
}

func (hc *Hellclient) formatWithPrefix(post *mastodon.Status, index string, prefix string) string {
	postString := fmt.Sprintf("%v %v>", prefix, index)
	return hyphenate(hc.formatStatusDetailed(post, "", postString))
}
func (hc *Hellclient) formatFavorite(post *mastodon.Status, index string) string {
	favString := fmt.Sprintf("Favorited: %v", index)
	return hyphenate(hc.formatStatusDetailed(post, "", favString))
}

func (hc *Hellclient) formatBookmark(post *mastodon.Status, index string) string {
	favString := fmt.Sprintf("Bookmarked: %v", index)
	return hyphenate(hc.formatStatusDetailed(post, "", favString))
}

func (hc *Hellclient) formatUnbookmark(post *mastodon.Status, index string) string {
	favString := fmt.Sprintf("Unbookmarked: %v", index)
	return hyphenate(hc.formatStatusDetailed(post, "", favString))
}

func (hc *Hellclient) formatStatus(post *mastodon.Status, index string) string {
	return hc.formatStatusDetailed(post, index, " ")
}

func (hc *Hellclient) formatStatusDetailed(post *mastodon.Status, index string, prefix string) string {
	renderedPost := hc.renderStatus(post.Content, index)
	return fmt.Sprintf("%v>%v <%v> %v", index, prefix, post.Account.Username, html2text.HTML2Text(renderedPost))
}

func (hc *Hellclient) formatEdit(post *mastodon.Status, index string) string {
	editString := fmt.Sprintf(" <%v> EDITED:", post.Account.Username)
	return hyphenate(hc.formatStatusDetailed(post, index, editString))
}

func printMastodonErr(err error) {
	fmt.Printf("\r%w\n", err)
}

func (hc *Hellclient) printPost(postref string, post *mastodon.Status) *mastodon.Status {
	return hc.printPostDetailed(postref, post, "")
}

func (hc *Hellclient) printPostDetailed(postref string, post *mastodon.Status, prefix string) *mastodon.Status {
	post, plaintext := hc.RenderPostPlaintext(post, postref, prefix)
	fmt.Println(hyphenate(plaintext))
	return post
}

func (hc *Hellclient) RenderPostPlaintext(post *mastodon.Status, postref string, prefix string) (selectedPost *mastodon.Status, plaintext string) {
	poststring := ""
	postfix := ""
	var media []mastodon.Attachment
	if post.Reblog != nil {
		poststring = hc.formatReblog(post, postref)
		selectedPost = post.Reblog
		media = post.Reblog.MediaAttachments
	} else {
		poststring = hc.formatStatusDetailed(post, postref, prefix)
		selectedPost = post
		media = post.MediaAttachments
	}

	for _, item := range media {
		if item.Description != "" {
			postfix += fmt.Sprintf("\n🖼️[%v]", item.Description)
			continue
		}
		postfix += "🖼️"
	}

	plaintext = fmt.Sprintf("%v %v", poststring, postfix)
	return
}

func StreamHomeTimeline(client *mastodon.Client, postMap map[string]*mastodon.Status, hc *Hellclient) {

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	eventCh, err := client.StreamingUser(ctx)
	if err != nil {
		log.Fatalf("Error starting user stream: %v\n", err)
	}

	initReferenceSystem()

	postref := "a"
	plaintext := ""
	idmap := make(map[mastodon.ID]*mastodon.Status)

	// Enter a loop to continuously listen for events from the event channel.
	for {
		select {
		case event, ok := <-eventCh: // Read from the event channel, checking 'ok' for closure

			if !ok {
				// The channel was closed, which indicates the stream has ended.
				fmt.Println("Stream closed.\n")
				return // Exit the function
			}
			func() {
				hc.lock()
				defer hc.unlock()
				switch post := event.(type) {
				case *mastodon.UpdateEvent:
					if hc.isPaused {
						currentPostRef := postref
						capturedPost := post
						hc.actionBuffer = append(hc.actionBuffer, func() {
							capturedPost.Status = hc.printPost(currentPostRef, capturedPost.Status)
						})
					} else {
						post.Status = hc.printPost(postref, post.Status)
					}
					saveRef(postMap, post.Status, postref)
					idmap[post.Status.ID] = post.Status
					postref = IncrementString(postref)

				case *mastodon.UpdateEditEvent:
					if hc.isPaused {
						currentPostRef := postref
						capturedPost := post
						hc.actionBuffer = append(hc.actionBuffer, func() {
							fmt.Println(hc.formatEdit(capturedPost.Status, currentPostRef))
						})
					} else {
						fmt.Println(hc.formatEdit(post.Status, postref))
					}
					saveRef(postMap, post.Status, postref)
					postref = IncrementString(postref)
					idmap[post.Status.ID] = post.Status

				case *mastodon.DeleteEvent:
					deleted, ok := idmap[post.ID]
					//didn't have this in the cache
					if !ok {
						capturedID := post.ID
						if hc.isPaused {
							hc.actionBuffer = append(hc.actionBuffer, func() {
								fmt.Printf("Deleted: ID %v\n", capturedID)
							})
						} else {
							fmt.Printf("Deleted: ID %v\n", capturedID)
						}
						return
					}
					if hc.isPaused {
						hc.actionBuffer = append(hc.actionBuffer, func() {
							hc.printPostDetailed("", deleted, "Deleted:")
						})
					} else {
						hc.printPostDetailed("", deleted, "Deleted:")
					}
					return

				case *mastodon.NotificationEvent:
					if post.Notification.Status == nil {
						notification := fmt.Sprintf("Notification [%v] from <%v>\n", post.Notification.Type, post.Notification.Account.Acct)
						if hc.isPaused {
							hc.actionBuffer = append(hc.actionBuffer, func() {
								fmt.Printf(hyphenate(notification))
							})
						} else {
							fmt.Printf(hyphenate(notification))
						}
						return
					}
					_, plaintext = hc.RenderPostPlaintext(post.Notification.Status, postref, "")
					notification := fmt.Sprintf("Notification [%v] from <%v>: %v\n", post.Notification.Type, post.Notification.Account.Acct, plaintext)
					if hc.isPaused {
						hc.actionBuffer = append(hc.actionBuffer, func() {
							fmt.Printf(hyphenate(notification))
						})
					} else {
						fmt.Printf(hyphenate(notification))
					}
					saveRef(postMap, post.Notification.Status, postref)
					postref = IncrementString(postref)
				default:
					// Catch any other unexpected event types.
					unhandledEvent := event
					if hc.isPaused {
						hc.actionBuffer = append(hc.actionBuffer, func() {
							fmt.Printf("Unhandled event: %T\n", unhandledEvent)
						})
					} else {
						fmt.Printf("Unhandled event: %T\n", unhandledEvent)
					}
				}
			}()
		}
	}
}