181 lines
4.3 KiB
Go
181 lines
4.3 KiB
Go
package author
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/slug"
|
|
"errors"
|
|
"net/url"
|
|
"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"
|
|
)
|
|
|
|
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}
|
|
}
|
|
|
|
func BylineIDs(bylines []Byline) []uint32 {
|
|
ids := make([]uint32, 0, len(bylines))
|
|
for _, byline := range bylines {
|
|
ids = append(ids, byline.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
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(candidate string) bool {
|
|
return slug.IsValid(candidate)
|
|
}
|
|
|
|
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 {
|
|
return slug.Generate(name, FallbackSlug)
|
|
}
|
|
|
|
func SlugWithSuffix(base string, attempt int) string {
|
|
return slug.WithSuffix(base, attempt)
|
|
}
|