mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-11 11:27:30 -06:00
*fiddles*
This commit is contained in:
parent
11b18f986c
commit
48ab34f71a
12 changed files with 221 additions and 74 deletions
|
|
@ -20,7 +20,9 @@ package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
|
@ -60,8 +62,17 @@ func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler
|
||||||
// Route attaches all routes from this module to the given router
|
// Route attaches all routes from this module to the given router
|
||||||
func (m *accountModule) Route(r router.Router) error {
|
func (m *accountModule) Route(r router.Router) error {
|
||||||
r.AttachHandler(http.MethodPost, basePath, m.accountCreatePOSTHandler)
|
r.AttachHandler(http.MethodPost, basePath, m.accountCreatePOSTHandler)
|
||||||
r.AttachHandler(http.MethodGet, verifyPath, m.accountVerifyGETHandler)
|
r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler)
|
||||||
r.AttachHandler(http.MethodPatch, updateCredentialsPath, m.accountUpdateCredentialsPATCHHandler)
|
|
||||||
r.AttachHandler(http.MethodGet, basePathWithID, m.accountGETHandler)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *accountModule) muxHandler(c *gin.Context) {
|
||||||
|
ru := c.Request.RequestURI
|
||||||
|
if strings.HasPrefix(ru, verifyPath) {
|
||||||
|
m.accountVerifyGETHandler(c)
|
||||||
|
} else if strings.HasPrefix(ru, updateCredentialsPath) {
|
||||||
|
m.accountUpdateCredentialsPATCHHandler(c)
|
||||||
|
} else {
|
||||||
|
m.accountGETHandler(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,13 +127,42 @@ func (m *accountModule) accountUpdateCredentialsPATCHHandler(c *gin.Context) {
|
||||||
|
|
||||||
if form.Locked != nil {
|
if form.Locked != nil {
|
||||||
if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &model.Account{}); err != nil {
|
if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &model.Account{}); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Source != nil {
|
if form.Source != nil {
|
||||||
// TODO: parse source nicely and update
|
if form.Source.Language != nil {
|
||||||
|
if err := util.ValidateLanguage(*form.Source.Language); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if err := m.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, &model.Account{}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Source.Sensitive != nil {
|
||||||
|
if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &model.Account{}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Source.Privacy != nil {
|
||||||
|
if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if err := m.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, &model.Account{}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.FieldsAttributes != nil {
|
if form.FieldsAttributes != nil {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -291,9 +290,9 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
// TODO: implement proper checks here
|
// TODO: implement proper checks here
|
||||||
//
|
//
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
// b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
// assert.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
|
// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccountUpdateTestSuite(t *testing.T) {
|
func TestAccountUpdateTestSuite(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -40,22 +40,14 @@ type Config struct {
|
||||||
|
|
||||||
// FromFile returns a new config from a file, or an error if something goes amiss.
|
// FromFile returns a new config from a file, or an error if something goes amiss.
|
||||||
func FromFile(path string) (*Config, error) {
|
func FromFile(path string) (*Config, error) {
|
||||||
c, err := loadFromFile(path)
|
if path != "" {
|
||||||
if err != nil {
|
c, err := loadFromFile(path)
|
||||||
return nil, fmt.Errorf("error creating config: %s", err)
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("error creating config: %s", err)
|
||||||
return c, nil
|
}
|
||||||
}
|
return c, nil
|
||||||
|
|
||||||
// Default returns a new config with default values.
|
|
||||||
// Not yet implemented.
|
|
||||||
func Default() *Config {
|
|
||||||
// TODO: find a way of doing this without code repetition, because having to
|
|
||||||
// repeat all values here and elsewhere is annoying and gonna be prone to mistakes.
|
|
||||||
return &Config{
|
|
||||||
DBConfig: &DBConfig{},
|
|
||||||
TemplateConfig: &TemplateConfig{},
|
|
||||||
}
|
}
|
||||||
|
return Empty(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty just returns an empty config
|
// Empty just returns an empty config
|
||||||
|
|
|
||||||
|
|
@ -507,7 +507,39 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment,
|
||||||
// https://docs.joinmastodon.org/methods/accounts/. Note that it's *sensitive* because it's only meant to be exposed to the user
|
// https://docs.joinmastodon.org/methods/accounts/. Note that it's *sensitive* because it's only meant to be exposed to the user
|
||||||
// that the account actually belongs to.
|
// that the account actually belongs to.
|
||||||
func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotypes.Account, error) {
|
func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotypes.Account, error) {
|
||||||
|
// we can build this sensitive account easily by first getting the public account....
|
||||||
|
mastoAccount, err := ps.AccountToMastoPublic(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// then adding the Source object to it...
|
||||||
|
|
||||||
|
// check pending follow requests aimed at this account
|
||||||
|
fr := []model.FollowRequest{}
|
||||||
|
if err := ps.GetFollowRequestsForAccountID(a.ID, &fr); err != nil {
|
||||||
|
if _, ok := err.(ErrNoEntries); !ok {
|
||||||
|
return nil, fmt.Errorf("error getting follow requests: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var frc int
|
||||||
|
if fr != nil {
|
||||||
|
frc = len(fr)
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoAccount.Source = &mastotypes.Source{
|
||||||
|
Privacy: a.Privacy,
|
||||||
|
Sensitive: a.Sensitive,
|
||||||
|
Language: a.Language,
|
||||||
|
Note: a.Note,
|
||||||
|
Fields: mastoAccount.Fields,
|
||||||
|
FollowRequestsCount: frc,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoAccount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.Account, error) {
|
||||||
// count followers
|
// count followers
|
||||||
followers := []model.Follow{}
|
followers := []model.Follow{}
|
||||||
if err := ps.GetFollowersByAccountID(a.ID, &followers); err != nil {
|
if err := ps.GetFollowersByAccountID(a.ID, &followers); err != nil {
|
||||||
|
|
@ -588,52 +620,34 @@ func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotype
|
||||||
fields = append(fields, mField)
|
fields = append(fields, mField)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check pending follow requests aimed at this account
|
var acct string
|
||||||
fr := []model.FollowRequest{}
|
if a.Domain != "" {
|
||||||
if err := ps.GetFollowRequestsForAccountID(a.ID, &fr); err != nil {
|
// this is a remote user
|
||||||
if _, ok := err.(ErrNoEntries); !ok {
|
acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
|
||||||
return nil, fmt.Errorf("error getting follow requests: %s", err)
|
} else {
|
||||||
}
|
// this is a local user
|
||||||
}
|
acct = a.Username
|
||||||
var frc int
|
|
||||||
if fr != nil {
|
|
||||||
frc = len(fr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive source from fields and other info
|
|
||||||
source := &mastotypes.Source{
|
|
||||||
Privacy: a.Privacy,
|
|
||||||
Sensitive: a.Sensitive,
|
|
||||||
Language: a.Language,
|
|
||||||
Note: a.Note,
|
|
||||||
Fields: fields,
|
|
||||||
FollowRequestsCount: frc,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &mastotypes.Account{
|
return &mastotypes.Account{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Username: a.Username,
|
Username: a.Username,
|
||||||
Acct: a.Username, // equivalent to username for local users only, which sensitive always is
|
Acct: acct,
|
||||||
DisplayName: a.DisplayName,
|
DisplayName: a.DisplayName,
|
||||||
Locked: a.Locked,
|
Locked: a.Locked,
|
||||||
Bot: a.Bot,
|
Bot: a.Bot,
|
||||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||||
Note: a.Note,
|
Note: a.Note,
|
||||||
URL: a.URL,
|
URL: a.URL,
|
||||||
Avatar: aviURL, // TODO: build this url properly using host and protocol from config
|
Avatar: aviURL,
|
||||||
AvatarStatic: aviURLStatic, // TODO: build this url properly using host and protocol from config
|
AvatarStatic: aviURLStatic,
|
||||||
Header: headerURL, // TODO: build this url properly using host and protocol from config
|
Header: headerURL,
|
||||||
HeaderStatic: headerURLStatic, // TODO: build this url properly using host and protocol from config
|
HeaderStatic: headerURLStatic,
|
||||||
FollowersCount: followersCount,
|
FollowersCount: followersCount,
|
||||||
FollowingCount: followingCount,
|
FollowingCount: followingCount,
|
||||||
StatusesCount: statusesCount,
|
StatusesCount: statusesCount,
|
||||||
LastStatusAt: lastStatusAt,
|
LastStatusAt: lastStatusAt,
|
||||||
Source: source,
|
|
||||||
Emojis: nil, // TODO: implement this
|
Emojis: nil, // TODO: implement this
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) AccountToMastoPublic(account *model.Account) (*mastotypes.Account, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,18 @@ import (
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/action"
|
"github.com/superseriousbusiness/gotosocial/internal/action"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule/app"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule/auth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run creates and starts a gotosocial server
|
// Run creates and starts a gotosocial server
|
||||||
|
|
@ -38,9 +48,45 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return fmt.Errorf("error creating dbservice: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if err := dbService.CreateSchema(ctx); err != nil {
|
router, err := router.New(c, log)
|
||||||
// return fmt.Errorf("error creating dbschema: %s", err)
|
if err != nil {
|
||||||
// }
|
return fmt.Errorf("error creating router: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageBackend, err := storage.NewInMem(c, log)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating storage backend: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build backend handlers
|
||||||
|
mediaHandler := media.New(c, dbService, storageBackend, log)
|
||||||
|
oauthServer := oauth.New(dbService, log)
|
||||||
|
|
||||||
|
// build client api modules
|
||||||
|
authModule := auth.New(oauthServer, dbService, log)
|
||||||
|
accountModule := account.New(c, dbService, oauthServer, mediaHandler, log)
|
||||||
|
appsModule := app.New(oauthServer, dbService, log)
|
||||||
|
|
||||||
|
apiModules := []apimodule.ClientAPIModule{
|
||||||
|
authModule, // this one has to go first so the other modules use its middleware
|
||||||
|
accountModule,
|
||||||
|
appsModule,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range apiModules {
|
||||||
|
if err := m.Route(router); err != nil {
|
||||||
|
return fmt.Errorf("routing error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gts, err := New(dbService, &cache.MockCache{}, router, federation.New(dbService), c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating gotosocial service: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gts.Start(ctx); err != nil {
|
||||||
|
return fmt.Errorf("error starting gotosocial service: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// catch shutdown signals from the operating system
|
// catch shutdown signals from the operating system
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
|
|
@ -49,8 +95,8 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
|
||||||
log.Infof("received signal %s, shutting down", sig)
|
log.Infof("received signal %s, shutting down", sig)
|
||||||
|
|
||||||
// close down all running services in order
|
// close down all running services in order
|
||||||
if err := dbService.Stop(ctx); err != nil {
|
if err := gts.Stop(ctx); err != nil {
|
||||||
return fmt.Errorf("error closing dbservice: %s", err)
|
return fmt.Errorf("error closing gotosocial service: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("done! exiting...")
|
log.Info("done! exiting...")
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import (
|
||||||
// The logic of stopping and starting the entire server is contained here.
|
// The logic of stopping and starting the entire server is contained here.
|
||||||
type Gotosocial interface {
|
type Gotosocial interface {
|
||||||
Start(context.Context) error
|
Start(context.Context) error
|
||||||
|
Stop(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new gotosocial server, initialized with the given configuration.
|
// New returns a new gotosocial server, initialized with the given configuration.
|
||||||
|
|
@ -56,10 +57,19 @@ type gotosocial struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts up the gotosocial server. It is a blocking call, so only call it when
|
// Start starts up the gotosocial server. If something goes wrong
|
||||||
// you're absolutely sure you want to start up the server. If something goes wrong
|
// while starting the server, then an error will be returned.
|
||||||
// while starting the server, then an error will be returned. You can treat this function a
|
|
||||||
// lot like you would treat http.ListenAndServe()
|
|
||||||
func (gts *gotosocial) Start(ctx context.Context) error {
|
func (gts *gotosocial) Start(ctx context.Context) error {
|
||||||
|
gts.apiRouter.Start()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gts *gotosocial) Stop(ctx context.Context) error {
|
||||||
|
if err := gts.apiRouter.Stop(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gts.db.Stop(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@ func (r *router) Start() {
|
||||||
r.logger.Fatalf("listen: %s", err)
|
r.logger.Fatalf("listen: %s", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// c := &gin.Context{}
|
|
||||||
// c.Get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop shuts down the router nicely
|
// Stop shuts down the router nicely
|
||||||
|
|
|
||||||
31
internal/storage/inmem.go
Normal file
31
internal/storage/inmem.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewInMem(c *config.Config, log *logrus.Logger) (Storage, error) {
|
||||||
|
return &inMemStorage{
|
||||||
|
stored: make(map[string][]byte),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type inMemStorage struct {
|
||||||
|
stored map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inMemStorage) StoreFileAt(path string, data []byte) error {
|
||||||
|
s.stored[path] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inMemStorage) RetrieveFileFrom(path string) ([]byte, error) {
|
||||||
|
d, ok := s.stored[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no data found at path %s", path)
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
21
internal/storage/local.go
Normal file
21
internal/storage/local.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLocal(c *config.Config, log *logrus.Logger) (Storage, error) {
|
||||||
|
return &localStorage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type localStorage struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *localStorage) StoreFileAt(path string, data []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *localStorage) RetrieveFileFrom(path string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -18,16 +18,7 @@
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
StoreFileAt(path string, data []byte) error
|
StoreFileAt(path string, data []byte) error
|
||||||
RetrieveFileFrom(path string) ([]byte, error)
|
RetrieveFileFrom(path string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
|
||||||
Data []byte
|
|
||||||
StorePath string
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -137,3 +137,8 @@ func ValidateNote(note string) error {
|
||||||
// TODO: add some validation logic here -- length, characters, etc
|
// TODO: add some validation logic here -- length, characters, etc
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidatePrivacy(privacy string) error {
|
||||||
|
// TODO: add some validation logic here -- length, characters, etc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue