diff --git a/internal/apimodule/apimodule.go b/internal/apimodule/apimodule.go
index 52275c6df..6d7dbdb83 100644
--- a/internal/apimodule/apimodule.go
+++ b/internal/apimodule/apimodule.go
@@ -25,8 +25,8 @@ import (
)
// ClientAPIModule represents a chunk of code (usually contained in a single package) that adds a set
-// of functionalities and side effects to a router, by mapping routes and handlers onto it--in other words, a REST API ;)
-// A ClientAPIMpdule corresponds roughly to one main path of the gotosocial REST api, for example /api/v1/accounts/ or /oauth/
+// of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;)
+// A ClientAPIMpdule with routes corresponds roughly to one main path of the gotosocial REST api, for example /api/v1/accounts/ or /oauth/
type ClientAPIModule interface {
Route(s router.Router) error
CreateTables(db db.DB) error
diff --git a/internal/apimodule/security/flocblock.go b/internal/apimodule/security/flocblock.go
new file mode 100644
index 000000000..4bb011d4d
--- /dev/null
+++ b/internal/apimodule/security/flocblock.go
@@ -0,0 +1,27 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package security
+
+import "github.com/gin-gonic/gin"
+
+// flocBlock prevents google chrome cohort tracking by writing the Permissions-Policy header after all other parts of the request have been completed.
+// See: https://plausible.io/blog/google-floc
+func (m *module) flocBlock(c *gin.Context) {
+ c.Header("Permissions-Policy", "interest-cohort=()")
+}
diff --git a/internal/apimodule/security/security.go b/internal/apimodule/security/security.go
new file mode 100644
index 000000000..d98d9703e
--- /dev/null
+++ b/internal/apimodule/security/security.go
@@ -0,0 +1,50 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package security
+
+import (
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/apimodule"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/router"
+)
+
+// module implements the apiclient interface
+type module struct {
+ config *config.Config
+ log *logrus.Logger
+}
+
+// New returns a new security module
+func New(config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
+ return &module{
+ config: config,
+ log: log,
+ }
+}
+
+func (m *module) Route(s router.Router) error {
+ s.AttachMiddleware(m.flocBlock)
+ return nil
+}
+
+func (m *module) CreateTables(db db.DB) error {
+ return nil
+}
diff --git a/internal/apimodule/status/status.go b/internal/apimodule/status/status.go
index 123ca0a56..965f8cad2 100644
--- a/internal/apimodule/status/status.go
+++ b/internal/apimodule/status/status.go
@@ -85,6 +85,7 @@ func (m *statusModule) CreateTables(db db.DB) error {
models := []interface{}{
>smodel.User{},
>smodel.Account{},
+ >smodel.Block{},
>smodel.Follow{},
>smodel.FollowRequest{},
>smodel.Status{},
diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go
index 1ae3d7135..6a0f7321c 100644
--- a/internal/apimodule/status/statuscreate.go
+++ b/internal/apimodule/status/statuscreate.go
@@ -135,39 +135,21 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
return
}
- // convert mentions to *gtsmodel.Mention
- menchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), authed.Account.ID, thisStatusID)
- if err != nil {
- l.Debugf("error generating mentions from status: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating mentions from status"})
+ // handle mentions
+ if err := m.parseMentions(form, authed.Account.ID, newStatus); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
- for _, menchie := range menchies {
- if err := m.db.Put(menchie); err != nil {
- l.Debugf("error putting mentions in db: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "db error while generating mentions from status"})
- return
- }
- }
- newStatus.GTSMentions = menchies
- // convert tags to *gtsmodel.Tag
- tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID)
- if err != nil {
- l.Debugf("error generating hashtags from status: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating hashtags from status"})
+ if err := m.parseTags(form, authed.Account.ID, newStatus); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
- newStatus.GTSTags = tags
- // convert emojis to *gtsmodel.Emoji
- emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID)
- if err != nil {
- l.Debugf("error generating emojis from status: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating emojis from status"})
+ if err := m.parseEmojis(form, authed.Account.ID, newStatus); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
- newStatus.GTSEmojis = emojis
/*
FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it
@@ -196,8 +178,9 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
Activity: newStatus,
}
- // now we need to build up the mastodon-style status object to return to the submitter
-
+ /*
+ FROM THIS POINT ONWARDS WE ARE JUST CREATING THE FRONTEND REPRESENTATION OF THE STATUS TO RETURN TO THE SUBMITTER
+ */
mastoVis := util.ParseMastoVisFromGTSVis(newStatus.Visibility)
mastoAccount, err := m.mastoConverter.AccountToMastoPublic(authed.Account)
@@ -232,6 +215,16 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
return
}
+ mastoTags := []mastotypes.Tag{}
+ for _, gtst := range newStatus.GTSTags {
+ mt, err := m.mastoConverter.TagToMasto(gtst)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ mastoTags = append(mastoTags, mt)
+ }
+
mastoEmojis := []mastotypes.Emoji{}
for _, gtse := range newStatus.GTSEmojis {
me, err := m.mastoConverter.EmojiToMasto(gtse)
@@ -258,7 +251,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
Account: mastoAccount,
MediaAttachments: mastoAttachments,
Mentions: mastoMentions,
- Tags: nil,
+ Tags: mastoTags,
Emojis: mastoEmojis,
Text: form.Status,
}
@@ -448,8 +441,8 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
return nil
}
- GTSMediaAttachments := []*gtsmodel.MediaAttachment{}
- Attachments := []string{}
+ gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
+ attachments := []string{}
for _, mediaID := range form.MediaIDs {
// check these attachments exist
a := >smodel.MediaAttachment{}
@@ -464,11 +457,11 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
if a.StatusID != "" || a.ScheduledStatusID != "" {
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
}
- GTSMediaAttachments = append(GTSMediaAttachments, a)
- Attachments = append(Attachments, a.ID)
+ gtsMediaAttachments = append(gtsMediaAttachments, a)
+ attachments = append(attachments, a.ID)
}
- status.GTSMediaAttachments = GTSMediaAttachments
- status.Attachments = Attachments
+ status.GTSMediaAttachments = gtsMediaAttachments
+ status.Attachments = attachments
return nil
}
@@ -483,3 +476,57 @@ func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string
}
return nil
}
+
+func (m *statusModule) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ menchies := []string{}
+ gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating mentions from status: %s", err)
+ }
+ for _, menchie := range gtsMenchies {
+ if err := m.db.Put(menchie); err != nil {
+ return fmt.Errorf("error putting mentions in db: %s", err)
+ }
+ menchies = append(menchies, menchie.ID)
+ }
+ // add full populated gts menchies to the status for passing them around conveniently
+ status.GTSMentions = gtsMenchies
+ // add just the ids of the mentioned accounts to the status for putting in the db
+ status.Mentions = menchies
+ return nil
+}
+
+func (m *statusModule) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ tags := []string{}
+ gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating hashtags from status: %s", err)
+ }
+ for _, tag := range gtsTags {
+ if err := m.db.Upsert(tag, "name"); err != nil {
+ return fmt.Errorf("error putting tags in db: %s", err)
+ }
+ tags = append(tags, tag.ID)
+ }
+ // add full populated gts tags to the status for passing them around conveniently
+ status.GTSTags = gtsTags
+ // add just the ids of the used tags to the status for putting in the db
+ status.Tags = tags
+ return nil
+}
+
+func (m *statusModule) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ emojis := []string{}
+ gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating emojis from status: %s", err)
+ }
+ for _, e := range gtsEmojis {
+ emojis = append(emojis, e.ID)
+ }
+ // add full populated gts emojis to the status for passing them around conveniently
+ status.GTSEmojis = gtsEmojis
+ // add just the ids of the used emojis to the status for putting in the db
+ status.Emojis = emojis
+ return nil
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index 89cdae53b..074d7926e 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -92,6 +92,11 @@ type DB interface {
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
Put(i interface{}) error
+ // Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/
+ // It is up to the implementation to figure out how to store it, and using what key.
+ // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
+ Upsert(i interface{}, conflictColumn string) error
+
// UpdateByID updates i with id id.
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
UpdateByID(id string, i interface{}) error
@@ -192,15 +197,16 @@ type DB interface {
// checks in the database for the mentioned accounts, and returns a slice of mentions generated based on the given parameters.
//
// Note: this func doesn't/shouldn't do any manipulation of the accounts in the DB, it's just for checking
- // if they exist in the db and conveniently returning them.
+ // if they exist in the db and conveniently returning them if they do.
MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error)
// TagStringsToTags takes a slice of deduplicated, lowercase tags in the form "somehashtag", which have been
// used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then
- // returns a slice of *model.Tag corresponding to the given tags.
+ // returns a slice of *model.Tag corresponding to the given tags. If the tag already exists in database, that tag
+ // will be returned. Otherwise a pointer to a new tag struct will be created and returned.
//
// Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking
- // if they exist in the db and conveniently returning them.
+ // if they exist in the db already, and conveniently returning them, or creating new tag structs.
TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error)
// EmojiStringsToEmojis takes a slice of deduplicated, lowercase emojis in the form ":emojiname:", which have been
@@ -208,7 +214,7 @@ type DB interface {
// returns a slice of *model.Emoji corresponding to the given emojis.
//
// Note: this func doesn't/shouldn't do any manipulation of the emoji in the DB, it's just for checking
- // if they exist in the db and conveniently returning them.
+ // if they exist in the db and conveniently returning them if they do.
EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error)
}
diff --git a/internal/db/gtsmodel/mention.go b/internal/db/gtsmodel/mention.go
index 6c1993740..18eb11082 100644
--- a/internal/db/gtsmodel/mention.go
+++ b/internal/db/gtsmodel/mention.go
@@ -25,15 +25,15 @@ type Mention struct {
// ID of this mention in the database
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
// ID of the status this mention originates from
- StatusID string
+ StatusID string `pg:",notnull"`
// When was this mention created?
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
// When was this mention last updated?
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
// Who created this mention?
- OriginAccountID string
+ OriginAccountID string `pg:",notnull"`
// Who does this mention target?
- TargetAccountID string
+ TargetAccountID string `pg:",notnull"`
// Prevent this mention from generating a notification?
Silent bool
}
diff --git a/internal/db/gtsmodel/status.go b/internal/db/gtsmodel/status.go
index 04ddc8f35..1e5c1d95b 100644
--- a/internal/db/gtsmodel/status.go
+++ b/internal/db/gtsmodel/status.go
@@ -31,7 +31,13 @@ type Status struct {
// the html-formatted content of this status
Content string
// Database IDs of any media attachments associated with this status
- Attachments []string
+ Attachments []string `pg:",array"`
+ // Database IDs of any tags used in this status
+ Tags []string `pg:",array"`
+ // Database IDs of any mentions in this status
+ Mentions []string `pg:",array"`
+ // Database IDs of any emojis used in this status
+ Emojis []string `pg:",array"`
// when was this status created?
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
// when was this status updated?
diff --git a/internal/db/gtsmodel/tag.go b/internal/db/gtsmodel/tag.go
index acc0549de..83c471958 100644
--- a/internal/db/gtsmodel/tag.go
+++ b/internal/db/gtsmodel/tag.go
@@ -20,17 +20,22 @@ package gtsmodel
import "time"
+// Tag represents a hashtag for gathering public statuses together
type Tag struct {
- ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
- Name string `pg:"unique,notnull"`
- CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
- UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
- Useable bool
- Trendable bool
- Listable bool
- ReviewedAt time.Time
- RequestedReviewAt time.Time
- LastStatusAt time.Time
- MaxScore float32
- MaxScoreAt time.Time
+ // id of this tag in the database
+ ID string `pg:",unique,type:uuid,default:gen_random_uuid(),pk,notnull"`
+ // name of this tag -- the tag without the hash part
+ Name string `pg:",unique,pk,notnull"`
+ // Which account ID is the first one we saw using this tag?
+ FirstSeenFromAccountID string
+ // when was this tag created
+ CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
+ // when was this tag last updated
+ UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
+ // can our instance users use this tag?
+ Useable bool `pg:",notnull,default:true"`
+ // can our instance users look up this tag?
+ Listable bool `pg:",notnull,default:true"`
+ // when was this tag last used?
+ LastStatusAt time.Time `pg:"type:timestamp,notnull,default:now()"`
}
diff --git a/internal/db/pg.go b/internal/db/pg.go
index 38f334159..291252875 100644
--- a/internal/db/pg.go
+++ b/internal/db/pg.go
@@ -34,6 +34,7 @@ import (
"github.com/go-pg/pg/extra/pgdebug"
"github.com/go-pg/pg/v10"
"github.com/go-pg/pg/v10/orm"
+ "github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@@ -273,8 +274,18 @@ func (ps *postgresService) Put(i interface{}) error {
return err
}
+func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error {
+ if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil {
+ if err == pg.ErrNoRows {
+ return ErrNoEntries{}
+ }
+ return err
+ }
+ return nil
+}
+
func (ps *postgresService) UpdateByID(id string, i interface{}) error {
- if _, err := ps.conn.Model(i).OnConflict("(id) DO UPDATE").Insert(); err != nil {
+ if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil {
if err == pg.ErrNoRows {
return ErrNoEntries{}
}
@@ -765,15 +776,33 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
return menchies, nil
}
-// for now this function doesn't really use the database, but it's here because:
-// A) it probably will later and
-// B) it's v. similar to MentionStringsToMentions
func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) {
newTags := []*gtsmodel.Tag{}
for _, t := range tags {
- newTags = append(newTags, >smodel.Tag{
- Name: t,
- })
+ tag := >smodel.Tag{}
+ // we can use selectorinsert here to create the new tag if it doesn't exist already
+ // inserted will be true if this is a new tag we just created
+ if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil {
+ if err == pg.ErrNoRows {
+ // tag doesn't exist yet so populate it
+ tag.ID = uuid.NewString()
+ tag.Name = t
+ tag.FirstSeenFromAccountID = originAccountID
+ tag.CreatedAt = time.Now()
+ tag.UpdatedAt = time.Now()
+ tag.Useable = true
+ tag.Listable = true
+ } else {
+ return nil, fmt.Errorf("error getting tag with name %s: %s", t, err)
+ }
+ }
+
+ // bail already if the tag isn't useable
+ if !tag.Useable {
+ continue
+ }
+ tag.LastStatusAt = time.Now()
+ newTags = append(newTags, tag)
}
return newTags, nil
}
diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go
index 3f12d7b65..76557ca3d 100644
--- a/internal/gotosocial/actions.go
+++ b/internal/gotosocial/actions.go
@@ -34,6 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/apimodule/auth"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver"
mediaModule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
+ "github.com/superseriousbusiness/gotosocial/internal/apimodule/security"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
"github.com/superseriousbusiness/gotosocial/internal/cache"
"github.com/superseriousbusiness/gotosocial/internal/config"
@@ -83,9 +84,14 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
fileServerModule := fileserver.New(c, dbService, storageBackend, log)
adminModule := admin.New(c, dbService, mediaHandler, mastoConverter, log)
statusModule := status.New(c, dbService, oauthServer, mediaHandler, mastoConverter, distributor, log)
+ securityModule := security.New(c, log)
apiModules := []apimodule.ClientAPIModule{
- authModule, // this one has to go first so the other modules use its middleware
+ // modules with middleware go first
+ securityModule,
+ authModule,
+
+ // now everything else
accountModule,
appsModule,
mm,
diff --git a/internal/mastotypes/converter.go b/internal/mastotypes/converter.go
index cec138058..9db39150d 100644
--- a/internal/mastotypes/converter.go
+++ b/internal/mastotypes/converter.go
@@ -60,6 +60,9 @@ type Converter interface {
// EmojiToMasto converts a gts model emoji into its mastodon (frontend) representation for serialization on the API.
EmojiToMasto(e *gtsmodel.Emoji) (mastotypes.Emoji, error)
+
+ // TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API.
+ TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error)
}
type converter struct {
@@ -290,7 +293,7 @@ func (c *converter) MentionToMasto(m *gtsmodel.Mention) (mastotypes.Mention, err
}
return mastotypes.Mention{
- ID: m.ID,
+ ID: target.ID,
Username: target.Username,
URL: target.URL,
Acct: acct,
@@ -306,3 +309,12 @@ func (c *converter) EmojiToMasto(e *gtsmodel.Emoji) (mastotypes.Emoji, error) {
Category: e.CategoryID,
}, nil
}
+
+func (c *converter) TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error) {
+ tagURL := fmt.Sprintf("%s://%s/tags/%s", c.config.Protocol, c.config.Host, t.Name)
+
+ return mastotypes.Tag{
+ Name: t.Name,
+ URL: tagURL, // we don't serve URLs with collections of tagged statuses (FOR NOW) so this is purely for mastodon compatibility ¯\_(ツ)_/¯
+ }, nil
+}
diff --git a/internal/mastotypes/mastomodel/tag.go b/internal/mastotypes/mastomodel/tag.go
index 4431ac3e9..6dda3fb5d 100644
--- a/internal/mastotypes/mastomodel/tag.go
+++ b/internal/mastotypes/mastomodel/tag.go
@@ -20,4 +20,8 @@ package mastotypes
// Tag represents a hashtag used within the content of a status. See https://docs.joinmastodon.org/entities/tag/
type Tag struct {
+ // The value of the hashtag after the # sign.
+ Name string `json:"name"`
+ // A link to the hashtag on the instance.
+ URL string `json:"url"`
}
diff --git a/internal/router/router.go b/internal/router/router.go
index ce924b26d..7ab208ef6 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -83,7 +83,17 @@ func (r *router) AttachMiddleware(middleware gin.HandlerFunc) {
// New returns a new Router with the specified configuration, using the given logrus logger.
func New(config *config.Config, logger *logrus.Logger) (Router, error) {
- engine := gin.New()
+ lvl, err := logrus.ParseLevel(config.LogLevel)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't parse log level %s to set router level: %s", config.LogLevel, err)
+ }
+ switch lvl {
+ case logrus.TraceLevel, logrus.DebugLevel:
+ gin.SetMode(gin.DebugMode)
+ default:
+ gin.SetMode(gin.ReleaseMode)
+ }
+ engine := gin.Default()
// create a new session store middleware
store, err := sessionStore()
diff --git a/scripts/auth_flow.sh b/scripts/auth_flow.sh
index 8bba39532..5552349a5 100755
--- a/scripts/auth_flow.sh
+++ b/scripts/auth_flow.sh
@@ -5,10 +5,9 @@ set -eux
SERVER_URL="http://localhost:8080"
REDIRECT_URI="${SERVER_URL}"
CLIENT_NAME="Test Application Name"
-
REGISTRATION_REASON="Testing whether or not this dang diggity thing works!"
-REGISTRATION_EMAIL="test@example.org"
-REGISTRATION_USERNAME="test_user"
+REGISTRATION_USERNAME="${1}"
+REGISTRATION_EMAIL="${2}"
REGISTRATION_PASSWORD="very safe password 123"
REGISTRATION_AGREEMENT="true"
REGISTRATION_LOCALE="en"
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index dde38912c..cbc21237b 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -681,8 +681,11 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
ID: "502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
- Content: "hello world! first post on the instance!",
+ Content: "hello world! #welcome ! first post on the instance :rainbow: !",
Attachments: []string{"b052241b-f30f-4dc6-92fc-2bad0be1f8d8"},
+ Tags: []string{"a7e8f5ca-88a1-4652-8079-a187eab8d56e"},
+ Mentions: []string{},
+ Emojis: []string{"a96ec4f3-1cae-47e4-a508-f9d66a6b221b"},
CreatedAt: time.Now().Add(-71 * time.Hour),
UpdatedAt: time.Now().Add(-71 * time.Hour),
Local: true,
@@ -865,3 +868,18 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
},
}
}
+
+func NewTestTags() map[string]*gtsmodel.Tag {
+ return map[string]*gtsmodel.Tag{
+ "welcome": {
+ ID: "a7e8f5ca-88a1-4652-8079-a187eab8d56e",
+ Name: "welcome",
+ FirstSeenFromAccountID: "",
+ CreatedAt: time.Now().Add(-71 * time.Hour),
+ UpdatedAt: time.Now().Add(-71 * time.Hour),
+ Useable: true,
+ Listable: true,
+ LastStatusAt: time.Now().Add(-71 * time.Hour),
+ },
+ }
+}