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 (including parent domains)
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 for the domain and its parent domains
81 addDomainRules := func(domainKey string) {
82 if domainKey == "" {
83 return
84 }
85 if domainSelectors, exists := ci.domainRules[domainKey]; exists {
86 for _, selector := range domainSelectors {
87 rules = append(rules, CosmeticRule{
88 Domain: domainKey,
89 Selector: selector,
90 })
91 }
92 }
93 }
94
95 addDomainRules(domain)
96
97 // Walk up parent domains to include base-domain rules for subdomains
98 parts := strings.Split(domain, ".")
99 for len(parts) > 2 {
100 parts = parts[1:]
101 parentDomain := strings.Join(parts, ".")
102 addDomainRules(parentDomain)
103 }
104
105 // Convert rules to JSON
106 rulesJSON, err := json.Marshal(rules)
107 if err != nil {
108 logging.Error(fmt.Sprintf("Failed to marshal cosmetic rules: %v", err))
109 return cosmeticScript
110 }
111
112 // Build the complete script with domain-specific rules
113 initScript := fmt.Sprintf(`
114// Initialize cosmetic filtering for domain: %s
115(function() {
116 if (typeof window.__dumber_cosmetic_init === 'function') {
117 window.__dumber_cosmetic_init(%s);
118 if (window.self === window.top) {
119 console.log('[dumber] Cosmetic filtering initialized with %d rules');
120 }
121 } else {
122 if (window.self === window.top) {
123 console.warn('[dumber] Cosmetic filtering not available');
124 }
125 }
126})();`, domain, string(rulesJSON), len(rules))
127
128 return cosmeticScript + "\n" + initScript
129}
130
131// UpdateRulesForDomain adds new rules for a specific domain
132func (ci *CosmeticInjector) UpdateRulesForDomain(domain string, newSelectors []string) {
133 if !ci.scriptEnabled {
134 return
135 }
136
137 if len(newSelectors) == 0 {
138 return
139 }
140
141 if domain == "" {
142 // Add to generic rules
143 ci.genericRules = append(ci.genericRules, newSelectors...)
144 } else {
145 // Add to domain-specific rules
146 if _, exists := ci.domainRules[domain]; !exists {
147 ci.domainRules[domain] = make([]string, 0)
148 }
149 ci.domainRules[domain] = append(ci.domainRules[domain], newSelectors...)
150 }
151
152 logging.Debug(fmt.Sprintf("Added %d new cosmetic rules for domain: %s", len(newSelectors), domain))
153}
154
155// GetUpdateScript returns JavaScript to update rules dynamically
156func (ci *CosmeticInjector) GetUpdateScript(newSelectors []string) string {
157 if !ci.scriptEnabled || len(newSelectors) == 0 {
158 return ""
159 }
160
161 selectorsJSON, err := json.Marshal(newSelectors)
162 if err != nil {
163 logging.Error(fmt.Sprintf("Failed to marshal update selectors: %v", err))
164 return ""
165 }
166
167 return fmt.Sprintf(`
168(function() {
169 if (typeof window.__dumber_cosmetic_update === 'function') {
170 window.__dumber_cosmetic_update(%s);
171 if (window.self === window.top) {
172 console.log('[dumber] Updated cosmetic rules with %d new selectors');
173 }
174 }
175})();`, string(selectorsJSON), len(newSelectors))
176}
177
178// GetCleanupScript returns JavaScript to cleanup cosmetic filtering on navigation
179func (ci *CosmeticInjector) GetCleanupScript() string {
180 return `
181(function() {
182 if (typeof window.__dumber_cosmetic_cleanup === 'function') {
183 window.__dumber_cosmetic_cleanup();
184 if (window.self === window.top) {
185 console.log('[dumber] Cosmetic filtering cleaned up');
186 }
187 }
188})();`
189}
190
191// GetRulesForDomain returns all cosmetic rules that apply to a domain
192func (ci *CosmeticInjector) GetRulesForDomain(domain string) []string {
193 var rules []string
194
195 // Add generic rules
196 rules = append(rules, ci.genericRules...)
197
198 // Add domain-specific rules
199 if domainSelectors, exists := ci.domainRules[domain]; exists {
200 rules = append(rules, domainSelectors...)
201 }
202
203 // Also check for subdomain matches
204 domainParts := strings.Split(domain, ".")
205 if len(domainParts) > 2 {
206 // Try parent domain (e.g., "example.com" for "www.example.com")
207 parentDomain := strings.Join(domainParts[1:], ".")
208 if parentSelectors, exists := ci.domainRules[parentDomain]; exists {
209 rules = append(rules, parentSelectors...)
210 }
211 }
212
213 return rules
214}
215
216// Enable enables or disables cosmetic filtering
217func (ci *CosmeticInjector) Enable(enabled bool) {
218 ci.scriptEnabled = enabled
219 if enabled {
220 logging.Info("Cosmetic filtering enabled")
221 } else {
222 logging.Info("Cosmetic filtering disabled")
223 }
224}
225
226// IsEnabled returns whether cosmetic filtering is enabled
227func (ci *CosmeticInjector) IsEnabled() bool {
228 return ci.scriptEnabled
229}
230
231// GetStats returns statistics about loaded cosmetic rules
232func (ci *CosmeticInjector) GetStats() map[string]interface{} {
233 domainCount := len(ci.domainRules)
234 totalRules := len(ci.genericRules)
235
236 for _, selectors := range ci.domainRules {
237 totalRules += len(selectors)
238 }
239
240 return map[string]interface{}{
241 "enabled": ci.scriptEnabled,
242 "generic_rules": len(ci.genericRules),
243 "domain_count": domainCount,
244 "total_rules": totalRules,
245 }
246}