[feature] Web profile pages for accounts (#449)

* add default avatars

* allow webModule to error

* return errWithCode from account get

* add AccountGetLocalByUsername

* check nil requesting account

* add timestampShort function for just month/year

* move loading logic to New + add default avatars

* add profile page view

* update swagger docs

* add excludeReblogs to GetAccountStatuses

* ignore casing when selecting local account by username

* appropriate redirects

* css fiddling

* add 'about' heading

* adjust thread page to work with routing

* return AP representation if requested + authorized

* simplify auth check

* go fmt

* golangci-lint ignore math/rand
This commit is contained in:
tobi 2022-04-15 14:33:01 +02:00 committed by GitHub
parent a7e9dee33d
commit 26683b3d49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1484 additions and 88 deletions

View File

@ -148,6 +148,12 @@ var Start action.GTSAction = func(ctx context.Context) error {
return fmt.Errorf("error creating oidc idp: %s", err) return fmt.Errorf("error creating oidc idp: %s", err)
} }
// build web module
webModule, err := web.New(processor)
if err != nil {
return fmt.Errorf("error creating web module: %s", err)
}
// build client api modules // build client api modules
authModule := auth.New(dbService, oauthServer, idp) authModule := auth.New(dbService, oauthServer, idp)
accountModule := account.New(processor) accountModule := account.New(processor)
@ -156,7 +162,6 @@ var Start action.GTSAction = func(ctx context.Context) error {
followRequestsModule := followrequest.New(processor) followRequestsModule := followrequest.New(processor)
webfingerModule := webfinger.New(processor) webfingerModule := webfinger.New(processor)
nodeInfoModule := nodeinfo.New(processor) nodeInfoModule := nodeinfo.New(processor)
webBaseModule := web.New(processor)
usersModule := user.New(processor) usersModule := user.New(processor)
timelineModule := timeline.New(processor) timelineModule := timeline.New(processor)
notificationModule := notification.New(processor) notificationModule := notification.New(processor)
@ -179,8 +184,10 @@ var Start action.GTSAction = func(ctx context.Context) error {
securityModule, securityModule,
authModule, authModule,
// now the web module
webModule,
// now everything else // now everything else
webBaseModule,
accountModule, accountModule,
instanceModule, instanceModule,
appsModule, appsModule,

View File

@ -95,6 +95,12 @@ var Start action.GTSAction = func(ctx context.Context) error {
return fmt.Errorf("error creating oidc idp: %s", err) return fmt.Errorf("error creating oidc idp: %s", err)
} }
// build web module
webModule, err := web.New(processor)
if err != nil {
return fmt.Errorf("error creating web module: %s", err)
}
// build client api modules // build client api modules
authModule := auth.New(dbService, oauthServer, idp) authModule := auth.New(dbService, oauthServer, idp)
accountModule := account.New(processor) accountModule := account.New(processor)
@ -103,7 +109,6 @@ var Start action.GTSAction = func(ctx context.Context) error {
followRequestsModule := followrequest.New(processor) followRequestsModule := followrequest.New(processor)
webfingerModule := webfinger.New(processor) webfingerModule := webfinger.New(processor)
nodeInfoModule := nodeinfo.New(processor) nodeInfoModule := nodeinfo.New(processor)
webBaseModule := web.New(processor)
usersModule := user.New(processor) usersModule := user.New(processor)
timelineModule := timeline.New(processor) timelineModule := timeline.New(processor)
notificationModule := notification.New(processor) notificationModule := notification.New(processor)
@ -126,8 +131,10 @@ var Start action.GTSAction = func(ctx context.Context) error {
securityModule, securityModule,
authModule, authModule,
// now the web module
webModule,
// now everything else // now everything else
webBaseModule,
accountModule, accountModule,
instanceModule, instanceModule,
appsModule, appsModule,

View File

@ -2086,6 +2086,11 @@ paths:
in: query in: query
name: exclude_replies name: exclude_replies
type: boolean type: boolean
- default: false
description: Exclude statuses that are a reblog/boost of another status.
in: query
name: exclude_reblogs
type: boolean
- description: |- - description: |-
Return only statuses *OLDER* than the given max status ID. Return only statuses *OLDER* than the given max status ID.
The status with the specified ID will not be included in the response. The status with the specified ID will not be included in the response.
@ -2099,7 +2104,7 @@ paths:
name: min_id name: min_id
type: string type: string
- default: false - default: false
description: Show only pinned statuses. In other words,e xclude statuses that description: Show only pinned statuses. In other words, exclude statuses that
are not pinned to the given account ID. are not pinned to the given account ID.
in: query in: query
name: pinned_only name: pinned_only

View File

@ -34,6 +34,8 @@ const (
LimitKey = "limit" LimitKey = "limit"
// ExcludeRepliesKey is for specifying whether to exclude replies in a list of returned statuses by an account. // ExcludeRepliesKey is for specifying whether to exclude replies in a list of returned statuses by an account.
ExcludeRepliesKey = "exclude_replies" ExcludeRepliesKey = "exclude_replies"
// ExcludeReblogsKey is for specifying whether to exclude reblogs in a list of returned statuses by an account.
ExcludeReblogsKey = "exclude_reblogs"
// PinnedKey is for specifying whether to include pinned statuses in a list of returned statuses by an account. // PinnedKey is for specifying whether to include pinned statuses in a list of returned statuses by an account.
PinnedKey = "pinned" PinnedKey = "pinned"
// MaxIDKey is for specifying the maximum ID of the status to retrieve. // MaxIDKey is for specifying the maximum ID of the status to retrieve.

View File

@ -22,6 +22,7 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
@ -76,9 +77,10 @@ func (m *Module) AccountGETHandler(c *gin.Context) {
return return
} }
acctInfo, err := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) logrus.Debug(errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return return
} }

View File

@ -60,6 +60,12 @@ import (
// default: false // default: false
// in: query // in: query
// required: false // required: false
// - name: exclude_reblogs
// type: boolean
// description: Exclude statuses that are a reblog/boost of another status.
// default: false
// in: query
// required: false
// - name: max_id // - name: max_id
// type: string // type: string
// description: |- // description: |-
@ -75,7 +81,7 @@ import (
// required: false // required: false
// - name: pinned_only // - name: pinned_only
// type: boolean // type: boolean
// description: Show only pinned statuses. In other words,e xclude statuses that are not pinned to the given account ID. // description: Show only pinned statuses. In other words, exclude statuses that are not pinned to the given account ID.
// default: false // default: false
// in: query // in: query
// required: false // required: false
@ -149,13 +155,25 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if excludeRepliesString != "" { if excludeRepliesString != "" {
i, err := strconv.ParseBool(excludeRepliesString) i, err := strconv.ParseBool(excludeRepliesString)
if err != nil { if err != nil {
l.Debugf("error parsing replies string: %s", err) l.Debugf("error parsing exclude replies string: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"}) c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"})
return return
} }
excludeReplies = i excludeReplies = i
} }
excludeReblogs := false
excludeReblogsString := c.Query(ExcludeReblogsKey)
if excludeReblogsString != "" {
i, err := strconv.ParseBool(excludeReblogsString)
if err != nil {
l.Debugf("error parsing exclude reblogs string: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"})
return
}
excludeReblogs = i
}
maxID := "" maxID := ""
maxIDString := c.Query(MaxIDKey) maxIDString := c.Query(MaxIDKey)
if maxIDString != "" { if maxIDString != "" {
@ -204,7 +222,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
publicOnly = i publicOnly = i
} }
statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly) statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if errWithCode != nil { if errWithCode != nil {
l.Debugf("error from processor account statuses get: %s", errWithCode) l.Debugf("error from processor account statuses get: %s", errWithCode)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})

View File

@ -440,7 +440,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
suite.ErrorIs(err, db.ErrNoEntries) suite.ErrorIs(err, db.ErrNoEntries)
// no statuses from foss satan should be left in the database // no statuses from foss satan should be left in the database
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false) dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false)
suite.ErrorIs(err, db.ErrNoEntries) suite.ErrorIs(err, db.ErrNoEntries)
suite.Empty(dbStatuses) suite.Empty(dbStatuses)

View File

@ -52,7 +52,7 @@ type Account interface {
// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can
// be very memory intensive so you probably shouldn't do this! // be very memory intensive so you probably shouldn't do this!
// In case of no entries, a 'no entries' error will be returned // In case of no entries, a 'no entries' error will be returned
GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error)
GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error)

View File

@ -199,7 +199,7 @@ func (a *accountDB) GetLocalAccountByUsername(ctx context.Context, username stri
account := new(gtsmodel.Account) account := new(gtsmodel.Account)
q := a.newAccountQ(account). q := a.newAccountQ(account).
Where("username = ?", username). Where("LOWER(?) = LOWER(?)", bun.Ident("username"), username). // ignore casing
WhereGroup(" AND ", whereEmptyOrNull("domain")) WhereGroup(" AND ", whereEmptyOrNull("domain"))
if err := q.Scan(ctx); err != nil { if err := q.Scan(ctx); err != nil {
@ -230,7 +230,7 @@ func (a *accountDB) CountAccountStatuses(ctx context.Context, accountID string)
Count(ctx) Count(ctx)
} }
func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) { func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) {
statuses := []*gtsmodel.Status{} statuses := []*gtsmodel.Status{}
q := a.conn. q := a.conn.
@ -250,6 +250,10 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
q = q.WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id")) q = q.WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id"))
} }
if excludeReblogs {
q = q.WhereGroup(" AND ", whereEmptyOrNull("boost_of_id"))
}
if maxID != "" { if maxID != "" {
q = q.Where("id < ?", maxID) q = q.Where("id < ?", maxID)
} }

View File

@ -34,16 +34,20 @@ func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth,
return p.accountProcessor.DeleteLocal(ctx, authed.Account, form) return p.accountProcessor.DeleteLocal(ctx, authed.Account, form)
} }
func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.Get(ctx, authed.Account, targetAccountID) return p.accountProcessor.Get(ctx, authed.Account, targetAccountID)
} }
func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username)
}
func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
return p.accountProcessor.Update(ctx, authed.Account, form) return p.accountProcessor.Update(ctx, authed.Account, form)
} }
func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly) return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
} }
func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {

View File

@ -47,12 +47,14 @@ type Processor interface {
// Unlike Delete, it will propagate the deletion out across the federating API to other instances. // Unlike Delete, it will propagate the deletion out across the federating API to other instances.
DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode
// Get processes the given request for account information. // Get processes the given request for account information.
Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
// GetLocalByUsername processes the given request for account information targeting a local account by username.
GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode)
// Update processes the update of an account with the given form // Update processes the update of an account with the given form
Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed. // the account given in authed.
StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
// FollowersGet fetches a list of the target account's followers. // FollowersGet fetches a list of the target account's followers.
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// FollowingGet fetches a list of the accounts that target account is following. // FollowingGet fetches a list of the accounts that target account is following.

View File

@ -143,7 +143,7 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
var maxID string var maxID string
selectStatusesLoop: selectStatusesLoop:
for { for {
statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, "", false, false, false) statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, false, maxID, "", false, false, false)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
// no statuses left for this instance so we're done // no statuses left for this instance so we're done

View File

@ -26,23 +26,41 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) { func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) {
targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
return nil, errors.New("account not found") return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
} }
return nil, fmt.Errorf("db error: %s", err) return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
} }
return p.getAccountFor(ctx, requestingAccount, targetAccount)
}
func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) {
targetAccount, err := p.db.GetLocalAccountByUsername(ctx, username)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(errors.New("account not found"))
}
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err))
}
return p.getAccountFor(ctx, requestingAccount, targetAccount)
}
func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) {
var blocked bool var blocked bool
var err error
if requestingAccount != nil { if requestingAccount != nil {
blocked, err = p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true) blocked, err = p.db.IsBlocked(ctx, requestingAccount.ID, targetAccount.ID, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error checking account block: %s", err) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking account block: %s", err))
} }
} }
@ -50,7 +68,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
if blocked { if blocked {
apiAccount, err = p.tc.AccountToAPIAccountBlocked(ctx, targetAccount) apiAccount, err = p.tc.AccountToAPIAccountBlocked(ctx, targetAccount)
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting account: %s", err) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err))
} }
return apiAccount, nil return apiAccount, nil
} }
@ -59,7 +77,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
if targetAccount.Domain != "" { if targetAccount.Domain != "" {
targetAccountURI, err := url.Parse(targetAccount.URI) targetAccountURI, err := url.Parse(targetAccount.URI)
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing url %s: %s", targetAccount.URI, err) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", targetAccount.URI, err))
} }
a, err := p.federator.GetRemoteAccount(ctx, requestingAccount.Username, targetAccountURI, true, false) a, err := p.federator.GetRemoteAccount(ctx, requestingAccount.Username, targetAccountURI, true, false)
@ -74,7 +92,7 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
apiAccount, err = p.tc.AccountToAPIAccountPublic(ctx, targetAccount) apiAccount, err = p.tc.AccountToAPIAccountPublic(ctx, targetAccount)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting account: %s", err) return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err))
} }
return apiAccount, nil return apiAccount, nil
} }

View File

@ -28,16 +28,18 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) {
if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { if requestingAccount != nil {
return nil, gtserror.NewErrorInternalError(err) if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
} else if blocked { return nil, gtserror.NewErrorInternalError(err)
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } else if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
}
} }
apiStatuses := []apimodel.Status{} apiStatuses := []apimodel.Status{}
statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly) statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if err != nil { if err != nil {
if err == db.ErrNoEntries { if err == db.ErrNoEntries {
return apiStatuses, nil return apiStatuses, nil

View File

@ -89,7 +89,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag
// scenario 2 -- get the requested page // scenario 2 -- get the requested page
// limit pages to 30 entries per page // limit pages to 30 entries per page
publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, maxID, minID, false, false, true) publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true)
if err != nil && err != db.ErrNoEntries { if err != nil && err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }

View File

@ -38,17 +38,20 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
} }
var requestedPerson vocab.ActivityStreamsPerson var requestedPerson vocab.ActivityStreamsPerson
switch { if uris.IsPublicKeyPath(requestURL) {
case uris.IsPublicKeyPath(requestURL):
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount) requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
case uris.IsUserPath(requestURL): } else {
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile // if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
if err != nil || !authenticated { if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err, "not authorized")
}
if !authenticated {
return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
} }
@ -73,8 +76,6 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
default:
return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
} }
data, err := streams.Serialize(requestedPerson) data, err := streams.Serialize(requestedPerson)

View File

@ -354,7 +354,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
suite.False(zorkFollowsSatan) suite.False(zorkFollowsSatan)
// no statuses from foss satan should be left in the database // no statuses from foss satan should be left in the database
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false) dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false)
suite.ErrorIs(err, db.ErrNoEntries) suite.ErrorIs(err, db.ErrNoEntries)
suite.Empty(dbStatuses) suite.Empty(dbStatuses)

View File

@ -76,12 +76,14 @@ type Processor interface {
// AccountDeleteLocal processes the delete of a LOCAL account using the given form. // AccountDeleteLocal processes the delete of a LOCAL account using the given form.
AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode
// AccountGet processes the given request for account information. // AccountGet processes the given request for account information.
AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode)
// AccountGet processes the given request for account information.
AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode)
// AccountUpdate processes the update of an account with the given form // AccountUpdate processes the update of an account with the given form
AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed. // the account given in authed.
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode)
// AccountFollowersGet fetches a list of the target account's followers. // AccountFollowersGet fetches a list of the target account's followers.
AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
// AccountFollowingGet fetches a list of the accounts that target account is following. // AccountFollowingGet fetches a list of the accounts that target account is following.

View File

@ -67,6 +67,11 @@ func timestamp(stamp string) string {
return t.Format("January 2, 2006, 15:04:05") return t.Format("January 2, 2006, 15:04:05")
} }
func timestampShort(stamp string) string {
t, _ := time.Parse(time.RFC3339, stamp)
return t.Format("January, 2006")
}
type iconWithLabel struct { type iconWithLabel struct {
faIcon string faIcon string
label string label string
@ -98,5 +103,6 @@ func LoadTemplateFunctions(engine *gin.Engine) {
"oddOrEven": oddOrEven, "oddOrEven": oddOrEven,
"visibilityIcon": visibilityIcon, "visibilityIcon": visibilityIcon,
"timestamp": timestamp, "timestamp": timestamp,
"timestampShort": timestampShort,
}) })
} }

View File

@ -132,7 +132,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() {
ctx := context.Background() ctx := context.Background()
// get public statuses from testaccount // get public statuses from testaccount
statuses, err := suite.db.GetAccountStatuses(ctx, testAccount.ID, 30, true, "", "", false, false, true) statuses, err := suite.db.GetAccountStatuses(ctx, testAccount.ID, 30, true, true, "", "", false, false, true)
suite.NoError(err) suite.NoError(err)
page, err := suite.typeconverter.StatusesToASOutboxPage(ctx, testAccount.OutboxURI, "", "", statuses) page, err := suite.typeconverter.StatusesToASOutboxPage(ctx, testAccount.OutboxURI, "", "", statuses)

View File

@ -20,8 +20,10 @@ package web
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -36,18 +38,68 @@ import (
const ( const (
confirmEmailPath = "/" + uris.ConfirmEmailPath confirmEmailPath = "/" + uris.ConfirmEmailPath
tokenParam = "token" tokenParam = "token"
usernameKey = "username"
statusIDKey = "status"
profilePath = "/@:" + usernameKey
statusPath = profilePath + "/statuses/:" + statusIDKey
) )
// Module implements the api.ClientModule interface for web pages. // Module implements the api.ClientModule interface for web pages.
type Module struct { type Module struct {
processor processing.Processor processor processing.Processor
assetsPath string
adminPath string
defaultAvatars []string
} }
// New returns a new api.ClientModule for web pages. // New returns a new api.ClientModule for web pages.
func New(processor processing.Processor) api.ClientModule { func New(processor processing.Processor) (api.ClientModule, error) {
return &Module{ assetsBaseDir := viper.GetString(config.Keys.WebAssetBaseDir)
processor: processor, if assetsBaseDir == "" {
return nil, fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.Keys.WebAssetBaseDir)
} }
assetsPath, err := filepath.Abs(assetsBaseDir)
if err != nil {
return nil, fmt.Errorf("error getting absolute path of %s: %s", assetsBaseDir, err)
}
defaultAvatarsPath := filepath.Join(assetsPath, "default_avatars")
defaultAvatarFiles, err := ioutil.ReadDir(defaultAvatarsPath)
if err != nil {
return nil, fmt.Errorf("error reading default avatars at %s: %s", defaultAvatarsPath, err)
}
defaultAvatars := []string{}
for _, f := range defaultAvatarFiles {
// ignore directories
if f.IsDir() {
continue
}
// ignore files bigger than 50kb
if f.Size() > 50000 {
continue
}
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(f.Name())), ".")
// take only files with simple extensions
switch extension {
case "svg", "jpeg", "jpg", "gif", "png":
defaultAvatarPath := fmt.Sprintf("/assets/default_avatars/%s", f.Name())
defaultAvatars = append(defaultAvatars, defaultAvatarPath)
default:
continue
}
}
return &Module{
processor: processor,
assetsPath: assetsPath,
adminPath: filepath.Join(assetsPath, "admin"),
defaultAvatars: defaultAvatars,
}, nil
} }
func (m *Module) baseHandler(c *gin.Context) { func (m *Module) baseHandler(c *gin.Context) {
@ -88,20 +140,11 @@ func (m *Module) NotFoundHandler(c *gin.Context) {
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *Module) Route(s router.Router) error { func (m *Module) Route(s router.Router) error {
// serve static files from assets dir at /assets // serve static files from assets dir at /assets
assetBaseDir := viper.GetString(config.Keys.WebAssetBaseDir) s.AttachStaticFS("/assets", fileSystem{http.Dir(m.assetsPath)})
if assetBaseDir == "" {
return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.Keys.WebAssetBaseDir)
}
assetPath, err := filepath.Abs(assetBaseDir)
if err != nil {
return fmt.Errorf("error getting absolute path of %s: %s", assetBaseDir, err)
}
s.AttachStaticFS("/assets", fileSystem{http.Dir(assetPath)})
// serve admin panel from within assets dir at /admin/ // serve admin panel from within assets dir at /admin/
// and redirect /admin to /admin/ // and redirect /admin to /admin/
adminPath := filepath.Join(assetPath, "admin") s.AttachStaticFS("/admin/", fileSystem{http.Dir(m.adminPath)})
s.AttachStaticFS("/admin/", fileSystem{http.Dir(adminPath)})
s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/admin/") c.Redirect(http.StatusMovedPermanently, "/admin/")
}) })
@ -109,8 +152,11 @@ func (m *Module) Route(s router.Router) error {
// serve front-page // serve front-page
s.AttachHandler(http.MethodGet, "/", m.baseHandler) s.AttachHandler(http.MethodGet, "/", m.baseHandler)
// serve profile pages at /@username
s.AttachHandler(http.MethodGet, profilePath, m.profileTemplateHandler)
// serve statuses // serve statuses
s.AttachHandler(http.MethodGet, "/:user/statuses/:id", m.threadTemplateHandler) s.AttachHandler(http.MethodGet, statusPath, m.threadTemplateHandler)
// serve email confirmation page at /confirm_email?token=whatever // serve email confirmation page at /confirm_email?token=whatever
s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)

139
internal/web/profile.go Normal file
View File

@ -0,0 +1,139 @@
/*
GoToSocial
Copyright (C) 2021-2022 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 <http://www.gnu.org/licenses/>.
*/
package web
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (m *Module) profileTemplateHandler(c *gin.Context) {
l := logrus.WithField("func", "profileTemplateHandler")
l.Trace("rendering profile template")
ctx := c.Request.Context()
username := c.Param(usernameKey)
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"})
return
}
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil {
l.Errorf("error authing profile GET request: %s", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
instance, errWithCode := m.processor.InstanceGet(ctx, viper.GetString(config.Keys.Host))
if errWithCode != nil {
l.Debugf("error getting instance from processor: %s", errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
account, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username)
if errWithCode != nil {
l.Debugf("error getting account from processor: %s", errWithCode.Error())
if errWithCode.Code() == http.StatusNotFound {
m.NotFoundHandler(c)
return
}
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
// if we're getting an AP request on this endpoint we should render the account's AP representation instead
accept := c.NegotiateFormat(string(api.TextHTML), string(api.AppActivityJSON), string(api.AppActivityLDJSON))
if accept == string(api.AppActivityJSON) || accept == string(api.AppActivityLDJSON) {
m.returnAPRepresentation(ctx, c, username, accept)
return
}
// get latest 10 top-level public statuses;
// ie., exclude replies and boosts, public only,
// with or without media
statuses, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true)
if errWithCode != nil {
l.Debugf("error getting statuses from processor: %s", errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
// pick a random dummy avatar if this account avatar isn't set yet
if account.Avatar == "" && len(m.defaultAvatars) > 0 {
//nolint:gosec
randomIndex := rand.Intn(len(m.defaultAvatars))
dummyAvatar := m.defaultAvatars[randomIndex]
account.Avatar = dummyAvatar
for _, s := range statuses {
s.Account.Avatar = dummyAvatar
}
}
c.HTML(http.StatusOK, "profile.tmpl", gin.H{
"instance": instance,
"account": account,
"statuses": statuses,
"stylesheets": []string{
"/assets/Fork-Awesome/css/fork-awesome.min.css",
"/assets/status.css",
"/assets/profile.css",
},
})
}
func (m *Module) returnAPRepresentation(ctx context.Context, c *gin.Context, username string, accept string) {
verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier))
if signed {
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier)
}
signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature))
if signed {
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature)
}
user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) // GetFediUser handles auth as well
if errWithCode != nil {
logrus.Infof(errWithCode.Error())
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
b, mErr := json.Marshal(user)
if mErr != nil {
err := fmt.Errorf("could not marshal json: %s", mErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, accept, b)
}

View File

@ -20,6 +20,7 @@ package web
import ( import (
"net/http" "net/http"
"strings"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -29,21 +30,21 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
type statusLink struct {
User string `uri:"user" binding:"required"`
ID string `uri:"id" binding:"required"`
}
func (m *Module) threadTemplateHandler(c *gin.Context) { func (m *Module) threadTemplateHandler(c *gin.Context) {
l := logrus.WithField("func", "threadTemplateGET") l := logrus.WithField("func", "threadTemplateGET")
l.Trace("rendering thread template") l.Trace("rendering thread template")
ctx := c.Request.Context() ctx := c.Request.Context()
var uriParts statusLink username := c.Param(usernameKey)
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"})
return
}
if err := c.ShouldBindUri(&uriParts); err != nil { statusID := c.Param(statusIDKey)
c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified"})
return return
} }
@ -62,18 +63,18 @@ func (m *Module) threadTemplateHandler(c *gin.Context) {
return return
} }
status, err := m.processor.StatusGet(ctx, authed, uriParts.ID) status, err := m.processor.StatusGet(ctx, authed, statusID)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
return return
} }
if uriParts.User[:1] != "@" || uriParts.User[1:] != status.Account.Username { if !strings.EqualFold(username, status.Account.Username) {
c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
return return
} }
context, err := m.processor.StatusGetContext(ctx, authed, uriParts.ID) context, err := m.processor.StatusGetContext(ctx, authed, statusID)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
return return

View File

@ -166,23 +166,24 @@ section.login form button {
} }
section.error { section.error {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
}
section.error span {
font-size: 2em;
}
section.error pre {
border: 1px solid #ff000080;
margin-left: 1em;
padding: 0 0.7em;
border-radius: 0.5em;
background-color: #ff000010;
font-size: 1.3em;
white-space: pre-wrap;
} }
section.error span {
font-size: 2em;
}
section.error pre {
border: 1px solid #ff000080;
margin-left: 1em;
padding: 0 0.7em;
border-radius: 0.5em;
background-color: #ff000010;
font-size: 1.3em;
white-space: pre-wrap;
}
input, select, textarea { input, select, textarea {
border: 1px solid #fafaff; border: 1px solid #fafaff;

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon1.svg"
inkscape:export-xdpi="95.999992"
inkscape:export-ydpi="95.999992">
<defs
id="defs5117">
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.70710678"
inkscape:cx="460.72691"
inkscape:cy="522.20279"
inkscape:document-units="px"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#d0d0d0;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect5876"
width="1000.0042"
height="1000"
x="0"
y="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g5890"
transform="translate(-10)">
<path
sodipodi:nodetypes="ssscccs"
inkscape:connector-curvature="0"
id="path5762"
d="M 861.29285,497.07031 C 861.65556,665.3247 774.21642,807.40548 511.60027,807.86794 270.63622,808.29226 154.54309,691.2756 155.19024,504.19228 155.7289,348.47535 251.17288,227.4551 422.3176,205.3802 c -35.32036,-75.85452 52.24232,-96.94648 73.77615,-32.00508 13.73451,-37.63439 108.24345,-49.1716 62.21106,24.77055 147.95052,3.75658 302.58353,111.28061 302.98804,298.92464 z"
style="display:inline;fill:#767676;fill-opacity:1;stroke:none;stroke-width:2.57058167;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5780"
d="m 809.15213,517.31679 c -4.83374,150.52526 -109.85544,235.22815 -297.81171,235.31839 -179.6675,0.0863 -290.56109,-70.98245 -298.50223,-235.31839 -4.6366,-95.95095 54.62861,-181.84442 144.83016,-194.18834 80.92123,-11.07393 99.7402,21.01802 153.67207,21.01802 59.21658,0 83.64871,-35.09608 162.84221,-21.85479 87.78391,14.67763 137.90533,103.6017 134.9695,195.02511 z"
style="display:inline;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="scssscs"
inkscape:connector-curvature="0"
id="path5780-9"
d="m 809.15213,517.31679 c -1.32872,41.37724 -10.22787,77.78081 -26.33906,108.8204 -46.60931,-39.48031 -99.53509,-10.7281 -171.50115,-39.43334 -44.77145,-17.85808 -51.41659,-56.56453 -51.21999,-81.3542 0.54836,-69.14384 48.17003,-93.45758 95.53601,-97.60875 55.74677,-4.88566 124.5246,36.1482 151.01547,66.79433 2.11531,14.01083 2.97167,28.36512 2.50872,42.78156 z"
style="display:inline;fill:#a1a1a1;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
ry="50.575684"
rx="37.800804"
cy="502.64291"
cx="646.85773"
id="path5816"
style="fill:#767676;fill-opacity:1;stroke:none;stroke-width:1.51185882;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="scssscs"
inkscape:connector-curvature="0"
id="path5780-9-1"
d="m 212.51463,517.3246 c 1.32872,41.37724 10.22787,77.78081 26.33906,108.8204 46.60931,-39.48031 99.57415,-10.73591 171.54021,-39.44115 44.77145,-17.85808 51.41659,-56.56453 51.21999,-81.3542 -0.54836,-69.14384 -48.20909,-93.44977 -95.57507,-97.60094 -55.74677,-4.88566 -124.5246,36.1482 -151.01547,66.79433 -2.11531,14.01083 -2.97167,28.36512 -2.50872,42.78156 z"
style="display:inline;fill:#a1a1a1;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
transform="scale(-1,1)"
ry="50.575684"
rx="37.800804"
cy="502.64294"
cx="-374.84808"
id="path5816-0"
style="display:inline;fill:#767676;fill-opacity:1;stroke:none;stroke-width:1.51185882;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path5862"
d="m 543.96613,556.96185 c 0,11.0622 -14.51648,20.02988 -32.42347,20.02988 -17.90698,0 -32.42347,-8.96769 -32.42347,-20.02988 0,-11.0622 14.14619,-15.58638 32.05318,-15.58638 17.90698,0 32.79376,4.52417 32.79376,15.58638 z"
style="display:inline;fill:#767676;fill-opacity:1;stroke:none;stroke-width:1.60515046;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path5865"
d="m 552.00195,620.36132 c 7.06643,13.89391 -19.38375,21.24024 -40.2832,21.24024 -20.89945,0 -47.71708,-7.02219 -41.50391,-21.24024 5.71775,-13.08435 20.11619,0.73243 41.01563,0.73243 20.89944,0 34.43888,-13.1835 40.77148,-0.73243 z"
style="display:inline;fill:#767676;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
transform="rotate(-6.669407)"
ry="24.882849"
rx="19.511755"
cy="560.95673"
cx="600.24731"
id="path5818"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.53898752;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
transform="rotate(-6.6694071)"
ry="24.882849"
rx="19.511755"
cy="529.32086"
cx="329.69714"
id="path5818-8"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.53898752;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon2.svg"
inkscape:export-xdpi="95.999992"
inkscape:export-ydpi="95.999992">
<defs
id="defs5117">
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="497.76221"
inkscape:cy="575.69254"
inkscape:document-units="px"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#d0d0d0;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect5876"
width="1000.0042"
height="1000"
x="0"
y="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g3978">
<path
sodipodi:nodetypes="sccccccccccscssscsccs"
inkscape:connector-curvature="0"
id="rect5876-5"
d="M 230.01724,321.98276 C 349.98851,210.42521 448.21823,215.25781 531.04596,212.10691 c -43.53882,-55.72028 69.90321,-110.15948 114.22539,-8.84147 88.6448,-15.41997 77.60551,48.58211 55.52314,59.56776 116.1765,70.90805 150.01289,158.27624 164.97213,224.4903 78.29027,48.93425 33.19569,98.38368 21.79073,93.92084 57.78384,113.21004 8.74508,163.48051 -19.17636,145.16042 C 847.88417,891.22533 777.25,1000 777.25,1000 H 0 V 335 c -0.79197362,-2.0633 86.752294,193.09344 193.48008,463.67555 8.22828,-21.75326 15.34189,-32.85227 29.8481,-31.82047 12.36034,0.87917 27.76558,15.1443 13.3047,74.3212 0.95528,-0.34561 22.71708,-7.34064 29.09712,1.6646 12.30907,17.37387 -7.57175,23.95646 -1.82859,33.94605 6.04771,10.51933 23.2419,-4.11346 24.38025,-22.42448 0.95349,-15.33743 -7.68068,-33.34588 -31.29939,-31.7437 8.19555,-53.54698 -12.66475,-64.63281 -12.97883,-65.12097 -4.1124,-6.39167 -26.81347,-22.42708 -49.42662,1.72757 -15.70939,-47.21646 -51.98321,-128.67679 -77.05729,-192.68773 31.46671,-57.43102 24.70494,-162.9191 112.49771,-244.55486 z"
style="display:inline;fill:#777777;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5899"
d="m 804.83231,554.45837 c 0,140.99987 -133.69246,255.30285 -298.61048,255.30285 -164.91801,0 -298.61047,-114.30298 -298.61047,-255.30285 0,-100.64813 59.92313,-198.68967 160.73026,-206.70182 60.18634,-4.78361 90.68629,22.30425 137.88021,22.81676 46.04207,0.5 70.12813,-25.6781 135.69855,-22.78348 115.9463,5.11848 162.91193,120.63643 162.91193,206.66854 z"
style="display:inline;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#a1a1a1;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 804.83231,554.45837 c 0,13.4968 -1.22499,26.74899 -3.58509,39.67972 -71.95206,24.77201 -204.63976,33.96213 -235.31322,-11.60451 -31.67182,-47.04972 13.47247,-156.40488 88.33815,-154.51226 68.45478,1.73055 101.55237,13.1539 132.47817,31.57199 12.36622,31.25081 18.08199,64.85216 18.08199,94.86506 z"
id="path5937"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsscs" />
<path
sodipodi:nodetypes="scsscs"
inkscape:connector-curvature="0"
id="path5953"
d="m 207.65971,554.45837 c 0,13.4968 1.22499,26.74899 3.58509,39.67972 71.95206,24.77201 204.63976,33.96213 235.31322,-11.60451 31.67182,-47.04972 -13.47247,-156.40488 -88.33815,-154.51226 -68.45478,1.73055 -101.55237,13.1539 -132.47817,31.57199 -12.36622,31.25081 -18.08199,64.85216 -18.08199,94.86506 z"
style="display:inline;fill:#a1a1a1;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path5955"
d="m 540.5,587.08433 c 0,11.71361 -14.7746,21.20938 -33,21.20938 -18.2254,0 -33,-9.49577 -33,-21.20938 0,-11.71362 14.0675,-13.51846 32.29289,-13.51846 18.2254,0 33.70711,1.80484 33.70711,13.51846 z"
style="fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.34833181;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
ry="44.492474"
rx="35.992474"
cy="533"
cx="641.5"
id="path5939"
style="display:inline;fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.34905314;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
transform="scale(-1,1)"
style="display:inline;fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.34905314;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="ellipse5951"
cx="-370.992"
cy="533"
rx="35.992474"
ry="44.492474" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon3.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs5117">
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="332.72586"
inkscape:cy="487.44283"
inkscape:document-units="px"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#dbe1ed;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect5876"
width="1000.0042"
height="1000"
x="0"
y="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g3968">
<path
sodipodi:nodetypes="sccccccccccscssscsccs"
inkscape:connector-curvature="0"
id="rect5876-5"
d="M 230.01724,321.98276 C 349.98851,210.42521 448.21823,215.25781 531.04596,212.10691 c -43.53882,-55.72028 69.90321,-110.15948 114.22539,-8.84147 88.6448,-15.41997 77.60551,48.58211 55.52314,59.56776 116.1765,70.90805 150.01289,158.27624 164.97213,224.4903 78.29027,48.93425 33.19569,98.38368 21.79073,93.92084 57.78384,113.21004 8.74508,163.48051 -19.17636,145.16042 C 847.88417,891.22533 777.25,1000 777.25,1000 H 0 V 335 c -0.79197362,-2.0633 86.752294,193.09344 193.48008,463.67555 8.22828,-21.75326 15.34189,-32.85227 29.8481,-31.82047 12.36034,0.87917 27.76558,15.1443 13.3047,74.3212 0.95528,-0.34561 22.71708,-7.34064 29.09712,1.6646 12.30907,17.37387 -7.57175,23.95646 -1.82859,33.94605 6.04771,10.51933 23.2419,-4.11346 24.38025,-22.42448 0.95349,-15.33743 -7.68068,-33.34588 -31.29939,-31.7437 8.19555,-53.54698 -12.66475,-64.63281 -12.97883,-65.12097 -4.1124,-6.39167 -26.81347,-22.42708 -49.42662,1.72757 -15.70939,-47.21646 -51.98321,-128.67679 -77.05729,-192.68773 31.46671,-57.43102 24.70494,-162.9191 112.49771,-244.55486 z"
style="display:inline;fill:#8a9bab;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5899"
d="m 804.83231,554.45837 c 0,140.99987 -133.69246,255.30285 -298.61048,255.30285 -164.91801,0 -298.61047,-114.30298 -298.61047,-255.30285 0,-100.64813 59.92313,-198.68967 160.73026,-206.70182 60.18634,-4.78361 90.68629,22.30425 137.88021,22.81676 46.04207,0.5 70.12813,-25.6781 135.69855,-22.78348 115.9463,5.11848 162.91193,120.63643 162.91193,206.66854 z"
style="display:inline;fill:#ecf1f5;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#b5becf;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 804.83231,554.45837 c 0,13.4968 -1.22499,26.74899 -3.58509,39.67972 -71.95206,24.77201 -204.63976,33.96213 -235.31322,-11.60451 -31.67182,-47.04972 13.47247,-156.40488 88.33815,-154.51226 68.45478,1.73055 101.55237,13.1539 132.47817,31.57199 12.36622,31.25081 18.08199,64.85216 18.08199,94.86506 z"
id="path5937"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsscs" />
<path
sodipodi:nodetypes="scsscs"
inkscape:connector-curvature="0"
id="path5953"
d="m 207.65971,554.45837 c 0,13.4968 1.22499,26.74899 3.58509,39.67972 71.95206,24.77201 204.63976,33.96213 235.31322,-11.60451 31.67182,-47.04972 -13.47247,-156.40488 -88.33815,-154.51226 -68.45478,1.73055 -101.55237,13.1539 -132.47817,31.57199 -12.36622,31.25081 -18.08199,64.85216 -18.08199,94.86506 z"
style="display:inline;fill:#b5becf;fill-opacity:1;stroke:none;stroke-width:1.63446391;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path5955"
d="m 540.5,587.08433 c 0,11.71361 -14.7746,21.20938 -33,21.20938 -18.2254,0 -33,-9.49577 -33,-21.20938 0,-11.71362 14.0675,-13.51846 32.29289,-13.51846 18.2254,0 33.70711,1.80484 33.70711,13.51846 z"
style="fill:#7a7d82;fill-opacity:1;stroke:none;stroke-width:0.34833181;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
ry="44.492474"
rx="35.992474"
cy="533"
cx="641.5"
id="path5939"
style="display:inline;fill:#7a7d82;fill-opacity:1;stroke:none;stroke-width:0.34905314;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<ellipse
transform="scale(-1,1)"
style="display:inline;fill:#7a7d82;fill-opacity:1;stroke:none;stroke-width:0.34905314;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="ellipse5951"
cx="-370.992"
cy="533"
rx="35.992474"
ry="44.492474" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon4.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs5117">
<inkscape:path-effect
is_visible="true"
id="path-effect6023"
effect="spiro" />
<inkscape:path-effect
effect="spiro"
id="path-effect6019"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="-238.86957"
inkscape:cy="384.20002"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.95512283;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect6025"
width="1000"
height="1000"
x="-1000"
y="-1000"
transform="scale(-1)" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g3958">
<path
sodipodi:nodetypes="ccssccccscccscscccccccc"
inkscape:connector-curvature="0"
id="rect5876"
d="m 250.42136,685.9676 c 27.70546,-52.03568 68.96026,3.65306 45.50576,51.34931 31.74726,11.3972 32.63935,48.28596 10.44891,60.73784 -5.04807,2.83267 -8.35938,4.85246 -6.24616,9.92615 6.82828,16.39412 69.24558,-37.84791 12.03356,-75.57698 17.99005,-83.53997 -44.99856,-94.84265 -61.49124,-71.10901 C 197.50575,583.4007 147.54378,487.681 131.62217,450.28384 c 0.12558,-13.14689 10.55647,-15.91833 16.57496,-17.76346 -52.047785,-29.70807 -27.82707,-79.31533 17.95281,-96.29615 46.52036,-17.25548 75.09848,-100.71517 158.45465,-139.02174 -41.91643,-28.95924 12.41179,-76.2933 69.63812,-60.09295 49.56384,-86.701637 128.76235,-51.997593 106.48829,-6.82033 160.08649,-21.81784 193.18485,37.08464 252.21617,29.88994 126.67808,-15.43944 115.11456,62.82898 97.64128,88.84198 21.70268,16.70276 30.36519,23.60378 61.89934,36.01667 40.08455,15.77862 59.1052,72.49434 0.74775,112.50038 40.62846,38.95859 78.93877,96.58176 32.79494,114.2793 51.96757,64.16898 32.13903,145.87433 -6.38744,145.34448 8.59645,23.3003 -5.32539,71.52714 -35.88758,62.06313 C 884.05327,852.44822 817.94577,1000 817.94577,1000 H 0 V 142.98659 c 0,0 89.549546,320.73624 250.42136,542.98101 z"
style="display:inline;fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5899"
d="m 839.83231,465.22371 c 0,130.63606 -133.69246,236.53751 -298.61048,236.53751 -164.91801,0 -298.61047,-105.90145 -298.61047,-236.53751 0,-93.25027 59.92313,-184.08554 160.73026,-191.50878 60.18634,-4.432 90.68629,20.66484 137.88021,21.13968 46.04207,0.46325 70.12813,-23.7907 135.69855,-21.10884 115.9463,4.74226 162.91193,111.76938 162.91193,191.47794 z"
style="display:inline;fill:#f6e7e0;fill-opacity:0.98113209;stroke:none;stroke-width:1.57324922;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.57324922;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 839.83231,465.22371 c 0,19.38025 -2.94239,38.21613 -8.4926,56.24262 -85.83868,33.0678 -208.68682,26.13484 -229.51106,-25.32493 -26.07758,-64.44157 31.43036,-103.22868 75.8166,-116.93012 51.17758,-15.79784 106.51724,0.003 150.63827,15.41887 7.86068,23.77511 11.54879,48.25697 11.54879,70.59356 z"
id="path6003"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scsscs" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path6005"
d="m 636.34279,468.2557 c -0.9957,7.73624 0.22345,23.8871 13.0952,23.55507 15.73663,-0.40594 8.01751,-24.25641 28.62411,-24.0854 20.42062,0.16948 12.59412,24.3415 27.18325,24.95261 15.02273,0.62928 16.26485,-17.17918 14.71283,-25.30616 -4.39441,-23.01076 -22.45132,-32.32534 -42.07286,-32.70368 -21.39109,-0.41246 -38.86298,12.76845 -41.54253,33.58756 z"
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:0.33399999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="scsscs"
inkscape:connector-curvature="0"
id="path6008"
d="m 242.46966,465.22371 c 0,19.38025 2.94239,38.21613 8.4926,56.24262 85.83868,33.0678 208.68682,26.13484 229.51106,-25.32493 26.07758,-64.44157 -31.43036,-103.22868 -75.8166,-116.93012 -51.17758,-15.79784 -106.20788,0.1024 -150.32891,15.51831 -8.25843,23.68672 -11.85815,48.15753 -11.85815,70.49412 z"
style="display:inline;fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.57324922;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:0.33399999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 445.95918,468.2557 c 0.9957,7.73624 -0.22345,23.8871 -13.0952,23.55507 -15.73663,-0.40594 -8.01751,-24.25641 -28.62411,-24.0854 -20.42062,0.16948 -12.59412,24.3415 -27.18325,24.95261 -15.02273,0.62928 -16.26485,-17.17918 -14.71283,-25.30616 4.39441,-23.01076 22.45132,-32.32534 42.07286,-32.70368 21.39109,-0.41246 38.86298,12.76845 41.54253,33.58756 z"
id="path6010"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssss" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path6014"
d="m 572.40294,496.00963 c 0,10.44653 -14.00878,18.91511 -31.28948,18.91511 -17.2807,0 -31.28947,-8.46858 -31.28947,-18.91511 1e-5,-10.44652 13.65522,-13.96535 30.93592,-13.96535 17.28069,0 31.64302,3.51883 31.64303,13.96535 z"
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:0.33399999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
transform="matrix(-1,0,0,1,1088.0012,0)"
sodipodi:nodetypes="sssscsssscs"
inkscape:original-d="m 561.38128,582.67417 c 4.16766,-0.91767 12.20351,2.22573 11.70451,-3.19194 -0.49904,-5.41767 3.22694,-10.0569 -0.43934,-12.36091 -3.71867,-2.33693 -4.66037,-0.70889 -10.62868,-3.03033 -6.7415,-2.62217 -12.89085,-5.33173 -18.0383,-9.27773 -5.14745,3.946 -11.25574,6.65556 -17.99724,9.27773 -5.96831,2.32144 -6.91001,0.6934 -10.62868,3.03033 -3.66628,2.30401 0.0597,6.94324 -0.43934,12.36091 -0.499,5.41767 7.53685,2.27427 11.70451,3.19194 4.16767,0.91767 12.6047,-2.8973 17.27236,-5.97963 4.66766,3.08233 13.32253,6.8973 17.4902,5.97963 z"
inkscape:path-effect="#path-effect6019"
inkscape:connector-curvature="0"
id="path6017"
d="m 561.38128,582.67417 c 2.08275,0.16617 4.19863,0.15523 6.23646,-0.30597 2.03783,-0.46119 4.00308,-1.39624 5.46805,-2.88597 1.62507,-1.65254 2.56385,-3.97206 2.49743,-6.2888 -0.0664,-2.31675 -1.15042,-4.59539 -2.93677,-6.07211 -1.43969,-1.19015 -3.25655,-1.83786 -5.08522,-2.21886 -1.82866,-0.38101 -3.69889,-0.51693 -5.54346,-0.81147 -6.78982,-1.08418 -13.20738,-4.38495 -18.0383,-9.27773 -4.81493,4.89182 -11.2195,8.19343 -17.99724,9.27773 -1.84447,0.29508 -3.7147,0.43095 -5.54339,0.8118 -1.82869,0.38084 -3.64567,1.02831 -5.08529,2.21853 -1.78611,1.47668 -2.86946,3.75542 -2.93562,6.07196 -0.0662,2.31655 0.87229,4.63564 2.49628,6.28895 1.46438,1.49081 3.42913,2.42785 5.46715,2.88979 2.03802,0.46195 4.15458,0.4723 6.23736,0.30215 6.13711,-0.50136 12.1389,-2.57916 17.27236,-5.97963 5.21036,3.40799 11.28399,5.48447 17.4902,5.97963 z"
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon5.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs5117">
<inkscape:path-effect
is_visible="true"
id="path-effect6023"
effect="spiro" />
<inkscape:path-effect
effect="spiro"
id="path-effect6019"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="429.92366"
inkscape:cy="322.81254"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#dbe1ed;fill-opacity:1;stroke:none;stroke-width:1.95512283;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect6025"
width="1000"
height="1000"
x="0"
y="-1000"
transform="scale(1,-1)" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g3356">
<path
sodipodi:nodetypes="cccsssssscscccccccssscc"
inkscape:connector-curvature="0"
id="rect5876"
d="m 874.87783,542.28384 c -26.11086,-42.88953 -41.80521,-83.16959 -54.18766,-152.89697 26.06687,-16.2006 24.83566,-45.46406 -15.39052,-66.46433 33.93989,-31.34323 19.98697,-65.52994 -14.72432,-74.60734 -17.82314,-4.66096 -47.16558,-7.88098 -57.36123,-10.85855 C 682.77372,222.7259 665.18888,198.90619 620.74191,184.15446 466.35494,132.9142 270.81297,170.61723 187.49013,275.95719 c -75.42408,95.35405 -76.23332,151.45419 -93.03896,234.27568 -3.607596,17.77894 -13.074363,43.57065 -18.984879,48.69769 -15.458276,13.40918 -34.278507,59.92672 20.246637,58.01857 -4.911621,26.63049 -16.202612,42.03875 -21.825893,56.4483 -17.846537,45.7314 11.791821,64.75835 49.726775,55.12543 -3.31228,29.06008 -3.80965,68.00974 41.7245,59.75469 -0.009,19.23781 7.68254,28.70603 16.36237,33.35477 C 190.37816,933.25823 216.37467,1000 216.37467,1000 H 1000 V 437.85011 c 0,0 -223.38954,219.6287 -348,370.63648 -12.36429,-39.94931 -41.10548,-27.13457 -47.02031,2.51737 -0.45414,2.27666 -3.24992,9.15209 -9.86438,8.83489 -5.83023,-0.2796 -6.27407,-11.31136 -2.97641,-22.14792 6.46074,-21.2309 39.80291,-48.85947 63.84617,-17.09603 45.93757,-54.017 128.43819,-141.91623 218.89276,-238.31106 z"
style="display:inline;fill:#8a9bab;fill-opacity:1;stroke:none;stroke-width:1.51092136;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5899"
d="m 164.41033,501.28694 c 0,134.96586 89.28037,248.33231 309.21473,247.20575 217.71706,-1.11521 307.80052,-112.23989 307.80052,-247.20575 0,-96.34096 -54.83816,-188.77266 -158.98644,-196.44194 -62.18116,-4.57889 -100.76307,28.42083 -149.52119,28.91141 -47.56808,0.4786 -79.52352,-31.65029 -147.26721,-28.87954 -119.78922,4.89943 -161.24041,114.05966 -161.24041,196.41007 z"
style="display:inline;fill:#ecf1f5;fill-opacity:1;stroke:none;stroke-width:1.62539303;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="cscsssc"
inkscape:connector-curvature="0"
id="path6029"
d="m 167.12764,541.06247 c -1.8472,-12.95996 -2.88472,-26.24998 -2.88472,-39.77553 0,-22.09219 2.88362,-43.97881 8.58421,-64.68492 40.44162,-29.67651 108.08468,-52.00242 167.32555,-41.22117 35.02394,6.37401 90.30164,43.89694 76.99739,113.45676 -7.83112,40.94415 -61.06671,49.31438 -94.96833,50.10049 -38.95967,0.9034 -90.29699,-2.69765 -155.0541,-17.87563 z"
style="display:inline;fill:#abb7c5;fill-opacity:1;stroke:none;stroke-width:1.62539303;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path6005"
d="m 384.49599,483.06729 c 1.26483,9.82737 -0.28385,30.34383 -16.63487,29.92206 -19.99028,-0.51567 -10.18467,-30.81298 -36.36126,-30.59574 -25.94038,0.21528 -15.99836,30.92106 -34.53097,31.69735 -19.08341,0.79938 -20.66128,-21.82275 -18.68974,-32.14647 5.58223,-29.23062 28.51996,-41.06296 53.44527,-41.54356 27.17314,-0.52397 49.36773,16.21978 52.77157,42.66636 z"
style="fill:#797c81;fill-opacity:1;stroke:none;stroke-width:0.424281;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#abb7c5;fill-opacity:1;stroke:none;stroke-width:1.62539303;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 778.67367,541.05856 c 1.8472,-12.95996 2.75191,-26.24607 2.75191,-39.77162 0,-22.09219 -2.88362,-43.97881 -8.58421,-64.68492 -40.44162,-29.67651 -108.08468,-52.00242 -167.32555,-41.22117 -35.02394,6.37401 -90.30164,43.89694 -76.99739,113.45676 7.83112,40.94415 61.06671,49.31438 94.96833,50.10049 38.95967,0.9034 90.4298,-2.70156 155.18691,-17.87954 z"
id="path6027"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscsssc" />
<path
style="fill:#797c81;fill-opacity:1;stroke:none;stroke-width:0.424281;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 559.16559,483.06729 c -1.26484,9.82737 0.28385,30.34383 16.63486,29.92206 19.99028,-0.51567 10.18468,-30.81298 36.36127,-30.59574 25.94037,0.21528 15.99835,30.92106 34.53096,31.69735 19.08341,0.79938 20.66127,-21.82275 18.68974,-32.14647 -5.58223,-29.23062 -28.51996,-41.06296 -53.44525,-41.54356 -27.17316,-0.52397 -49.36774,16.21978 -52.77158,42.66636 z"
id="path6010"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssssss" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path6014"
d="m 439.99627,540.8714 c 0,10.79277 14.47309,19.54203 32.32654,19.54203 17.85345,0 32.32653,-8.74926 32.32653,-19.54203 -10e-6,-10.79276 -14.10781,-14.42822 -31.96127,-14.42822 -17.85343,0 -32.69179,3.63546 -32.6918,14.42822 z"
style="fill:#7a7d82;fill-opacity:1;stroke:none;stroke-width:0.34507009;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
transform="matrix(1.3299533,0,0,1.0420847,-252.3233,16.933424)"
sodipodi:nodetypes="sssscsssscs"
inkscape:original-d="m 561.38128,582.67417 c 4.16766,-0.91767 12.20351,2.22573 11.70451,-3.19194 -0.49904,-5.41767 3.22694,-10.0569 -0.43934,-12.36091 -3.71867,-2.33693 -4.66037,-0.70889 -10.62868,-3.03033 -6.7415,-2.62217 -12.89085,-5.33173 -18.0383,-9.27773 -5.14745,3.946 -11.25574,6.65556 -17.99724,9.27773 -5.96831,2.32144 -6.91001,0.6934 -10.62868,3.03033 -3.66628,2.30401 0.0597,6.94324 -0.43934,12.36091 -0.499,5.41767 7.53685,2.27427 11.70451,3.19194 4.16767,0.91767 12.6047,-2.8973 17.27236,-5.97963 4.66766,3.08233 13.32253,6.8973 17.4902,5.97963 z"
inkscape:path-effect="#path-effect6019"
inkscape:connector-curvature="0"
id="path6017"
d="m 561.38128,582.67417 c 2.08275,0.16617 4.19863,0.15523 6.23646,-0.30597 2.03783,-0.46119 4.00308,-1.39624 5.46805,-2.88597 1.62507,-1.65254 2.56385,-3.97206 2.49743,-6.2888 -0.0664,-2.31675 -1.15042,-4.59539 -2.93677,-6.07211 -1.43969,-1.19015 -3.25655,-1.83786 -5.08522,-2.21886 -1.82866,-0.38101 -3.69889,-0.51693 -5.54346,-0.81147 -6.78982,-1.08418 -13.20738,-4.38495 -18.0383,-9.27773 -4.81493,4.89182 -11.2195,8.19343 -17.99724,9.27773 -1.84447,0.29508 -3.7147,0.43095 -5.54339,0.8118 -1.82869,0.38084 -3.64567,1.02831 -5.08529,2.21853 -1.78611,1.47668 -2.86946,3.75542 -2.93562,6.07196 -0.0662,2.31655 0.87229,4.63564 2.49628,6.28895 1.46438,1.49081 3.42913,2.42785 5.46715,2.88979 2.03802,0.46195 4.15458,0.4723 6.23736,0.30215 6.13711,-0.50136 12.1389,-2.57916 17.27236,-5.97963 5.21036,3.40799 11.28399,5.48447 17.4902,5.97963 z"
style="fill:#7a7d82;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000px"
height="1000px"
viewBox="0 0 1000 1000"
version="1.1"
id="SVGRoot"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="GoToSocial_icon6.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs5117">
<inkscape:path-effect
is_visible="true"
id="path-effect6113"
effect="spiro" />
<inkscape:path-effect
effect="spiro"
id="path-effect6099"
is_visible="true" />
<inkscape:path-effect
is_visible="true"
id="path-effect6023"
effect="spiro" />
<inkscape:path-effect
effect="spiro"
id="path-effect6019"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5760"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5756"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5752"
is_visible="true" />
<inkscape:path-effect
effect="spiro"
id="path-effect5748"
is_visible="true" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="-343.35188"
inkscape:cy="451.98165"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata5120">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.95512283;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect6025"
width="1000"
height="1000"
x="0"
y="-1000"
transform="scale(1,-1)" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="sloth"
style="display:inline">
<g
id="g3345">
<path
sodipodi:nodetypes="sssccsccssscccs"
inkscape:connector-curvature="0"
id="path6057"
d="m 858.9826,509 c 0,21.30135 0.61819,51.5972 6.701,73.24153 7.01258,24.95272 16.3214,50.02921 6.75447,64.21164 -8.7497,12.97093 -21.25315,18.56008 -44.58836,14.66624 3.93369,101.42215 -77.47601,125.19748 -96.07496,88.79767 C 674.80251,788.46301 600.2861,811.26681 511.5,810.9826 c -93.0785,-0.29795 -169.41132,-24.34907 -226.58487,-64.56 -18.96636,55.61282 -89.53019,15.93142 -94.48786,-66.57373 -18.83753,13.90334 -40.21111,9.34959 -48.24161,-1.87396 -9.87886,-13.80685 -0.11866,-44.10059 6.46153,-66.65226 C 158.90467,576.16815 164.0174,526.56854 164.0174,509 c 0,-144.1474 114.89352,-263.91342 269.97979,-293.76458 -11.39947,-30.68702 33.19193,-42.65736 68.23504,-27.65736 51.70913,-51.5 126.88111,-8.44549 91.59407,29.31477 C 745.54432,249.30521 858.9826,367.38679 858.9826,509 Z"
style="display:inline;fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1.56877995;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="sssssss"
inkscape:connector-curvature="0"
id="path5899"
d="m 221.40138,525.50517 c 0,127.7494 118.50857,234.80637 292.6814,233.98797 167.18781,-0.78557 291.3428,-106.23857 291.3428,-233.98797 0,-91.18972 -51.90603,-178.67921 -150.48563,-185.93843 -58.85641,-4.33406 -95.37539,26.90121 -141.52648,27.36556 -45.02467,0.45301 -75.27149,-29.95799 -139.39301,-27.33539 -113.38424,4.63746 -152.61908,107.96103 -152.61908,185.90826 z"
style="display:inline;fill:#f6e7e0;fill-opacity:0.99460916;stroke:none;stroke-width:1.53848529;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.53848529;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 789.95211,601.79814 c 10.07014,-23.96206 15.47347,-49.64233 15.47347,-76.29297 0,-24.59922 -3.77719,-48.92918 -11.22886,-71.48757 -113.05163,-72.24038 -211.71014,-23.66044 -216.29587,55.37861 -0.92941,43.30708 14.85596,73.14738 66.59091,84.14256 40.60772,8.63033 109.03729,1.65487 145.46035,8.25937 z"
id="path6060"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsc" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="path6014"
d="m 489.46176,567.69891 c 0,9.26255 12.42107,16.77133 27.74324,16.77133 15.32216,0 27.74323,-7.50878 27.74323,-16.77133 -10e-6,-9.26255 -12.10759,-12.38257 -27.42976,-12.38257 -15.32215,0 -28.0567,3.12002 -28.05671,12.38257 z"
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:0.29614559;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="aasaasa"
inkscape:connector-curvature="0"
id="path6062"
d="m 708.01455,512.02942 c 3.98124,8.47407 4.66411,23.87376 -3.82321,27.82668 -15.14244,7.05251 -13.43948,-27.87381 -42.39106,-26.726 -27.91154,1.10657 -23.83539,31.91957 -38.41145,26.49209 -8.14703,-3.03359 -9.71336,-17.14283 -6.5565,-25.2429 6.70872,-17.21364 23.33215,-30.04549 46.55894,-30.06845 21.25806,-0.021 37.1774,11.87004 44.62328,27.71858 z"
style="display:inline;fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1.49886632;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
sodipodi:nodetypes="csccsc"
inkscape:connector-curvature="0"
id="path6091"
d="m 236.44687,601.79814 c -10.07014,-23.96206 -15.47347,-49.64233 -15.47347,-76.29297 0,-24.59922 3.77719,-48.92918 11.22886,-71.48757 113.05163,-72.24038 211.71014,-23.66044 216.29587,55.37861 0.92941,43.30708 -14.85596,73.14738 -66.59091,84.14256 -40.60772,8.63033 -109.03729,1.65487 -145.46035,8.25937 z"
style="display:inline;fill:#e3ccbe;fill-opacity:1;stroke:none;stroke-width:1.53848529;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
<path
style="display:inline;fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1.49886632;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 318.38443,512.02942 c -3.98124,8.47407 -4.66411,23.87376 3.82321,27.82668 15.14244,7.05251 13.43948,-27.87381 42.39106,-26.726 27.91154,1.10657 23.83539,31.91957 38.41145,26.49209 8.14703,-3.03359 9.71336,-17.14283 6.5565,-25.2429 -6.70872,-17.21364 -23.33215,-30.04549 -46.55894,-30.06845 -21.25806,-0.021 -37.1774,11.87004 -44.62328,27.71858 z"
id="path6093"
inkscape:connector-curvature="0"
sodipodi:nodetypes="aasaasa" />
<path
transform="matrix(0.96767168,0,0,1.0314138,16.012714,-19.869878)"
sodipodi:nodetypes="sscccsscs"
inkscape:original-d="m 581.94888,642.38075 c -0.11685,-5.42216 7.07007,-17.61775 2.82843,-16.79379 -4.24164,0.82396 -9.19339,2.47588 -16.26346,5.3033 -7.74,3.20278 -35.48656,-1.28199 -51.42842,-9.77919 -17.49196,7.64885 -42.65501,12.98197 -50.39501,9.77919 -7.07007,-2.82742 -12.02182,-4.47934 -16.26346,-5.3033 -4.24164,-0.82396 2.94528,11.37163 2.82843,16.79379 -0.11685,5.42215 41.7595,1.3486 63.7987,2.05671 22.03919,-0.70811 65.01164,3.36544 64.89479,-2.05671 z"
inkscape:path-effect="#path-effect6099"
inkscape:connector-curvature="0"
id="path6097"
d="m 581.94888,642.38075 c 2.38253,-2.24081 4.60569,-4.83839 5.47508,-7.99146 0.4347,-1.57654 0.5088,-3.26889 0.081,-4.84729 -0.42785,-1.57841 -1.37681,-3.0332 -2.7276,-3.95504 -1.26545,-0.8636 -2.83258,-1.23063 -4.36364,-1.17554 -1.53106,0.0551 -3.02872,0.51417 -4.40643,1.1843 -2.75543,1.34027 -5.02124,3.48424 -7.49339,5.29454 -7.89483,5.7812 -18.22631,8.11406 -27.83929,6.28614 -9.61298,-1.82792 -18.36718,-7.78995 -23.58913,-16.06533 -5.088,8.13656 -13.65079,14.01754 -23.07148,15.84563 -9.42069,1.82809 -19.56159,-0.42342 -27.32353,-6.06644 -2.47705,-1.80085 -4.7434,-3.94147 -7.49681,-5.28222 -1.3767,-0.67037 -2.8729,-1.13084 -4.40308,-1.18819 -1.53017,-0.0574 -3.09706,0.30649 -4.36357,1.16711 -1.35354,0.91976 -2.30564,2.37417 -2.73564,3.95314 -0.43,1.57896 -0.35707,3.27272 0.0776,4.8504 0.86942,3.15535 3.097,5.75356 5.48643,7.99025 8.48113,7.93899 19.90111,12.66409 31.51218,13.0384 11.61107,0.37431 23.31167,-3.60544 32.28652,-10.98169 9.10954,7.53977 21.01716,11.61649 32.83628,11.24191 11.81911,-0.37458 23.44469,-5.19714 32.05851,-13.29862 z"
style="fill:#c89172;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

90
web/assets/profile.css Normal file
View File

@ -0,0 +1,90 @@
main {
background: transparent;
}
.headerimage img {
width: 100%;
height: 15em;
object-fit: cover;
border-radius: 10px;
}
.profile {
position: relative;
background: rgb(75, 84, 93);
padding: 2rem;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 0.5rem;
margin-bottom: 0.2rem;
}
.profile .basic {
display: flex;
flex-direction: column;
flex: 1 1 25em;
gap: 0.5rem;
}
.profile .basic a {
position: relative;
z-index: 1;
color: inherit;
text-decoration: none;
}
.profile .basic .avatar img {
height: 25em;
width: 25em;
object-fit: cover;
border-radius: 10px;
}
.profile .basic .displayname {
font-weight: bold;
font-size: 1.6rem;
align-self: start;
}
.profile .detailed {
display: flex;
flex-direction: column;
flex: 1 1 25em;
}
.profile .detailed h2 {
margin-top: 0;
}
.profile .detailed .bio {
margin: 0;
}
.profile .detailed .bio a {
color: #de8957;
text-decoration: underline;
}
.accountstats {
position: relative;
background: rgb(75, 84, 93);
padding: 0.5rem;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
gap: 0.5rem;
margin-bottom: 0.2rem;
}
.accountstats .entry {
background: rgb(89, 99, 110);
padding: 0.5rem;
flex-grow: 1;
text-align: center;
}
footer + div {
/* something weird from the devstack.. */
display: none;
}

View File

@ -0,0 +1,94 @@
main {
background: transparent;
}
.headerimage {
img {
width: 100%;
height: 15em;
object-fit: cover;
border-radius: 10px;
}
}
.profile {
position: relative;
background: color($bg lightness(-3%));
padding: 2rem;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 0.5rem;
margin-bottom: 0.2rem;
.basic {
display: flex;
flex-direction: column;
flex: 1 1 25em;
gap: 0.5rem;
a {
position: relative;
z-index: 1;
color: inherit;
text-decoration: none;
}
.avatar {
img {
height: 25em;
width: 25em;
object-fit: cover;
border-radius: 10px;
}
}
.displayname {
font-weight: bold;
font-size: 1.6rem;
align-self: start;
}
}
.detailed {
display: flex;
flex-direction: column;
flex: 1 1 25em;
h2 {
margin-top: 0;
}
.bio {
margin: 0;
a {
color: $acc1;
text-decoration: underline;
}
}
}
}
.accountstats {
position: relative;
background: color($bg lightness(-3%));
padding: 0.5rem;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
gap: 0.5rem;
margin-bottom: 0.2rem;
.entry {
background: color($bg lightness(+3%));
padding: 0.5rem;
flex-grow: 1;
text-align: center;
}
}
footer + div {
/* something weird from the devstack.. */
display: none;
}

47
web/template/profile.tmpl Normal file
View File

@ -0,0 +1,47 @@
{{ template "header.tmpl" .}}
<main>
{{ if .account.Header }}<a href="{{.account.Header}}" class="headerimage"><img src="{{.account.Header}}"></a>{{ end }}
<div class="profile">
<div class="basic">
<a href="{{.account.URL}}" class="displayname">{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}</a>
<a href="{{.account.URL}}" class="username">@{{.account.Username}}</a>
<a href="{{.account.Avatar}}" class="avatar"><img src="{{.account.Avatar}}"></a>
</div>
<div class="detailed">
<h2>About @{{.account.Username}}</h2>
<div class="bio">
{{ if .account.Note }}{{ .account.Note | noescape }}{{else}}This GoToSocial user hasn't written a bio yet!{{end}}
</div>
</div>
</div>
<div class="accountstats">
<div class="entry">Joined {{.account.CreatedAt | timestampShort}}</div>
<div class="entry">Followed by {{.account.FollowersCount}}</div>
<div class="entry">Following {{.account.FollowingCount}}</div>
<div class="entry">Posted {{.account.StatusesCount}}</div>
</div>
<h2>Recent public posts by @{{.account.Username}}</h2>
<div class="thread">
{{range .statuses}}
<div class="toot expanded">
{{ template "status.tmpl" .}}
</div>
{{end}}
</div>
</main>
<script>
Array.from(document.getElementsByClassName("spoiler-label")).forEach((label) => {
let checkbox = document.getElementById(label.htmlFor);
function update() {
if(checkbox.checked) {
label.innerHTML = "Show more";
} else {
label.innerHTML = "Show less";
}
}
update();
label.addEventListener("click", () => {setTimeout(update, 1)});
});
</script>
{{ template "footer.tmpl" .}}