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)}")