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