diff --git a/internal/db/db.go b/internal/db/db.go index 9952e5e..9074c23 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -25,6 +25,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/gotosocial/gotosocial/internal/config" + "github.com/gotosocial/gotosocial/internal/gtsmodel" "github.com/sirupsen/logrus" ) @@ -68,6 +69,15 @@ type DB interface { // Delete where deletes i where key = value DeleteWhere(key string, value interface{}, i interface{}) error + + // GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID + GetAccountByUserID(userID string, account *gtsmodel.Account) error + + // GetFollowingByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is following + GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error + + // GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by + GetFollowersByAccountID(accountID string, following *[]gtsmodel.Follow) error } // New returns a new database service that satisfies the DB interface and, by extension, diff --git a/internal/db/pg.go b/internal/db/pg.go index 487af18..c07f00e 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -249,3 +249,21 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac _, err := ps.conn.Model(i).Where(fmt.Sprintf("%s = ?", key), value).Delete() return err } + +func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { + user := >smodel.User{ + ID: userID, + } + if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { + return err + } + return ps.conn.Model(account).Where("id = ?", user.AccountID).Select() +} + +func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { + return ps.conn.Model(following).Where("account_id = ?", accountID).Select() +} + +func (ps *postgresService) GetFollowersByAccountID(accountID string, following *[]gtsmodel.Follow) error { + return ps.conn.Model(following).Where("target_account_id = ?", accountID).Select() +} diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 6c17b90..d11f676 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -24,6 +24,8 @@ package gtsmodel import ( "net/url" "time" + + "github.com/gotosocial/gotosocial/pkg/mastotypes" ) // Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc) @@ -63,6 +65,8 @@ type Account struct { UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When should this account function until SubscriptionExpiresAt time.Time `pg:"type:timestamp"` + // Does this account identify itself as a bot? + Bot bool /* PRIVACY SETTINGS @@ -153,3 +157,22 @@ type Header struct { HeaderRemoteURL *url.URL `pg:"type:text"` HeaderStorageSchemaVersion int } + +// ToMastoSensitive returns this account as a mastodon api type, ready for serialization +func (a *Account) ToMastoSensitive() *mastotypes.Account { + return &mastotypes.Account{ + ID: a.ID, + Username: a.Username, + Acct: a.Username, // equivalent to username for local users only, which sensitive always is + DisplayName: a.DisplayName, + Locked: a.Locked, + Bot: a.Bot, + CreatedAt: a.CreatedAt.Format(time.RFC3339), + Note: a.Note, + URL: a.URL, + Avatar: a.Avatar.AvatarRemoteURL.String(), + AvatarStatic: a.AvatarRemoteURL.String(), + Header: a.Header.HeaderRemoteURL.String(), + HeaderStatic: a.Header.HeaderRemoteURL.String(), + } +} diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index fd0fa6a..1478a24 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -41,8 +41,8 @@ type Application struct { VapidKey string } -// ToMastotype returns this application as a mastodon api type, ready for serialization -func (a *Application) ToMastotype() *mastotypes.Application { +// ToMasto returns this application as a mastodon api type, ready for serialization +func (a *Application) ToMasto() *mastotypes.Application { return &mastotypes.Application{ ID: a.ID, Name: a.Name, diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go new file mode 100644 index 0000000..e0c6616 --- /dev/null +++ b/internal/gtsmodel/follow.go @@ -0,0 +1,38 @@ +/* + 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 gtsmodel + +import "time" + +type Follow struct { + // id of this follow in the database + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + // When was this follow created? + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // When was this follow last updated? + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + // Who does this follow belong to? + AccountID string `pg:",unique:srctarget,notnull"` + // Who does AccountID follow? + TargetAccountID string `pg:",unique:srctarget,notnull"` + // Does this follow also want to see reblogs and not just posts? + ShowReblogs bool `pg:"default:true"` + // What is the activitypub URI of this follow? + URI string `pg:",unique"` +} diff --git a/internal/module/account/account.go b/internal/module/account/account.go index d82d96e..06d40dd 100644 --- a/internal/module/account/account.go +++ b/internal/module/account/account.go @@ -19,19 +19,56 @@ package account import ( + "net/http" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/gotosocial/gotosocial/internal/config" + "github.com/gotosocial/gotosocial/internal/db" + "github.com/gotosocial/gotosocial/internal/gtsmodel" "github.com/gotosocial/gotosocial/internal/module" + "github.com/gotosocial/gotosocial/internal/module/oauth" "github.com/gotosocial/gotosocial/internal/router" ) +const ( + basePath = "/api/v1/accounts" + basePathWithID = basePath + "/:id" + verifyPath = basePath + "/verify_credentials" +) + type accountModule struct { + config *config.Config + db db.DB } // New returns a new account module -func New() module.ClientAPIModule { - return &accountModule{} +func New(config *config.Config, db db.DB) module.ClientAPIModule { + return &accountModule{ + config: config, + db: db, + } } // Route attaches all routes from this module to the given router func (m *accountModule) Route(r router.Router) error { + r.AttachHandler(http.MethodGet, verifyPath, m.AccountVerifyGETHandler) return nil } + +func (m *accountModule) AccountVerifyGETHandler(c *gin.Context) { + s := sessions.Default(c) + userID, ok := s.Get(oauth.SessionAuthorizedUser).(string) + if !ok || userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "The access token is invalid"}) + return + } + + acct := >smodel.Account{} + if err := m.db.GetAccountByUserID(userID, acct); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + + c.JSON(http.StatusOK, acct.ToMastoSensitive()) +} diff --git a/internal/module/oauth/oauth.go b/internal/module/oauth/oauth.go index 4436f7a..9536dcd 100644 --- a/internal/module/oauth/oauth.go +++ b/internal/module/oauth/oauth.go @@ -51,6 +51,7 @@ const ( authSignInPath = "/auth/sign_in" oauthTokenPath = "/oauth/token" oauthAuthorizePath = "/oauth/authorize" + SessionAuthorizedUser = "authorized_user" ) // oauthModule is an oauth2 oauthModule that satisfies the ClientAPIModule interface @@ -209,7 +210,7 @@ func (m *oauthModule) appsPOSTHandler(c *gin.Context) { } // done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ - c.JSON(http.StatusOK, app.ToMastotype()) + c.JSON(http.StatusOK, app.ToMasto()) } // signInGETHandler should be served at https://example.org/auth/sign_in. @@ -411,7 +412,7 @@ func (m *oauthModule) oauthTokenMiddleware(c *gin.Context) { l.Trace("entering OauthTokenMiddleware") if ti, err := m.oauthServer.ValidationBearerToken(c.Request); err == nil { l.Tracef("authenticated user %s with bearer token, scope is %s", ti.GetUserID(), ti.GetScope()) - c.Set("authenticated_user", ti.GetUserID()) + c.Set(SessionAuthorizedUser, ti.GetUserID()) } else { l.Trace("continuing with unauthenticated request")