Files
RenewCMS/internal/domain/author/authorDomain.go
T

217 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package author
import (
"errors"
"net/url"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
var (
ErrAuthorNotFound = errors.New("author not found")
ErrSlugTaken = errors.New("an author with that slug already exists")
ErrAuthorHasArticles = errors.New("the author is still credited on articles")
ErrUserAlreadyLinked = errors.New("the user already has an author")
ErrAuthorAlreadyLinked = errors.New("the author is already linked to a user")
ErrAuthorRights = errors.New("only an owner or admin can change this author")
ErrBioTooLong = errors.New("the bio is too long")
ErrInvalidURL = errors.New("invalid url")
ErrTooManyLinks = errors.New("too many social links")
ErrInvalidSlug = errors.New("invalid slug")
ErrInvalidReassign = errors.New("invalid reassign destination")
ErrInvalidSocialType = errors.New("invalid social link type")
)
const (
MinNameRunes = 1
MaxNameRunes = 100
MaxSlugRunes = 100
MaxBioRunes = 280
MaxURLLength = 2048
MaxLinks = 5
FallbackSlug = "author"
SocialX = "x"
SocialLinkedIn = "linkedin"
SocialGitHub = "github"
SocialMastodon = "mastodon"
SocialCustom = "custom"
)
var slugPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
var nonSlugRun = regexp.MustCompile(`[^a-z0-9]+`)
var diacriticFolder = strings.NewReplacer(
"à", "a", "á", "a", "â", "a", "ã", "a", "ä", "a", "å", "a", "ā", "a", "ă", "a", "ą", "a",
"æ", "ae",
"ç", "c", "ć", "c", "č", "c", "ĉ", "c",
"ď", "d", "đ", "d",
"è", "e", "é", "e", "ê", "e", "ë", "e", "ē", "e", "ė", "e", "ę", "e", "ě", "e",
"ğ", "g",
"ì", "i", "í", "i", "î", "i", "ï", "i", "ī", "i", "į", "i", "ı", "i",
"ł", "l", "ľ", "l",
"ñ", "n", "ń", "n", "ň", "n",
"ò", "o", "ó", "o", "ô", "o", "õ", "o", "ö", "o", "ø", "o", "ō", "o", "ő", "o",
"œ", "oe",
"ř", "r",
"ś", "s", "š", "s", "ş", "s", "ș", "s",
"ß", "ss",
"ť", "t", "ț", "t", "ţ", "t",
"ù", "u", "ú", "u", "û", "u", "ü", "u", "ū", "u", "ů", "u", "ű", "u", "ų", "u",
"ý", "y", "ÿ", "y",
"ź", "z", "ż", "z", "ž", "z",
)
type SocialLink struct {
Type string `json:"type"`
URL string `json:"url"`
}
type Author struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Bio string `json:"bio"`
AvatarID *uint32 `json:"avatar_id"`
Website *string `json:"website"`
SocialLinks []SocialLink `json:"social_links"`
UserID *uint32 `json:"user_id"`
IsLinked bool `json:"is_linked"`
ArticleCount int `json:"article_count"`
UserEmail *string `json:"user_email,omitempty"`
}
type Byline struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Bio string `json:"bio"`
AvatarID *uint32 `json:"avatar_id"`
Website *string `json:"website"`
SocialLinks []SocialLink `json:"social_links"`
}
func (a Author) Byline() Byline {
links := a.SocialLinks
if links == nil {
links = []SocialLink{}
}
return Byline{ID: a.ID, Name: a.Name, Slug: a.Slug, Bio: a.Bio, AvatarID: a.AvatarID, Website: a.Website, SocialLinks: links}
}
type Nullable[T any] struct {
Set bool
Value *T
}
type Patch struct {
Name *string
Slug *string
Bio *string
AvatarID Nullable[uint32]
Website Nullable[string]
SocialLinks *[]SocialLink
UserID Nullable[uint32]
}
type ListQuery struct {
Q string
Page int
Size int
}
type Page struct {
Items []Author
Total int
}
func IsValidSlug(slug string) bool {
length := utf8.RuneCountInString(slug)
return length >= 1 && length <= MaxSlugRunes && slugPattern.MatchString(slug)
}
func IsValidName(name string) bool {
length := utf8.RuneCountInString(strings.TrimSpace(name))
return length >= MinNameRunes && length <= MaxNameRunes
}
func ValidateBio(bio string) error {
if utf8.RuneCountInString(bio) > MaxBioRunes {
return ErrBioTooLong
}
return nil
}
func ValidateURL(raw string) error {
if raw == "" || len(raw) > MaxURLLength {
return ErrInvalidURL
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" {
return ErrInvalidURL
}
return nil
}
func NormalizeWebsite(website *string) (*string, error) {
if website == nil || *website == "" {
return nil, nil
}
if err := ValidateURL(*website); err != nil {
return nil, err
}
return website, nil
}
func IsValidSocialType(linkType string) bool {
switch linkType {
case SocialX, SocialLinkedIn, SocialGitHub, SocialMastodon, SocialCustom:
return true
}
return false
}
func ValidateLinks(links []SocialLink) error {
if len(links) > MaxLinks {
return ErrTooManyLinks
}
for _, link := range links {
if !IsValidSocialType(link.Type) {
return ErrInvalidSocialType
}
if err := ValidateURL(link.URL); err != nil {
return err
}
}
return nil
}
func GenerateSlug(name string) string {
folded := diacriticFolder.Replace(strings.ToLower(name))
slug := strings.Trim(nonSlugRun.ReplaceAllString(folded, "-"), "-")
slug = truncateSlug(slug, MaxSlugRunes)
if slug == "" {
return FallbackSlug
}
return slug
}
func truncateSlug(slug string, limit int) string {
if len(slug) <= limit {
return slug
}
return strings.TrimRight(slug[:limit], "-")
}
func SlugWithSuffix(base string, attempt int) string {
if attempt <= 1 {
return base
}
suffix := "-" + strconv.Itoa(attempt)
return truncateSlug(base, MaxSlugRunes-len(suffix)) + suffix
}