This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/kutt"
|
||||
)
|
||||
|
||||
const inviteShortLinkCachePrefix = "cache:invite:short_link:"
|
||||
|
||||
type inviteLinkCustomData struct {
|
||||
ShareURL string `json:"shareUrl"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type InviteLinkResolver struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
createShortLink func(ctx context.Context, targetURL, domain string) (string, error)
|
||||
}
|
||||
|
||||
func NewInviteLinkResolver(ctx context.Context, svcCtx *svc.ServiceContext) *InviteLinkResolver {
|
||||
resolver := &InviteLinkResolver{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
resolver.createShortLink = func(ctx context.Context, targetURL, domain string) (string, error) {
|
||||
client := kutt.NewClient(svcCtx.Config.Kutt.ApiURL, svcCtx.Config.Kutt.ApiKey)
|
||||
link, err := client.CreateShortLink(ctx, &kutt.CreateLinkRequest{
|
||||
Target: targetURL,
|
||||
Reuse: true,
|
||||
Domain: domain,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
shortLink := strings.TrimSpace(link.Link)
|
||||
if strings.HasPrefix(shortLink, "http://") {
|
||||
shortLink = strings.Replace(shortLink, "http://", "https://", 1)
|
||||
}
|
||||
return shortLink, nil
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) ResolveInviteLink(referCode string) string {
|
||||
normalizedCode := strings.TrimSpace(referCode)
|
||||
if normalizedCode == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
longLink := r.buildLongInviteLink(normalizedCode)
|
||||
if !r.canUseKutt() || longLink == "" {
|
||||
return longLink
|
||||
}
|
||||
|
||||
if cached := r.getCachedShortLink(normalizedCode); cached != "" {
|
||||
return cached
|
||||
}
|
||||
|
||||
shortLink, err := r.generateShortLinkWithTimeout(normalizedCode, 1500*time.Millisecond)
|
||||
if err != nil || strings.TrimSpace(shortLink) == "" {
|
||||
return longLink
|
||||
}
|
||||
|
||||
r.cacheShortLink(normalizedCode, shortLink)
|
||||
return shortLink
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) ResolveInviteLinksBatch(referCodes []string, maxGenerate, maxConcurrency int, timeout time.Duration) map[string]string {
|
||||
result := make(map[string]string)
|
||||
uniqueCodes := uniqueReferCodes(referCodes)
|
||||
if len(uniqueCodes) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, referCode := range uniqueCodes {
|
||||
result[referCode] = r.buildLongInviteLink(referCode)
|
||||
}
|
||||
|
||||
if !r.canUseKutt() {
|
||||
return result
|
||||
}
|
||||
|
||||
toGenerate := make([]string, 0, len(uniqueCodes))
|
||||
for _, referCode := range uniqueCodes {
|
||||
if cached := r.getCachedShortLink(referCode); cached != "" {
|
||||
result[referCode] = cached
|
||||
continue
|
||||
}
|
||||
toGenerate = append(toGenerate, referCode)
|
||||
}
|
||||
|
||||
if maxGenerate > 0 && len(toGenerate) > maxGenerate {
|
||||
toGenerate = toGenerate[:maxGenerate]
|
||||
}
|
||||
|
||||
if len(toGenerate) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
if maxConcurrency <= 0 {
|
||||
maxConcurrency = 1
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 1500 * time.Millisecond
|
||||
}
|
||||
|
||||
limiter := make(chan struct{}, maxConcurrency)
|
||||
var waitGroup sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
|
||||
for _, referCode := range toGenerate {
|
||||
waitGroup.Add(1)
|
||||
currentCode := referCode
|
||||
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
limiter <- struct{}{}
|
||||
defer func() { <-limiter }()
|
||||
|
||||
shortLink, err := r.generateShortLinkWithTimeout(currentCode, timeout)
|
||||
if err != nil || strings.TrimSpace(shortLink) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
result[currentCode] = shortLink
|
||||
mutex.Unlock()
|
||||
|
||||
r.cacheShortLink(currentCode, shortLink)
|
||||
}()
|
||||
}
|
||||
|
||||
waitGroup.Wait()
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) canUseKutt() bool {
|
||||
if r == nil || r.svcCtx == nil {
|
||||
return false
|
||||
}
|
||||
if !r.svcCtx.Config.Kutt.Enable {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(r.svcCtx.Config.Kutt.ApiURL) == "" || strings.TrimSpace(r.svcCtx.Config.Kutt.ApiKey) == "" {
|
||||
return false
|
||||
}
|
||||
return r.createShortLink != nil
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) resolveShareURLAndDomain() (string, string) {
|
||||
if r == nil || r.svcCtx == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
shareURL := strings.TrimSpace(r.svcCtx.Config.Kutt.TargetURL)
|
||||
domain := strings.TrimSpace(r.svcCtx.Config.Kutt.Domain)
|
||||
|
||||
customData := strings.TrimSpace(r.svcCtx.Config.Site.CustomData)
|
||||
if customData == "" {
|
||||
return shareURL, domain
|
||||
}
|
||||
|
||||
var parsedData inviteLinkCustomData
|
||||
if err := json.Unmarshal([]byte(customData), &parsedData); err != nil {
|
||||
return shareURL, domain
|
||||
}
|
||||
|
||||
if strings.TrimSpace(parsedData.ShareURL) != "" {
|
||||
shareURL = strings.TrimSpace(parsedData.ShareURL)
|
||||
}
|
||||
if strings.TrimSpace(parsedData.Domain) != "" {
|
||||
domain = strings.TrimSpace(parsedData.Domain)
|
||||
}
|
||||
|
||||
return shareURL, domain
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) buildLongInviteLink(referCode string) string {
|
||||
normalizedCode := strings.TrimSpace(referCode)
|
||||
if normalizedCode == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
shareURL, _ := r.resolveShareURLAndDomain()
|
||||
if shareURL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(shareURL)
|
||||
if err != nil {
|
||||
return fallbackLongInviteLink(shareURL, normalizedCode)
|
||||
}
|
||||
|
||||
queryValues := parsedURL.Query()
|
||||
queryValues.Set("ic", normalizedCode)
|
||||
parsedURL.RawQuery = queryValues.Encode()
|
||||
|
||||
return parsedURL.String()
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) generateShortLinkWithTimeout(referCode string, timeout time.Duration) (string, error) {
|
||||
longLink := r.buildLongInviteLink(referCode)
|
||||
if longLink == "" {
|
||||
return "", nil
|
||||
}
|
||||
_, domain := r.resolveShareURLAndDomain()
|
||||
|
||||
requestCtx := r.ctx
|
||||
var cancel context.CancelFunc
|
||||
if timeout > 0 {
|
||||
requestCtx, cancel = context.WithTimeout(r.ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
shortLink, err := r.createShortLink(requestCtx, longLink, domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(shortLink), nil
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) getCachedShortLink(referCode string) string {
|
||||
if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
cacheKey := inviteShortLinkCachePrefix + referCode
|
||||
shortLink, err := r.svcCtx.Redis.Get(r.ctx, cacheKey).Result()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(shortLink)
|
||||
}
|
||||
|
||||
func (r *InviteLinkResolver) cacheShortLink(referCode, shortLink string) {
|
||||
if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(referCode) == "" || strings.TrimSpace(shortLink) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := inviteShortLinkCachePrefix + referCode
|
||||
_ = r.svcCtx.Redis.Set(r.ctx, cacheKey, shortLink, 0).Err()
|
||||
}
|
||||
|
||||
func uniqueReferCodes(referCodes []string) []string {
|
||||
uniqueCodes := make([]string, 0, len(referCodes))
|
||||
seen := make(map[string]struct{}, len(referCodes))
|
||||
|
||||
for _, referCode := range referCodes {
|
||||
normalizedCode := strings.TrimSpace(referCode)
|
||||
if normalizedCode == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[normalizedCode]; exists {
|
||||
continue
|
||||
}
|
||||
seen[normalizedCode] = struct{}{}
|
||||
uniqueCodes = append(uniqueCodes, normalizedCode)
|
||||
}
|
||||
|
||||
return uniqueCodes
|
||||
}
|
||||
|
||||
func fallbackLongInviteLink(baseURL, referCode string) string {
|
||||
normalizedBase := strings.TrimSpace(baseURL)
|
||||
normalizedCode := strings.TrimSpace(referCode)
|
||||
if normalizedBase == "" || normalizedCode == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
separator := "?"
|
||||
if strings.Contains(normalizedBase, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
trimmedBase := strings.TrimRight(normalizedBase, "?&")
|
||||
return fmt.Sprintf("%s%sic=%s", trimmedBase, separator, url.QueryEscape(normalizedCode))
|
||||
}
|
||||
Reference in New Issue
Block a user