forked from mirrors/gotosocial
Instance settings updates (#59)
Allow admins to set instance settings through a PATCH to /api/v1/instance Update templates to reflect some of the new fields
This commit is contained in:
parent
fd57cca437
commit
8c9a853343
13 changed files with 340 additions and 29 deletions
|
@ -34,5 +34,6 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg
|
||||||
// Route satisfies the ClientModule interface
|
// Route satisfies the ClientModule interface
|
||||||
func (m *Module) Route(s router.Router) error {
|
func (m *Module) Route(s router.Router) error {
|
||||||
s.AttachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler)
|
s.AttachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler)
|
||||||
|
s.AttachHandler(http.MethodPatch, InstanceInformationPath, m.InstanceUpdatePATCHHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
50
internal/api/client/instance/instancepatch.go
Normal file
50
internal/api/client/instance/instancepatch.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package instance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithField("func", "InstanceUpdatePATCHHandler")
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugf("couldn't auth: %s", err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// only admins can update instance settings
|
||||||
|
if !authed.User.Admin {
|
||||||
|
l.Debug("user is not an admin so cannot update instance settings")
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("parsing request form %s", c.Request.Form)
|
||||||
|
form := &model.InstanceSettingsUpdateRequest{}
|
||||||
|
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": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if everything on the form is nil, then nothing has been set and we shouldn't continue
|
||||||
|
if form.SiteTitle == nil && form.SiteContactUsername == nil && form.SiteContactEmail == nil && form.SiteShortDescription == nil && form.SiteDescription == nil && form.SiteTerms == nil && form.Avatar == nil && form.Header == nil {
|
||||||
|
l.Debugf("could not parse form from request")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i, errWithCode := m.processor.InstancePatch(form)
|
||||||
|
if errWithCode != nil {
|
||||||
|
l.Debugf("error with instance patch request: %s", errWithCode.Error())
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, i)
|
||||||
|
}
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "mime/multipart"
|
||||||
|
|
||||||
// Instance represents the software instance of Mastodon running on this domain. See https://docs.joinmastodon.org/entities/instance/
|
// Instance represents the software instance of Mastodon running on this domain. See https://docs.joinmastodon.org/entities/instance/
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
// REQUIRED
|
// REQUIRED
|
||||||
|
@ -45,7 +47,7 @@ type Instance struct {
|
||||||
// URLs of interest for clients apps.
|
// URLs of interest for clients apps.
|
||||||
URLS *InstanceURLs `json:"urls,omitempty"`
|
URLS *InstanceURLs `json:"urls,omitempty"`
|
||||||
// Statistics about how much information the instance contains.
|
// Statistics about how much information the instance contains.
|
||||||
Stats *InstanceStats `json:"stats,omitempty"`
|
Stats map[string]int `json:"stats,omitempty"`
|
||||||
// Banner image for the website.
|
// Banner image for the website.
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
// A user that can be contacted, as an alternative to email.
|
// A user that can be contacted, as an alternative to email.
|
||||||
|
@ -70,3 +72,15 @@ type InstanceStats struct {
|
||||||
// Domains federated with this instance.
|
// Domains federated with this instance.
|
||||||
DomainCount int `json:"domain_count"`
|
DomainCount int `json:"domain_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstanceSettingsUpdateRequest is the form to be parsed on a PATCH to /api/v1/instance
|
||||||
|
type InstanceSettingsUpdateRequest struct {
|
||||||
|
SiteTitle *string `form:"site_title" json:"site_title" xml:"site_title"`
|
||||||
|
SiteContactUsername *string `form:"site_contact_username" json:"site_contact_username" xml:"site_contact_username"`
|
||||||
|
SiteContactEmail *string `form:"site_contact_email" json:"site_contact_email" xml:"site_contact_email"`
|
||||||
|
SiteShortDescription *string `form:"site_short_description" json:"site_short_description" xml:"site_short_description"`
|
||||||
|
SiteDescription *string `form:"site_description" json:"site_description" xml:"site_description"`
|
||||||
|
SiteTerms *string `form:"site_terms" json:"site_terms" xml:"site_terms"`
|
||||||
|
Avatar *multipart.FileHeader `form:"avatar" json:"avatar" xml:"avatar"`
|
||||||
|
Header *multipart.FileHeader `form:"header" json:"header" xml:"header"`
|
||||||
|
}
|
||||||
|
|
|
@ -253,6 +253,14 @@ type DB interface {
|
||||||
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
|
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
|
||||||
GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error)
|
GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error)
|
||||||
|
|
||||||
|
// GetUserCountForInstance returns the number of known accounts registered with the given domain.
|
||||||
|
GetUserCountForInstance(domain string) (int, error)
|
||||||
|
|
||||||
|
// GetStatusCountForInstance returns the number of known statuses posted from the given domain.
|
||||||
|
GetStatusCountForInstance(domain string) (int, error)
|
||||||
|
|
||||||
|
// GetDomainCountForInstance returns the number of known instances known that the given domain federates with.
|
||||||
|
GetDomainCountForInstance(domain string) (int, error)
|
||||||
/*
|
/*
|
||||||
USEFUL CONVERSION FUNCTIONS
|
USEFUL CONVERSION FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
|
52
internal/db/pg/instancestats.go
Normal file
52
internal/db/pg/instancestats.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package pg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-pg/pg/v10"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ps *postgresService) GetUserCountForInstance(domain string) (int, error) {
|
||||||
|
q := ps.conn.Model(&[]*gtsmodel.Account{})
|
||||||
|
|
||||||
|
if domain == ps.config.Host {
|
||||||
|
// if the domain is *this* domain, just count where the domain field is null
|
||||||
|
q = q.Where("? IS NULL", pg.Ident("domain"))
|
||||||
|
} else {
|
||||||
|
q = q.Where("domain = ?", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't count the instance account or suspended users
|
||||||
|
q = q.Where("username != ?", domain).Where("? IS NULL", pg.Ident("suspended_at"))
|
||||||
|
|
||||||
|
return q.Count()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *postgresService) GetStatusCountForInstance(domain string) (int, error) {
|
||||||
|
q := ps.conn.Model(&[]*gtsmodel.Status{})
|
||||||
|
|
||||||
|
if domain == ps.config.Host {
|
||||||
|
// if the domain is *this* domain, just count where local is true
|
||||||
|
q = q.Where("local = ?", true)
|
||||||
|
} else {
|
||||||
|
// join on the domain of the account
|
||||||
|
q = q.Join("JOIN accounts AS account ON account.id = status.account_id").
|
||||||
|
Where("account.domain = ?", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Count()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) {
|
||||||
|
q := ps.conn.Model(&[]*gtsmodel.Instance{})
|
||||||
|
|
||||||
|
if domain == ps.config.Host {
|
||||||
|
// if the domain is *this* domain, just count other instances it knows about
|
||||||
|
// TODO: exclude domains that are blocked or silenced
|
||||||
|
q = q.Where("domain != ?", domain)
|
||||||
|
} else {
|
||||||
|
// TODO: implement federated domain counting properly for remote domains
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Count()
|
||||||
|
}
|
|
@ -24,6 +24,8 @@ type Instance struct {
|
||||||
ShortDescription string
|
ShortDescription string
|
||||||
// Longer description of this instance
|
// Longer description of this instance
|
||||||
Description string
|
Description string
|
||||||
|
// Terms and conditions of this instance
|
||||||
|
Terms string
|
||||||
// Contact email address for this instance
|
// Contact email address for this instance
|
||||||
ContactEmail string
|
ContactEmail string
|
||||||
// Contact account ID in the database for this instance
|
// Contact account ID in the database for this instance
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) {
|
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) {
|
||||||
|
@ -40,3 +41,118 @@ func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.Wit
|
||||||
|
|
||||||
return ai, nil
|
return ai, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode) {
|
||||||
|
// fetch the instance entry from the db for processing
|
||||||
|
i := >smodel.Instance{}
|
||||||
|
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: p.config.Host}}, i); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the instance account from the db for processing
|
||||||
|
ia := >smodel.Account{}
|
||||||
|
if err := p.db.GetLocalAccountByUsername(p.config.Host, ia); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", p.config.Host, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site title if it's set on the form
|
||||||
|
if form.SiteTitle != nil {
|
||||||
|
if err := util.ValidateSiteTitle(*form.SiteTitle); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("site title invalid: %s", err))
|
||||||
|
}
|
||||||
|
i.Title = *form.SiteTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site contact account if it's set on the form
|
||||||
|
if form.SiteContactUsername != nil {
|
||||||
|
// make sure the account with the given username exists in the db
|
||||||
|
contactAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetLocalAccountByUsername(*form.SiteContactUsername, contactAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.SiteContactUsername))
|
||||||
|
}
|
||||||
|
// make sure it has a user associated with it
|
||||||
|
contactUser := >smodel.User{}
|
||||||
|
if err := p.db.GetWhere([]db.Where{{Key: "account_id", Value: contactAccount.ID}}, contactUser); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("user for account with username %s not retrievable", *form.SiteContactUsername))
|
||||||
|
}
|
||||||
|
// suspended accounts cannot be contact accounts
|
||||||
|
if !contactAccount.SuspendedAt.IsZero() {
|
||||||
|
err := fmt.Errorf("selected contact account %s is suspended", contactAccount.Username)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
// unconfirmed or unapproved users cannot be contacts
|
||||||
|
if contactUser.ConfirmedAt.IsZero() {
|
||||||
|
err := fmt.Errorf("user of selected contact account %s is not confirmed", contactAccount.Username)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
if !contactUser.Approved {
|
||||||
|
err := fmt.Errorf("user of selected contact account %s is not approved", contactAccount.Username)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
// contact account user must be admin or moderator otherwise what's the point of contacting them
|
||||||
|
if !contactUser.Admin && !contactUser.Moderator {
|
||||||
|
err := fmt.Errorf("user of selected contact account %s is neither admin nor moderator", contactAccount.Username)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
i.ContactAccountID = contactAccount.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site contact email if it's set on the form
|
||||||
|
if form.SiteContactEmail != nil {
|
||||||
|
if err := util.ValidateEmail(*form.SiteContactEmail); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
i.ContactEmail = *form.SiteContactEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site short description if it's set on the form
|
||||||
|
if form.SiteShortDescription != nil {
|
||||||
|
if err := util.ValidateSiteShortDescription(*form.SiteShortDescription); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
i.ShortDescription = *form.SiteShortDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site description if it's set on the form
|
||||||
|
if form.SiteDescription != nil {
|
||||||
|
if err := util.ValidateSiteDescription(*form.SiteDescription); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
i.Description = *form.SiteDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate & update site terms if it's set on the form
|
||||||
|
if form.SiteTerms != nil {
|
||||||
|
if err := util.ValidateSiteTerms(*form.SiteTerms); err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
i.Terms = *form.SiteTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
// process avatar if provided
|
||||||
|
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||||
|
_, err := p.updateAccountAvatar(form.Avatar, ia.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process header if provided
|
||||||
|
if form.Header != nil && form.Header.Size != 0 {
|
||||||
|
_, err := p.updateAccountHeader(form.Header, ia.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.db.UpdateByID(i.ID, i); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance %s: %s", p.config.Host, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ai, err := p.tc.InstanceToMasto(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ai, nil
|
||||||
|
}
|
||||||
|
|
|
@ -95,6 +95,10 @@ type Processor interface {
|
||||||
|
|
||||||
// InstanceGet retrieves instance information for serving at api/v1/instance
|
// InstanceGet retrieves instance information for serving at api/v1/instance
|
||||||
InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode)
|
InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode)
|
||||||
|
// InstancePatch updates this instance according to the given form.
|
||||||
|
//
|
||||||
|
// It should already be ascertained that the requesting account is authenticated and an admin.
|
||||||
|
InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode)
|
||||||
|
|
||||||
// MediaCreate handles the creation of a media attachment, using the given form.
|
// MediaCreate handles the creation of a media attachment, using the given form.
|
||||||
MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
|
MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
|
||||||
|
|
|
@ -511,9 +511,31 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
|
||||||
ShortDescription: i.ShortDescription,
|
ShortDescription: i.ShortDescription,
|
||||||
Email: i.ContactEmail,
|
Email: i.ContactEmail,
|
||||||
Version: i.Version,
|
Version: i.Version,
|
||||||
|
Stats: make(map[string]int),
|
||||||
|
ContactAccount: &model.Account{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the requested instance is *this* instance, we can add some extra information
|
||||||
if i.Domain == c.config.Host {
|
if i.Domain == c.config.Host {
|
||||||
|
userCountKey := "user_count"
|
||||||
|
statusCountKey := "status_count"
|
||||||
|
domainCountKey := "domain_count"
|
||||||
|
|
||||||
|
userCount, err := c.db.GetUserCountForInstance(c.config.Host)
|
||||||
|
if err == nil {
|
||||||
|
mi.Stats[userCountKey] = userCount
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCount, err := c.db.GetStatusCountForInstance(c.config.Host)
|
||||||
|
if err == nil {
|
||||||
|
mi.Stats[statusCountKey] = statusCount
|
||||||
|
}
|
||||||
|
|
||||||
|
domainCount, err := c.db.GetDomainCountForInstance(c.config.Host)
|
||||||
|
if err == nil {
|
||||||
|
mi.Stats[domainCountKey] = domainCount
|
||||||
|
}
|
||||||
|
|
||||||
mi.Registrations = c.config.AccountsConfig.OpenRegistration
|
mi.Registrations = c.config.AccountsConfig.OpenRegistration
|
||||||
mi.ApprovalRequired = c.config.AccountsConfig.RequireApproval
|
mi.ApprovalRequired = c.config.AccountsConfig.RequireApproval
|
||||||
mi.InvitesEnabled = false // TODO
|
mi.InvitesEnabled = false // TODO
|
||||||
|
@ -523,6 +545,17 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the instance account if it exists and just skip if it doesn't
|
||||||
|
ia := >smodel.Account{}
|
||||||
|
if err := c.db.GetWhere([]db.Where{{Key: "username", Value: i.Domain}}, ia); err == nil {
|
||||||
|
// instance account exists, get the header for the account if it exists
|
||||||
|
attachment := >smodel.MediaAttachment{}
|
||||||
|
if err := c.db.GetHeaderForAccountID(attachment, ia.ID); err == nil {
|
||||||
|
// header exists, set it on the api model
|
||||||
|
mi.Thumbnail = attachment.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// contact account is optional but let's try to get it
|
// contact account is optional but let's try to get it
|
||||||
if i.ContactAccountID != "" {
|
if i.ContactAccountID != "" {
|
||||||
ia := >smodel.Account{}
|
ia := >smodel.Account{}
|
||||||
|
|
|
@ -24,12 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator
|
|
||||||
minimumReasonLength = 40
|
|
||||||
maximumReasonLength = 500
|
|
||||||
maximumEmailLength = 256
|
|
||||||
maximumUsernameLength = 64
|
maximumUsernameLength = 64
|
||||||
maximumPasswordLength = 64
|
|
||||||
maximumEmojiShortcodeLength = 30
|
maximumEmojiShortcodeLength = 30
|
||||||
maximumHashtagLength = 30
|
maximumHashtagLength = 30
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,6 +27,17 @@ import (
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maximumPasswordLength = 64
|
||||||
|
minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator
|
||||||
|
minimumReasonLength = 40
|
||||||
|
maximumReasonLength = 500
|
||||||
|
maximumSiteTitleLength = 40
|
||||||
|
maximumShortDescriptionLength = 500
|
||||||
|
maximumDescriptionLength = 5000
|
||||||
|
maximumSiteTermsLength = 5000
|
||||||
|
)
|
||||||
|
|
||||||
// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok.
|
// ValidateNewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok.
|
||||||
func ValidateNewPassword(password string) error {
|
func ValidateNewPassword(password string) error {
|
||||||
if password == "" {
|
if password == "" {
|
||||||
|
@ -47,12 +58,8 @@ func ValidateUsername(username string) error {
|
||||||
return errors.New("no username provided")
|
return errors.New("no username provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(username) > maximumUsernameLength {
|
|
||||||
return fmt.Errorf("username should be no more than %d chars but '%s' was %d", maximumUsernameLength, username, len(username))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !usernameValidationRegex.MatchString(username) {
|
if !usernameValidationRegex.MatchString(username) {
|
||||||
return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores", username)
|
return fmt.Errorf("given username %s was invalid: must contain only lowercase letters, numbers, and underscores, max %d characters", username, maximumUsernameLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -65,10 +72,6 @@ func ValidateEmail(email string) error {
|
||||||
return errors.New("no email provided")
|
return errors.New("no email provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(email) > maximumEmailLength {
|
|
||||||
return fmt.Errorf("email address should be no more than %d chars but '%s' was %d", maximumEmailLength, email, len(email))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := mail.ParseAddress(email)
|
_, err := mail.ParseAddress(email)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -132,3 +135,39 @@ func ValidateEmojiShortcode(shortcode string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateSiteTitle ensures that the given site title is within spec.
|
||||||
|
func ValidateSiteTitle(siteTitle string) error {
|
||||||
|
if len(siteTitle) > maximumSiteTitleLength {
|
||||||
|
return fmt.Errorf("site title should be no more than %d chars but given title was %d", maximumSiteTitleLength, len(siteTitle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSiteShortDescription ensures that the given site short description is within spec.
|
||||||
|
func ValidateSiteShortDescription(d string) error {
|
||||||
|
if len(d) > maximumShortDescriptionLength {
|
||||||
|
return fmt.Errorf("short description should be no more than %d chars but given description was %d", maximumShortDescriptionLength, len(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSiteDescription ensures that the given site description is within spec.
|
||||||
|
func ValidateSiteDescription(d string) error {
|
||||||
|
if len(d) > maximumDescriptionLength {
|
||||||
|
return fmt.Errorf("description should be no more than %d chars but given description was %d", maximumDescriptionLength, len(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSiteTerms ensures that the given site terms string is within spec.
|
||||||
|
func ValidateSiteTerms(t string) error {
|
||||||
|
if len(t) > maximumSiteTermsLength {
|
||||||
|
return fmt.Errorf("terms should be no more than %d chars but given terms was %d", maximumSiteTermsLength, len(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
<a href="https://github.com/superseriousbusiness/gotosocial">Source Code</a>
|
<a href="https://github.com/superseriousbusiness/gotosocial">Source Code</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="contact">
|
<div id="contact">
|
||||||
Contact: <a href="/{{.instance.ContactAccount}}" class="nounderline">{{.instance.ContactAccount}}</a><br>
|
Contact: <a href="{{.instance.ContactAccount.URL}}" class="nounderline">{{.instance.ContactAccount.Username}}</a><br>
|
||||||
<!-- <a href="/moderation">Moderation team</a> -->
|
</div>
|
||||||
|
<div id="email">
|
||||||
|
Email: <a href="mailto:{{.instance.Email}}" class="nounderline">{{.instance.Email}}</a><br>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -3,24 +3,19 @@
|
||||||
<img src="/assets/sloth.png" alt="Clipart styled sloth logo">
|
<img src="/assets/sloth.png" alt="Clipart styled sloth logo">
|
||||||
</aside>
|
</aside>
|
||||||
<section>
|
<section>
|
||||||
<!-- <h1>Home to <span class="count">{ {.instance.Stats.UserCount}}</span> users
|
<h1>Home to <span class="count">{{.instance.Stats.user_count}}</span> users
|
||||||
who posted <span class="count">{ {.instance.Stats.StatusCount}}</span> statuses,
|
who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
|
||||||
federating with <span class="count">{ {.instance.Stats.DomainCount}}</span> other instances.</h1> -->
|
federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.</h1>
|
||||||
<h1>Home to <span class="count">3</span> users
|
|
||||||
who posted <span class="count">42069</span> statuses,
|
|
||||||
federating with <span class="count">9001</span> other instances.</h1>
|
|
||||||
|
|
||||||
<h3>This is the default landing page, you can edit it from <span class="accent">./web/template/index.tmpl</span></h1>
|
<h3>This is the default landing page, you can edit it from <span class="accent">./web/template/index.tmpl</span></h1>
|
||||||
<ul>
|
<p>
|
||||||
<li>Some explanation about the instance (description) with instance header and profile images.</li>
|
{{.instance.ShortDescription}}
|
||||||
<li>Instructions for registering.</li>
|
</p>
|
||||||
<li>Etc.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="apps">
|
<section class="apps">
|
||||||
<p>
|
<p>
|
||||||
GoToSocial does not provide it's own frontend, but implements the Mastodon client API.
|
GoToSocial does not provide its own frontend, but implements the Mastodon client API.
|
||||||
You can use this server through a variety of clients:
|
You can use this server through a variety of clients:
|
||||||
</p>
|
</p>
|
||||||
<div class="applist">
|
<div class="applist">
|
||||||
|
|
Loading…
Reference in a new issue