feat: add support posting one line statuses #16

Manually merged
dananglin merged 1 commit from post-one-line-status into main 2024-05-30 18:48:34 +01:00
9 changed files with 322 additions and 36 deletions

View file

@ -1,6 +1,8 @@
package client package client
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -22,3 +24,37 @@ func (g *Client) GetStatus(statusID string) (model.Status, error) {
return status, nil return status, nil
} }
type CreateStatusForm struct {
Content string `json:"status"`
Language string `json:"language"`
SpoilerText string `json:"spoiler_text"`
Boostable bool `json:"boostable"`
Federated bool `json:"federated"`
Likeable bool `json:"likeable"`
Replyable bool `json:"replyable"`
Sensitive bool `json:"sensitive"`
ContentType model.StatusContentType `json:"content_type"`
Visibility model.StatusVisibility `json:"visibility"`
}
func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) {
data, err := json.Marshal(form)
if err != nil {
return model.Status{}, fmt.Errorf("unable to create the JSON form; %w", err)
}
requestBody := bytes.NewBuffer(data)
url := g.Authentication.Instance + "/api/v1/statuses"
var status model.Status
if err := g.sendRequest(http.MethodPost, url, requestBody, &status); err != nil {
return model.Status{}, fmt.Errorf(
"received an error after sending the request to create the status; %w",
err,
)
}
return status, nil
}

View file

@ -3,23 +3,32 @@ package executor
const ( const (
flagAccountName = "account-name" flagAccountName = "account-name"
flagBrowser = "browser" flagBrowser = "browser"
flagContentType = "content-type"
flagContent = "content" flagContent = "content"
flagEnableFederation = "enable-federation"
flagEnableLikes = "enable-likes"
flagEnableReplies = "enable-replies"
flagEnableReposts = "enable-reposts"
flagFrom = "from"
flagInstance = "instance" flagInstance = "instance"
flagLanguage = "language"
flagLimit = "limit" flagLimit = "limit"
flagListID = "list-id" flagListID = "list-id"
flagListTitle = "list-title" flagListTitle = "list-title"
flagListRepliesPolicy = "list-replies-policy" flagListRepliesPolicy = "list-replies-policy"
flagMyAccount = "my-account" flagMyAccount = "my-account"
flagNotify = "notify" flagNotify = "notify"
flagFrom = "from" flagSensitive = "sensitive"
flagType = "type"
flagSkipRelationship = "skip-relationship" flagSkipRelationship = "skip-relationship"
flagShowPreferences = "show-preferences" flagShowPreferences = "show-preferences"
flagShowReposts = "show-reposts" flagShowReposts = "show-reposts"
flagSpoilerText = "spoiler-text"
flagStatusID = "status-id" flagStatusID = "status-id"
flagTag = "tag" flagTag = "tag"
flagTimelineCategory = "timeline-category" flagTimelineCategory = "timeline-category"
flagTo = "to" flagTo = "to"
flagType = "type"
flagVisibility = "visibility"
resourceAccount = "account" resourceAccount = "account"
resourceBlocked = "blocked" resourceBlocked = "blocked"

View file

@ -12,9 +12,19 @@ type CreateExecutor struct {
*flag.FlagSet *flag.FlagSet
topLevelFlags TopLevelFlags topLevelFlags TopLevelFlags
boostable bool
federated bool
likeable bool
replyable bool
sensitive bool
content string
contentType string
language string
spoilerText string
resourceType string resourceType string
listTitle string listTitle string
listRepliesPolicy string listRepliesPolicy string
visibility string
} }
func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor { func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor {
@ -24,6 +34,16 @@ func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor
topLevelFlags: tlf, topLevelFlags: tlf,
} }
createExe.BoolVar(&createExe.boostable, flagEnableReposts, true, "specify if the status can be reposted/boosted by others")
createExe.BoolVar(&createExe.federated, flagEnableFederation, true, "specify if the status can be federated beyond the local timelines")
createExe.BoolVar(&createExe.likeable, flagEnableLikes, true, "specify if the status can be liked/favourited")
createExe.BoolVar(&createExe.replyable, flagEnableReplies, true, "specify if the status can be replied to")
createExe.BoolVar(&createExe.sensitive, flagSensitive, false, "specify if the status should be marked as sensitive")
createExe.StringVar(&createExe.content, flagContent, "", "the content of the status to create")
createExe.StringVar(&createExe.contentType, flagContentType, "plain", "the type that the contents should be parsed from (valid values are plain and markdown)")
createExe.StringVar(&createExe.language, flagLanguage, "", "the ISO 639 language code for this status")
createExe.StringVar(&createExe.spoilerText, flagSpoilerText, "", "the text to display as the status' warning or subject")
createExe.StringVar(&createExe.visibility, flagVisibility, "", "the visibility of the posted status")
createExe.StringVar(&createExe.resourceType, flagType, "", "specify the type of resource to create") createExe.StringVar(&createExe.resourceType, flagType, "", "specify the type of resource to create")
createExe.StringVar(&createExe.listTitle, flagListTitle, "", "specify the title of the list") createExe.StringVar(&createExe.listTitle, flagListTitle, "", "specify the title of the list")
createExe.StringVar(&createExe.listRepliesPolicy, flagListRepliesPolicy, "list", "specify the policy of the replies for this list (valid values are followed, list and none)") createExe.StringVar(&createExe.listRepliesPolicy, flagListRepliesPolicy, "list", "specify the policy of the replies for this list (valid values are followed, list and none)")
@ -44,7 +64,8 @@ func (c *CreateExecutor) Execute() error {
} }
funcMap := map[string]func(*client.Client) error{ funcMap := map[string]func(*client.Client) error{
resourceList: c.createList, resourceList: c.createList,
resourceStatus: c.createStatus,
} }
doFunc, ok := funcMap[c.resourceType] doFunc, ok := funcMap[c.resourceType]
@ -75,3 +96,64 @@ func (c *CreateExecutor) createList(gtsClient *client.Client) error {
return nil return nil
} }
func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
if c.content == "" {
return FlagNotSetError{flagText: flagContent}
}
var (
language string
visibility string
)
preferences, err := gtsClient.GetUserPreferences()
if err != nil {
fmt.Println("WARNING: Unable to get your posting preferences; %w", err)
}
if c.language != "" {
language = c.language
} else {
language = preferences.PostingDefaultLanguage
}
if c.visibility != "" {
visibility = c.visibility
} else {
visibility = preferences.PostingDefaultVisibility
}
parsedVisibility := model.ParseStatusVisibility(visibility)
if parsedVisibility == model.StatusVisibilityUnknown {
return InvalidStatusVisibilityError{Visibility: visibility}
}
parsedContentType := model.ParseStatusContentType(c.contentType)
if parsedContentType == model.StatusContentTypeUnknown {
return InvalidStatusContentTypeError{ContentType: c.contentType}
}
form := client.CreateStatusForm{
Content: c.content,
ContentType: parsedContentType,
Language: language,
SpoilerText: c.spoilerText,
Boostable: c.boostable,
Federated: c.federated,
Likeable: c.likeable,
Replyable: c.replyable,
Sensitive: c.sensitive,
Visibility: parsedVisibility,
}
status, err := gtsClient.CreateStatus(form)
if err != nil {
return fmt.Errorf("unable to create the status; %w", err)
}
fmt.Println("Successfully created the following status:")
fmt.Println(status)
return nil
}

View file

@ -53,3 +53,19 @@ type EmptyContentError struct{}
func (e EmptyContentError) Error() string { func (e EmptyContentError) Error() string {
return "content should not be empty" return "content should not be empty"
} }
type InvalidStatusVisibilityError struct {
Visibility string
}
func (e InvalidStatusVisibilityError) Error() string {
return "'" + e.Visibility + "' is an invalid status visibility (valid values are public, unlisted, private, mutuals_only and direct)"
}
type InvalidStatusContentTypeError struct {
ContentType string
}
func (e InvalidStatusContentTypeError) Error() string {
return "'" + e.ContentType + "' is an invalid status content type (valid values are plain and markdown)"
}

5
internal/model/const.go Normal file
View file

@ -0,0 +1,5 @@
package model
const (
unknownValue = "unknown"
)

View file

@ -26,7 +26,7 @@ func ParseListRepliesPolicy(policy string) (ListRepliesPolicy, error) {
return ListRepliesPolicyNone, nil return ListRepliesPolicyNone, nil
} }
return ListRepliesPolicy(-1), errors.New("invalid list replies policy") return ListRepliesPolicy(-1), fmt.Errorf("%q is not a valid list replies policy", policy)
} }
func (l ListRepliesPolicy) String() string { func (l ListRepliesPolicy) String() string {
@ -43,12 +43,12 @@ func (l ListRepliesPolicy) String() string {
} }
func (l ListRepliesPolicy) MarshalJSON() ([]byte, error) { func (l ListRepliesPolicy) MarshalJSON() ([]byte, error) {
str := l.String() value := l.String()
if str == "" { if value == "" {
return nil, errors.New("invalid list replies policy") return nil, errors.New("invalid list replies policy")
} }
return json.Marshal(str) return json.Marshal(value)
} }
func (l *ListRepliesPolicy) UnmarshalJSON(data []byte) error { func (l *ListRepliesPolicy) UnmarshalJSON(data []byte) error {

View file

@ -36,7 +36,7 @@ type Status struct {
Text string `json:"text"` Text string `json:"text"`
URI string `json:"uri"` URI string `json:"uri"`
URL string `json:"url"` URL string `json:"url"`
Visibility string `json:"visibility"` Visibility StatusVisibility `json:"visibility"`
} }
type Card struct { type Card struct {
@ -82,34 +82,34 @@ type PollOption struct {
} }
type StatusReblogged struct { type StatusReblogged struct {
Account Account `json:"account"` Account Account `json:"account"`
Application Application `json:"application"` Application Application `json:"application"`
Bookmarked bool `json:"bookmarked"` Bookmarked bool `json:"bookmarked"`
Card Card `json:"card"` Card Card `json:"card"`
Content string `json:"content"` Content string `json:"content"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Emojis []Emoji `json:"emojis"` Emojis []Emoji `json:"emojis"`
Favourited bool `json:"favourited"` Favourited bool `json:"favourited"`
FavouritesCount int `json:"favourites_count"` FavouritesCount int `json:"favourites_count"`
ID string `json:"id"` ID string `json:"id"`
InReplyToAccountID string `json:"in_reply_to_account_id"` InReplyToAccountID string `json:"in_reply_to_account_id"`
InReplyToID string `json:"in_reply_to_id"` InReplyToID string `json:"in_reply_to_id"`
Language string `json:"language"` Language string `json:"language"`
MediaAttachments []Attachment `json:"media_attachments"` MediaAttachments []Attachment `json:"media_attachments"`
Mentions []Mention `json:"mentions"` Mentions []Mention `json:"mentions"`
Muted bool `json:"muted"` Muted bool `json:"muted"`
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
Poll Poll `json:"poll"` Poll Poll `json:"poll"`
Reblogged bool `json:"reblogged"` Reblogged bool `json:"reblogged"`
RebloggsCount int `json:"reblogs_count"` RebloggsCount int `json:"reblogs_count"`
RepliesCount int `json:"replies_count"` RepliesCount int `json:"replies_count"`
Sensitive bool `json:"sensitive"` Sensitive bool `json:"sensitive"`
SpolierText string `json:"spoiler_text"` SpolierText string `json:"spoiler_text"`
Tags []Tag `json:"tags"` Tags []Tag `json:"tags"`
Text string `json:"text"` Text string `json:"text"`
URI string `json:"uri"` URI string `json:"uri"`
URL string `json:"url"` URL string `json:"url"`
Visibility string `json:"visibility"` Visibility StatusVisibility `json:"visibility"`
} }
type Tag struct { type Tag struct {

View file

@ -0,0 +1,55 @@
package model
import (
"encoding/json"
"fmt"
)
type StatusContentType int
const (
StatusContentTypePlainText StatusContentType = iota
StatusContentTypeMarkdown
StatusContentTypeUnknown
)
const (
statusContentTypeTextPlainValue = "text/plain"
statusContentTypePlainValue = "plain"
statusContentTypeTextMarkdownValue = "text/markdown"
statusContentTypeMarkdownValue = "markdown"
)
func (s StatusContentType) String() string {
mapped := map[StatusContentType]string{
StatusContentTypeMarkdown: statusContentTypeTextMarkdownValue,
StatusContentTypePlainText: statusContentTypeTextPlainValue,
}
output, ok := mapped[s]
if !ok {
return unknownValue
}
return output
}
func ParseStatusContentType(value string) StatusContentType {
switch value {
case statusContentTypePlainValue, statusContentTypeTextPlainValue:
return StatusContentTypePlainText
case statusContentTypeMarkdownValue, statusContentTypeTextMarkdownValue:
return StatusContentTypeMarkdown
}
return StatusContentTypeUnknown
}
func (s StatusContentType) MarshalJSON() ([]byte, error) {
value := s.String()
if value == unknownValue {
return nil, fmt.Errorf("%q is not a valid status content type", value)
}
return json.Marshal(value)
}

View file

@ -0,0 +1,83 @@
package model
import (
"encoding/json"
"fmt"
)
type StatusVisibility int
const (
StatusVisibilityPublic StatusVisibility = iota
StatusVisibilityPrivate
StatusVisibilityUnlisted
StatusVisibilityMutualsOnly
StatusVisibilityDirect
StatusVisibilityUnknown
)
const (
statusVisibilityPublicValue = "public"
statusVisibilityPrivateValue = "private"
statusVisibilityUnlistedValue = "unlisted"
statusVisibilityMutualsOnlyValue = "mutuals_only"
statusVisibilityDirectValue = "direct"
)
func (s StatusVisibility) String() string {
mapped := map[StatusVisibility]string{
StatusVisibilityPublic: statusVisibilityPublicValue,
StatusVisibilityPrivate: statusVisibilityPrivateValue,
StatusVisibilityUnlisted: statusVisibilityUnlistedValue,
StatusVisibilityMutualsOnly: statusVisibilityMutualsOnlyValue,
StatusVisibilityDirect: statusVisibilityDirectValue,
}
output, ok := mapped[s]
if !ok {
return unknownValue
}
return output
}
func ParseStatusVisibility(value string) StatusVisibility {
mapped := map[string]StatusVisibility{
statusVisibilityPublicValue: StatusVisibilityPublic,
statusVisibilityPrivateValue: StatusVisibilityPrivate,
statusVisibilityUnlistedValue: StatusVisibilityUnlisted,
statusVisibilityMutualsOnlyValue: StatusVisibilityMutualsOnly,
statusVisibilityDirectValue: StatusVisibilityDirect,
}
output, ok := mapped[value]
if !ok {
return StatusVisibilityUnknown
}
return output
}
func (s StatusVisibility) MarshalJSON() ([]byte, error) {
value := s.String()
if value == unknownValue {
return nil, fmt.Errorf("%q is not a valid status visibility", value)
}
return json.Marshal(value)
}
func (s *StatusVisibility) UnmarshalJSON(data []byte) error {
var (
value string
err error
)
if err = json.Unmarshal(data, &value); err != nil {
return fmt.Errorf("unable to unmarshal the data; %w", err)
}
*s = ParseStatusVisibility(value)
return nil
}