129 lines
4.0 KiB
Go
129 lines
4.0 KiB
Go
package usecase
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"apps/backend/internal/module/scout/domain"
|
||
)
|
||
|
||
type classifiedPost struct {
|
||
classification string
|
||
score int
|
||
reason string
|
||
}
|
||
|
||
func planScanTerms(brief *domain.RunBrief) []string {
|
||
return dedupeTerms(brief.Pains, brief.Tags, brief.Periphery, []string{brief.Intent})
|
||
}
|
||
|
||
func dedupeTerms(groups ...[]string) []string {
|
||
seen := make(map[string]struct{})
|
||
var out []string
|
||
for _, group := range groups {
|
||
for _, term := range group {
|
||
term = strings.Join(strings.Fields(strings.TrimSpace(term)), " ")
|
||
key := strings.ToLower(term)
|
||
if term == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[key]; ok {
|
||
continue
|
||
}
|
||
seen[key] = struct{}{}
|
||
out = append(out, term)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func classifyPost(mode, text string, terms []string) classifiedPost {
|
||
lower := strings.ToLower(text)
|
||
signals := matchedSignals(lower, terms)
|
||
if hasAny(lower, "giveaway", "抽獎", "follow for follow", "互追", "crypto", "賺錢") {
|
||
return classifiedPost{domain.ClassificationNoise, 0, "noise signal"}
|
||
}
|
||
if mode == domain.ModeActivity {
|
||
return classifyActivity(lower, signals)
|
||
}
|
||
if hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
|
||
return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal")
|
||
}
|
||
if hasAny(lower, "推薦", "求推", "有沒有推薦", "any recommendation", "what do you recommend") {
|
||
return scored(domain.ClassificationSeekingRecommendation, 60, signals, "recommendation signal")
|
||
}
|
||
if strings.Contains(lower, "?") || strings.Contains(lower, "?") || hasAny(lower, "怎麼", "如何", "請問", "求助", "help") {
|
||
return scored(domain.ClassificationSeekingHelp, 55, signals, "help-seeking signal")
|
||
}
|
||
return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal")
|
||
}
|
||
|
||
func classifyActivity(text string, signals []string) classifiedPost {
|
||
if hasAny(text, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
|
||
return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal; recency unavailable (neutral)")
|
||
}
|
||
if strings.Contains(text, "?") || strings.Contains(text, "?") || hasAny(text, "請問", "怎麼", "如何", "有人知道", "求") {
|
||
return scored(domain.ClassificationAsking, 60, signals, "asking signal; recency unavailable (neutral)")
|
||
}
|
||
if hasAny(text, "活動", "event", "開幕", "launch", "發布", "報名", "登記", "公告") {
|
||
return scored(domain.ClassificationAnnouncement, 50, signals, "announcement signal; recency unavailable (neutral)")
|
||
}
|
||
return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal; recency unavailable (neutral)")
|
||
}
|
||
|
||
func scored(classification string, base int, signals []string, label string) classifiedPost {
|
||
score := base + len(signals)*15
|
||
if score > 100 {
|
||
score = 100
|
||
}
|
||
reason := label
|
||
if len(signals) > 0 {
|
||
reason += "; matched: " + strings.Join(signals, ", ")
|
||
}
|
||
return classifiedPost{classification, score, reason}
|
||
}
|
||
|
||
func matchedSignals(text string, terms []string) []string {
|
||
var signals []string
|
||
for _, term := range dedupeTerms(terms) {
|
||
if strings.Contains(text, strings.ToLower(term)) {
|
||
signals = append(signals, term)
|
||
}
|
||
}
|
||
return signals
|
||
}
|
||
|
||
func hasAny(text string, signals ...string) bool {
|
||
for _, signal := range signals {
|
||
if strings.Contains(text, signal) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func canonicalPermalink(raw string) string {
|
||
u, err := url.Parse(strings.TrimSpace(raw))
|
||
if err != nil || u.Host == "" {
|
||
return ""
|
||
}
|
||
u.Scheme = "https"
|
||
u.Host = strings.ToLower(u.Host)
|
||
u.RawQuery = ""
|
||
u.Fragment = ""
|
||
u.Path = strings.TrimRight(u.Path, "/")
|
||
return u.String()
|
||
}
|
||
|
||
func permalinkID(ownerUID int64, permalink string) string {
|
||
sum := sha256.Sum256([]byte(strings.TrimSpace(permalink) + "|" + formatOwnerUID(ownerUID)))
|
||
return "sp_" + hex.EncodeToString(sum[:])[:20]
|
||
}
|
||
|
||
func formatOwnerUID(ownerUID int64) string {
|
||
return strconv.FormatInt(ownerUID, 10)
|
||
}
|