printer/plainprinter.go
author rjp <zimpenfish@gmail.com>
Mon, 23 Jan 2023 16:39:02 +0000
changeset 267 5b91a65ba95a
parent 241 e77dad242f4c
child 268 4dd196a4ee7c
permissions -rw-r--r--
Update to handle non-int64 IDs Pleroma/Akkoma and GotoSocial use opaque IDs rather than `int64`s like Mastodon which means that `madon` can't talk to either of those. This commit updates everything that can be an ID to `madon.ActivityID` which is an alias for `string` - can't create a specific type for it since there's more than a few places where they're concatenated directly to strings for URLs, etc. Which means it could just as easily be a direct `string` type itself but I find that having distinct types can often make the code more readable and understandable. One extra bit is that `statusOpts` has grown a `_hasReplyTo` boolean to indicate whether the `--in-reply-to` flag was given or not because we can't distinguish because "empty because default" or "empty because given and empty". Another way around this would be to set the default to some theoretically impossible or unlikely string but you never know when someone might spin up an instance where, e.g., admin posts have negative integer IDs.

// Copyright © 2017-2018 Mikael Berthe <mikael@lilotux.net>
//
// Licensed under the MIT license.
// Please see the LICENSE file is this directory.

package printer

import (
	"fmt"
	"io"
	"os"
	"reflect"
	"time"

	"github.com/McKael/madon/v2"
	"github.com/McKael/madonctl/printer/html2text"
)

// PlainPrinter is the default "plain text" printer
type PlainPrinter struct {
	Indent      string
	NoSubtitles bool
}

// NewPrinterPlain returns a plaintext ResourcePrinter
// For PlainPrinter, the option parameter contains the indent prefix.
func NewPrinterPlain(options Options) (*PlainPrinter, error) {
	indentInc := "  "
	if i, ok := options["indent"]; ok {
		indentInc = i
	}
	return &PlainPrinter{Indent: indentInc}, nil
}

// PrintObj sends the object as text to the writer
// If the writer w is nil, standard output will be used.
// For PlainPrinter, the option parameter contains the initial indent.
func (p *PlainPrinter) PrintObj(obj interface{}, w io.Writer, initialIndent string) error {
	if w == nil {
		w = os.Stdout
	}
	switch o := obj.(type) {
	case []madon.Account, []madon.Attachment, []madon.Card, []madon.Context,
		[]madon.Emoji, []madon.Instance, []madon.InstancePeer,
		[]madon.List, []madon.Mention, []madon.Notification,
		[]madon.Relationship, []madon.Report, []madon.Results,
		[]madon.Status, []madon.StreamEvent, []madon.Tag,
		[]madon.WeekActivity, []madon.DomainName:
		return p.plainForeach(o, w, initialIndent)
	case *madon.DomainName:
		return p.plainPrintDomainName(o, w, initialIndent)
	case madon.DomainName:
		return p.plainPrintDomainName(&o, w, initialIndent)
	case *madon.Account:
		return p.plainPrintAccount(o, w, initialIndent)
	case madon.Account:
		return p.plainPrintAccount(&o, w, initialIndent)
	case *madon.Attachment:
		return p.plainPrintAttachment(o, w, initialIndent)
	case madon.Attachment:
		return p.plainPrintAttachment(&o, w, initialIndent)
	case *madon.Card:
		return p.plainPrintCard(o, w, initialIndent)
	case madon.Card:
		return p.plainPrintCard(&o, w, initialIndent)
	case *madon.Context:
		return p.plainPrintContext(o, w, initialIndent)
	case madon.Context:
		return p.plainPrintContext(&o, w, initialIndent)
	case *madon.Emoji:
		return p.plainPrintEmoji(o, w, initialIndent)
	case madon.Emoji:
		return p.plainPrintEmoji(&o, w, initialIndent)
	case *madon.Instance:
		return p.plainPrintInstance(o, w, initialIndent)
	case madon.Instance:
		return p.plainPrintInstance(&o, w, initialIndent)
	case *madon.InstancePeer:
		return p.plainPrintInstancePeer(o, w, initialIndent)
	case madon.InstancePeer:
		return p.plainPrintInstancePeer(&o, w, initialIndent)
	case *madon.List:
		return p.plainPrintList(o, w, initialIndent)
	case madon.List:
		return p.plainPrintList(&o, w, initialIndent)
	case *madon.Notification:
		return p.plainPrintNotification(o, w, initialIndent)
	case madon.Notification:
		return p.plainPrintNotification(&o, w, initialIndent)
	case *madon.Relationship:
		return p.plainPrintRelationship(o, w, initialIndent)
	case madon.Relationship:
		return p.plainPrintRelationship(&o, w, initialIndent)
	case *madon.Report:
		return p.plainPrintReport(o, w, initialIndent)
	case madon.Report:
		return p.plainPrintReport(&o, w, initialIndent)
	case *madon.Results:
		return p.plainPrintResults(o, w, initialIndent)
	case madon.Results:
		return p.plainPrintResults(&o, w, initialIndent)
	case *madon.Status:
		return p.plainPrintStatus(o, w, initialIndent)
	case madon.Status:
		return p.plainPrintStatus(&o, w, initialIndent)
	case *madon.UserToken:
		return p.plainPrintUserToken(o, w, initialIndent)
	case madon.UserToken:
		return p.plainPrintUserToken(&o, w, initialIndent)
	case *madon.WeekActivity:
		return p.plainPrintWeekActivity(o, w, initialIndent)
	case madon.WeekActivity:
		return p.plainPrintWeekActivity(&o, w, initialIndent)
	}
	// TODO: Mention
	// TODO: StreamEvent
	// TODO: Tag

	return fmt.Errorf("PlainPrinter not yet implemented for %T (try json or yaml...)", obj)
}

func (p *PlainPrinter) plainForeach(ol interface{}, w io.Writer, ii string) error {
	switch reflect.TypeOf(ol).Kind() {
	case reflect.Slice:
		s := reflect.ValueOf(ol)

		for i := 0; i < s.Len(); i++ {
			o := s.Index(i).Interface()
			if err := p.PrintObj(o, w, ii); err != nil {
				return err
			}
		}
	}
	return nil
}

func html2string(h string) string {
	t, err := html2text.Textify(h)
	if err == nil {
		return t
	}
	return h // Failed: return initial string
}

// unix2time convert a UNIX timestamp to a time.Time
func unix2time(ts interface{}) (time.Time, error) {
	switch t := ts.(type) {
	case int64:
		return time.Unix(t, 0), nil
	case int:
		return time.Unix(int64(t), 0), nil
	case float64:
		return time.Unix(int64(t), 0), nil
	}
	return time.Time{}, fmt.Errorf("invalid timestamp type")
}

func indentedPrint(w io.Writer, indent string, title, skipIfEmpty bool, label string, format string, args ...interface{}) {
	prefix := indent
	if title {
		prefix += "- "
	} else {
		prefix += "  "
	}
	value := fmt.Sprintf(format, args...)
	if !title && skipIfEmpty && len(value) == 0 {
		return
	}
	fmt.Fprintf(w, "%s%s: %s\n", prefix, label, value)
}

func (p *PlainPrinter) plainPrintDomainName(d *madon.DomainName, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Domain Name", "%s", string(*d))
	return nil
}

func (p *PlainPrinter) plainPrintAccount(a *madon.Account, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Account ID", "%s (%s)", a.ID, a.Username)
	indentedPrint(w, indent, false, false, "User ID", "%s", a.Acct)
	indentedPrint(w, indent, false, false, "Display name", "%s", a.DisplayName)
	indentedPrint(w, indent, false, false, "Creation date", "%v", a.CreatedAt.Local())
	indentedPrint(w, indent, false, false, "URL", "%s", a.URL)
	indentedPrint(w, indent, false, false, "Statuses count", "%d", a.StatusesCount)
	indentedPrint(w, indent, false, false, "Followers count", "%d", a.FollowersCount)
	indentedPrint(w, indent, false, false, "Following count", "%d", a.FollowingCount)
	if a.Locked {
		indentedPrint(w, indent, false, false, "Locked", "%v", a.Locked)
	}
	if a.Bot {
		indentedPrint(w, indent, false, false, "Bot", "%v", a.Bot)
	}
	indentedPrint(w, indent, false, true, "User note", "%s", html2string(a.Note)) // XXX too long?
	if a.Moved != nil {
		m := a.Moved
		indentedPrint(w, indent+p.Indent, true, false, "Moved to account ID", "%s (%s)", m.ID, m.Username)
		indentedPrint(w, indent+p.Indent, false, false, "New user ID", "%s", m.Acct)
		indentedPrint(w, indent+p.Indent, false, false, "New display name", "%s", m.DisplayName)
	}
	if a.Source != nil {
		s := a.Source
		if s.Privacy != nil {
			indentedPrint(w, indent, false, true, "Default Privacy", "%s", *s.Privacy)
		}
		if s.Language != nil {
			indentedPrint(w, indent, false, true, "Default Language", "%s", *s.Language)
		}
		if s.Sensitive != nil {
			indentedPrint(w, indent, false, true, "Sensitive by default", "%v", *s.Sensitive)
		}
	}
	if a.Fields != nil {
		for _, f := range *a.Fields {
			indentedPrint(w, indent, false, false, ". Metadata field",
				"[%s] » %s", f.Name, html2string(f.Value))
		}
	}
	return nil
}

func (p *PlainPrinter) plainPrintAttachment(a *madon.Attachment, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Attachment ID", "%s", a.ID)
	indentedPrint(w, indent, false, false, "Type", "%s", a.Type)
	indentedPrint(w, indent, false, false, "Local URL", "%s", a.URL)
	if a.RemoteURL != nil {
		indentedPrint(w, indent, false, true, "Remote URL", "%s", *a.RemoteURL)
	}
	indentedPrint(w, indent, false, true, "Preview URL", "%s", a.PreviewURL)
	if a.TextURL != nil {
		indentedPrint(w, indent, false, true, "Text URL", "%s", *a.TextURL)
	}
	if a.Description != nil {
		indentedPrint(w, indent, false, true, "Description", "%s", *a.Description)
	}
	return nil
}

func (p *PlainPrinter) plainPrintCard(c *madon.Card, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Card title", "%s", c.Title)
	indentedPrint(w, indent, false, true, "Description", "%s", c.Description)
	indentedPrint(w, indent, false, true, "URL", "%s", c.URL)
	indentedPrint(w, indent, false, true, "Image", "%s", c.Image)
	return nil
}

func (p *PlainPrinter) plainPrintContext(c *madon.Context, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Context", "%d relative(s)", len(c.Ancestors)+len(c.Descendants))
	if len(c.Ancestors) > 0 {
		indentedPrint(w, indent, false, false, "Ancestors", "")
		p.PrintObj(c.Ancestors, w, indent+p.Indent)
	}
	if len(c.Descendants) > 0 {
		indentedPrint(w, indent, false, false, "Descendants", "")
		p.PrintObj(c.Descendants, w, indent+p.Indent)
	}
	return nil
}

func (p *PlainPrinter) plainPrintEmoji(e *madon.Emoji, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Emoji shortcode", "%s", e.ShortCode)
	indentedPrint(w, indent, false, false, "URL", "%s", e.URL)
	return nil
}

func (p *PlainPrinter) plainPrintInstance(i *madon.Instance, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Instance title", "%s", i.Title)
	indentedPrint(w, indent, false, true, "Description", "%s", html2string(i.Description))
	indentedPrint(w, indent, false, true, "URL", "%s", i.URI)
	indentedPrint(w, indent, false, true, "Email", "%s", i.Email)
	indentedPrint(w, indent, false, true, "Version", "%s", i.Version)
	if i.ContactAccount != nil {
		c := i.ContactAccount
		indentedPrint(w, indent+p.Indent, true, false, "Contact account ID", "%s (%s)", c.ID, c.Username)
		indentedPrint(w, indent+p.Indent, false, false, "Contact user ID", "%s", c.Acct)
		indentedPrint(w, indent+p.Indent, false, false, "Contact display name", "%s", c.DisplayName)
	}
	return nil
}

func (p *PlainPrinter) plainPrintInstancePeer(i *madon.InstancePeer, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Peer", "%s", *i)
	return nil
}

func (p *PlainPrinter) plainPrintList(l *madon.List, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "List ID", "%s", l.ID)
	indentedPrint(w, indent, false, false, "Title", "%s", l.Title)
	return nil
}

func (p *PlainPrinter) plainPrintNotification(n *madon.Notification, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Notification ID", "%s", n.ID)
	indentedPrint(w, indent, false, false, "Type", "%s", n.Type)
	indentedPrint(w, indent, false, false, "Timestamp", "%v", n.CreatedAt.Local())
	if n.Account != nil {
		indentedPrint(w, indent+p.Indent, true, false, "Account", "(%s) @%s - %s",
			n.Account.ID, n.Account.Acct, n.Account.DisplayName)
	}
	if n.Status != nil {
		p.plainPrintStatus(n.Status, w, indent+p.Indent)
	}
	return nil
}

func (p *PlainPrinter) plainPrintRelationship(r *madon.Relationship, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Account ID", "%s", r.ID)
	indentedPrint(w, indent, false, false, "Following", "%v", r.Following)
	//indentedPrint(w, indent, false, false, "Showing reblogs", "%v", r.ShowingReblogs)
	indentedPrint(w, indent, false, false, "Followed-by", "%v", r.FollowedBy)
	indentedPrint(w, indent, false, false, "Blocking", "%v", r.Blocking)
	indentedPrint(w, indent, false, false, "Muting", "%v", r.Muting)
	indentedPrint(w, indent, false, false, "Muting notifications", "%v", r.MutingNotifications)
	indentedPrint(w, indent, false, false, "Endorsed", "%v", r.Endorsed)
	indentedPrint(w, indent, false, false, "Requested", "%v", r.Requested)
	return nil
}

func (p *PlainPrinter) plainPrintReport(r *madon.Report, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Report ID", "%s", r.ID)
	indentedPrint(w, indent, false, false, "Action taken", "%s", r.ActionTaken)
	return nil
}

func (p *PlainPrinter) plainPrintResults(r *madon.Results, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Results", "%d account(s), %d status(es), %d hashtag(s)",
		len(r.Accounts), len(r.Statuses), len(r.Hashtags))
	if len(r.Accounts) > 0 {
		indentedPrint(w, indent, false, false, "Accounts", "")
		p.PrintObj(r.Accounts, w, indent+p.Indent)
	}
	if len(r.Statuses) > 0 {
		indentedPrint(w, indent, false, false, "Statuses", "")
		p.PrintObj(r.Statuses, w, indent+p.Indent)
	}
	if len(r.Hashtags) > 0 {
		indentedPrint(w, indent, false, false, "Hashtags", "")
		for _, tag := range r.Hashtags {
			indentedPrint(w, indent+p.Indent, true, false, "Tag", "%s", tag.Name)
			indentedPrint(w, indent+p.Indent, false, true, "URL", "%s", tag.URL)
		}
	}
	return nil
}

func (p *PlainPrinter) plainPrintStatus(s *madon.Status, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Status ID", "%s", s.ID)
	if s.Account != nil {
		author := s.Account.Acct
		if s.Account.DisplayName != "" {
			author += " (" + s.Account.DisplayName + ")"
		}
		indentedPrint(w, indent, false, false, "From", "%s", author)
	}
	if s.Pinned {
		indentedPrint(w, indent, false, false, "Pinned", "%v", s.Pinned)
	}
	if s.Visibility == "private" {
		indentedPrint(w, indent, false, false, "Private", "true")
	}
	indentedPrint(w, indent, false, false, "Timestamp", "%v", s.CreatedAt.Local())

	if s.Reblog != nil {
		if s.Reblog.Account != nil {
			indentedPrint(w, indent, false, false, "Reblogged from", "%s", s.Reblog.Account.Username)
		}
		return p.plainPrintStatus(s.Reblog, w, indent+p.Indent)
	}

	if s.Sensitive {
		indentedPrint(w, indent, false, false, "Sensitive (NSFW)", "%v", s.Sensitive)
	}

	indentedPrint(w, indent, false, false, "Contents", "%s", html2string(s.Content))
	if s.InReplyToID != nil && *s.InReplyToID != "" {
		indentedPrint(w, indent, false, false, "In-Reply-To", "%s", *s.InReplyToID)
	}
	if s.Reblogged {
		indentedPrint(w, indent, false, false, "Reblogged", "%v", s.Reblogged)
	}
	indentedPrint(w, indent, false, false, "URL", "%s", s.URL)
	// Display minimum details of attachments
	//return p.PrintObj(s.MediaAttachments, w, indent+p.Indent)
	for _, a := range s.MediaAttachments {
		indentedPrint(w, indent+p.Indent, true, false, "Attachment ID", "%s", a.ID)
		if a.TextURL != nil && *a.TextURL != "" {
			indentedPrint(w, indent+p.Indent, true, false, "Text URL", "%s", *a.TextURL)
		} else if a.URL != "" {
			indentedPrint(w, indent+p.Indent, false, false, "URL", "%s", a.URL)
		} else if a.RemoteURL != nil {
			indentedPrint(w, indent+p.Indent, false, false, "Remote URL", "%s", *a.RemoteURL)
		}
		if a.Description != nil && *a.Description != "" {
			indentedPrint(w, indent+p.Indent, false, true, "Description", "%s", *a.Description)
		}
	}
	return nil
}

func (p *PlainPrinter) plainPrintUserToken(s *madon.UserToken, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "User token", "%s", s.AccessToken)
	indentedPrint(w, indent, false, true, "Type", "%s", s.TokenType)
	if s.CreatedAt != 0 {
		indentedPrint(w, indent, false, true, "Timestamp", "%v", time.Unix(s.CreatedAt, 0))
	}
	indentedPrint(w, indent, false, true, "Scope", "%s", s.Scope)
	return nil
}

func (p *PlainPrinter) plainPrintWeekActivity(a *madon.WeekActivity, w io.Writer, indent string) error {
	indentedPrint(w, indent, true, false, "Activity week", "%v", a.Week.Format("2006-01-02"))
	indentedPrint(w, indent, false, true, "Weekly logins", "%d", a.Logins)
	indentedPrint(w, indent, false, true, "Weekly statuses", "%d", a.Statuses)
	indentedPrint(w, indent, false, true, "Weekly registrations", "%d", a.Registrations)
	return nil
}