link.go
go
package models
import (
	"crypto/rand"
	"errors"
	"fmt"
	"html"
	"html/template"
	"math/big"
	"net/url"
	"strings"
	"time"
	"theskyscape.com/repo/skykit"
)
type Link struct {
	skykit.Model
	ShortCode string
	TargetURL string
	UserID    string
}
func (l *Link) GetModel() *skykit.Model {
	return &l.Model
}
func (l *Link) DisplayURL() template.HTML {
	return template.HTML(html.EscapeString(l.TargetURL))
}
var pacific = mustLoadLocation()
func mustLoadLocation() *time.Location {
	loc, err := time.LoadLocation("America/Los_Angeles")
	if err != nil {
		return time.FixedZone("PST", -8*3600)
	}
	return loc
}
func (l *Link) CreatedAtPST() string {
	return l.CreatedAt.In(pacific).Format("Jan 2, 2006 3:04 PM")
}
var (
	userLookup func(id string) (*skykit.User, error)
	anonUser   = &skykit.User{
		Handle: "anon",
		Avatar: "https://robots.skysca.pe/anon",
	}
)
// SetUserLookup wires a lookup function for link ownership metadata.
func SetUserLookup(fn func(id string) (*skykit.User, error)) {
	userLookup = fn
}
// User returns the creator of the link or an anonymous user placeholder.
func (l *Link) User() *skykit.User {
	if l.UserID == "" {
		return anonUser
	}
	if userLookup == nil {
		return anonUser
	}
	user, err := userLookup(l.UserID)
	if err != nil || user == nil {
		return anonUser
	}
	return user
}
func (l *Link) CanDelete(user *skykit.User) bool {
	if l.UserID == "" {
		return true
	}
	if user == nil {
		return false
	}
	return l.UserID == user.ID
}
func CreateLink(targetURL, customShort string, user *skykit.User) (*Link, error) {
	targetURL = strings.TrimSpace(targetURL)
	if targetURL == "" {
		return nil, errors.New("target URL is required")
	}
	parsed, err := url.ParseRequestURI(targetURL)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return nil, errors.New("enter a valid URL including http or https")
	}
	short, err := sanitizeShortCode(customShort)
	if err != nil {
		return nil, err
	}
	if short == "" {
		short, err = generateAvailableShortCode()
		if err != nil {
			return nil, err
		}
	} else if err := ensureShortCodeAvailable(short); err != nil {
		return nil, err
	}
	link := Links.New()
	link.ShortCode = short
	link.TargetURL = targetURL
	if user != nil {
		link.UserID = user.ID
	}
	if _, err := Links.Insert(link); err != nil {
		return nil, fmt.Errorf("unable to save link: %w", err)
	}
	return link, nil
}
func sanitizeShortCode(code string) (string, error) {
	code = strings.ToLower(strings.TrimSpace(code))
	builder := strings.Builder{}
	for _, r := range code {
		switch {
		case r >= 'a' && r <= 'z':
			builder.WriteRune(r)
		case r >= '0' && r <= '9':
			builder.WriteRune(r)
		case r == '-' || r == '_':
			builder.WriteRune(r)
		default:
			return "", errors.New("short codes may use letters, numbers, dashes, and underscores")
		}
	}
	return builder.String(), nil
}
func generateAvailableShortCode() (string, error) {
	for i := 0; i < 5; i++ {
		code, err := randomShortCode(6)
		if err != nil {
			return "", errors.New("failed generating short code")
		}
		if err := ensureShortCodeAvailable(code); err == nil {
			return code, nil
		}
	}
	return "", errors.New("unable to generate unique short code, please provide one manually")
}
func ensureShortCodeAvailable(code string) error {
	_, err := Links.First("WHERE ShortCode = ?", code)
	if err == nil {
		return errors.New("short code already taken")
	}
	if errors.Is(err, skykit.ErrNotFound) {
		return nil
	}
	return fmt.Errorf("failed to check short code: %w", err)
}
func randomShortCode(length int) (string, error) {
	const alphabet = "23456789abcdefghjkmnpqrstuvwxyz"
	result := make([]byte, length)
	max := big.NewInt(int64(len(alphabet)))
	for i := range result {
		n, err := rand.Int(rand.Reader, max)
		if err != nil {
			return "", err
		}
		result[i] = alphabet[n.Int64()]
	}
	return string(result), nil
}
No comments yet.