webtap-tool 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of webtap-tool might be problematic. Click here for more details.
- webtap/VISION.md +234 -0
- webtap/__init__.py +56 -0
- webtap/api.py +222 -0
- webtap/app.py +76 -0
- webtap/cdp/README.md +268 -0
- webtap/cdp/__init__.py +14 -0
- webtap/cdp/query.py +107 -0
- webtap/cdp/schema/README.md +41 -0
- webtap/cdp/schema/cdp_protocol.json +32785 -0
- webtap/cdp/schema/cdp_version.json +8 -0
- webtap/cdp/session.py +365 -0
- webtap/commands/DEVELOPER_GUIDE.md +314 -0
- webtap/commands/TIPS.md +153 -0
- webtap/commands/__init__.py +7 -0
- webtap/commands/_builders.py +127 -0
- webtap/commands/_errors.py +108 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +227 -0
- webtap/commands/body.py +161 -0
- webtap/commands/connection.py +168 -0
- webtap/commands/console.py +69 -0
- webtap/commands/events.py +109 -0
- webtap/commands/fetch.py +219 -0
- webtap/commands/filters.py +224 -0
- webtap/commands/inspect.py +146 -0
- webtap/commands/javascript.py +87 -0
- webtap/commands/launch.py +86 -0
- webtap/commands/navigation.py +199 -0
- webtap/commands/network.py +85 -0
- webtap/commands/setup.py +127 -0
- webtap/filters.py +289 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/body.py +113 -0
- webtap/services/console.py +116 -0
- webtap/services/fetch.py +397 -0
- webtap/services/main.py +175 -0
- webtap/services/network.py +105 -0
- webtap/services/setup.py +219 -0
- webtap_tool-0.1.1.dist-info/METADATA +427 -0
- webtap_tool-0.1.1.dist-info/RECORD +43 -0
- webtap_tool-0.1.1.dist-info/WHEEL +4 -0
- webtap_tool-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""CDP event querying with dynamic field discovery."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.cdp import build_query
|
|
5
|
+
from webtap.commands._errors import check_connection
|
|
6
|
+
from webtap.commands._builders import table_response
|
|
7
|
+
from webtap.commands._tips import get_tips, get_mcp_description
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
mcp_desc = get_mcp_description("events")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command(
|
|
14
|
+
display="markdown",
|
|
15
|
+
truncate={
|
|
16
|
+
"Value": {"max": 80, "mode": "end"} # Only truncate values for display
|
|
17
|
+
},
|
|
18
|
+
fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"},
|
|
19
|
+
)
|
|
20
|
+
def events(state, filters: dict = None, limit: int = 20) -> dict: # pyright: ignore[reportArgumentType]
|
|
21
|
+
"""
|
|
22
|
+
Query CDP events by field values with automatic discovery.
|
|
23
|
+
|
|
24
|
+
Searches across ALL event types - network, console, page, etc.
|
|
25
|
+
Field names are discovered automatically and case-insensitive.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
filters: Field filters to apply
|
|
29
|
+
- {"method": "Network.*"} - Events matching pattern
|
|
30
|
+
- {"status": 200, "method": "Network.responseReceived"}
|
|
31
|
+
- {"url": "*"} - Extract field without filtering
|
|
32
|
+
limit: Maximum results (default: 20)
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
events() # Recent 20 events
|
|
36
|
+
events({"method": "Runtime.*"}) # Runtime events
|
|
37
|
+
events({"requestId": "123"}, limit=100) # Specific request
|
|
38
|
+
events({"url": "*api*"}) # Find all API calls
|
|
39
|
+
events({"status": 200}) # Find successful responses
|
|
40
|
+
events({"method": "POST", "url": "*login*"}) # POST requests to login
|
|
41
|
+
events({"level": "error"}) # Console errors
|
|
42
|
+
events({"type": "Document"}) # Page navigations
|
|
43
|
+
events({"headers": "*"}) # Extract all header fields
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Table showing rowid and extracted field values in markdown
|
|
47
|
+
"""
|
|
48
|
+
# Check connection - return error dict if not connected
|
|
49
|
+
if error := check_connection(state):
|
|
50
|
+
return error
|
|
51
|
+
|
|
52
|
+
# Use filters dict, default to empty
|
|
53
|
+
fields = filters or {}
|
|
54
|
+
|
|
55
|
+
# Build query using the query module with fuzzy field discovery
|
|
56
|
+
sql, discovered_fields = build_query(state.cdp, fields, limit=limit)
|
|
57
|
+
|
|
58
|
+
# If no fields discovered, return empty
|
|
59
|
+
if not discovered_fields or not any(discovered_fields.values()):
|
|
60
|
+
return table_response(
|
|
61
|
+
title="Event Query Results", headers=["ID", "Field", "Value"], rows=[], summary="No matching fields found"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Execute query
|
|
65
|
+
results = state.cdp.query(sql)
|
|
66
|
+
|
|
67
|
+
# Process results into rows with FULL data
|
|
68
|
+
rows = []
|
|
69
|
+
for result_row in results:
|
|
70
|
+
rowid = result_row[0]
|
|
71
|
+
col_index = 1 # Skip rowid column
|
|
72
|
+
|
|
73
|
+
for field_name, field_paths in discovered_fields.items():
|
|
74
|
+
for field_path in field_paths:
|
|
75
|
+
if col_index < len(result_row):
|
|
76
|
+
value = result_row[col_index]
|
|
77
|
+
if value is not None:
|
|
78
|
+
rows.append(
|
|
79
|
+
{
|
|
80
|
+
"ID": str(rowid),
|
|
81
|
+
"Field": field_path,
|
|
82
|
+
"Value": str(value), # Full value, no truncation
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
col_index += 1
|
|
86
|
+
|
|
87
|
+
# Build warnings if needed
|
|
88
|
+
warnings = []
|
|
89
|
+
if limit and len(results) == limit:
|
|
90
|
+
warnings.append(f"Showing first {limit} results (use limit parameter to see more)")
|
|
91
|
+
|
|
92
|
+
# Get tips from TIPS.md
|
|
93
|
+
tips = None
|
|
94
|
+
if rows:
|
|
95
|
+
# Get unique event IDs for examples
|
|
96
|
+
event_ids = list(set(row["ID"] for row in rows))[:1]
|
|
97
|
+
if event_ids:
|
|
98
|
+
example_id = event_ids[0]
|
|
99
|
+
tips = get_tips("events", context={"id": example_id})
|
|
100
|
+
|
|
101
|
+
# Build markdown response
|
|
102
|
+
return table_response(
|
|
103
|
+
title="Event Query Results",
|
|
104
|
+
headers=["ID", "Field", "Value"],
|
|
105
|
+
rows=rows,
|
|
106
|
+
summary=f"{len(rows)} field values from {len(results)} events",
|
|
107
|
+
warnings=warnings,
|
|
108
|
+
tips=tips,
|
|
109
|
+
)
|
webtap/commands/fetch.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""HTTP fetch request interception and debugging commands."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.commands._errors import check_connection
|
|
5
|
+
from webtap.commands._builders import error_response, info_response, table_response
|
|
6
|
+
from webtap.commands._tips import get_tips
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
10
|
+
def fetch(state, action: str, options: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
11
|
+
"""Control fetch interception.
|
|
12
|
+
|
|
13
|
+
When enabled, requests pause for inspection.
|
|
14
|
+
Use requests() to see paused items, resume() or fail() to proceed.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
action: Action to perform
|
|
18
|
+
- "enable" - Enable interception
|
|
19
|
+
- "disable" - Disable interception
|
|
20
|
+
- "status" - Get current status
|
|
21
|
+
options: Action-specific options
|
|
22
|
+
- For enable: {"response": true} - Also intercept responses
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
fetch("status") # Check status
|
|
26
|
+
fetch("enable") # Enable request stage
|
|
27
|
+
fetch("enable", {"response": true}) # Both stages
|
|
28
|
+
fetch("disable") # Disable
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Fetch interception status
|
|
32
|
+
"""
|
|
33
|
+
fetch_service = state.service.fetch
|
|
34
|
+
|
|
35
|
+
if action == "disable":
|
|
36
|
+
result = fetch_service.disable()
|
|
37
|
+
if "error" in result:
|
|
38
|
+
return error_response(result["error"])
|
|
39
|
+
return info_response(title="Fetch Disabled", fields={"Status": "Interception disabled"})
|
|
40
|
+
|
|
41
|
+
elif action == "enable":
|
|
42
|
+
# Check connection first
|
|
43
|
+
if error := check_connection(state):
|
|
44
|
+
return error
|
|
45
|
+
|
|
46
|
+
opts = options or {}
|
|
47
|
+
response_stage = opts.get("response", False)
|
|
48
|
+
|
|
49
|
+
result = fetch_service.enable(state.cdp, response_stage=response_stage)
|
|
50
|
+
if "error" in result:
|
|
51
|
+
return error_response(result["error"])
|
|
52
|
+
return info_response(
|
|
53
|
+
title="Fetch Enabled",
|
|
54
|
+
fields={
|
|
55
|
+
"Stages": result.get("stages", "Request stage only"),
|
|
56
|
+
"Status": "Requests will pause",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
elif action == "status":
|
|
61
|
+
# Show status
|
|
62
|
+
return info_response(
|
|
63
|
+
title=f"Fetch Status: {'Enabled' if fetch_service.enabled else 'Disabled'}",
|
|
64
|
+
fields={
|
|
65
|
+
"Status": "Enabled" if fetch_service.enabled else "Disabled",
|
|
66
|
+
"Paused": f"{fetch_service.paused_count} requests paused" if fetch_service.enabled else "None",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
return error_response(f"Unknown action: {action}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "application/json"})
|
|
75
|
+
def requests(state, limit: int = 50) -> dict:
|
|
76
|
+
"""Show paused requests and responses.
|
|
77
|
+
|
|
78
|
+
Lists all paused HTTP traffic. Use the ID with inspect() to examine
|
|
79
|
+
details or resume() / fail() to proceed.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
limit: Maximum items to show
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
requests() # Show all paused
|
|
86
|
+
inspect(event=47) # Examine request with rowid 47
|
|
87
|
+
resume(47) # Continue request 47
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Table of paused requests/responses in markdown
|
|
91
|
+
"""
|
|
92
|
+
# Check connection first
|
|
93
|
+
if error := check_connection(state):
|
|
94
|
+
return error
|
|
95
|
+
|
|
96
|
+
fetch_service = state.service.fetch
|
|
97
|
+
|
|
98
|
+
if not fetch_service.enabled:
|
|
99
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
100
|
+
|
|
101
|
+
rows = fetch_service.get_paused_list()
|
|
102
|
+
|
|
103
|
+
# Apply limit
|
|
104
|
+
if limit and len(rows) > limit:
|
|
105
|
+
rows = rows[:limit]
|
|
106
|
+
|
|
107
|
+
# Build warnings if needed
|
|
108
|
+
warnings = []
|
|
109
|
+
if limit and len(rows) == limit:
|
|
110
|
+
warnings.append(f"Showing first {limit} paused requests (use limit parameter to see more)")
|
|
111
|
+
|
|
112
|
+
# Get tips from TIPS.md
|
|
113
|
+
tips = None
|
|
114
|
+
if rows:
|
|
115
|
+
example_id = rows[0]["ID"]
|
|
116
|
+
tips = get_tips("requests", context={"id": example_id})
|
|
117
|
+
|
|
118
|
+
# Build markdown response
|
|
119
|
+
return table_response(
|
|
120
|
+
title="Paused Requests",
|
|
121
|
+
headers=["ID", "Stage", "Method", "Status", "URL"],
|
|
122
|
+
rows=rows,
|
|
123
|
+
summary=f"{len(rows)} requests paused",
|
|
124
|
+
warnings=warnings,
|
|
125
|
+
tips=tips,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
130
|
+
def resume(state, request: int, wait: float = 0.5, modifications: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
131
|
+
"""Resume a paused request.
|
|
132
|
+
|
|
133
|
+
For Request stage, can modify:
|
|
134
|
+
url, method, headers, postData
|
|
135
|
+
|
|
136
|
+
For Response stage, can modify:
|
|
137
|
+
responseCode, responseHeaders
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
request: Request row ID from requests() table
|
|
141
|
+
wait: Wait time for next event in seconds (default: 0.5)
|
|
142
|
+
modifications: Request/response modifications dict
|
|
143
|
+
- {"url": "..."} - Change URL
|
|
144
|
+
- {"method": "POST"} - Change method
|
|
145
|
+
- {"headers": [{"name": "X-Custom", "value": "test"}]} - Set headers
|
|
146
|
+
- {"responseCode": 404} - Change response code
|
|
147
|
+
- {"responseHeaders": [...]} - Modify response headers
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
resume(123) # Simple resume
|
|
151
|
+
resume(123, wait=1.0) # Wait for redirect
|
|
152
|
+
resume(123, modifications={"url": "..."}) # Change URL
|
|
153
|
+
resume(123, modifications={"method": "POST"}) # Change method
|
|
154
|
+
resume(123, modifications={"headers": [{"name":"X-Custom","value":"test"}]})
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Continuation status with any follow-up events detected
|
|
158
|
+
"""
|
|
159
|
+
fetch_service = state.service.fetch
|
|
160
|
+
|
|
161
|
+
if not fetch_service.enabled:
|
|
162
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
163
|
+
|
|
164
|
+
mods = modifications or {}
|
|
165
|
+
result = fetch_service.continue_request(request, mods, wait_for_next=wait)
|
|
166
|
+
|
|
167
|
+
if "error" in result:
|
|
168
|
+
return error_response(result["error"])
|
|
169
|
+
|
|
170
|
+
fields = {"Stage": result["stage"], "Continued": f"Row {result['continued']}"}
|
|
171
|
+
|
|
172
|
+
# Report follow-up if detected
|
|
173
|
+
if next_event := result.get("next_event"):
|
|
174
|
+
fields["Next Event"] = next_event["description"]
|
|
175
|
+
fields["Next ID"] = str(next_event["rowid"])
|
|
176
|
+
if next_event.get("status"):
|
|
177
|
+
fields["Status"] = next_event["status"]
|
|
178
|
+
|
|
179
|
+
if result.get("remaining"):
|
|
180
|
+
fields["Remaining"] = f"{result['remaining']} requests paused"
|
|
181
|
+
|
|
182
|
+
return info_response(title="Request Resumed", fields=fields)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
186
|
+
def fail(state, request: int, reason: str = "BlockedByClient") -> dict:
|
|
187
|
+
"""Fail a paused request.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
request: Row ID from requests() table
|
|
191
|
+
reason: CDP error reason (default: BlockedByClient)
|
|
192
|
+
Options: Failed, Aborted, TimedOut, AccessDenied,
|
|
193
|
+
ConnectionClosed, ConnectionReset, ConnectionRefused,
|
|
194
|
+
ConnectionAborted, ConnectionFailed, NameNotResolved,
|
|
195
|
+
InternetDisconnected, AddressUnreachable, BlockedByClient,
|
|
196
|
+
BlockedByResponse
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
fail(47) # Fail specific request
|
|
200
|
+
fail(47, reason="AccessDenied") # Fail with specific reason
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Failure status
|
|
204
|
+
"""
|
|
205
|
+
fetch_service = state.service.fetch
|
|
206
|
+
|
|
207
|
+
if not fetch_service.enabled:
|
|
208
|
+
return error_response("Fetch interception is disabled. Use fetch('enable') first.")
|
|
209
|
+
|
|
210
|
+
result = fetch_service.fail_request(request, reason)
|
|
211
|
+
|
|
212
|
+
if "error" in result:
|
|
213
|
+
return error_response(result["error"])
|
|
214
|
+
|
|
215
|
+
fields = {"Failed": f"Row {result['failed']}", "Reason": result["reason"]}
|
|
216
|
+
if result.get("remaining") is not None:
|
|
217
|
+
fields["Remaining"] = f"{result['remaining']} requests paused"
|
|
218
|
+
|
|
219
|
+
return info_response(title="Request Failed", fields=fields)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Network request filtering and categorization management commands."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.commands._builders import info_response, error_response
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
8
|
+
def filters(state, action: str = "list", config: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
9
|
+
"""
|
|
10
|
+
Manage network request filters.
|
|
11
|
+
|
|
12
|
+
Filters are managed by the service and can be persisted to .webtap/filters.json.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
action: Filter operation
|
|
16
|
+
- "list" - List all filter categories (default)
|
|
17
|
+
- "show" - Show specific category details
|
|
18
|
+
- "add" - Add patterns to category
|
|
19
|
+
- "remove" - Remove patterns from category
|
|
20
|
+
- "update" - Update entire category
|
|
21
|
+
- "delete" - Delete entire category
|
|
22
|
+
- "enable" - Enable category
|
|
23
|
+
- "disable" - Disable category
|
|
24
|
+
- "save" - Save filters to disk
|
|
25
|
+
- "load" - Load filters from disk
|
|
26
|
+
config: Action-specific configuration
|
|
27
|
+
- For show: {"category": "ads"}
|
|
28
|
+
- For add: {"category": "ads", "patterns": ["*ad*"], "type": "domain"}
|
|
29
|
+
- For remove: {"patterns": ["*ad*"], "type": "domain"}
|
|
30
|
+
- For update: {"category": "ads", "domains": [...], "types": [...]}
|
|
31
|
+
- For delete/enable/disable: {"category": "ads"}
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
filters() # List all categories
|
|
35
|
+
filters("list") # Same as above
|
|
36
|
+
filters("show", {"category": "ads"}) # Show ads category details
|
|
37
|
+
filters("add", {"category": "ads",
|
|
38
|
+
"patterns": ["*doubleclick*"]}) # Add domain pattern
|
|
39
|
+
filters("add", {"category": "tracking",
|
|
40
|
+
"patterns": ["Ping"], "type": "type"}) # Add type pattern
|
|
41
|
+
filters("remove", {"patterns": ["*doubleclick*"]}) # Remove pattern
|
|
42
|
+
filters("update", {"category": "ads",
|
|
43
|
+
"domains": ["*google*", "*facebook*"]}) # Replace patterns
|
|
44
|
+
filters("delete", {"category": "ads"}) # Delete category
|
|
45
|
+
filters("save") # Persist to disk
|
|
46
|
+
filters("load") # Load from disk
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Current filter configuration or operation result
|
|
50
|
+
"""
|
|
51
|
+
fm = state.service.filters
|
|
52
|
+
cfg = config or {}
|
|
53
|
+
|
|
54
|
+
# Handle load operation
|
|
55
|
+
if action == "load":
|
|
56
|
+
if fm.load():
|
|
57
|
+
# Convert display info to markdown
|
|
58
|
+
display_info = fm.get_display_info()
|
|
59
|
+
return {
|
|
60
|
+
"elements": [
|
|
61
|
+
{"type": "heading", "content": "Filters Loaded", "level": 2},
|
|
62
|
+
{"type": "code_block", "content": display_info, "language": ""},
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
else:
|
|
66
|
+
return error_response(f"No filters found at {fm.filter_path}")
|
|
67
|
+
|
|
68
|
+
# Handle save operation
|
|
69
|
+
elif action == "save":
|
|
70
|
+
if fm.save():
|
|
71
|
+
return info_response(
|
|
72
|
+
title="Filters Saved", fields={"Categories": f"{len(fm.filters)}", "Path": str(fm.filter_path)}
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
return error_response("Failed to save filters")
|
|
76
|
+
|
|
77
|
+
# Handle add operation
|
|
78
|
+
elif action == "add":
|
|
79
|
+
if not cfg:
|
|
80
|
+
return error_response("Config required for add action")
|
|
81
|
+
|
|
82
|
+
category = cfg.get("category", "custom")
|
|
83
|
+
patterns = cfg.get("patterns", [])
|
|
84
|
+
pattern_type = cfg.get("type", "domain")
|
|
85
|
+
|
|
86
|
+
if not patterns:
|
|
87
|
+
# Legacy single pattern support
|
|
88
|
+
if pattern_type == "domain" and "domain" in cfg:
|
|
89
|
+
patterns = [cfg["domain"]]
|
|
90
|
+
elif pattern_type == "type" and "type" in cfg:
|
|
91
|
+
patterns = [cfg["type"]]
|
|
92
|
+
else:
|
|
93
|
+
return error_response("Patterns required for add action")
|
|
94
|
+
|
|
95
|
+
added = []
|
|
96
|
+
failed = []
|
|
97
|
+
for pattern in patterns:
|
|
98
|
+
if fm.add_pattern(pattern, category, pattern_type):
|
|
99
|
+
added.append(pattern)
|
|
100
|
+
else:
|
|
101
|
+
failed.append(pattern)
|
|
102
|
+
|
|
103
|
+
if added and not failed:
|
|
104
|
+
return info_response(
|
|
105
|
+
title="Filter(s) Added",
|
|
106
|
+
fields={
|
|
107
|
+
"Type": "Domain pattern" if pattern_type == "domain" else "Resource type",
|
|
108
|
+
"Patterns": ", ".join(added),
|
|
109
|
+
"Category": category,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
elif failed:
|
|
113
|
+
return error_response(f"Pattern(s) already exist in category '{category}': {', '.join(failed)}")
|
|
114
|
+
else:
|
|
115
|
+
# This shouldn't happen unless patterns list was empty after all
|
|
116
|
+
return error_response("No valid patterns provided")
|
|
117
|
+
|
|
118
|
+
# Handle remove operation
|
|
119
|
+
elif action == "remove":
|
|
120
|
+
if not cfg:
|
|
121
|
+
return error_response("Config required for remove action")
|
|
122
|
+
|
|
123
|
+
patterns = cfg.get("patterns", [])
|
|
124
|
+
pattern_type = cfg.get("type", "domain")
|
|
125
|
+
|
|
126
|
+
if not patterns:
|
|
127
|
+
return error_response("Patterns required for remove action")
|
|
128
|
+
|
|
129
|
+
removed = []
|
|
130
|
+
for pattern in patterns:
|
|
131
|
+
category = fm.remove_pattern(pattern, pattern_type)
|
|
132
|
+
if category:
|
|
133
|
+
removed.append((pattern, category))
|
|
134
|
+
|
|
135
|
+
if removed:
|
|
136
|
+
return info_response(
|
|
137
|
+
title="Filter(s) Removed",
|
|
138
|
+
fields={
|
|
139
|
+
"Type": "Domain pattern" if pattern_type == "domain" else "Resource type",
|
|
140
|
+
"Removed": ", ".join(f"{p} from {c}" for p, c in removed),
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
return error_response("Pattern(s) not found")
|
|
145
|
+
|
|
146
|
+
# Handle update operation
|
|
147
|
+
elif action == "update":
|
|
148
|
+
if not cfg or "category" not in cfg:
|
|
149
|
+
return error_response("'category' required for update action")
|
|
150
|
+
|
|
151
|
+
category = cfg["category"]
|
|
152
|
+
fm.update_category(category, domains=cfg.get("domains"), types=cfg.get("types"))
|
|
153
|
+
return info_response(title="Category Updated", fields={"Category": category})
|
|
154
|
+
|
|
155
|
+
# Handle delete operation
|
|
156
|
+
elif action == "delete":
|
|
157
|
+
if not cfg or "category" not in cfg:
|
|
158
|
+
return error_response("'category' required for delete action")
|
|
159
|
+
|
|
160
|
+
category = cfg["category"]
|
|
161
|
+
if fm.delete_category(category):
|
|
162
|
+
return info_response(title="Category Deleted", fields={"Category": category})
|
|
163
|
+
return error_response(f"Category '{category}' not found")
|
|
164
|
+
|
|
165
|
+
# Handle enable operation
|
|
166
|
+
elif action == "enable":
|
|
167
|
+
if not cfg or "category" not in cfg:
|
|
168
|
+
return error_response("'category' required for enable action")
|
|
169
|
+
|
|
170
|
+
category = cfg["category"]
|
|
171
|
+
if category in fm.filters:
|
|
172
|
+
fm.enabled_categories.add(category)
|
|
173
|
+
return info_response(title="Category Enabled", fields={"Category": category})
|
|
174
|
+
return error_response(f"Category '{category}' not found")
|
|
175
|
+
|
|
176
|
+
# Handle disable operation
|
|
177
|
+
elif action == "disable":
|
|
178
|
+
if not cfg or "category" not in cfg:
|
|
179
|
+
return error_response("'category' required for disable action")
|
|
180
|
+
|
|
181
|
+
category = cfg["category"]
|
|
182
|
+
if category in fm.filters:
|
|
183
|
+
fm.enabled_categories.discard(category)
|
|
184
|
+
return info_response(title="Category Disabled", fields={"Category": category})
|
|
185
|
+
return error_response(f"Category '{category}' not found")
|
|
186
|
+
|
|
187
|
+
# Handle show operation (specific category)
|
|
188
|
+
elif action == "show":
|
|
189
|
+
if not cfg or "category" not in cfg:
|
|
190
|
+
return error_response("'category' required for show action")
|
|
191
|
+
|
|
192
|
+
category = cfg["category"]
|
|
193
|
+
if category in fm.filters:
|
|
194
|
+
filters = fm.filters[category]
|
|
195
|
+
enabled = "Enabled" if category in fm.enabled_categories else "Disabled"
|
|
196
|
+
|
|
197
|
+
elements = [
|
|
198
|
+
{"type": "heading", "content": f"Category: {category}", "level": 2},
|
|
199
|
+
{"type": "text", "content": f"**Status:** {enabled}"},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
if filters.get("domains"):
|
|
203
|
+
elements.append({"type": "text", "content": "**Domain Patterns:**"})
|
|
204
|
+
elements.append({"type": "list", "items": filters["domains"]})
|
|
205
|
+
|
|
206
|
+
if filters.get("types"):
|
|
207
|
+
elements.append({"type": "text", "content": "**Resource Types:**"})
|
|
208
|
+
elements.append({"type": "list", "items": filters["types"]})
|
|
209
|
+
|
|
210
|
+
return {"elements": elements}
|
|
211
|
+
return error_response(f"Category '{category}' not found")
|
|
212
|
+
|
|
213
|
+
# Default list action: show all filters
|
|
214
|
+
elif action == "list" or action == "":
|
|
215
|
+
display_info = fm.get_display_info()
|
|
216
|
+
return {
|
|
217
|
+
"elements": [
|
|
218
|
+
{"type": "heading", "content": "Filter Configuration", "level": 2},
|
|
219
|
+
{"type": "code_block", "content": display_info, "language": ""},
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
return error_response(f"Unknown action: {action}")
|