shithub: hell

ref: 566d628e2c8e1a34fc4ab72a6c6a3be41071460b
dir: /mastodon.go/

View raw version
package main

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

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

func ConfigureClient() *mastodon.Client {
	appConfig := &mastodon.AppConfig{
		Server:       "",
		ClientName:   "hellclient",
		Scopes:       "read write follow",
		Website:      "https://hell.limitedideas.org/",
		RedirectURIs: "urn:ietf:wg:oauth:2.0:oob",
	}
	fmt.Print("Enter server URL:")
	fmt.Scanln(&appConfig.Server)
	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 *mastodon_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 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
		case "markdown":
			toot.ContentType = "text/markdown"
		case "html":
			toot.ContentType = "text/html"
		}
	}
	*postpointer = posttext
}

func postReply(posttext string, subject string, replyto mastodon.ID, currentuser mastodon.ID, postItem *mastodon.Status) (status *mastodon.Toot) {
	toot := mastodon.Toot{
		Status:      posttext,
		InReplyToID: replyto,
		SpoilerText: subject,
	}

	toot.Visibility = postItem.Visibility

	processStatusHints(&toot, &posttext)
	var sb strings.Builder
	if currentuser != postItem.Account.ID {
		sb.WriteString(fmt.Sprintf("@%s ", getUserString(postItem)))
	}

	for user := range postItem.Mentions {
		if currentuser != postItem.Mentions[user].ID {
			sb.WriteString(fmt.Sprintf("@%s ", postItem.Mentions[user].Acct))
		}
	}

	sb.WriteString(posttext)
	toot.Status = sb.String()
	return &toot
}

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

	processStatusHints(&toot, &posttext)

	return &toot
}

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 (hc *Hellclient) formatAccount(account *mastodon.Account) string {
	var sb strings.Builder

	sb.WriteString(fmt.Sprintf("%s\n", account.DisplayName))
	sb.WriteString(fmt.Sprintf("%s <%s>\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("%s\n\n", html2text.HTML2TextWithOptions(account.Note, html2text.WithUnixLineBreaks(), html2text.WithListSupport())))

	relationships, err := hc.client.GetAccountRelationships(context.Background(), []string{string(account.ID)})
	relationship := relationships[0]
	if err == nil {
		if relationship.Following {
			sb.WriteString("You follow them\n")
		}
		if relationship.FollowedBy {
			sb.WriteString("They follow you\n")
		}
		if relationship.Blocking {
			sb.WriteString("Account is blocked\n")
		}
		if relationship.Muting {
			sb.WriteString("Account is muted\n")
		}
		if relationship.Requested {
			sb.WriteString("Follow request pending\n")
		}
	}
	sb.WriteString("\n")
	accountstring, _ := hyphenate(sb.String())
	return accountstring
}

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

func (hc *Hellclient) RenderPostPlaintext(post *mastodon.Status, ref postref) (selectedPost *mastodon.Status, plaintext string) {
	if post.Reblog != nil {
		selectedPost = post.Reblog
		plaintext = "$standard_reblog"
	} else {
		selectedPost = post
		plaintext = "$standard_or_subject"
	}
	formatter := &StatusFormatter{prefs: hc.preferences, status: post, postContext: &ref}
	templater := newStatusTemplateRenderer(formatter)
	plaintext, _ = templater.render(plaintext)
	return selectedPost, plaintext
}

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()

	idmap := make(map[mastodon.ID]*mastodon.Status)
	readchan := hc.readMarkerUpdater()

	// Enter a loop to continuously listen for events from the event channel.
	for event := range eventCh {
		func() {
			hc.lock()
			defer hc.unlock()
			switch post := event.(type) {
			case *mastodon.UpdateEvent:
				//Count the statuses
				hc.stats.slock.Lock()
				hc.stats.IncomingStatuses++
				hc.stats.slock.Unlock()
				//Tell the timeline marker updater the most recent post
				readchan <- &post.Status.ID
				if hc.isPaused {
					currentPostRef := *hc.homeref
					capturedPost := post
					hc.actionBuffer = append(hc.actionBuffer, func() {
						_, plaintext := hc.RenderPostPlaintext(capturedPost.Status, currentPostRef)
						fmt.Print(plaintext)
					})
					justIncrementPostref(hc.homeref, post.Status)
					idmap[post.Status.ID] = post.Status
					return
				}
				hc.printAndIncrement(hc.homeref, post.Status)
				idmap[post.Status.ID] = post.Status
				return

			case *mastodon.UpdateEditEvent:
				//Count the statuses
				hc.stats.slock.Lock()
				hc.stats.IncomingStatuses++
				hc.stats.slock.Unlock()
				formatter := &StatusFormatter{prefs: hc.preferences, status: post.Status, postContext: hc.homeref}
				templater := newStatusTemplateRenderer(formatter)
				if hc.isPaused {
					hc.actionBuffer = append(hc.actionBuffer, func() {
						line, _ := templater.render("$username EDITED: $index $content $media_descriptions\n")
						fmt.Print(line)
					})
					justIncrementPostref(hc.homeref, post.Status)
					idmap[post.Status.ID] = post.Status
					return
				}
				line, _ := templater.render("$username EDITED: $index $content $media_descriptions\n")
				fmt.Print(line)
				justIncrementPostref(hc.homeref, post.Status)
				idmap[post.Status.ID] = post.Status
				return

			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
				}
				formatter := &StatusFormatter{prefs: hc.preferences, status: deleted, postContext: hc.homeref}
				templater := newStatusTemplateRenderer(formatter)
				if hc.isPaused {
					hc.actionBuffer = append(hc.actionBuffer, func() {
						line, _ := templater.render("Deleted: $standard_status")
						fmt.Print(line)
					})
				} else {
					line, _ := templater.render("Deleted: $standard_status")
					fmt.Print(line)
				}
				return

			case *mastodon.NotificationEvent:
				hc.prompt.UpdatePrompt()
				if post.Notification.Status == nil {
					if hc.isPaused {
						hc.actionBuffer = append(hc.actionBuffer, func() {
							hc.PrintReceivedNotification(post.Notification)
						})
					} else {
						hc.PrintReceivedNotification(post.Notification)
					}
					return
				}
				if hc.isPaused {
					hc.actionBuffer = append(hc.actionBuffer, func() {
						hc.PrintReceivedNotification(post.Notification)
					})
				} else {
					hc.PrintReceivedNotification(post.Notification)
				}
				justIncrementPostref(hc.homeref, post.Notification.Status)
			case *mastodon.ErrorEvent:
				fmt.Printf("Timeline error: %s\n", post)
			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)
				}
			}
		}()
	}
}

// Options are home and notifications
func (hc *Hellclient) updateReadMarker(ID *mastodon.ID, timeline string) {
	marker := &mastodon.Marker{
		Timeline: timeline,
		ID:       *ID,
	}
	var err error
	setmarker := func(job *GenericJob) {
		err = hc.client.SetTimelineMarkers(context.Background(), &[]mastodon.Marker{*marker})
	}
	job := hc.dispatchFunc(setmarker)
	job.Wait()
	if err != nil {
		fmt.Printf("Error: %s", err)
	}

}

// Periodically set the timeline read marker to the most recent status
// Currently this will leak if a client is destroyed
func (hc *Hellclient) readMarkerUpdater() (statuschan chan *mastodon.ID) {
	statuschan = make(chan *mastodon.ID)
	var ID *mastodon.ID
	var lastfire time.Time
	go func() {
		for {
			select {
			case ID = <-statuschan:
				continue
			case <-time.After((time.Minute * 4) - time.Since(lastfire)):
				lastfire = time.Now()
				if ID != nil {
					hc.updateReadMarker(ID, "home")
				}
			}
		}
	}()
	return
}

func (hc *Hellclient) resolveAccount(lookup string) *mastodon.Account {
	var accounts []*mastodon.Account
	var err error
	searchfunc := func(job *GenericJob) {
		accounts, err = hc.client.AccountsSearchResolve(context.Background(), lookup, 1, true)
	}
	searchjob := hc.dispatchFunc(searchfunc)
	searchjob.Wait()
	if err != nil {
		fmt.Printf("Error resolving account %s: %v\n", lookup, err)
		return nil
	}
	if len(accounts) < 1 {
		fmt.Printf("No account matched %s\n", lookup)
		return nil
	}
	return accounts[0]
}