opengraph.go (5072B)
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 "html" 22 "strconv" 23 "strings" 24 25 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 26 "github.com/superseriousbusiness/gotosocial/internal/text" 27 ) 28 29 const maxOGDescriptionLength = 300 30 31 // ogMeta represents supported OpenGraph Meta tags 32 // 33 // see eg https://ogp.me/ 34 type ogMeta struct { 35 // vanilla og tags 36 Title string // og:title 37 Type string // og:type 38 Locale string // og:locale 39 URL string // og:url 40 SiteName string // og:site_name 41 Description string // og:description 42 43 // image tags 44 Image string // og:image 45 ImageWidth string // og:image:width 46 ImageHeight string // og:image:height 47 ImageAlt string // og:image:alt 48 49 // article tags 50 ArticlePublisher string // article:publisher 51 ArticleAuthor string // article:author 52 ArticleModifiedTime string // article:modified_time 53 ArticlePublishedTime string // article:published_time 54 55 // profile tags 56 ProfileUsername string // profile:username 57 } 58 59 // ogBase returns an *ogMeta suitable for serving at 60 // the base root of an instance. It also serves as a 61 // foundation for building account / status ogMeta on 62 // top of. 63 func ogBase(instance *apimodel.InstanceV1) *ogMeta { 64 var locale string 65 if len(instance.Languages) > 0 { 66 locale = instance.Languages[0] 67 } 68 69 og := &ogMeta{ 70 Title: text.SanitizePlaintext(instance.Title) + " - GoToSocial", 71 Type: "website", 72 Locale: locale, 73 URL: instance.URI, 74 SiteName: instance.AccountDomain, 75 Description: parseDescription(instance.ShortDescription), 76 77 Image: instance.Thumbnail, 78 ImageAlt: instance.ThumbnailDescription, 79 } 80 81 return og 82 } 83 84 // withAccount uses the given account to build an ogMeta 85 // struct specific to that account. It's suitable for serving 86 // at account profile pages. 87 func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta { 88 og.Title = parseTitle(account, og.SiteName) 89 og.Type = "profile" 90 og.URL = account.URL 91 if account.Note != "" { 92 og.Description = parseDescription(account.Note) 93 } else { 94 og.Description = `content="This GoToSocial user hasn't written a bio yet!"` 95 } 96 97 og.Image = account.Avatar 98 og.ImageAlt = "Avatar for " + account.Username 99 100 og.ProfileUsername = account.Username 101 102 return og 103 } 104 105 // withStatus uses the given status to build an ogMeta 106 // struct specific to that status. It's suitable for serving 107 // at status pages. 108 func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta { 109 og.Title = "Post by " + parseTitle(status.Account, og.SiteName) 110 og.Type = "article" 111 if status.Language != nil { 112 og.Locale = *status.Language 113 } 114 og.URL = status.URL 115 switch { 116 case status.SpoilerText != "": 117 og.Description = parseDescription("CW: " + status.SpoilerText) 118 case status.Text != "": 119 og.Description = parseDescription(status.Text) 120 default: 121 og.Description = og.Title 122 } 123 124 if !status.Sensitive && len(status.MediaAttachments) > 0 { 125 a := status.MediaAttachments[0] 126 og.Image = a.PreviewURL 127 og.ImageWidth = strconv.Itoa(a.Meta.Small.Width) 128 og.ImageHeight = strconv.Itoa(a.Meta.Small.Height) 129 if a.Description != nil { 130 og.ImageAlt = *a.Description 131 } 132 } else { 133 og.Image = status.Account.Avatar 134 og.ImageAlt = "Avatar for " + status.Account.Username 135 } 136 137 og.ArticlePublisher = status.Account.URL 138 og.ArticleAuthor = status.Account.URL 139 og.ArticlePublishedTime = status.CreatedAt 140 og.ArticleModifiedTime = status.CreatedAt 141 142 return og 143 } 144 145 // parseTitle parses a page title from account and accountDomain 146 func parseTitle(account *apimodel.Account, accountDomain string) string { 147 user := "@" + account.Acct + "@" + accountDomain 148 149 if len(account.DisplayName) == 0 { 150 return user 151 } 152 153 return account.DisplayName + " (" + user + ")" 154 } 155 156 // parseDescription returns a string description which is 157 // safe to use as a template.HTMLAttr inside templates. 158 func parseDescription(in string) string { 159 i := text.SanitizePlaintext(in) 160 i = strings.ReplaceAll(i, "\n", " ") 161 i = strings.Join(strings.Fields(i), " ") 162 i = html.EscapeString(i) 163 i = strings.ReplaceAll(i, `\`, "\") 164 i = trim(i, maxOGDescriptionLength) 165 return `content="` + i + `"` 166 } 167 168 // trim strings trim s to specified length 169 func trim(s string, length int) string { 170 if len(s) < length { 171 return s 172 } 173 174 return s[:length] 175 }