From 23e2c4a5678867ffdf49429eb709c6dc43cb8640 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sat, 3 Apr 2021 19:40:15 +0200 Subject: [PATCH] fiddle-de-dee! --- internal/apimodule/status/statuscreate.go | 130 ++++++++++++++++++++-- internal/db/db.go | 10 ++ internal/db/model/account.go | 8 +- internal/db/model/activitystreams.go | 127 +++++++++++++++++++++ internal/db/model/status.go | 12 +- internal/db/pg.go | 63 ++++++++++- internal/util/parse.go | 19 +++- internal/util/status.go | 29 +++-- pkg/mastotypes/source.go | 2 +- 9 files changed, 360 insertions(+), 40 deletions(-) create mode 100644 internal/db/model/activitystreams.go diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go index a309239..fbae673 100644 --- a/internal/apimodule/status/statuscreate.go +++ b/internal/apimodule/status/statuscreate.go @@ -23,6 +23,7 @@ import ( "fmt" "net" "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -34,6 +35,24 @@ import ( "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" ) +type advancedStatusCreateForm struct { + mastotypes.StatusCreateRequest + AdvancedVisibility *advancedVisibilityFlagsForm `form:"visibility_advanced"` +} + +type advancedVisibilityFlagsForm struct { + // The gotosocial visibility model + Visibility *model.Visibility + // This status will be federated beyond the local timeline(s) + Federated *bool `form:"federated"` + // This status can be boosted/reblogged + Boostable *bool `form:"boostable"` + // This status can be replied to + Replyable *bool `form:"replyable"` + // This status can be liked/faved + Likeable *bool `form:"likeable"` +} + func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { l := m.log.WithField("func", "statusCreatePOSTHandler") authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything* @@ -51,7 +70,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { } l.Trace("parsing request form") - form := &mastotypes.StatusCreateRequest{} + form := &advancedStatusCreateForm{} if err := c.ShouldBind(form); err != nil || form == nil { l.Debugf("could not parse form from request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) @@ -65,6 +84,10 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { return } + // here we check if any advanced visibility flags have been set and fiddle with them if so + l.Trace("deriving visibility") + basicVis, advancedVis, err := deriveTotalVisibility(form.Visibility, form.AdvancedVisibility, authed.Account.Privacy) + clientIP := c.ClientIP() l.Tracef("attempting to parse client ip address %s", clientIP) signUpIP := net.ParseIP(clientIP) @@ -75,23 +98,35 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { } uris := util.GenerateURIs(authed.Account.Username, m.config.Protocol, m.config.Host) - newStatusID := uuid.NewString() + thisStatusID := uuid.NewString() + thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID) + thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID) newStatus := &model.Status{ - ID: newStatusID, - URI: fmt.Sprintf("%s/%s", uris.StatusesURI, newStatusID), - URL: fmt.Sprintf("%s/%s", uris.StatusesURL, newStatusID), + ID: thisStatusID, + URI: thisStatusURI, + URL: thisStatusURL, Content: util.HTMLFormat(form.Status), - Local: true, // will always be true if this status is being created through the client API + Local: true, // will always be true if this status is being created through the client API, since only local users can do that AccountID: authed.Account.ID, InReplyToID: form.InReplyToID, ContentWarning: form.SpoilerText, - ActivityStreamsType: "Note", + Visibility: basicVis, + VisibilityAdvanced: *advancedVis, + ActivityStreamsType: model.ActivityStreamsNote, + } + + // take care of side effects -- mentions, updating metadata, etc, etc + menchies, err := m.db.AccountStringsToMentions(util.DeriveMentions(form.Status), authed.Account.ID, thisStatusID) + if err != nil { + l.Debugf("error generating mentions from status: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating mentions from status"}) + return } } -func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.StatusesConfig, accountID string, db db.DB) error { +func validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig, accountID string, db db.DB) error { // validate that, structurally, we have a valid status/post if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { return errors.New("no status, media, or poll provided") @@ -146,7 +181,7 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S if err := db.GetByID(form.InReplyToID, s); err != nil { return fmt.Errorf("status id %s cannot be retrieved from the db: %s", form.InReplyToID, err) } - if !*s.VisibilityAdvanced.Replyable { + if !s.VisibilityAdvanced.Replyable { return fmt.Errorf("status with id %s is not replyable", form.InReplyToID) } } @@ -167,3 +202,80 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S return nil } + +func deriveTotalVisibility(basicVisForm mastotypes.Visibility, advancedVisForm *advancedVisibilityFlagsForm, accountDefaultVis model.Visibility) (model.Visibility, *model.VisibilityAdvanced, error) { + // by default all flags are set to true + gtsAdvancedVis := &model.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: true, + Likeable: true, + } + + var gtsBasicVis model.Visibility + // Advanced takes priority if it's set. + // If it's not set, take whatever masto visibility is set. + // If *that's* not set either, then just take the account default. + if advancedVisForm != nil && advancedVisForm.Visibility != nil { + gtsBasicVis = *advancedVisForm.Visibility + } else if basicVisForm != "" { + gtsBasicVis = util.ParseGTSVisFromMastoVis(basicVisForm) + } else { + gtsBasicVis = accountDefaultVis + } + + switch gtsBasicVis { + case model.VisibilityPublic: + // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out + return gtsBasicVis, gtsAdvancedVis, nil + case model.VisibilityUnlocked: + // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them + if advancedVisForm != nil { + if advancedVisForm.Federated != nil { + gtsAdvancedVis.Federated = *advancedVisForm.Federated + } + + if advancedVisForm.Boostable != nil { + gtsAdvancedVis.Boostable = *advancedVisForm.Boostable + } + + if advancedVisForm.Replyable != nil { + gtsAdvancedVis.Replyable = *advancedVisForm.Replyable + } + + if advancedVisForm.Likeable != nil { + gtsAdvancedVis.Likeable = *advancedVisForm.Likeable + } + } + return gtsBasicVis, gtsAdvancedVis, nil + case model.VisibilityFollowersOnly, model.VisibilityMutualsOnly: + // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them + gtsAdvancedVis.Boostable = false + + if advancedVisForm != nil { + if advancedVisForm.Federated != nil { + gtsAdvancedVis.Federated = *advancedVisForm.Federated + } + + if advancedVisForm.Replyable != nil { + gtsAdvancedVis.Replyable = *advancedVisForm.Replyable + } + + if advancedVisForm.Likeable != nil { + gtsAdvancedVis.Likeable = *advancedVisForm.Likeable + } + } + + return gtsBasicVis, gtsAdvancedVis, nil + case model.VisibilityDirect: + // direct is pretty easy: there's only one possible setting so return it + gtsAdvancedVis.Federated = true + gtsAdvancedVis.Boostable = false + gtsAdvancedVis.Federated = true + gtsAdvancedVis.Likeable = true + return gtsBasicVis, gtsAdvancedVis, nil + } + + // this should never happen but just in case... + return "", nil, errors.New("could not parse visibility") +} diff --git a/internal/db/db.go b/internal/db/db.go index 4921270..9ab4139 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -79,6 +79,11 @@ type DB interface { // In case of no entries, a 'no entries' error will be returned GetWhere(key string, value interface{}, i interface{}) error + // // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where". + // // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second + // // being Key domain and Value example.org, only entries will be returned where BOTH conditions are true. + // GetWhereMany(i interface{}, where ...model.Where) error + // GetAll will try to get all entries of type i. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. // In case of no entries, a 'no entries' error will be returned @@ -182,6 +187,11 @@ type DB interface { // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. // In other words, this is the public record that the server has of an account. AccountToMastoPublic(account *model.Account) (*mastotypes.Account, error) + + // AccountStringsToMentions takes a slice of deduplicated account names in the form "@test@whatever.example.org", which have been + // mentioned in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then + // checks in the database for the mentioned accounts, and returns a slice of mentions generated based on the given parameters. + AccountStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*model.Mention, error) } // New returns a new database service that satisfies the DB interface and, by extension, diff --git a/internal/db/model/account.go b/internal/db/model/account.go index 70ee929..70028be 100644 --- a/internal/db/model/account.go +++ b/internal/db/model/account.go @@ -38,8 +38,8 @@ type Account struct { ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` // Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org`` Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other - // Domain of the account, will be empty if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. - Domain string `pg:",unique:userdomain"` // username and domain should be unique *with* each other + // Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. + Domain string `pg:"default:null,unique:userdomain"` // username and domain should be unique *with* each other /* ACCOUNT METADATA @@ -95,7 +95,7 @@ type Account struct { // Should this account be shown in the instance's profile directory? Discoverable bool // Default post privacy for this account - Privacy string + Privacy Visibility // Set posts from this account to sensitive by default? Sensitive bool // What language does this account post in? @@ -122,7 +122,7 @@ type Account struct { // URL for getting the featured collection list of this account FeaturedCollectionURL string `pg:",unique"` // What type of activitypub actor is this account? - ActorType string + ActorType ActivityStreamsActor // This account is associated with x account id AlsoKnownAs string diff --git a/internal/db/model/activitystreams.go b/internal/db/model/activitystreams.go new file mode 100644 index 0000000..b6c9df6 --- /dev/null +++ b/internal/db/model/activitystreams.go @@ -0,0 +1,127 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package model + +// ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types +type ActivityStreamsObject string + +const ( + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article + ActivityStreamsArticle ActivityStreamsObject = "Article" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio + ActivityStreamsAudio ActivityStreamsObject = "Audio" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document + ActivityStreamsDocument ActivityStreamsObject = "Event" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event + ActivityStreamsEvent ActivityStreamsObject = "Event" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image + ActivityStreamsImage ActivityStreamsObject = "Image" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note + ActivityStreamsNote ActivityStreamsObject = "Note" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page + ActivityStreamsPage ActivityStreamsObject = "Page" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place + ActivityStreamsPlace ActivityStreamsObject = "Place" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile + ActivityStreamsProfile ActivityStreamsObject = "Profile" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship + ActivityStreamsRelationship ActivityStreamsObject = "Relationship" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone + ActivityStreamsTombstone ActivityStreamsObject = "Tombstone" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video + ActivityStreamsVideo ActivityStreamsObject = "Video" +) + +// ActivityStreamsActor refers to https://www.w3.org/TR/activitystreams-vocabulary/#actor-types +type ActivityStreamsActor string + +const ( + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application + ActivityStreamsApplication ActivityStreamsActor = "Application" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group + ActivityStreamsGroup ActivityStreamsActor = "Group" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization + ActivityStreamsOrganization ActivityStreamsActor = "Organization" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person + ActivityStreamsPerson ActivityStreamsActor = "Person" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service + ActivityStreamsService ActivityStreamsActor = "Service" +) + +// ActivityStreamsActivity refers to https://www.w3.org/TR/activitystreams-vocabulary/#activity-types +type ActivityStreamsActivity string + +const ( + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept + ActivityStreamsAccept ActivityStreamsActivity = "Accept" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add + ActivityStreamsAdd ActivityStreamsActivity = "Add" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce + ActivityStreamsAnnounce ActivityStreamsActivity = "Announce" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive + ActivityStreamsArrive ActivityStreamsActivity = "Arrive" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block + ActivityStreamsBlock ActivityStreamsActivity = "Block" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create + ActivityStreamsCreate ActivityStreamsActivity = "Create" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete + ActivityStreamsDelete ActivityStreamsActivity = "Delete" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike + ActivityStreamsDislike ActivityStreamsActivity = "Dislike" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag + ActivityStreamsFlag ActivityStreamsActivity = "Flag" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow + ActivityStreamsFollow ActivityStreamsActivity = "Follow" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore + ActivityStreamsIgnore ActivityStreamsActivity = "Ignore" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite + ActivityStreamsInvite ActivityStreamsActivity = "Invite" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join + ActivityStreamsJoin ActivityStreamsActivity = "Join" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave + ActivityStreamsLeave ActivityStreamsActivity = "Leave" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like + ActivityStreamsLike ActivityStreamsActivity = "Like" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen + ActivityStreamsListen ActivityStreamsActivity = "Listen" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move + ActivityStreamsMove ActivityStreamsActivity = "Move" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer + ActivityStreamsOffer ActivityStreamsActivity = "Offer" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question + ActivityStreamsQuestion ActivityStreamsActivity = "Question" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject + ActivityStreamsReject ActivityStreamsActivity = "Reject" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read + ActivityStreamsRead ActivityStreamsActivity = "Read" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove + ActivityStreamsRemove ActivityStreamsActivity = "Remove" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject + ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept + ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel + ActivityStreamsTravel ActivityStreamsActivity = "Travel" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo + ActivityStreamsUndo ActivityStreamsActivity = "Undo" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update + ActivityStreamsUpdate ActivityStreamsActivity = "Update" + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view + ActivityStreamsView ActivityStreamsActivity = "View" +) diff --git a/internal/db/model/status.go b/internal/db/model/status.go index 31f5623..97d0de6 100644 --- a/internal/db/model/status.go +++ b/internal/db/model/status.go @@ -49,8 +49,8 @@ type Status struct { // advanced visibility for this status VisibilityAdvanced VisibilityAdvanced // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types - // Will probably almost always be a note. - ActivityStreamsType string + // Will probably almost always be Note but who knows!. + ActivityStreamsType ActivityStreamsObject } // Visibility represents the visibility granularity of a status. @@ -79,11 +79,11 @@ type VisibilityAdvanced struct { If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE. */ // This status will be federated beyond the local timeline(s) - Federated *bool `pg:"default:true"` + Federated bool `pg:"default:true"` // This status can be boosted/reblogged - Boostable *bool `pg:"default:true"` + Boostable bool `pg:"default:true"` // This status can be replied to - Replyable *bool `pg:"default:true"` + Replyable bool `pg:"default:true"` // This status can be liked/faved - Likeable *bool `pg:"default:true"` + Likeable bool `pg:"default:true"` } diff --git a/internal/db/pg.go b/internal/db/pg.go index 4ea963a..b8ba8a7 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -254,6 +254,10 @@ func (ps *postgresService) GetWhere(key string, value interface{}, i interface{} return nil } +// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error { +// return nil +// } + func (ps *postgresService) GetAll(i interface{}) error { if err := ps.conn.Model(i).Select(); err != nil { if err == pg.ErrNoRows { @@ -451,7 +455,7 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr URL: uris.UserURL, PrivateKey: key, PublicKey: &key.PublicKey, - ActorType: "Person", + ActorType: model.ActivityStreamsPerson, URI: uris.UserURI, InboxURL: uris.InboxURI, OutboxURL: uris.OutboxURI, @@ -537,7 +541,7 @@ func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotype } mastoAccount.Source = &mastotypes.Source{ - Privacy: a.Privacy, + Privacy: util.ParseMastoVisFromGTSVis(a.Privacy), Sensitive: a.Sensitive, Language: a.Language, Note: a.Note, @@ -660,3 +664,58 @@ func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.A Fields: fields, }, nil } + +func (ps *postgresService) AccountStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*model.Mention, error) { + menchies := []*model.Mention{} + for _, a := range targetAccounts { + // A mentioned account looks like "@test@example.org" -- we can guarantee this from the regex that targetAccounts should have been derived from. + // But we still need to do a bit of fiddling to get what we need here -- the username and domain. + + // 1. trim off the first @ + t := strings.TrimPrefix(a, "@") + + // 2. split the username and domain + s := strings.Split(t, "@") + + // 3. it should *always* be length 2 so if it's not then something is seriously wrong + if len(s) != 2 { + return nil, fmt.Errorf("mentioned account format %s was not valid", a) + } + username := s[0] + domain := s[1] + + // 4. check we now have a proper username and domain + if username == "" || domain == "" { + return nil, fmt.Errorf("username or domain for %s was nil", a) + } + + // okay we're good now, we can start pulling accounts out of the database + mentionedAccount := &model.Account{} + var err error + if domain == ps.config.Host { + // local user -- should have a null domain + err = ps.conn.Model(mentionedAccount).Where("id = ?", username).Where("domain = null").Select() + } else { + // remote user -- should have domain defined + err = ps.conn.Model(mentionedAccount).Where("id = ?", username).Where("domain = ?", domain).Select() + } + + if err != nil { + if err == pg.ErrNoRows { + // no result found for this username/domain so just don't include it as a mencho and carry on about our business + ps.log.Debugf("no account found with username %s and domain %s, skipping it", username, domain) + continue + } + // a serious error has happened so bail + return nil, fmt.Errorf("error getting account with username %s and domain %s: %s", username, domain, err) + } + + // id, createdat and updatedat will be populated by the db, so we have everything we need! + menchies = append(menchies, &model.Mention{ + StatusID: statusID, + OriginAccountID: originAccountID, + TargetAccountID: mentionedAccount.ID, + }) + } + return menchies, nil +} diff --git a/internal/util/parse.go b/internal/util/parse.go index 89be053..6bc4e7d 100644 --- a/internal/util/parse.go +++ b/internal/util/parse.go @@ -18,7 +18,12 @@ package util -import "fmt" +import ( + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" +) type URIs struct { HostURL string @@ -48,7 +53,7 @@ func GenerateURIs(username string, protocol string, host string) *URIs { HostURL: hostURL, UserURL: userURL, StatusesURL: statusesURL, - + UserURI: userURI, StatusesURI: statusesURI, InboxURI: inboxURI, @@ -57,3 +62,13 @@ func GenerateURIs(username string, protocol string, host string) *URIs { CollectionURI: collectionURI, } } + +func ParseGTSVisFromMastoVis(m mastotypes.Visibility) model.Visibility { + // TODO: convert a masto vis into a gts vis + return "" +} + +func ParseMastoVisFromGTSVis(m model.Visibility) mastotypes.Visibility { + // TODO: convert a gts vis into a masto vis + return "" +} diff --git a/internal/util/status.go b/internal/util/status.go index f528a42..3667937 100644 --- a/internal/util/status.go +++ b/internal/util/status.go @@ -37,35 +37,32 @@ var ( // It will look for fully-qualified account names in the form "@user@example.org". // Mentions that are just in the form "@username" will not be detected. func DeriveMentions(status string) []string { - menchies := []string{} - for _, match := range mentionRegex.FindAllStringSubmatch(status, -1) { - menchies = append(menchies, match[1]) + mentionedAccounts := []string{} + for _, m := range mentionRegex.FindAllStringSubmatch(status, -1) { + mentionedAccounts = append(mentionedAccounts, m[1]) } - return Unique(menchies) + return Unique(mentionedAccounts) } // Unique returns a deduplicated version of a given string slice. func Unique(s []string) []string { - keys := make(map[string]bool) - list := []string{} - for _, entry := range s { - if _, value := keys[entry]; !value { - keys[entry] = true - list = append(list, entry) - } - } - return list + keys := make(map[string]bool) + list := []string{} + for _, entry := range s { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list } // HTMLFormat takes a plaintext formatted status string, and converts it into // a nice HTML-formatted string. // // This includes: -// // - Replacing line-breaks with

-// // - Replacing URLs with hrefs. -// // - Replacing mentions with links to that account's URL as stored in the database. func HTMLFormat(status string) string { // TODO: write proper HTML formatting logic for a status diff --git a/pkg/mastotypes/source.go b/pkg/mastotypes/source.go index 4142540..0445a1f 100644 --- a/pkg/mastotypes/source.go +++ b/pkg/mastotypes/source.go @@ -27,7 +27,7 @@ type Source struct { // unlisted = Unlisted post // private = Followers-only post // direct = Direct post - Privacy string `json:"privacy,omitempty"` + Privacy Visibility `json:"privacy,omitempty"` // Whether new statuses should be marked sensitive by default. Sensitive bool `json:"sensitive,omitempty"` // The default posting language for new statuses.