// GoToSocial // Copyright (C) GoToSocial Authors admin@gotosocial.org // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package text import ( "errors" "strings" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/util" "golang.org/x/text/unicode/norm" ) const ( maximumHashtagLength = 30 ) // given a mention or a hashtag string, the methods in this file will attempt to parse it, // add it to the database, and render it as HTML. If any of these steps fails, the method // will just return the original string and log an error. // replaceMention takes a string in the form @username@domain.com or @localusername func (r *customRenderer) replaceMention(text string) string { mention, err := r.parseMention(r.ctx, text, r.accountID, r.statusID) if err != nil { log.Errorf(r.ctx, "error parsing mention %s from status: %s", text, err) return text } if r.statusID != "" { if err := r.f.db.PutMention(r.ctx, mention); err != nil { log.Errorf(r.ctx, "error putting mention in db: %s", err) return text } } // only append if it's not been listed yet listed := false for _, m := range r.result.Mentions { if mention.ID == m.ID { listed = true break } } if !listed { r.result.Mentions = append(r.result.Mentions, mention) } if mention.TargetAccount == nil { // Fetch mention target account if not yet populated. mention.TargetAccount, err = r.f.db.GetAccountByID( gtscontext.SetBarebones(r.ctx), mention.TargetAccountID, ) if err != nil { log.Errorf(r.ctx, "error populating mention target account: %v", err) return text } } // The mention's target is our target targetAccount := mention.TargetAccount var b strings.Builder // replace the mention with the formatted mention content // @targetAccount.Username b.WriteString(`@`) b.WriteString(targetAccount.Username) b.WriteString(``) return b.String() } // replaceMention takes a string in the form #HashedTag, and will normalize it before // adding it to the db and turning it into HTML. func (r *customRenderer) replaceHashtag(text string) string { // this normalization is specifically to avoid cases where visually-identical // hashtags are stored with different unicode representations (e.g. with combining // diacritics). It allows a tasteful number of combining diacritics to be used, // as long as they can be combined with parent characters to form regular letter // symbols. normalized := norm.NFC.String(text[1:]) for i, r := range normalized { if i >= maximumHashtagLength || !util.IsPermittedInHashtag(r) { return text } } tag, err := r.f.db.TagStringToTag(r.ctx, normalized, r.accountID) if err != nil { log.Errorf(r.ctx, "error generating hashtags from status: %s", err) return text } // only append if it's not been listed yet listed := false for _, t := range r.result.Tags { if tag.ID == t.ID { listed = true break } } if !listed { err = r.f.db.Put(r.ctx, tag) if err != nil { if !errors.Is(err, db.ErrAlreadyExists) { log.Errorf(r.ctx, "error putting tags in db: %s", err) return text } } r.result.Tags = append(r.result.Tags, tag) } var b strings.Builder // replace the #tag with the formatted tag content // ` b.WriteString(``) return b.String() }