1package cosmetic
2
3import (
4 _ "embed"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "github.com/bnema/dumber/internal/logging"
10)
11
12//go:embed injector.js
13var cosmeticScript string
14
15// CosmeticInjector manages cosmetic filter injection
16type CosmeticInjector struct {
17 domainRules map[string][]string
18 genericRules []string
19 scriptEnabled bool
20}
21
22// CosmeticRule represents a cosmetic filter rule for JavaScript injection
23type CosmeticRule struct {
24 Domain string `json:"domain,omitempty"`
25 Selector string `json:"selector"`
26}
27
28// NewCosmeticInjector creates a new cosmetic filter injector
29func NewCosmeticInjector() *CosmeticInjector {
30 return &CosmeticInjector{
31 domainRules: make(map[string][]string),
32 genericRules: make([]string, 0),
33 scriptEnabled: true,
34 }
35}
36
37// InjectRules stores cosmetic rules for domain-specific filtering
38func (ci *CosmeticInjector) InjectRules(rules map[string][]string) error {
39 if !ci.scriptEnabled {
40 return fmt.Errorf("cosmetic filtering disabled")
41 }
42
43 // Clear existing rules
44 ci.domainRules = make(map[string][]string)
45 ci.genericRules = make([]string, 0)
46
47 // Process rules
48 for domain, selectors := range rules {
49 if domain == "" {
50 // Generic rules (apply to all domains)
51 ci.genericRules = append(ci.genericRules, selectors...)
52 } else {
53 // Domain-specific rules
54 ci.domainRules[domain] = selectors
55 }
56 }
57
58 logging.Debug(fmt.Sprintf("Injected %d domain-specific rules and %d generic rules",
59 len(ci.domainRules), len(ci.genericRules)))
60
61 return nil
62}
63
64// GetScriptForDomain returns the complete cosmetic filtering script for a domain
65func (ci *CosmeticInjector) GetScriptForDomain(domain string) string {
66 if !ci.scriptEnabled {
67 return ""
68 }
69
70 // Build rules for this specific domain
71 var rules []CosmeticRule
72
73 // Add generic rules
74 for _, selector := range ci.genericRules {
75 rules = append(rules, CosmeticRule{
76 Selector: selector,
77 })
78 }
79
80 // Add domain-specific rules
81 if domainSelectors, exists := ci.domainRules[domain]; exists {
82 for _, selector := range domainSelectors {
83 rules = append(rules, CosmeticRule{
84 Domain: domain,
85 Selector: selector,
86 })
87 }
88 }
89
90 // Convert rules to JSON
91 rulesJSON, err := json.Marshal(rules)
92 if err != nil {
93 logging.Error(fmt.Sprintf("Failed to marshal cosmetic rules: %v", err))
94 return cosmeticScript
95 }
96
97 // Build the complete script with domain-specific rules
98 initScript := fmt.Sprintf(`
99// Initialize cosmetic filtering for domain: %s
100(function() {
101 if (typeof window.__dumber_cosmetic_init === 'function') {
102 window.__dumber_cosmetic_init(%s);
103 console.log('[dumber] Cosmetic filtering initialized with %d rules');
104 } else {
105 console.warn('[dumber] Cosmetic filtering not available');
106 }
107})();`, domain, string(rulesJSON), len(rules))
108
109 return cosmeticScript + "\n" + initScript
110}
111
112// UpdateRulesForDomain adds new rules for a specific domain
113func (ci *CosmeticInjector) UpdateRulesForDomain(domain string, newSelectors []string) {
114 if !ci.scriptEnabled {
115 return
116 }
117
118 if len(newSelectors) == 0 {
119 return
120 }
121
122 if domain == "" {
123 // Add to generic rules
124 ci.genericRules = append(ci.genericRules, newSelectors...)
125 } else {
126 // Add to domain-specific rules
127 if _, exists := ci.domainRules[domain]; !exists {
128 ci.domainRules[domain] = make([]string, 0)
129 }
130 ci.domainRules[domain] = append(ci.domainRules[domain], newSelectors...)
131 }
132
133 logging.Debug(fmt.Sprintf("Added %d new cosmetic rules for domain: %s", len(newSelectors), domain))
134}
135
136// GetUpdateScript returns JavaScript to update rules dynamically
137func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
138 if !ci.scriptEnabled || len(newSelectors) == 0 {
139 return ""
140 }
141
142 selectorsJSON, err := json.Marshal(newSelectors)
143 if err != nil {
144 logging.Error(fmt.Sprintf("Failed to marshal update selectors: %v", err))
145 return ""
146 }
147
148 return fmt.Sprintf(`
149(function() {
150 if (typeof window.__dumber_cosmetic_update === 'function') {
151 window.__dumber_cosmetic_update(%s);
152 console.log('[dumber] Updated cosmetic rules with %d new selectors');
153 }
154})();`, string(selectorsJSON), len(newSelectors))
155}
156
157// GetCleanupScript returns JavaScript to cleanup cosmetic filtering on navigation
158func (ci *CosmeticInjector) GetCleanupScript() string {
159 return `
160(function() {
161 if (typeof window.__dumber_cosmetic_cleanup === 'function') {
162 window.__dumber_cosmetic_cleanup();
163 console.log('[dumber] Cosmetic filtering cleaned up');
164 }
165})();`
166}
167
168// GetRulesForDomain returns all cosmetic rules that apply to a domain
169func (ci *CosmeticInjector) GetRulesForDomain(domain string) []string {
170 var rules []string
171
172 // Add generic rules
173 rules = append(rules, ci.genericRules...)
174
175 // Add domain-specific rules
176 if domainSelectors, exists := ci.domainRules[domain]; exists {
177 rules = append(rules, domainSelectors...)
178 }
179
180 // Also check for subdomain matches
181 domainParts := strings.Split(domain, ".")
182 if len(domainParts) > 2 {
183 // Try parent domain (e.g., "example.com" for "www.example.com")
184 parentDomain := strings.Join(domainParts[1:], ".")
185 if parentSelectors, exists := ci.domainRules[parentDomain]; exists {
186 rules = append(rules, parentSelectors...)
187 }
188 }
189
190 return rules
191}
192
193// Enable enables or disables cosmetic filtering
194func (ci *CosmeticInjector) Enable(enabled bool) {
195 ci.scriptEnabled = enabled
196 if enabled {
197 logging.Info("Cosmetic filtering enabled")
198 } else {
199 logging.Info("Cosmetic filtering disabled")
200 }
201}
202
203// IsEnabled returns whether cosmetic filtering is enabled
204func (ci *CosmeticInjector) IsEnabled() bool {
205 return ci.scriptEnabled
206}
207
208// GetStats returns statistics about loaded cosmetic rules
209func (ci *CosmeticInjector) GetStats() map[string]interface{} {
210 domainCount := len(ci.domainRules)
211 totalRules := len(ci.genericRules)
212
213 for _, selectors := range ci.domainRules {
214 totalRules += len(selectors)
215 }
216
217 return map[string]interface{}{
218 "enabled": ci.scriptEnabled,
219 "generic_rules": len(ci.genericRules),
220 "domain_count": domainCount,
221 "total_rules": totalRules,
222 }
223}