All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m16s
290 lines
7.0 KiB
Go
290 lines
7.0 KiB
Go
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))
|
|
}
|