diff --git a/docs/admin/settings.md b/docs/admin/settings.md
index e19bc91bc..2429b5ffe 100644
--- a/docs/admin/settings.md
+++ b/docs/admin/settings.md
@@ -96,6 +96,68 @@ Through the 'remote' section, you can look up a link to any remote toots (provid
### Instance Settings
-![Screenshot of the GoToSocial admin panel, showing the fields to change an instance's settings](../assets/admin-settings.png)
+![Screenshot of the GoToSocial admin panel, showing the fields to change an instance's settings](../assets/admin-settings-instance.png)
-Here you can set various metadata for your instance, like the displayed name/title, thumbnail image, description (HTML accepted), and contact username and email.
+Here you can set various metadata for your instance, like the displayed name/title, thumbnail image, (short) description, and contact info.
+
+#### Instance Appearance
+
+These settings primary affect how your instance appears to others and on the web.
+
+Your **instance title** will appear at the top of every web page on your instance, and in OpenGraph meta tags, so pick something that represents the vibe of your instance.
+
+The **instance avatar** is sort of like the mascot of your instance. It will appear next to the instance title at the top of every page, and as the preview image in browser tabs, OpenGraph links, and that sort of thing.
+
+If you set an instance avatar, we highly recommend setting the **avatar image description** as well. This will provide alt text for the image you set as avatar, helping screenreader users to understand what's depicted in the image. Keep it short and sweet.
+
+#### Instance Descriptors
+
+You can use these fields to set short and full descriptions of your instance, as well as to provide terms and conditions for current and prospective users of your instance.
+
+The **short description** will be shown on the instance home page, right near the top, and in response to `/api/v1/instance` queries.
+
+It's a good idea to provide something pithy in here, to give visitors to your instance an immediate impression of what you're all about. For example:
+
+> This is an instance for enthusiasts of classic synthesizers.
+>
+> Sick beats are for life, and not just for Christmas!
+
+or:
+
+> This is a single-user instance just for me!
+>
+> Here's my profile: @your_username
+
+The **full description** will appear on your instance's /about page, and in response to `/api/v1/instance` queries.
+
+You can use this to provide info like:
+
+- your instance's history, ethos, attitude, and vibe
+- the kinds of things your instance denizens tend to post about
+- how to get an account on your instance (if it's possible at all)
+- a list of users with accounts on the instance, who want to be found more easily
+
+The **terms and conditions** box also appears on your instance's /about page, and in response to `/api/v1/instance` queries.
+
+Use it for filling in stuff like:
+
+- legal jargon (imprint, GDPR, or links thereto)
+- federation policy
+- data policy
+- account deletion/suspension policy
+
+All of the above fields accept **markdown** input, so you can write proper lists, codeblocks, horizontal rules, block quotes, or whatever you like.
+
+You can also mention accounts using the standard `@user[@domain]` format.
+
+Have a look at the [markdown cheat sheet](https://markdownguide.offshoot.io/cheat-sheet/) to see what else you can do.
+
+### Instance Contact Info
+
+In this section, you can provide visitors to your instance with a convenient way of reaching your instance admin.
+
+Links to the set contact account and/or email address will appear on the footer of every web page of your instance, on the /about page in the "contact" section, and in response to `/api/v1/instance` queries.
+
+The selected **contact user** must be an active (not suspended) admin and/or moderator on the instance.
+
+If you're on a single-user instance and you give admin privileges to your main account, you can just fill in your own username here; you don't need to make a separate admin account just for this.
diff --git a/docs/assets/admin-settings-instance.png b/docs/assets/admin-settings-instance.png
new file mode 100644
index 000000000..181a35a7c
Binary files /dev/null and b/docs/assets/admin-settings-instance.png differ
diff --git a/docs/assets/admin-settings.png b/docs/assets/admin-settings.png
deleted file mode 100644
index 3d16ca2f7..000000000
Binary files a/docs/assets/admin-settings.png and /dev/null differ
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index 420bcd79e..936d6efd9 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -78,8 +78,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "Example Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "someone@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -173,7 +175,9 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
}
@@ -195,8 +199,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "Geoff's Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "admin@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -290,13 +296,15 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
}
func (suite *InstancePatchTestSuite) TestInstancePatch3() {
code, b := suite.instancePatch("", "", map[string][]string{
- "short_description": {"
This is some html, which is allowed in short descriptions.
"},
+ "short_description": {"This is some html, which is allowed in short descriptions."},
})
if expectedCode := http.StatusOK; code != expectedCode {
@@ -312,8 +320,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "GoToSocial Testrig Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is some html, which is allowed in short descriptions.
",
+ "short_description_text": "This is some html, which is allowed in short descriptions.",
"email": "admin@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -407,7 +417,9 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
}
@@ -480,8 +492,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "GoToSocial Testrig Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "",
"version": "0.0.0-testrig",
"languages": [
@@ -575,7 +589,9 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
}
@@ -619,8 +635,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "GoToSocial Testrig Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "admin@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -716,7 +734,9 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
// extra bonus: check the v2 model thumbnail after the patch
@@ -773,8 +793,10 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "GoToSocial Testrig Instance",
- "description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "description": "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "admin@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -868,7 +890,9 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
- ]
+ ],
+ "terms": "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, dst.String())
}
diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go
index 2e5fef123..bec719941 100644
--- a/internal/api/model/instancev1.go
+++ b/internal/api/model/instancev1.go
@@ -38,12 +38,16 @@ type InstanceV1 struct {
//
// This should be displayed on the 'about' page for an instance.
Description string `json:"description"`
+ // Raw (unparsed) version of description.
+ DescriptionText string `json:"description_text,omitempty"`
// A shorter description of the instance.
//
// Should be HTML formatted, but might be plaintext.
//
// This should be displayed on the instance splash/landing page.
ShortDescription string `json:"short_description"`
+ // Raw (unparsed) version of short description.
+ ShortDescriptionText string `json:"short_description_text,omitempty"`
// An email address that may be used for inquiries.
// example: admin@example.org
Email string `json:"email"`
@@ -92,6 +96,8 @@ type InstanceV1 struct {
Rules []InstanceRule `json:"rules"`
// Terms and conditions for accounts on this instance.
Terms string `json:"terms,omitempty"`
+ // Raw (unparsed) version of terms.
+ TermsRaw string `json:"terms_text,omitempty"`
}
// InstanceV1URLs models instance-relevant URLs for client application consumption.
diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go
index 6af657813..dda9033b4 100644
--- a/internal/api/model/instancev2.go
+++ b/internal/api/model/instancev2.go
@@ -49,6 +49,8 @@ type InstanceV2 struct {
//
// This should be displayed on the 'about' page for an instance.
Description string `json:"description"`
+ // Raw (unparsed) version of description.
+ DescriptionText string `json:"description_text,omitempty"`
// Basic anonymous usage data for this instance.
Usage InstanceV2Usage `json:"usage"`
// An image used to represent this instance.
@@ -66,6 +68,8 @@ type InstanceV2 struct {
Rules []InstanceRule `json:"rules"`
// Terms and conditions for accounts on this instance.
Terms string `json:"terms,omitempty"`
+ // Raw (unparsed) version of terms.
+ TermsText string `json:"terms_text,omitempty"`
}
// Usage data for this instance.
diff --git a/internal/db/bundb/migrations/20231227110845_instance_description_updates.go b/internal/db/bundb/migrations/20231227110845_instance_description_updates.go
new file mode 100644
index 000000000..94914ff55
--- /dev/null
+++ b/internal/db/bundb/migrations/20231227110845_instance_description_updates.go
@@ -0,0 +1,64 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 migrations
+
+import (
+ "context"
+ "strings"
+
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ columns := []string{
+ "short_description_text",
+ "description_text",
+ "terms_text",
+ }
+
+ for _, column := range columns {
+ _, err := tx.ExecContext(ctx,
+ "ALTER TABLE ? ADD COLUMN ? TEXT",
+ bun.Ident("instances"), bun.Ident(column),
+ )
+ if err != nil {
+ e := err.Error()
+ if !(strings.Contains(e, "already exists") ||
+ strings.Contains(e, "duplicate column name") ||
+ strings.Contains(e, "SQLSTATE 42701")) {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go
index 6d572f519..027d8fba4 100644
--- a/internal/gtsmodel/instance.go
+++ b/internal/gtsmodel/instance.go
@@ -31,8 +31,11 @@ type Instance struct {
DomainBlockID string `bun:"type:CHAR(26),nullzero"` // ID of any existing domain block for this instance in the database
DomainBlock *DomainBlock `bun:"rel:belongs-to"` // Domain block corresponding to domainBlockID
ShortDescription string `bun:""` // Short description of this instance
- Description string `bun:""` // Longer description of this instance
- Terms string `bun:""` // Terms and conditions of this instance
+ ShortDescriptionText string `bun:""` // Raw text version of short description (before parsing).
+ Description string `bun:""` // Longer description of this instance.
+ DescriptionText string `bun:""` // Raw text version of long description (before parsing).
+ Terms string `bun:""` // Terms and conditions of this instance.
+ TermsText string `bun:""` // Raw text version of terms (before parsing).
ContactEmail string `bun:""` // Contact email address for this instance
ContactAccountUsername string `bun:",nullzero"` // Username of the contact account for this instance
ContactAccountID string `bun:"type:CHAR(26),nullzero"` // Contact account ID in the database for this instance
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index caf2b9fc1..a93936425 100644
--- a/internal/processing/instance.go
+++ b/internal/processing/instance.go
@@ -33,15 +33,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
-func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) {
- instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
- if err != nil {
- return nil, err
- }
-
- return instance, nil
-}
-
func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
i, err := p.getThisInstance(ctx)
if err != nil {
@@ -146,67 +137,55 @@ func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRu
}
func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) {
- // fetch the instance entry from the db for processing
- host := config.GetHost()
-
- instance, err := p.state.DB.GetInstance(ctx, host)
+ // Fetch this instance from the db for processing.
+ instance, err := p.getThisInstance(ctx)
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", host, err))
+ err = fmt.Errorf("db error fetching instance: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // fetch the instance account from the db for processing
- ia, err := p.state.DB.GetInstanceAccount(ctx, "")
+ // Fetch this instance account from the db for processing.
+ instanceAcc, err := p.state.DB.GetInstanceAccount(ctx, "")
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", host, err))
+ err = fmt.Errorf("db error fetching instance account: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- updatingColumns := []string{}
+ // Columns to update
+ // in the database.
+ var columns []string
- // validate & update site title if it's set on the form
+ // Validate & update site
+ // title if set on the form.
if form.Title != nil {
- if err := validate.SiteTitle(*form.Title); err != nil {
- return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("site title invalid: %s", err))
+ title := *form.Title
+ if err := validate.SiteTitle(title); err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- updatingColumns = append(updatingColumns, "title")
- instance.Title = text.SanitizeToPlaintext(*form.Title) // don't allow html in site title
+
+ // Don't allow html in site title.
+ instance.Title = text.SanitizeToPlaintext(title)
+ columns = append(columns, "title")
}
- // validate & update site contact account if it's set on the form
+ // Validate & update site contact
+ // account if set on the form.
+ //
+ // Empty username unsets contact.
if form.ContactUsername != nil {
- // make sure the account with the given username exists in the db
- contactAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, *form.ContactUsername, "")
+ contactAccountID, err := p.contactAccountIDForUsername(ctx, *form.ContactUsername)
if err != nil {
- return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.ContactUsername))
- }
- // make sure it has a user associated with it
- contactUser, err := p.state.DB.GetUserByAccountID(ctx, contactAccount.ID)
- if err != nil {
- return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("user for account with username %s not retrievable", *form.ContactUsername))
- }
- // 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())
- }
- updatingColumns = append(updatingColumns, "contact_account_id")
- instance.ContactAccountID = contactAccount.ID
+
+ columns = append(columns, "contact_account_id")
+ instance.ContactAccountID = contactAccountID
}
- // validate & update site contact email if it's set on the form
+ // Validate & update contact
+ // email if set on the form.
+ //
+ // Empty email unsets contact.
if form.ContactEmail != nil {
contactEmail := *form.ContactEmail
if contactEmail != "" {
@@ -214,87 +193,162 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
}
- updatingColumns = append(updatingColumns, "contact_email")
+
+ columns = append(columns, "contact_email")
instance.ContactEmail = contactEmail
}
- // validate & update site short description if it's set on the form
+ // Validate & update site short
+ // description if set on the form.
if form.ShortDescription != nil {
- if err := validate.SiteShortDescription(*form.ShortDescription); err != nil {
+ shortDescription := *form.ShortDescription
+ if err := validate.SiteShortDescription(shortDescription); err != nil {
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- updatingColumns = append(updatingColumns, "short_description")
- instance.ShortDescription = text.SanitizeToHTML(*form.ShortDescription) // html is OK in site description, but we should sanitize it
+
+ // Parse description as Markdown, keep
+ // the raw version for later editing.
+ instance.ShortDescriptionText = shortDescription
+ instance.ShortDescription = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, instanceAcc.ID, "", shortDescription).HTML
+ columns = append(columns, []string{"short_description", "short_description_text"}...)
}
// validate & update site description if it's set on the form
if form.Description != nil {
- if err := validate.SiteDescription(*form.Description); err != nil {
+ description := *form.Description
+ if err := validate.SiteDescription(description); err != nil {
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- updatingColumns = append(updatingColumns, "description")
- instance.Description = text.SanitizeToHTML(*form.Description) // html is OK in site description, but we should sanitize it
+
+ // Parse description as Markdown, keep
+ // the raw version for later editing.
+ instance.DescriptionText = description
+ instance.Description = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, instanceAcc.ID, "", description).HTML
+ columns = append(columns, []string{"description", "description_text"}...)
}
- // validate & update site terms if it's set on the form
+ // Validate & update site
+ // terms if set on the form.
if form.Terms != nil {
- if err := validate.SiteTerms(*form.Terms); err != nil {
+ terms := *form.Terms
+ if err := validate.SiteTerms(terms); err != nil {
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- updatingColumns = append(updatingColumns, "terms")
- instance.Terms = text.SanitizeToHTML(*form.Terms) // html is OK in site terms, but we should sanitize it
+
+ // Parse terms as Markdown, keep
+ // the raw version for later editing.
+ instance.TermsText = terms
+ instance.Terms = p.formatter.FromMarkdown(ctx, p.parseMentionFunc, "", "", terms).HTML
+ columns = append(columns, []string{"terms", "terms_text"}...)
}
var updateInstanceAccount bool
if form.Avatar != nil && form.Avatar.Size != 0 {
- // process instance avatar image + description
- avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID)
+ // Process instance avatar image + description.
+ avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, instanceAcc.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
}
- ia.AvatarMediaAttachmentID = avatarInfo.ID
- ia.AvatarMediaAttachment = avatarInfo
+ instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
+ instanceAcc.AvatarMediaAttachment = avatarInfo
updateInstanceAccount = true
- } else if form.AvatarDescription != nil && ia.AvatarMediaAttachment != nil {
- // process just the description for the existing avatar
- ia.AvatarMediaAttachment.Description = *form.AvatarDescription
- if err := p.state.DB.UpdateAttachment(ctx, ia.AvatarMediaAttachment, "description"); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance avatar description: %s", err))
+ } else if form.AvatarDescription != nil && instanceAcc.AvatarMediaAttachment != nil {
+ // Process just the description for the existing avatar.
+ instanceAcc.AvatarMediaAttachment.Description = *form.AvatarDescription
+ if err := p.state.DB.UpdateAttachment(ctx, instanceAcc.AvatarMediaAttachment, "description"); err != nil {
+ err = fmt.Errorf("db error updating instance avatar description: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
}
if form.Header != nil && form.Header.Size != 0 {
// process instance header image
- headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, ia.ID)
+ headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, instanceAcc.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
}
- ia.HeaderMediaAttachmentID = headerInfo.ID
- ia.HeaderMediaAttachment = headerInfo
+ instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
+ instanceAcc.HeaderMediaAttachment = headerInfo
updateInstanceAccount = true
}
if updateInstanceAccount {
- // if either avatar or header is updated, we need
- // to update the instance account that stores them
- if err := p.state.DB.UpdateAccount(ctx, ia); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance account: %s", err))
+ // If either avatar or header is updated, we need
+ // to update the instance account that stores them.
+ if err := p.state.DB.UpdateAccount(ctx, instanceAcc); err != nil {
+ err = fmt.Errorf("db error updating instance account: %w", err)
+ return nil, gtserror.NewErrorInternalError(err, err.Error())
}
}
- if len(updatingColumns) != 0 {
- if err := p.state.DB.UpdateInstance(ctx, instance, updatingColumns...); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error updating instance %s: %s", host, err))
+ if len(columns) != 0 {
+ if err := p.state.DB.UpdateInstance(ctx, instance, columns...); err != nil {
+ err = fmt.Errorf("db error updating instance: %w", err)
+ return nil, gtserror.NewErrorInternalError(err, err.Error())
}
}
- ai, err := p.converter.InstanceToAPIV1Instance(ctx, instance)
+ return p.InstanceGetV1(ctx)
+}
+
+func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) {
+ instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
+ return nil, err
}
- return ai, nil
+ return instance, nil
+}
+
+func (p *Processor) contactAccountIDForUsername(ctx context.Context, username string) (string, error) {
+ if username == "" {
+ // Easy: unset
+ // contact account.
+ return "", nil
+ }
+
+ // Make sure local account with the given username exists in the db.
+ contactAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
+ if err != nil {
+ err = fmt.Errorf("db error getting selected contact account with username %s: %w", username, err)
+ return "", err
+ }
+
+ // Make sure account corresponds to a user.
+ contactUser, err := p.state.DB.GetUserByAccountID(ctx, contactAccount.ID)
+ if err != nil {
+ err = fmt.Errorf("db error getting user for selected contact account %s: %w", username, err)
+ return "", err
+ }
+
+ // Ensure account/user is:
+ //
+ // - confirmed and approved
+ // - not suspended
+ // - an admin or a moderator
+ if contactUser.ConfirmedAt.IsZero() {
+ err := fmt.Errorf("user of selected contact account %s is not confirmed", contactAccount.Username)
+ return "", err
+ }
+
+ if !*contactUser.Approved {
+ err := fmt.Errorf("user of selected contact account %s is not approved", contactAccount.Username)
+ return "", err
+ }
+
+ if !contactAccount.SuspendedAt.IsZero() {
+ err := fmt.Errorf("selected contact account %s is suspended", contactAccount.Username)
+ return "", err
+ }
+
+ if !*contactUser.Admin && !*contactUser.Moderator {
+ err := fmt.Errorf("user of selected contact account %s is neither admin nor moderator", contactAccount.Username)
+ return "", err
+ }
+
+ // All good!
+ return contactAccount.ID, nil
}
func obfuscate(domain string) string {
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index ac930aeb2..6ac7a6bf0 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -21,6 +21,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
mm "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
@@ -39,6 +40,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
"github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
@@ -55,6 +57,13 @@ type Processor struct {
oauthServer oauth.Server
state *state.State
+ /*
+ Required for instance description / terms updating.
+ */
+
+ formatter *text.Formatter
+ parseMentionFunc gtsmodel.ParseMentionFunc
+
/*
SUB-PROCESSORS
*/
@@ -147,9 +156,11 @@ func NewProcessor(
)
processor := &Processor{
- converter: converter,
- oauthServer: oauthServer,
- state: state,
+ converter: converter,
+ oauthServer: oauthServer,
+ state: state,
+ formatter: text.NewFormatter(state.DB),
+ parseMentionFunc: parseMentionFunc,
}
// Instantiate sub processors.
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 9d1f7edeb..abae81d04 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -941,20 +941,23 @@ func (c *Converter) InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.Admin
// InstanceToAPIV1Instance converts a gts instance into its api equivalent for serving at /api/v1/instance
func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) {
instance := &apimodel.InstanceV1{
- URI: i.URI,
- AccountDomain: config.GetAccountDomain(),
- Title: i.Title,
- Description: i.Description,
- ShortDescription: i.ShortDescription,
- Email: i.ContactEmail,
- Version: config.GetSoftwareVersion(),
- Languages: config.GetInstanceLanguages().TagStrs(),
- Registrations: config.GetAccountsRegistrationOpen(),
- ApprovalRequired: config.GetAccountsApprovalRequired(),
- InvitesEnabled: false, // todo: not supported yet
- MaxTootChars: uint(config.GetStatusesMaxChars()),
- Rules: c.InstanceRulesToAPIRules(i.Rules),
- Terms: i.Terms,
+ URI: i.URI,
+ AccountDomain: config.GetAccountDomain(),
+ Title: i.Title,
+ Description: i.Description,
+ DescriptionText: i.DescriptionText,
+ ShortDescription: i.ShortDescription,
+ ShortDescriptionText: i.ShortDescriptionText,
+ Email: i.ContactEmail,
+ Version: config.GetSoftwareVersion(),
+ Languages: config.GetInstanceLanguages().TagStrs(),
+ Registrations: config.GetAccountsRegistrationOpen(),
+ ApprovalRequired: config.GetAccountsApprovalRequired(),
+ InvitesEnabled: false, // todo: not supported yet
+ MaxTootChars: uint(config.GetStatusesMaxChars()),
+ Rules: c.InstanceRulesToAPIRules(i.Rules),
+ Terms: i.Terms,
+ TermsRaw: i.TermsText,
}
if config.GetInstanceInjectMastodonVersion() {
@@ -1050,16 +1053,18 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
// InstanceToAPIV2Instance converts a gts instance into its api equivalent for serving at /api/v2/instance
func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error) {
instance := &apimodel.InstanceV2{
- Domain: i.Domain,
- AccountDomain: config.GetAccountDomain(),
- Title: i.Title,
- Version: config.GetSoftwareVersion(),
- SourceURL: instanceSourceURL,
- Description: i.Description,
- Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
- Languages: config.GetInstanceLanguages().TagStrs(),
- Rules: c.InstanceRulesToAPIRules(i.Rules),
- Terms: i.Terms,
+ Domain: i.Domain,
+ AccountDomain: config.GetAccountDomain(),
+ Title: i.Title,
+ Version: config.GetSoftwareVersion(),
+ SourceURL: instanceSourceURL,
+ Description: i.Description,
+ DescriptionText: i.DescriptionText,
+ Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
+ Languages: config.GetInstanceLanguages().TagStrs(),
+ Rules: c.InstanceRulesToAPIRules(i.Rules),
+ Terms: i.Terms,
+ TermsText: i.TermsText,
}
if config.GetInstanceInjectMastodonVersion() {
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index d65d47bf1..68b075310 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -841,8 +841,10 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
"title": "GoToSocial Testrig Instance",
- "description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e",
+ "description": "\u003cp\u003eHere's a fuller description of the GoToSocial testrig instance.\u003c/p\u003e\u003cp\u003eThis instance is for testing purposes only. It doesn't federate at all. Go check out \u003ca href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://github.com/superseriousbusiness/gotosocial/tree/main/testrig\u003c/a\u003e and \u003ca href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\u003c/a\u003e\u003c/p\u003e\u003cp\u003eUsers on this instance:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (admin!).\u003c/li\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003e1happyturtle\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (posts about turtles, we don't know why).\u003c/li\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003ethe_mighty_zork\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (who knows).\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eIf you need to edit the models for the testrig, you can do so at \u003ccode\u003einternal/testmodels.go\u003c/code\u003e.\u003c/p\u003e",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"short_description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e",
+ "short_description_text": "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
"email": "admin@example.org",
"version": "0.0.0-testrig",
"languages": [
@@ -927,7 +929,9 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
}
},
"max_toot_chars": 5000,
- "rules": []
+ "rules": [],
+ "terms": "\u003cp\u003eThis is where a list of terms and conditions might go.\u003c/p\u003e\u003cp\u003eFor example:\u003c/p\u003e\u003cp\u003eIf you want to sign up on this instance, you oughta know that we:\u003c/p\u003e\u003col\u003e\u003cli\u003eWill sell your data to whoever offers.\u003c/li\u003e\u003cli\u003eSecure the server with password \u003ccode\u003epassword\u003c/code\u003e wherever possible.\u003c/li\u003e\u003c/ol\u003e",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, string(b))
}
@@ -953,7 +957,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() {
"title": "GoToSocial Testrig Instance",
"version": "0.0.0-testrig",
"source_url": "https://github.com/superseriousbusiness/gotosocial",
- "description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e",
+ "description": "\u003cp\u003eHere's a fuller description of the GoToSocial testrig instance.\u003c/p\u003e\u003cp\u003eThis instance is for testing purposes only. It doesn't federate at all. Go check out \u003ca href=\"https://github.com/superseriousbusiness/gotosocial/tree/main/testrig\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://github.com/superseriousbusiness/gotosocial/tree/main/testrig\u003c/a\u003e and \u003ca href=\"https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003ehttps://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\u003c/a\u003e\u003c/p\u003e\u003cp\u003eUsers on this instance:\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (admin!).\u003c/li\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003e1happyturtle\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (posts about turtles, we don't know why).\u003c/li\u003e\u003cli\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@the_mighty_zork\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003ethe_mighty_zork\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e (who knows).\u003c/li\u003e\u003c/ul\u003e\u003cp\u003eIf you need to edit the models for the testrig, you can do so at \u003ccode\u003einternal/testmodels.go\u003c/code\u003e.\u003c/p\u003e",
+ "description_text": "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `+"`"+`internal/testmodels.go`+"`"+`.",
"usage": {
"users": {
"active_month": 0
@@ -1045,7 +1050,9 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() {
}
}
},
- "rules": []
+ "rules": [],
+ "terms": "\u003cp\u003eThis is where a list of terms and conditions might go.\u003c/p\u003e\u003cp\u003eFor example:\u003c/p\u003e\u003cp\u003eIf you want to sign up on this instance, you oughta know that we:\u003c/p\u003e\u003col\u003e\u003cli\u003eWill sell your data to whoever offers.\u003c/li\u003e\u003cli\u003eSecure the server with password \u003ccode\u003epassword\u003c/code\u003e wherever possible.\u003c/li\u003e\u003c/ol\u003e",
+ "terms_text": "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `+"`"+`password`+"`"+` wherever possible."
}`, string(b))
}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index e7c1f0c02..804ad19cd 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -1312,7 +1312,11 @@ func NewTestInstances() map[string]*gtsmodel.Instance {
URI: "http://localhost:8080",
Title: "GoToSocial Testrig Instance",
ShortDescription: "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
- Description: "
This is the GoToSocial testrig. It doesn't federate or anything.
When the testrig is shut down, all data on it will be deleted.
Don't use this in production!
",
+ ShortDescriptionText: "This is the GoToSocial testrig. It doesn't federate or anything.\n\nWhen the testrig is shut down, all data on it will be deleted.\n\nDon't use this in production!",
+ Description: "
Here's a fuller description of the GoToSocial testrig instance.
If you need to edit the models for the testrig, you can do so at internal/testmodels.go.
",
+ DescriptionText: "Here's a fuller description of the GoToSocial testrig instance.\n\nThis instance is for testing purposes only. It doesn't federate at all. Go check out https://github.com/superseriousbusiness/gotosocial/tree/main/testrig and https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#testing\n\nUsers on this instance:\n\n- @admin (admin!).\n- @1happyturtle (posts about turtles, we don't know why).\n- @the_mighty_zork (who knows).\n\nIf you need to edit the models for the testrig, you can do so at `internal/testmodels.go`.",
+ Terms: "
This is where a list of terms and conditions might go.
For example:
If you want to sign up on this instance, you oughta know that we:
Will sell your data to whoever offers.
Secure the server with password password wherever possible.
",
+ TermsText: "This is where a list of terms and conditions might go.\n\nFor example:\n\nIf you want to sign up on this instance, you oughta know that we:\n\n1. Will sell your data to whoever offers.\n2. Secure the server with password `password` wherever possible.",
ContactEmail: "admin@example.org",
ContactAccountUsername: "admin",
ContactAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
diff --git a/web/source/css/about.css b/web/source/css/about.css
index 55318572c..0a2ac763f 100644
--- a/web/source/css/about.css
+++ b/web/source/css/about.css
@@ -27,12 +27,8 @@
border: $boxshadow-border;
border-radius: $br;
- .about-section {
- ul, ol {
- margin-top: 0;
- }
-
- h3, h4 {
+ .about-section {
+ h1, h2, h3, h4, h5 {
margin-top: 0;
}
}
diff --git a/web/source/css/base.css b/web/source/css/base.css
index 0d3c436c8..5198ce6b8 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -16,7 +16,12 @@
along with this program. If not, see .
*/
+/***************************************
+***** SECTION 0: IMPORTS AND FONTS *****
+****************************************/
+
@import "modern-normalize/modern-normalize.css";
+@import "./prism.css";
/* noto-sans-regular - latin */
@font-face {
@@ -261,6 +266,77 @@ label {
cursor: pointer;
}
+/*
+ Set our own nice background for
+ monospace code and pre blocks.
+*/
+pre, pre[class*="language-"],
+code, code[class*="language-"] {
+ background-color: $gray2;
+}
+
+/*
+ Just code on its own inside status
+ content, ie, `here is some code`.
+*/
+code {
+ padding: 0.25rem;
+ border-radius: $br-inner;
+ white-space: pre-wrap;
+}
+
+/*
+ Restyle Prism code highlighting toolbar
+ plugin buttons to our own button style.
+
+ We have to use really specific selectors
+ because of how specific prism.css is.
+*/
+div.code-toolbar > div.toolbar {
+ margin-right: 0.5rem;
+ display: flex;
+ gap: 0.25rem;
+
+ > div.toolbar-item {
+ > span, > button {
+ color: $button-fg;
+ background: $button-bg;
+ font-weight: bold;
+ box-shadow: $boxshadow;
+
+ &:hover, &:focus {
+ color: $button-fg;
+ }
+ }
+
+ .copy-to-clipboard-button:hover {
+ background: $button-hover-bg;
+ }
+ }
+}
+
+pre, pre[class*="language-"] {
+ border-radius: $br;
+ padding: 0.5rem;
+ white-space: pre;
+ overflow-x: auto;
+
+ /*
+ Code inside a pre block, ie.,
+
+ ```
+ here is some code
+ ```
+ */
+ code {
+ width: 100%;
+ padding: 0;
+ white-space: pre;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
/*************************************
***** SECTION 3: UTILITY CLASSES *****
**************************************/
diff --git a/web/source/css/status.css b/web/source/css/status.css
index 019fbd0b4..009dd9f2b 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -19,7 +19,6 @@
@import "photoswipe/dist/photoswipe.css";
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
@import "plyr/dist/plyr.css";
-@import "./prism.css";
main {
background: transparent;
@@ -194,68 +193,6 @@ main {
line-height: initial;
}
- pre, code {
- background-color: $gray2;
- }
-
- /*
- Just code on its own inside status
- content, ie, `here is some code`.
- */
- code {
- padding: 0.25rem;
- border-radius: $br-inner;
- white-space: pre-wrap;
- }
-
- /*
- Restyle Prism code highlighting toolbar
- plugin buttons to our own button style.
- */
- .code-toolbar .toolbar {
- margin-right: 0.5rem;
- display: flex;
- gap: 0.25rem;
-
- .toolbar-item {
- span, button {
- color: $button-fg;
- background: $button-bg;
- font-weight: bold;
- }
-
- .copy-to-clipboard-button, span {
- box-shadow: $boxshadow;
- }
-
- .copy-to-clipboard-button:hover, .copy-to-clipboard-button:hover span {
- background: $button-hover-bg;
- }
- }
- }
-
- pre, pre[class*="language-"] {
- border-radius: $br;
- padding: 0.5rem;
- white-space: pre;
- overflow-x: auto;
-
- /*
- Code inside a pre block, ie.,
-
- ```
- here is some code
- ```
- */
- code {
- width: 100%;
- padding: 0;
- white-space: pre;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- }
- }
-
img {
max-width: 100%;
margin: 5px auto;
diff --git a/web/source/settings/admin/settings/index.jsx b/web/source/settings/admin/settings/index.jsx
deleted file mode 100644
index c0da83a2a..000000000
--- a/web/source/settings/admin/settings/index.jsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- 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 .
-*/
-
-const React = require("react");
-
-const {
- useTextInput,
- useFileInput
-} = require("../../lib/form");
-
-const useFormSubmit = require("../../lib/form/submit").default;
-
-const {
- TextInput,
- TextArea,
- FileInput
-} = require("../../components/form/inputs");
-
-const FormWithData = require("../../lib/form/form-with-data").default;
-const MutationButton = require("../../components/form/mutation-button");
-
-const { useInstanceV1Query } = require("../../lib/query");
-const { useUpdateInstanceMutation } = require("../../lib/query/admin");
-
-module.exports = function AdminSettings() {
- return (
-
- );
-};
-
-function AdminSettingsForm({ data: instance }) {
- const form = {
- title: useTextInput("title", {
- source: instance,
- validator: (val) => val.length <= 40 ? "" : "Instance title must be 40 characters or less"
- }),
- thumbnail: useFileInput("thumbnail", { withPreview: true }),
- thumbnailDesc: useTextInput("thumbnail_description", { source: instance }),
- shortDesc: useTextInput("short_description", { source: instance }),
- description: useTextInput("description", { source: instance }),
- contactUser: useTextInput("contact_username", { source: instance, valueSelector: (s) => s.contact_account?.username }),
- contactEmail: useTextInput("contact_email", { source: instance, valueSelector: (s) => s.email }),
- terms: useTextInput("terms", { source: instance })
- };
-
- const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation());
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/web/source/settings/admin/settings/index.tsx b/web/source/settings/admin/settings/index.tsx
new file mode 100644
index 000000000..4e5ea343a
--- /dev/null
+++ b/web/source/settings/admin/settings/index.tsx
@@ -0,0 +1,191 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ 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 .
+*/
+
+import React from "react";
+
+import { useTextInput, useFileInput } from "../../lib/form";
+
+const useFormSubmit = require("../../lib/form/submit").default;
+
+import { TextInput, TextArea, FileInput } from "../../components/form/inputs";
+
+const FormWithData = require("../../lib/form/form-with-data").default;
+import MutationButton from "../../components/form/mutation-button";
+
+import { useInstanceV1Query } from "../../lib/query";
+import { useUpdateInstanceMutation } from "../../lib/query/admin";
+import { InstanceV1 } from "../../lib/types/instance";
+
+export default function AdminSettings() {
+ return (
+
+ );
+}
+
+interface AdminSettingsFormProps{
+ data: InstanceV1;
+}
+
+function AdminSettingsForm({ data: instance }: AdminSettingsFormProps) {
+ const titleLimit = 40;
+ const shortDescLimit = 500;
+ const descLimit = 5000;
+ const termsLimit = 5000;
+
+ const form = {
+ title: useTextInput("title", {
+ source: instance,
+ validator: (val: string) => val.length <= titleLimit ? "" : `Instance title is ${val.length} characters; must be ${titleLimit} characters or less`
+ }),
+ thumbnail: useFileInput("thumbnail", { withPreview: true }),
+ thumbnailDesc: useTextInput("thumbnail_description", { source: instance }),
+ shortDesc: useTextInput("short_description", {
+ source: instance,
+ // Select "raw" text version of parsed field for editing.
+ valueSelector: (s: InstanceV1) => s.short_description_text,
+ validator: (val: string) => val.length <= shortDescLimit ? "" : `Instance short description is ${val.length} characters; must be ${shortDescLimit} characters or less`
+ }),
+ description: useTextInput("description", {
+ source: instance,
+ // Select "raw" text version of parsed field for editing.
+ valueSelector: (s: InstanceV1) => s.description_text,
+ validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less`
+ }),
+ terms: useTextInput("terms", {
+ source: instance,
+ // Select "raw" text version of parsed field for editing.
+ valueSelector: (s: InstanceV1) => s.terms_text,
+ validator: (val: string) => val.length <= termsLimit ? "" : `Instance terms and conditions is ${val.length} characters; must be ${termsLimit} characters or less`
+ }),
+ contactUser: useTextInput("contact_username", { source: instance, valueSelector: (s) => s.contact_account?.username }),
+ contactEmail: useTextInput("contact_email", { source: instance, valueSelector: (s) => s.email })
+ };
+
+ const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation());
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
index 57c89be6f..2ca396ed5 100644
--- a/web/source/settings/index.js
+++ b/web/source/settings/index.js
@@ -33,6 +33,8 @@ const { RoleContext } = require("./lib/navigation/util");
const DomainPerms = require("./admin/domain-permissions").default;
const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
+const InstanceSettings = require("./admin/settings").default;
+
require("./style.css");
const { Sidebar, ViewRouter } = createNavigation("/settings", [
@@ -66,7 +68,7 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
]),
Menu("Settings", { icon: "fa-sliders" }, [
- Item("Settings", { icon: "fa-sliders", url: "" }, require("./admin/settings")),
+ Item("Settings", { icon: "fa-sliders", url: "" }, InstanceSettings),
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
]),
])
diff --git a/web/source/settings/lib/types/instance.ts b/web/source/settings/lib/types/instance.ts
index a0a75366e..adc55687c 100644
--- a/web/source/settings/lib/types/instance.ts
+++ b/web/source/settings/lib/types/instance.ts
@@ -18,24 +18,28 @@
*/
export interface InstanceV1 {
- uri: string;
- account_domain: string;
- title: string;
- description: string;
- short_description: string;
- email: string;
- version: string;
- languages: any[]; // TODO: define this
- registrations: boolean;
- approval_required: boolean;
- invites_enabled: boolean;
- configuration: InstanceConfiguration;
- urls: InstanceUrls;
- stats: InstanceStats;
- thumbnail: string;
- contact_account: Object; // TODO: define this.
- max_toot_chars: number;
- rules: any[]; // TODO: define this
+ uri: string;
+ account_domain: string;
+ title: string;
+ description: string;
+ description_text?: string;
+ short_description: string;
+ short_description_text?: string;
+ email: string;
+ version: string;
+ languages: any[]; // TODO: define this
+ registrations: boolean;
+ approval_required: boolean;
+ invites_enabled: boolean;
+ configuration: InstanceConfiguration;
+ urls: InstanceUrls;
+ stats: InstanceStats;
+ thumbnail: string;
+ contact_account: Object; // TODO: define this.
+ max_toot_chars: number;
+ rules: any[]; // TODO: define this
+ terms?: string;
+ terms_text?: string;
}
export interface InstanceConfiguration {
diff --git a/web/template/about.tmpl b/web/template/about.tmpl
index a23dfa953..46349a4c3 100644
--- a/web/template/about.tmpl
+++ b/web/template/about.tmpl
@@ -21,7 +21,15 @@
{{- if .instance.Description }}
{{ .instance.Description | noescape }}
{{- else }}
-
No description has yet been set for this instance.
+
No description has yet been set for this instance.
+{{- end }}
+{{- end -}}
+
+{{- define "termsAndConditions" -}}
+{{- if .instance.Terms }}
+{{ .instance.Terms | noescape }}
+{{- else }}
+
No terms and conditions have yet been set for this instance.
{{- end }}
{{- end -}}
@@ -60,90 +68,124 @@ Polls can have up to
{{- with . }}
+
About {{ .instance.Title -}}
- {{- with . }}
- {{- include "description" . | indent 2 }}
- {{- end }}
+
+ {{- with . }}
+ {{- include "description" . | indent 3 }}
+ {{- end }}
+
This instance has not yet set a contact email address.
{{- end }}
-
- {{- else }}
-
This instance has not yet set any rules.
- {{- end }}
+
Instance Features
-
-
{{- template "registrationLimits" . -}}
-
{{- template "customCSSLimits" . -}}
-
{{- template "statusLimits" . -}}
-
{{- template "pollLimits" . -}}
-
+
+
+
{{- template "registrationLimits" . -}}
+
{{- template "customCSSLimits" . -}}
+
{{- template "statusLimits" . -}}
+
{{- template "pollLimits" . -}}
+
+
+
+
+
Languages
+
+ {{- if .languages }}
+
This instance prefers the following languages:
+
+ {{- range .languages }}
+
{{- . -}}
+ {{- end }}
+
+ {{- else }}
+
This instance does not have any preferred languages.
+ {{- end }}
+
+
+
+
Instance Rules
+
+
This instance has the following rules:
+ {{- if .instance.Rules }}
+
+ {{- range .instance.Rules }}
+
{{- .Text -}}
+ {{- end }}
+
+ {{- else }}
+
This instance has not yet set any rules.
+ {{- end }}
+
+
+
+
Terms and Conditions
+
+ {{- with . }}
+ {{- include "termsAndConditions" . | indent 3 }}
+ {{- end }}
+
Moderated servers
-
- ActivityPub instances federate with other instances by exchanging data with them over the network.
- Exchanged data includes things like accounts, statuses, likes, boosts, and media attachments.
- This exchange of data can prevented for instances on specific domains via a domain block created
- by an instance admin. When an instance is domain blocked by another instance:
-
-
-
Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.
-
Interaction between the two instances is cut off in both directions; neither instance can interact with the other.
-
No new data from the blocked instance will be created on the instance that blocks it.
+ ActivityPub instances federate with other instances by exchanging data with them over the network.
+ Exchanged data includes things like accounts, statuses, likes, boosts, and media attachments.
+ This exchange of data can prevented for instances on specific domains via a domain block created
+ by an instance admin. When an instance is domain blocked by another instance:
+
+
+
Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.
+
Interaction between the two instances is cut off in both directions; neither instance can interact with the other.
+
No new data from the blocked instance will be created on the instance that blocks it.
{{- end }}
\ No newline at end of file
diff --git a/web/template/index.tmpl b/web/template/index.tmpl
index f27cf8570..7e7b7118e 100644
--- a/web/template/index.tmpl
+++ b/web/template/index.tmpl
@@ -21,7 +21,7 @@
{{- if .instance.ShortDescription }}
{{ .instance.ShortDescription | noescape }}
{{- else }}
-
No short description has yet been set for this instance.
+
No short description has yet been set for this instance.
{{- end }}
{{- end -}}
@@ -29,8 +29,10 @@
About this instance
- {{- include "shortDescription" . | indent 2 }}
- See more details
+
+ {{- include "shortDescription" . | indent 3 }}
+ See more details
+
{{- include "index_apps.tmpl" . | indent 1 }}
diff --git a/web/template/index_apps.tmpl b/web/template/index_apps.tmpl
index 05a4a9517..bb835c4da 100644
--- a/web/template/index_apps.tmpl
+++ b/web/template/index_apps.tmpl
@@ -20,96 +20,98 @@
{{- with . }}
Client applications
-
- GoToSocial does not provide its own webclient, but implements the Mastodon client API.
- You can use this server through a variety of other clients:
-
-
-
-
-
Semaphore is a web client designed for speed and simplicity.