| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- package state
- import (
- "context"
- "database/sql"
- "fmt"
- "log/slog"
- "regexp"
- "strings"
- "time"
- )
- // VanityURL represents a user's vanity URL configuration.
- type VanityURL struct {
- ScreenName string `json:"screenName"`
- VanityURL string `json:"vanityUrl"`
- DisplayName string `json:"displayName,omitempty"`
- Bio string `json:"bio,omitempty"`
- Location string `json:"location,omitempty"`
- Website string `json:"website,omitempty"`
- CreatedAt time.Time `json:"createdAt"`
- UpdatedAt time.Time `json:"updatedAt"`
- IsActive bool `json:"isActive"`
- ClickCount int `json:"clickCount"`
- LastAccessed *time.Time `json:"lastAccessed,omitempty"`
- }
- // VanityURLRedirect represents a vanity URL access record.
- type VanityURLRedirect struct {
- ID int64 `json:"id"`
- VanityURL string `json:"vanityUrl"`
- AccessedAt time.Time `json:"accessedAt"`
- IPAddress string `json:"ipAddress,omitempty"`
- UserAgent string `json:"userAgent,omitempty"`
- Referer string `json:"referer,omitempty"`
- }
- // VanityInfo represents the response for vanity URL lookups.
- type VanityInfo struct {
- ScreenName string `json:"screenName"`
- VanityURL string `json:"vanityUrl"`
- DisplayName string `json:"displayName,omitempty"`
- Bio string `json:"bio,omitempty"`
- Location string `json:"location,omitempty"`
- Website string `json:"website,omitempty"`
- ProfileURL string `json:"profileUrl"`
- IsActive bool `json:"isActive"`
- Extra map[string]interface{} `json:"extra,omitempty"`
- }
- // VanityURLManager manages vanity URL operations.
- type VanityURLManager struct {
- db *sql.DB
- logger *slog.Logger
- baseURL string // Base URL for the service (e.g., "https://aim.example.com")
- reserved []string // Reserved URLs that cannot be claimed
- }
- // NewVanityURLManager creates a new vanity URL manager.
- func NewVanityURLManager(db *sql.DB, logger *slog.Logger, baseURL string) *VanityURLManager {
- return &VanityURLManager{
- db: db,
- logger: logger,
- baseURL: baseURL,
- reserved: []string{
- "api", "admin", "help", "support", "about", "terms", "privacy",
- "login", "logout", "register", "signup", "signin", "settings",
- "profile", "user", "users", "aim", "aol", "webapi", "oscar",
- "chat", "im", "message", "buddy", "buddies", "feed", "rss",
- },
- }
- }
- // CreateOrUpdateVanityURL creates or updates a vanity URL for a user.
- func (m *VanityURLManager) CreateOrUpdateVanityURL(ctx context.Context, screenName string, vanityURL string, info map[string]interface{}) error {
- // Validate vanity URL
- if err := m.validateVanityURL(vanityURL); err != nil {
- return err
- }
- // Check if URL is reserved
- if m.isReserved(vanityURL) {
- return fmt.Errorf("vanity URL '%s' is reserved", vanityURL)
- }
- // Extract optional fields from info
- displayName, _ := info["displayName"].(string)
- bio, _ := info["bio"].(string)
- location, _ := info["location"].(string)
- website, _ := info["website"].(string)
- now := time.Now()
- // Try to update existing record first
- updateQuery := `
- UPDATE vanity_urls
- SET vanity_url = ?, display_name = ?, bio = ?, location = ?,
- website = ?, updated_at = ?, is_active = ?
- WHERE screen_name = ?
- `
- result, err := m.db.ExecContext(ctx, updateQuery,
- vanityURL, displayName, bio, location, website,
- now.Unix(), true, screenName,
- )
- if err != nil {
- return fmt.Errorf("failed to update vanity URL: %w", err)
- }
- rowsAffected, _ := result.RowsAffected()
- if rowsAffected > 0 {
- m.logger.InfoContext(ctx, "updated vanity URL",
- "screenName", screenName,
- "vanityURL", vanityURL,
- )
- return nil
- }
- // Insert new record
- insertQuery := `
- INSERT INTO vanity_urls (
- screen_name, vanity_url, display_name, bio, location,
- website, created_at, updated_at, is_active, click_count
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `
- _, err = m.db.ExecContext(ctx, insertQuery,
- screenName, vanityURL, displayName, bio, location,
- website, now.Unix(), now.Unix(), true, 0,
- )
- if err != nil {
- if strings.Contains(err.Error(), "UNIQUE") {
- return fmt.Errorf("vanity URL '%s' is already taken", vanityURL)
- }
- return fmt.Errorf("failed to create vanity URL: %w", err)
- }
- m.logger.InfoContext(ctx, "created vanity URL",
- "screenName", screenName,
- "vanityURL", vanityURL,
- )
- return nil
- }
- // GetVanityInfo retrieves vanity URL information.
- func (m *VanityURLManager) GetVanityInfo(ctx context.Context, vanityURL string) (*VanityInfo, error) {
- // Clean the vanity URL
- vanityURL = strings.ToLower(strings.TrimSpace(vanityURL))
- query := `
- SELECT screen_name, vanity_url, display_name, bio, location,
- website, created_at, updated_at, is_active, click_count, last_accessed
- FROM vanity_urls
- WHERE vanity_url = ? AND is_active = 1
- `
- var v VanityURL
- var createdAt, updatedAt int64
- var lastAccessed sql.NullInt64
- err := m.db.QueryRowContext(ctx, query, vanityURL).Scan(
- &v.ScreenName, &v.VanityURL, &v.DisplayName, &v.Bio, &v.Location,
- &v.Website, &createdAt, &updatedAt, &v.IsActive, &v.ClickCount, &lastAccessed,
- )
- if err == sql.ErrNoRows {
- return nil, fmt.Errorf("vanity URL not found: %s", vanityURL)
- }
- if err != nil {
- return nil, fmt.Errorf("failed to get vanity info: %w", err)
- }
- v.CreatedAt = time.Unix(createdAt, 0)
- v.UpdatedAt = time.Unix(updatedAt, 0)
- if lastAccessed.Valid {
- t := time.Unix(lastAccessed.Int64, 0)
- v.LastAccessed = &t
- }
- // Create response info
- info := &VanityInfo{
- ScreenName: v.ScreenName,
- VanityURL: v.VanityURL,
- DisplayName: v.DisplayName,
- Bio: v.Bio,
- Location: v.Location,
- Website: v.Website,
- ProfileURL: m.buildProfileURL(v.VanityURL),
- IsActive: v.IsActive,
- Extra: map[string]interface{}{
- "createdAt": v.CreatedAt.Unix(),
- "clickCount": v.ClickCount,
- },
- }
- // Update click count and last accessed asynchronously
- go m.recordAccess(context.Background(), vanityURL)
- return info, nil
- }
- // GetVanityInfoByScreenName retrieves vanity URL info by screen name.
- func (m *VanityURLManager) GetVanityInfoByScreenName(ctx context.Context, screenName string) (*VanityInfo, error) {
- query := `
- SELECT screen_name, vanity_url, display_name, bio, location,
- website, created_at, updated_at, is_active, click_count, last_accessed
- FROM vanity_urls
- WHERE screen_name = ? AND is_active = 1
- `
- var v VanityURL
- var createdAt, updatedAt int64
- var lastAccessed sql.NullInt64
- err := m.db.QueryRowContext(ctx, query, screenName).Scan(
- &v.ScreenName, &v.VanityURL, &v.DisplayName, &v.Bio, &v.Location,
- &v.Website, &createdAt, &updatedAt, &v.IsActive, &v.ClickCount, &lastAccessed,
- )
- if err == sql.ErrNoRows {
- return nil, nil // No vanity URL configured
- }
- if err != nil {
- return nil, fmt.Errorf("failed to get vanity info: %w", err)
- }
- v.CreatedAt = time.Unix(createdAt, 0)
- v.UpdatedAt = time.Unix(updatedAt, 0)
- if lastAccessed.Valid {
- t := time.Unix(lastAccessed.Int64, 0)
- v.LastAccessed = &t
- }
- // Create response info
- info := &VanityInfo{
- ScreenName: v.ScreenName,
- VanityURL: v.VanityURL,
- DisplayName: v.DisplayName,
- Bio: v.Bio,
- Location: v.Location,
- Website: v.Website,
- ProfileURL: m.buildProfileURL(v.VanityURL),
- IsActive: v.IsActive,
- Extra: map[string]interface{}{
- "createdAt": v.CreatedAt.Unix(),
- "clickCount": v.ClickCount,
- },
- }
- return info, nil
- }
- // DeleteVanityURL removes a user's vanity URL.
- func (m *VanityURLManager) DeleteVanityURL(ctx context.Context, screenName string) error {
- query := `UPDATE vanity_urls SET is_active = 0, updated_at = ? WHERE screen_name = ?`
- _, err := m.db.ExecContext(ctx, query, time.Now().Unix(), screenName)
- if err != nil {
- return fmt.Errorf("failed to delete vanity URL: %w", err)
- }
- m.logger.InfoContext(ctx, "deleted vanity URL", "screenName", screenName)
- return nil
- }
- // CheckAvailability checks if a vanity URL is available.
- func (m *VanityURLManager) CheckAvailability(ctx context.Context, vanityURL string) (bool, error) {
- // Validate format
- if err := m.validateVanityURL(vanityURL); err != nil {
- return false, err
- }
- // Check if reserved
- if m.isReserved(vanityURL) {
- return false, nil
- }
- // Check database
- query := `SELECT COUNT(*) FROM vanity_urls WHERE vanity_url = ? AND is_active = 1`
- var count int
- err := m.db.QueryRowContext(ctx, query, vanityURL).Scan(&count)
- if err != nil {
- return false, fmt.Errorf("failed to check availability: %w", err)
- }
- return count == 0, nil
- }
- // GetPopularVanityURLs retrieves the most accessed vanity URLs.
- func (m *VanityURLManager) GetPopularVanityURLs(ctx context.Context, limit int) ([]VanityInfo, error) {
- query := `
- SELECT screen_name, vanity_url, display_name, bio, location,
- website, is_active, click_count
- FROM vanity_urls
- WHERE is_active = 1
- ORDER BY click_count DESC
- LIMIT ?
- `
- rows, err := m.db.QueryContext(ctx, query, limit)
- if err != nil {
- return nil, fmt.Errorf("failed to get popular vanity URLs: %w", err)
- }
- defer rows.Close()
- var results []VanityInfo
- for rows.Next() {
- var info VanityInfo
- var displayName, bio, location, website sql.NullString
- err := rows.Scan(
- &info.ScreenName, &info.VanityURL, &displayName, &bio,
- &location, &website, &info.IsActive, &info.Extra,
- )
- if err != nil {
- return nil, fmt.Errorf("failed to scan vanity info: %w", err)
- }
- if displayName.Valid {
- info.DisplayName = displayName.String
- }
- if bio.Valid {
- info.Bio = bio.String
- }
- if location.Valid {
- info.Location = location.String
- }
- if website.Valid {
- info.Website = website.String
- }
- info.ProfileURL = m.buildProfileURL(info.VanityURL)
- results = append(results, info)
- }
- return results, nil
- }
- // recordAccess records a vanity URL access.
- func (m *VanityURLManager) recordAccess(ctx context.Context, vanityURL string) {
- // Update click count and last accessed time
- updateQuery := `
- UPDATE vanity_urls
- SET click_count = click_count + 1, last_accessed = ?
- WHERE vanity_url = ?
- `
- _, err := m.db.ExecContext(ctx, updateQuery, time.Now().Unix(), vanityURL)
- if err != nil {
- m.logger.Error("failed to record vanity URL access", "error", err, "vanityURL", vanityURL)
- }
- }
- // LogRedirect logs a vanity URL redirect for analytics.
- func (m *VanityURLManager) LogRedirect(ctx context.Context, redirect VanityURLRedirect) error {
- query := `
- INSERT INTO vanity_url_redirects (vanity_url, accessed_at, ip_address, user_agent, referer)
- VALUES (?, ?, ?, ?, ?)
- `
- _, err := m.db.ExecContext(ctx, query,
- redirect.VanityURL, redirect.AccessedAt.Unix(),
- redirect.IPAddress, redirect.UserAgent, redirect.Referer,
- )
- if err != nil {
- return fmt.Errorf("failed to log redirect: %w", err)
- }
- return nil
- }
- // validateVanityURL validates the format of a vanity URL.
- func (m *VanityURLManager) validateVanityURL(vanityURL string) error {
- // Clean and lowercase
- vanityURL = strings.ToLower(strings.TrimSpace(vanityURL))
- // Check length
- if len(vanityURL) < 3 || len(vanityURL) > 30 {
- return fmt.Errorf("vanity URL must be between 3 and 30 characters")
- }
- // Check format (alphanumeric, hyphens, underscores only)
- validFormat := regexp.MustCompile(`^[a-z0-9_-]+$`)
- if !validFormat.MatchString(vanityURL) {
- return fmt.Errorf("vanity URL can only contain letters, numbers, hyphens, and underscores")
- }
- // Can't start or end with special characters
- if strings.HasPrefix(vanityURL, "-") || strings.HasPrefix(vanityURL, "_") ||
- strings.HasSuffix(vanityURL, "-") || strings.HasSuffix(vanityURL, "_") {
- return fmt.Errorf("vanity URL cannot start or end with hyphens or underscores")
- }
- return nil
- }
- // isReserved checks if a vanity URL is in the reserved list.
- func (m *VanityURLManager) isReserved(vanityURL string) bool {
- vanityURL = strings.ToLower(vanityURL)
- for _, reserved := range m.reserved {
- if vanityURL == reserved {
- return true
- }
- }
- return false
- }
- // buildProfileURL builds the full profile URL for a vanity URL.
- func (m *VanityURLManager) buildProfileURL(vanityURL string) string {
- if m.baseURL == "" {
- return fmt.Sprintf("/profile/%s", vanityURL)
- }
- return fmt.Sprintf("%s/profile/%s", strings.TrimRight(m.baseURL, "/"), vanityURL)
- }
|