profile.go (5268B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package web 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "net/http" 26 "strings" 27 28 "github.com/gin-gonic/gin" 29 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 30 apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" 31 "github.com/superseriousbusiness/gotosocial/internal/config" 32 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 33 "github.com/superseriousbusiness/gotosocial/internal/oauth" 34 ) 35 36 const ( 37 // MaxStatusIDKey is for specifying the maximum ID of the status to retrieve. 38 MaxStatusIDKey = "max_id" 39 ) 40 41 func (m *Module) profileGETHandler(c *gin.Context) { 42 ctx := c.Request.Context() 43 44 authed, err := oauth.Authed(c, false, false, false, false) 45 if err != nil { 46 apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) 47 return 48 } 49 50 username := strings.ToLower(c.Param(usernameKey)) 51 if username == "" { 52 err := errors.New("no account username specified") 53 apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 54 return 55 } 56 57 instance, err := m.processor.InstanceGetV1(ctx) 58 if err != nil { 59 apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) 60 return 61 } 62 63 instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { 64 return instance, nil 65 } 66 67 account, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, username) 68 if errWithCode != nil { 69 apiutil.WebErrorHandler(c, errWithCode, instanceGet) 70 return 71 } 72 73 // if we're getting an AP request on this endpoint we 74 // should render the account's AP representation instead 75 accept := apiutil.NegotiateFormat(c, string(apiutil.TextHTML), string(apiutil.AppActivityJSON), string(apiutil.AppActivityLDJSON)) 76 if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) { 77 m.returnAPProfile(c, username, accept) 78 return 79 } 80 81 var rssFeed string 82 if account.EnableRSS { 83 rssFeed = "/@" + account.Username + "/feed.rss" 84 } 85 86 // only allow search engines / robots to view this page if account is discoverable 87 var robotsMeta string 88 if account.Discoverable { 89 robotsMeta = robotsMetaAllowSome 90 } 91 92 // We need to change our response slightly if the 93 // profile visitor is paging through statuses. 94 var ( 95 paging bool 96 pinnedResp = &apimodel.PageableResponse{} 97 maxStatusID string 98 ) 99 100 if maxStatusIDString := c.Query(MaxStatusIDKey); maxStatusIDString != "" { 101 maxStatusID = maxStatusIDString 102 paging = true 103 } 104 105 statusResp, errWithCode := m.processor.Account().WebStatusesGet(ctx, account.ID, maxStatusID) 106 if errWithCode != nil { 107 apiutil.WebErrorHandler(c, errWithCode, instanceGet) 108 return 109 } 110 111 // If we're not paging, then the profile visitor 112 // is currently just opening the bare profile, so 113 // load pinned statuses so we can show them at the 114 // top of the profile. 115 if !paging { 116 pinnedResp, errWithCode = m.processor.Account().StatusesGet(ctx, authed.Account, account.ID, 0, false, false, "", "", true, false, false) 117 if errWithCode != nil { 118 apiutil.WebErrorHandler(c, errWithCode, instanceGet) 119 return 120 } 121 } 122 123 stylesheets := []string{ 124 assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css", 125 distPathPrefix + "/status.css", 126 distPathPrefix + "/profile.css", 127 } 128 if config.GetAccountsAllowCustomCSS() { 129 stylesheets = append(stylesheets, "/@"+account.Username+"/custom.css") 130 } 131 132 c.HTML(http.StatusOK, "profile.tmpl", gin.H{ 133 "instance": instance, 134 "account": account, 135 "ogMeta": ogBase(instance).withAccount(account), 136 "rssFeed": rssFeed, 137 "robotsMeta": robotsMeta, 138 "statuses": statusResp.Items, 139 "statuses_next": statusResp.NextLink, 140 "pinned_statuses": pinnedResp.Items, 141 "show_back_to_top": paging, 142 "stylesheets": stylesheets, 143 "javascript": []string{distPathPrefix + "/frontend.js"}, 144 }) 145 } 146 147 func (m *Module) returnAPProfile(c *gin.Context, username string, accept string) { 148 user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), username, c.Request.URL) 149 if errWithCode != nil { 150 apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 151 return 152 } 153 154 b, mErr := json.Marshal(user) 155 if mErr != nil { 156 err := fmt.Errorf("could not marshal json: %s", mErr) 157 apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) 158 return 159 } 160 161 c.Data(http.StatusOK, accept, b) 162 }