Files
gochat/scripts/extract_chatwoot_routes.py
T
2026-06-04 15:44:48 +08:00

137 lines
5.7 KiB
Python

import re, os, sys
# Parse Chatwoot routes.rb to extract all API routes
routes_rb = '/home/yanghao05/Workspace/chatwoot-reference/config/routes.rb'
with open(routes_rb) as f:
text = f.read()
# We need to parse nested namespace/resource blocks
# Strategy: manually parse the nested structure
# Known Chatwoot API route structure from routes.rb analysis:
# - /api/v1/ namespace with nested resources
# - /api/v2/ namespace
# - /public/ namespace
# - /widget/ namespace
# - Webhook routes
# - Auth routes
# Let's extract all controller#action pairs from the controllers directory
# and map them to expected routes
controllers_dir = '/home/yanghao05/Workspace/chatwoot-reference/app/controllers/api'
routes = []
for root, dirs, files in os.walk(controllers_dir):
for fname in files:
if fname.endswith('_controller.rb') and 'base_controller' not in fname and 'concerns' not in root:
fpath = os.path.join(root, fname)
# Convert file path to controller name
rel_path = os.path.relpath(fpath, '/home/yanghao05/Workspace/chatwoot-reference/app/controllers')
rel_path = rel_path.replace('.rb', '')
# Convert path separators to :: for Ruby module notation
controller_name = rel_path.replace('/', '::')
# Read the file to extract public action methods
with open(fpath) as cf:
controller_text = cf.read()
# Find all public method definitions (actions)
# In Rails controllers, actions are public methods defined with def
actions = []
in_private = False
in_protected = False
for line in controller_text.split('\n'):
stripped = line.strip()
if stripped == 'private' or stripped.startswith('private :'):
in_private = True
continue
if stripped == 'protected' or stripped.startswith('protected :'):
in_protected = True
continue
if re.match(r'def\s+\w+', stripped) and not in_private and not in_protected:
action_name = re.match(r'def\s+(\w+)', stripped).group(1)
# Skip internal/helper methods
if not action_name.startswith('_') and action_name not in ['initialize', 'setup', 'teardown']:
actions.append(action_name)
# Reset private/protected scope when encountering new visibility keyword
if stripped == 'public':
in_private = False
in_protected = False
for action in actions:
routes.append((controller_name, action))
# Now map controller names to API routes
# Standard Rails routing conventions:
# api/v1/accounts/conversations_controller -> /api/v1/accounts/:account_id/conversations
# api/v1/accounts/conversations/messages_controller -> /api/v1/accounts/:account_id/conversations/:conversation_id/messages
# Build route path from controller name
def controller_to_route_path(controller_name):
"""Convert Ruby controller name to expected route path prefix."""
parts = controller_name.split('::')
path_parts = []
resource_stack = []
for part in parts:
# Skip 'api' and 'v1'/'v2' - they become path prefixes
if part == 'api':
continue
if part in ('v1', 'v2'):
path_parts.append(f'/api/{part}')
continue
# Singularize: Rails uses plural for resources, singular for nested resource names
# Check if this is a nested singular resource (like 'contacts' inside conversations)
name = part
# Convert CamelCase to snake_case
name = re.sub(r'([A-Z])', r'_\1', name).lower().lstrip('_')
# Handle special cases
if name == 'captain':
path_parts.append('/captain')
continue
# This part represents a resource
# If it's a plural resource name, it gets :id parameter in routes
# Common plural resources
plural_resources = [
'accounts', 'conversations', 'contacts', 'inboxes', 'messages',
'agents', 'teams', 'team_members', 'labels', 'notifications',
'campaigns', 'automation_rules', 'canned_responses', 'webhooks',
'portals', 'categories', 'articles', 'csat_survey_responses',
'macros', 'custom_filters', 'custom_attribute_definitions',
'agent_bots', 'dashboard_apps', 'assignment_policies',
'sla_policies', 'custom_roles', 'whatsapp_calls',
'bulk_actions', 'inbox_members', 'notes',
'agent_capacity_policies', 'inbox_limits',
'copilot_threads', 'copilot_messages'
]
if name in plural_resources:
path_parts.append(f'/{name}')
path_parts.append(f'/:{"id" if name == "accounts" else name.replace("s", "") + "_id" if not name.endswith("es") else name[:-2] + "_id"}')
else:
path_parts.append(f'/{name}')
return ''.join(path_parts)
# Print summary
print("=== Chatwoot Controllers & Actions ===")
for controller, action in sorted(routes):
print(f" {controller}#{action}")
print(f"\nTotal controller actions: {len(routes)}")
# Also group by controller
controller_actions = {}
for controller, action in routes:
if controller not in controller_actions:
controller_actions[controller] = []
controller_actions[controller].append(action)
print(f"\nTotal controllers: {len(controller_actions)}")
for controller in sorted(controller_actions.keys()):
actions = controller_actions[controller]
print(f" {controller}: {len(actions)} actions -> {', '.join(actions)}")