diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/client/instance/instance.go | 38 | ||||
| -rw-r--r-- | internal/api/client/instance/instanceget.go | 20 | ||||
| -rw-r--r-- | internal/api/model/instance.go | 20 | ||||
| -rw-r--r-- | internal/db/db.go | 5 | ||||
| -rw-r--r-- | internal/db/pg.go | 47 | ||||
| -rw-r--r-- | internal/gotosocial/actions.go | 8 | ||||
| -rw-r--r-- | internal/gtsmodel/instance.go | 33 | ||||
| -rw-r--r-- | internal/message/instanceprocess.go | 22 | ||||
| -rw-r--r-- | internal/message/processor.go | 18 | ||||
| -rw-r--r-- | internal/typeutils/converter.go | 3 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend.go | 30 | 
11 files changed, 222 insertions, 22 deletions
| diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go new file mode 100644 index 000000000..ed7c18718 --- /dev/null +++ b/internal/api/client/instance/instance.go @@ -0,0 +1,38 @@ +package instance + +import ( +	"net/http" + +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/api" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/message" +	"github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( +	// InstanceInformationPath +	InstanceInformationPath = "api/v1/instance" +) + +// Module implements the ClientModule interface +type Module struct { +	config    *config.Config +	processor message.Processor +	log       *logrus.Logger +} + +// New returns a new instance information module +func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { +	return &Module{ +		config:    config, +		processor: processor, +		log:       log, +	} +} + +// Route satisfies the ClientModule interface +func (m *Module) Route(s router.Router) error { +	s.AttachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler) +	return nil +} diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go new file mode 100644 index 000000000..f8e82c096 --- /dev/null +++ b/internal/api/client/instance/instanceget.go @@ -0,0 +1,20 @@ +package instance + +import ( +	"net/http" + +	"github.com/gin-gonic/gin" +) + +func (m *Module) InstanceInformationGETHandler(c *gin.Context) { +	l := m.log.WithField("func", "InstanceInformationGETHandler") + +	instance, err := m.processor.InstanceGet(m.config.Host) +	if err != nil { +		l.Debugf("error getting instance from processor: %s", err) +		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) +		return +	} + +	c.JSON(http.StatusOK, instance) +} diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go index 857a8acc5..75ef5392e 100644 --- a/internal/api/model/instance.go +++ b/internal/api/model/instance.go @@ -23,9 +23,9 @@ type Instance struct {  	// REQUIRED  	// The domain name of the instance. -	URI string `json:"uri"` +	URI string `json:"uri,omitempty"`  	// The title of the website. -	Title string `json:"title"` +	Title string `json:"title,omitempty"`  	// Admin-defined description of the Mastodon site.  	Description string `json:"description"`  	// A shorter description defined by the admin. @@ -33,9 +33,9 @@ type Instance struct {  	// An email that may be contacted for any inquiries.  	Email string `json:"email"`  	// The version of Mastodon installed on the instance. -	Version string `json:"version"` +	Version string `json:"version,omitempty"`  	// Primary langauges of the website and its staff. -	Languages []string `json:"languages"` +	Languages []string `json:"languages,omitempty"`  	// Whether registrations are enabled.  	Registrations bool `json:"registrations"`  	// Whether registrations require moderator approval. @@ -43,16 +43,16 @@ type Instance struct {  	// Whether invites are enabled.  	InvitesEnabled bool `json:"invites_enabled"`  	// URLs of interest for clients apps. -	URLS *InstanceURLs `json:"urls"` +	URLS *InstanceURLs `json:"urls,omitempty"`  	// Statistics about how much information the instance contains. -	Stats *InstanceStats `json:"stats"` - -	// OPTIONAL - +	Stats *InstanceStats `json:"stats,omitempty"`  	// Banner image for the website. -	Thumbnail string `json:"thumbnail,omitempty"` +	Thumbnail string `json:"thumbnail"`  	// A user that can be contacted, as an alternative to email.  	ContactAccount *Account `json:"contact_account,omitempty"` +	// What's the maximum allowed length of a post on this instance? +	// This is provided for compatibility with Tusky. +	MaxTootChars uint `json:"max_toot_chars"`  }  // InstanceURLs represents URLs necessary for successfully connecting to the instance as a user. See https://docs.joinmastodon.org/entities/instance/ diff --git a/internal/db/db.go b/internal/db/db.go index 3e085e180..b281dd8d7 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -117,6 +117,11 @@ type DB interface {  	// This is needed for things like serving files that belong to the instance and not an individual user/account.  	CreateInstanceAccount() error +	// CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. +	// Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. +	// This is needed for things like serving instance information through /api/v1/instance +	CreateInstanceInstance() error +  	// GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID.  	// The given account pointer will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned diff --git a/internal/db/pg.go b/internal/db/pg.go index 647285032..f59103af7 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -307,17 +307,54 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac  func (ps *postgresService) CreateInstanceAccount() error {  	username := ps.config.Host -	instanceAccount := >smodel.Account{ -		Username: username, +	key, err := rsa.GenerateKey(rand.Reader, 2048) +	if err != nil { +		ps.log.Errorf("error creating new rsa key: %s", err) +		return err +	} + +	newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) +	a := >smodel.Account{ +		Username:              ps.config.Host, +		DisplayName:           username, +		URL:                   newAccountURIs.UserURL, +		PrivateKey:            key, +		PublicKey:             &key.PublicKey, +		PublicKeyURI:          newAccountURIs.PublicKeyURI, +		ActorType:             gtsmodel.ActivityStreamsPerson, +		URI:                   newAccountURIs.UserURI, +		InboxURI:              newAccountURIs.InboxURI, +		OutboxURI:             newAccountURIs.OutboxURI, +		FollowersURI:          newAccountURIs.FollowersURI, +		FollowingURI:          newAccountURIs.FollowingURI, +		FeaturedCollectionURI: newAccountURIs.CollectionURI, +	} +	inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert() +	if err != nil { +		return err +	} +	if inserted { +		ps.log.Infof("created instance account %s with id %s", username, a.ID) +	} else { +		ps.log.Infof("instance account %s already exists with id %s", username, a.ID) +	} +	return nil +} + +func (ps *postgresService) CreateInstanceInstance() error { +	i := >smodel.Instance{ +		Domain: ps.config.Host, +		Title: ps.config.Host, +		URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host),  	} -	inserted, err := ps.conn.Model(instanceAccount).Where("username = ?", username).SelectOrInsert() +	inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert()  	if err != nil {  		return err  	}  	if inserted { -		ps.log.Infof("created instance account %s with id %s", username, instanceAccount.ID) +		ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID)  	} else { -		ps.log.Infof("instance account %s already exists with id %s", username, instanceAccount.ID) +		ps.log.Infof("instance instance %s already exists with id %s",  ps.config.Host, i.ID)  	}  	return nil  } diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 0cdb29de5..6d130ed2d 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -34,6 +34,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/api/client/app"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"  	mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/status"  	"github.com/superseriousbusiness/gotosocial/internal/api/security" @@ -68,6 +69,7 @@ var models []interface{} = []interface{}{  	>smodel.Tag{},  	>smodel.User{},  	>smodel.Emoji{}, +	>smodel.Instance{},  	&oauth.Token{},  	&oauth.Client{},  } @@ -105,6 +107,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr  	// build client api modules  	authModule := auth.New(c, dbService, oauthServer, log)  	accountModule := account.New(c, processor, log) +	instanceModule := instance.New(c, processor, log)  	appsModule := app.New(c, processor, log)  	mm := mediaModule.New(c, processor, log)  	fileServerModule := fileserver.New(c, processor, log) @@ -119,6 +122,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr  		// now everything else  		accountModule, +		instanceModule,  		appsModule,  		mm,  		fileServerModule, @@ -142,6 +146,10 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr  		return fmt.Errorf("error creating instance account: %s", err)  	} +	if err := dbService.CreateInstanceInstance(); err != nil { +		return fmt.Errorf("error creating instance instance: %s", err) +	} +  	gts, err := New(dbService, router, federator, c)  	if err != nil {  		return fmt.Errorf("error creating gotosocial service: %s", err) diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go new file mode 100644 index 000000000..ac7c990e3 --- /dev/null +++ b/internal/gtsmodel/instance.go @@ -0,0 +1,33 @@ +package gtsmodel + +import "time" + +// Instance represents a federated instance, either local or remote. +type Instance struct { +	// ID of this instance in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// Instance domain eg example.org +	Domain string `pg:",notnull,unique"` +	// Title of this instance as it would like to be displayed. +	Title string +	// base URI of this instance eg https://example.org +	URI string `pg:",notnull,unique"` +	// When was this instance created in the db? +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// When was this instance last updated in the db? +	UpdatedAt   time.Time `pg:"type:timestamp,notnull,default:now()"` +	// When was this instance suspended, if at all? +	SuspendedAt time.Time +	// ID of any existing domain block for this instance in the database +	DomainBlockID string +	// Short description of this instance +	ShortDescription string +	// Longer description of this instance +	Description string +	// Contact email address for this instance +	ContactEmail string +	// Contact account ID in the database for this instance +	ContactAccountID string +	// Reputation score of this instance +	Reputation int64 `pg:",notnull,default:0"` +} diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go new file mode 100644 index 000000000..16a5594de --- /dev/null +++ b/internal/message/instanceprocess.go @@ -0,0 +1,22 @@ +package message + +import ( +	"fmt" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) { +	i := >smodel.Instance{} +	if err := p.db.GetWhere("domain", domain, i); err != nil { +		return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) +	} + +	ai, err := p.tc.InstanceToMasto(i) +	if err != nil { +		return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) +	} + +	return  ai, nil +} diff --git a/internal/message/processor.go b/internal/message/processor.go index 2126c9597..0c0334e20 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -68,9 +68,20 @@ type Processor interface {  	// AccountUpdate processes the update of an account with the given form  	AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) +	// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. +	AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) +  	// AppCreate processes the creation of a new API application  	AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) +	// InstanceGet retrieves instance information for serving at api/v1/instance +	InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) + +	// MediaCreate handles the creation of a media attachment, using the given form. +	MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) +	// MediaGet handles the fetching of a media attachment, using the given request form. +	MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) +  	// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.  	StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)  	// StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. @@ -86,13 +97,6 @@ type Processor interface {  	// StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.  	StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) -	// MediaCreate handles the creation of a media attachment, using the given form. -	MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) -	// MediaGet handles the fetching of a media attachment, using the given request form. -	MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) -	// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. -	AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) -  	/*  		FEDERATION API-FACING PROCESSING FUNCTIONS  		These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 5118386a9..f269fa182 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -74,6 +74,9 @@ type TypeConverter interface {  	// VisToMasto converts a gts visibility into its mastodon equivalent  	VisToMasto(m gtsmodel.Visibility) model.Visibility +	// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance +	InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) +  	/*  		FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL  	*/ diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 66c21b98a..6b0c743ff 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -551,3 +551,33 @@ func (c *converter) VisToMasto(m gtsmodel.Visibility) model.Visibility {  	}  	return ""  } + +func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) { +	mi := &model.Instance{ +		URI: i.URI, +		Title: i.Title, +		Description: i.Description, +		ShortDescription: i.ShortDescription, +		Email: i.ContactEmail, +	} + +	if i.Domain == c.config.Host { +		mi.Registrations = c.config.AccountsConfig.OpenRegistration +		mi.ApprovalRequired = c.config.AccountsConfig.RequireApproval +		mi.InvitesEnabled = false // TODO +		mi.MaxTootChars = uint(c.config.StatusesConfig.MaxChars) +	} + +	// contact account is optional but let's try to get it +	if i.ContactAccountID != "" { +		ia := >smodel.Account{} +		if err := c.db.GetByID(i.ContactAccountID, ia); err == nil { +			ma, err := c.AccountToMastoPublic(ia) +			if err == nil { +				mi.ContactAccount = ma +			} +		} +	} + +	return mi, nil +} | 
