replace.go (4580B)
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 text 19 20 import ( 21 "errors" 22 "strings" 23 24 "github.com/superseriousbusiness/gotosocial/internal/db" 25 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 26 "github.com/superseriousbusiness/gotosocial/internal/log" 27 "github.com/superseriousbusiness/gotosocial/internal/util" 28 "golang.org/x/text/unicode/norm" 29 ) 30 31 const ( 32 maximumHashtagLength = 30 33 ) 34 35 // given a mention or a hashtag string, the methods in this file will attempt to parse it, 36 // add it to the database, and render it as HTML. If any of these steps fails, the method 37 // will just return the original string and log an error. 38 39 // replaceMention takes a string in the form @username@domain.com or @localusername 40 func (r *customRenderer) replaceMention(text string) string { 41 mention, err := r.parseMention(r.ctx, text, r.accountID, r.statusID) 42 if err != nil { 43 log.Errorf(r.ctx, "error parsing mention %s from status: %s", text, err) 44 return text 45 } 46 47 if r.statusID != "" { 48 if err := r.f.db.PutMention(r.ctx, mention); err != nil { 49 log.Errorf(r.ctx, "error putting mention in db: %s", err) 50 return text 51 } 52 } 53 54 // only append if it's not been listed yet 55 listed := false 56 for _, m := range r.result.Mentions { 57 if mention.ID == m.ID { 58 listed = true 59 break 60 } 61 } 62 if !listed { 63 r.result.Mentions = append(r.result.Mentions, mention) 64 } 65 66 if mention.TargetAccount == nil { 67 // Fetch mention target account if not yet populated. 68 mention.TargetAccount, err = r.f.db.GetAccountByID( 69 gtscontext.SetBarebones(r.ctx), 70 mention.TargetAccountID, 71 ) 72 if err != nil { 73 log.Errorf(r.ctx, "error populating mention target account: %v", err) 74 return text 75 } 76 } 77 78 // The mention's target is our target 79 targetAccount := mention.TargetAccount 80 81 var b strings.Builder 82 83 // replace the mention with the formatted mention content 84 // <span class="h-card"><a href="targetAccount.URL" class="u-url mention">@<span>targetAccount.Username</span></a></span> 85 b.WriteString(`<span class="h-card"><a href="`) 86 b.WriteString(targetAccount.URL) 87 b.WriteString(`" class="u-url mention">@<span>`) 88 b.WriteString(targetAccount.Username) 89 b.WriteString(`</span></a></span>`) 90 return b.String() 91 } 92 93 // replaceMention takes a string in the form #HashedTag, and will normalize it before 94 // adding it to the db and turning it into HTML. 95 func (r *customRenderer) replaceHashtag(text string) string { 96 // this normalization is specifically to avoid cases where visually-identical 97 // hashtags are stored with different unicode representations (e.g. with combining 98 // diacritics). It allows a tasteful number of combining diacritics to be used, 99 // as long as they can be combined with parent characters to form regular letter 100 // symbols. 101 normalized := norm.NFC.String(text[1:]) 102 103 for i, r := range normalized { 104 if i >= maximumHashtagLength || !util.IsPermittedInHashtag(r) { 105 return text 106 } 107 } 108 109 tag, err := r.f.db.TagStringToTag(r.ctx, normalized, r.accountID) 110 if err != nil { 111 log.Errorf(r.ctx, "error generating hashtags from status: %s", err) 112 return text 113 } 114 115 // only append if it's not been listed yet 116 listed := false 117 for _, t := range r.result.Tags { 118 if tag.ID == t.ID { 119 listed = true 120 break 121 } 122 } 123 if !listed { 124 err = r.f.db.Put(r.ctx, tag) 125 if err != nil { 126 if !errors.Is(err, db.ErrAlreadyExists) { 127 log.Errorf(r.ctx, "error putting tags in db: %s", err) 128 return text 129 } 130 } 131 r.result.Tags = append(r.result.Tags, tag) 132 } 133 134 var b strings.Builder 135 // replace the #tag with the formatted tag content 136 // `<a href="tag.URL" class="mention hashtag" rel="tag">#<span>tagAsEntered</span></a> 137 b.WriteString(`<a href="`) 138 b.WriteString(tag.URL) 139 b.WriteString(`" class="mention hashtag" rel="tag">#<span>`) 140 b.WriteString(normalized) 141 b.WriteString(`</span></a>`) 142 143 return b.String() 144 }