ref: 1c0491dd109e2ade98c33741dbef530f80dd2c91
dir: /mastodon.go/
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, replyto mastodon.ID, currentuser mastodon.ID, postItem *mastodon.Status) (status *mastodon.Toot) {
toot := mastodon.Toot{
Status: posttext,
InReplyToID: replyto,
}
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 {
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.HTML2Text(account.Note)))
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")
}
}
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]
}