Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
72
plugins/wordpress/handlers/__init__.py
Normal file
72
plugins/wordpress/handlers/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
WordPress Handlers
|
||||
|
||||
Modular handlers for WordPress functionality.
|
||||
Each handler is responsible for a specific domain of WordPress operations.
|
||||
|
||||
Part of Option B clean architecture refactoring.
|
||||
"""
|
||||
|
||||
from plugins.wordpress.handlers.comments import CommentsHandler
|
||||
from plugins.wordpress.handlers.comments import get_tool_specifications as get_comments_specs
|
||||
from plugins.wordpress.handlers.coupons import CouponsHandler
|
||||
from plugins.wordpress.handlers.coupons import get_tool_specifications as get_coupons_specs
|
||||
from plugins.wordpress.handlers.customers import CustomersHandler
|
||||
from plugins.wordpress.handlers.customers import get_tool_specifications as get_customers_specs
|
||||
from plugins.wordpress.handlers.media import MediaHandler
|
||||
from plugins.wordpress.handlers.media import get_tool_specifications as get_media_specs
|
||||
from plugins.wordpress.handlers.menus import MenusHandler
|
||||
from plugins.wordpress.handlers.menus import get_tool_specifications as get_menus_specs
|
||||
from plugins.wordpress.handlers.orders import OrdersHandler
|
||||
from plugins.wordpress.handlers.orders import get_tool_specifications as get_orders_specs
|
||||
from plugins.wordpress.handlers.posts import PostsHandler
|
||||
from plugins.wordpress.handlers.posts import get_tool_specifications as get_posts_specs
|
||||
from plugins.wordpress.handlers.products import ProductsHandler
|
||||
from plugins.wordpress.handlers.products import get_tool_specifications as get_products_specs
|
||||
from plugins.wordpress.handlers.reports import ReportsHandler
|
||||
from plugins.wordpress.handlers.reports import get_tool_specifications as get_reports_specs
|
||||
from plugins.wordpress.handlers.seo import SEOHandler
|
||||
from plugins.wordpress.handlers.seo import get_tool_specifications as get_seo_specs
|
||||
from plugins.wordpress.handlers.site import SiteHandler
|
||||
from plugins.wordpress.handlers.site import get_tool_specifications as get_site_specs
|
||||
from plugins.wordpress.handlers.taxonomy import TaxonomyHandler
|
||||
from plugins.wordpress.handlers.taxonomy import get_tool_specifications as get_taxonomy_specs
|
||||
from plugins.wordpress.handlers.users import UsersHandler
|
||||
from plugins.wordpress.handlers.users import get_tool_specifications as get_users_specs
|
||||
from plugins.wordpress.handlers.wp_cli import WPCLIHandler
|
||||
from plugins.wordpress.handlers.wp_cli import get_tool_specifications as get_wp_cli_specs
|
||||
|
||||
__all__ = [
|
||||
# Core Handlers
|
||||
"PostsHandler",
|
||||
"MediaHandler",
|
||||
"TaxonomyHandler",
|
||||
"CommentsHandler",
|
||||
"UsersHandler",
|
||||
"SiteHandler",
|
||||
# WooCommerce Handlers
|
||||
"ProductsHandler",
|
||||
"OrdersHandler",
|
||||
"CustomersHandler",
|
||||
"ReportsHandler",
|
||||
"CouponsHandler",
|
||||
# Advanced Handlers
|
||||
"SEOHandler",
|
||||
"WPCLIHandler",
|
||||
"MenusHandler",
|
||||
# Tool specifications
|
||||
"get_posts_specs",
|
||||
"get_media_specs",
|
||||
"get_taxonomy_specs",
|
||||
"get_comments_specs",
|
||||
"get_users_specs",
|
||||
"get_site_specs",
|
||||
"get_products_specs",
|
||||
"get_orders_specs",
|
||||
"get_customers_specs",
|
||||
"get_reports_specs",
|
||||
"get_coupons_specs",
|
||||
"get_seo_specs",
|
||||
"get_wp_cli_specs",
|
||||
"get_menus_specs",
|
||||
]
|
||||
365
plugins/wordpress/handlers/comments.py
Normal file
365
plugins/wordpress/handlers/comments.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""Comments Handler - manages WordPress comments and moderation"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === COMMENTS ===
|
||||
{
|
||||
"name": "list_comments",
|
||||
"method_name": "list_comments",
|
||||
"description": "List WordPress comments. Returns paginated list of comments with author, content, and moderation status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of comments per page (1-100)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"post_id": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Filter by post ID (optional)",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Filter by comment status",
|
||||
"enum": ["approve", "hold", "spam", "trash", "all"],
|
||||
"default": "approve",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_comment",
|
||||
"method_name": "get_comment",
|
||||
"description": "Get a specific WordPress comment by ID. Returns complete comment data including content, author, and moderation status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment_id": {
|
||||
"type": "integer",
|
||||
"description": "Comment ID to retrieve",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["comment_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_comment",
|
||||
"method_name": "create_comment",
|
||||
"description": "Create a new WordPress comment on a post. Supports author information and moderation status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"post_id": {
|
||||
"type": "integer",
|
||||
"description": "Post ID to attach comment to",
|
||||
"minimum": 1,
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Comment content/text",
|
||||
"minLength": 1,
|
||||
},
|
||||
"author_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Author name (for unauthenticated comments)",
|
||||
},
|
||||
"author_email": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Author email address",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Comment moderation status",
|
||||
"enum": ["approve", "hold", "spam"],
|
||||
"default": "hold",
|
||||
},
|
||||
},
|
||||
"required": ["post_id", "content"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_comment",
|
||||
"method_name": "update_comment",
|
||||
"description": "Update an existing WordPress comment. Can update content, status, author information, etc.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment_id": {
|
||||
"type": "integer",
|
||||
"description": "Comment ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"content": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Comment content/text",
|
||||
},
|
||||
"author_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Author name",
|
||||
},
|
||||
"author_email": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Author email",
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Comment moderation status",
|
||||
"enum": ["approve", "hold", "spam"],
|
||||
},
|
||||
},
|
||||
"required": ["comment_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_comment",
|
||||
"method_name": "delete_comment",
|
||||
"description": "Delete or trash a WordPress comment. Can permanently delete or move to trash.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment_id": {
|
||||
"type": "integer",
|
||||
"description": "Comment ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Permanently delete (true) or move to trash (false)",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["comment_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class CommentsHandler:
|
||||
"""Handle comment-related operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize comments handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def list_comments(
|
||||
self, per_page: int = 10, page: int = 1, post_id: int | None = None, status: str = "approve"
|
||||
) -> str:
|
||||
"""
|
||||
List WordPress comments.
|
||||
|
||||
Args:
|
||||
per_page: Number of comments per page (1-100)
|
||||
page: Page number
|
||||
post_id: Optional filter by post ID
|
||||
status: Comment status filter (approve, hold, spam, trash, all)
|
||||
|
||||
Returns:
|
||||
JSON string with comments list
|
||||
"""
|
||||
try:
|
||||
params = {"per_page": per_page, "page": page, "status": status}
|
||||
if post_id:
|
||||
params["post"] = post_id
|
||||
|
||||
comments = await self.client.get("comments", params=params)
|
||||
|
||||
# Format response
|
||||
result = {
|
||||
"total": len(comments),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"comments": [
|
||||
{
|
||||
"id": c["id"],
|
||||
"post_id": c["post"],
|
||||
"author_name": c["author_name"],
|
||||
"author_email": c.get("author_email", ""),
|
||||
"content": c["content"]["rendered"][:200],
|
||||
"status": c["status"],
|
||||
"date": c["date"],
|
||||
"link": c.get("link", ""),
|
||||
}
|
||||
for c in comments
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list comments: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_comment(self, comment_id: int) -> str:
|
||||
"""
|
||||
Get a specific WordPress comment.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string with comment data
|
||||
"""
|
||||
try:
|
||||
comment = await self.client.get(f"comments/{comment_id}")
|
||||
|
||||
result = {
|
||||
"id": comment["id"],
|
||||
"post_id": comment["post"],
|
||||
"author_name": comment["author_name"],
|
||||
"author_email": comment.get("author_email", ""),
|
||||
"author_url": comment.get("author_url", ""),
|
||||
"content": comment["content"]["rendered"],
|
||||
"status": comment["status"],
|
||||
"date": comment["date"],
|
||||
"link": comment.get("link", ""),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get comment {comment_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def create_comment(
|
||||
self,
|
||||
post_id: int,
|
||||
content: str,
|
||||
author_name: str | None = None,
|
||||
author_email: str | None = None,
|
||||
status: str = "hold",
|
||||
) -> str:
|
||||
"""
|
||||
Create a new WordPress comment.
|
||||
|
||||
Args:
|
||||
post_id: Post ID to attach comment to
|
||||
content: Comment content/text
|
||||
author_name: Author name (for unauthenticated comments)
|
||||
author_email: Author email address
|
||||
status: Comment moderation status (approve, hold, spam)
|
||||
|
||||
Returns:
|
||||
JSON string with created comment data
|
||||
"""
|
||||
try:
|
||||
data = {"post": post_id, "content": content, "status": status}
|
||||
if author_name:
|
||||
data["author_name"] = author_name
|
||||
if author_email:
|
||||
data["author_email"] = author_email
|
||||
|
||||
comment = await self.client.post("comments", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": comment["id"],
|
||||
"post_id": comment["post"],
|
||||
"author_name": comment["author_name"],
|
||||
"content": comment["content"]["rendered"],
|
||||
"status": comment["status"],
|
||||
"message": f"Comment created successfully with ID {comment['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create comment: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_comment(
|
||||
self,
|
||||
comment_id: int,
|
||||
content: str | None = None,
|
||||
author_name: str | None = None,
|
||||
author_email: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing WordPress comment.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID to update
|
||||
content: Comment content/text
|
||||
author_name: Author name
|
||||
author_email: Author email
|
||||
status: Comment moderation status
|
||||
|
||||
Returns:
|
||||
JSON string with updated comment data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if content is not None:
|
||||
data["content"] = content
|
||||
if author_name is not None:
|
||||
data["author_name"] = author_name
|
||||
if author_email is not None:
|
||||
data["author_email"] = author_email
|
||||
if status is not None:
|
||||
data["status"] = status
|
||||
|
||||
comment = await self.client.post(f"comments/{comment_id}", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": comment["id"],
|
||||
"post_id": comment["post"],
|
||||
"content": comment["content"]["rendered"],
|
||||
"status": comment["status"],
|
||||
"message": f"Comment {comment_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update comment {comment_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def delete_comment(self, comment_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete or trash a WordPress comment.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID to delete
|
||||
force: Permanently delete (True) or move to trash (False)
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(f"comments/{comment_id}", params=params)
|
||||
|
||||
message = f"Comment {comment_id} {'permanently deleted' if force else 'moved to trash'}"
|
||||
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete comment {comment_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
495
plugins/wordpress/handlers/coupons.py
Normal file
495
plugins/wordpress/handlers/coupons.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""Coupons Handler - manages WooCommerce coupons"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
{
|
||||
"name": "list_coupons",
|
||||
"method_name": "list_coupons",
|
||||
"description": "List WooCommerce coupons. Returns paginated coupon list with discount details and usage restrictions.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of coupons per page (1-100)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Search term to filter coupons by code",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_coupon",
|
||||
"method_name": "create_coupon",
|
||||
"description": "Create a new WooCommerce coupon. Supports percentage and fixed discounts with usage limits and restrictions.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Coupon code (e.g., 'SAVE20')",
|
||||
"minLength": 1,
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"description": "Discount amount (e.g., '20' for 20% or $20)",
|
||||
},
|
||||
"discount_type": {
|
||||
"type": "string",
|
||||
"description": "Type of discount",
|
||||
"enum": ["percent", "fixed_cart", "fixed_product"],
|
||||
"default": "percent",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Coupon description (internal note)",
|
||||
},
|
||||
"date_expires": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Expiration date in ISO 8601 format (e.g., '2024-12-31T23:59:59')",
|
||||
},
|
||||
"minimum_amount": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Minimum order amount required to use coupon",
|
||||
},
|
||||
"maximum_amount": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Maximum order amount allowed to use coupon",
|
||||
},
|
||||
"individual_use": {
|
||||
"type": "boolean",
|
||||
"description": "If true, coupon cannot be combined with other coupons",
|
||||
"default": False,
|
||||
},
|
||||
"product_ids": {
|
||||
"anyOf": [
|
||||
{"type": "array", "items": {"type": "integer"}},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Array of product IDs coupon applies to",
|
||||
},
|
||||
"excluded_product_ids": {
|
||||
"anyOf": [
|
||||
{"type": "array", "items": {"type": "integer"}},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Array of product IDs coupon does NOT apply to",
|
||||
},
|
||||
"usage_limit": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum number of times coupon can be used",
|
||||
},
|
||||
"usage_limit_per_user": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum number of times coupon can be used per user",
|
||||
},
|
||||
"limit_usage_to_x_items": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum number of items coupon applies to",
|
||||
},
|
||||
"free_shipping": {
|
||||
"type": "boolean",
|
||||
"description": "If true, grants free shipping",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["code", "amount"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_coupon",
|
||||
"method_name": "update_coupon",
|
||||
"description": "Update an existing WooCommerce coupon. Can update discount amount, restrictions, and expiration.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coupon_id": {
|
||||
"type": "integer",
|
||||
"description": "Coupon ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"code": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Coupon code (e.g., 'SAVE20')",
|
||||
},
|
||||
"discount_type": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Type of discount",
|
||||
"enum": ["percent", "fixed_cart", "fixed_product"],
|
||||
},
|
||||
"amount": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Discount amount",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Coupon description (internal note)",
|
||||
},
|
||||
"date_expires": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Expiration date in ISO 8601 format",
|
||||
},
|
||||
"minimum_amount": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Minimum order amount",
|
||||
},
|
||||
"maximum_amount": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Maximum order amount",
|
||||
},
|
||||
"individual_use": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "If true, coupon cannot be combined with other coupons",
|
||||
},
|
||||
"product_ids": {
|
||||
"anyOf": [
|
||||
{"type": "array", "items": {"type": "integer"}},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Array of product IDs coupon applies to",
|
||||
},
|
||||
"excluded_product_ids": {
|
||||
"anyOf": [
|
||||
{"type": "array", "items": {"type": "integer"}},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Array of product IDs coupon does NOT apply to",
|
||||
},
|
||||
"usage_limit": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum number of uses",
|
||||
},
|
||||
"usage_limit_per_user": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum uses per user",
|
||||
},
|
||||
"limit_usage_to_x_items": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Maximum number of items coupon applies to",
|
||||
},
|
||||
"free_shipping": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "If true, grants free shipping",
|
||||
},
|
||||
},
|
||||
"required": ["coupon_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_coupon",
|
||||
"method_name": "delete_coupon",
|
||||
"description": "Delete a WooCommerce coupon. Can force delete or move to trash.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coupon_id": {
|
||||
"type": "integer",
|
||||
"description": "Coupon ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Force permanent delete (bypass trash)",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["coupon_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class CouponsHandler:
|
||||
"""Handle coupon-related operations for WooCommerce"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize coupons handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def list_coupons(
|
||||
self, per_page: int = 10, page: int = 1, search: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
List WooCommerce coupons.
|
||||
|
||||
Args:
|
||||
per_page: Number of coupons per page (1-100)
|
||||
page: Page number
|
||||
search: Search term to filter coupons by code
|
||||
|
||||
Returns:
|
||||
JSON string with coupons list
|
||||
"""
|
||||
try:
|
||||
params = {"per_page": per_page, "page": page}
|
||||
if search:
|
||||
params["search"] = search
|
||||
|
||||
coupons = await self.client.get("coupons", params=params, use_woocommerce=True)
|
||||
|
||||
result = {
|
||||
"total": len(coupons),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"coupons": [
|
||||
{
|
||||
"id": coupon["id"],
|
||||
"code": coupon["code"],
|
||||
"discount_type": coupon["discount_type"],
|
||||
"amount": coupon["amount"],
|
||||
"description": coupon.get("description", ""),
|
||||
"date_expires": coupon.get("date_expires"),
|
||||
"usage_count": coupon.get("usage_count", 0),
|
||||
"usage_limit": coupon.get("usage_limit"),
|
||||
"individual_use": coupon.get("individual_use", False),
|
||||
"free_shipping": coupon.get("free_shipping", False),
|
||||
"minimum_amount": coupon.get("minimum_amount", "0"),
|
||||
"maximum_amount": coupon.get("maximum_amount", "0"),
|
||||
}
|
||||
for coupon in coupons
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list coupons: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def create_coupon(
|
||||
self,
|
||||
code: str,
|
||||
amount: str,
|
||||
discount_type: str = "percent",
|
||||
description: str | None = None,
|
||||
date_expires: str | None = None,
|
||||
minimum_amount: str | None = None,
|
||||
maximum_amount: str | None = None,
|
||||
individual_use: bool = False,
|
||||
product_ids: list[int] | None = None,
|
||||
excluded_product_ids: list[int] | None = None,
|
||||
usage_limit: int | None = None,
|
||||
usage_limit_per_user: int | None = None,
|
||||
limit_usage_to_x_items: int | None = None,
|
||||
free_shipping: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Create a new WooCommerce coupon.
|
||||
|
||||
Args:
|
||||
code: Coupon code (e.g., 'SAVE20')
|
||||
amount: Discount amount (e.g., '20' for 20% or $20)
|
||||
discount_type: Type of discount (percent, fixed_cart, fixed_product)
|
||||
description: Coupon description (internal note)
|
||||
date_expires: Expiration date in ISO 8601 format
|
||||
minimum_amount: Minimum order amount required
|
||||
maximum_amount: Maximum order amount allowed
|
||||
individual_use: If true, coupon cannot be combined with others
|
||||
product_ids: Product IDs coupon applies to
|
||||
excluded_product_ids: Product IDs coupon does NOT apply to
|
||||
usage_limit: Maximum number of times coupon can be used
|
||||
usage_limit_per_user: Maximum uses per user
|
||||
limit_usage_to_x_items: Maximum number of items coupon applies to
|
||||
free_shipping: If true, grants free shipping
|
||||
|
||||
Returns:
|
||||
JSON string with created coupon data
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
"code": code,
|
||||
"discount_type": discount_type,
|
||||
"amount": amount,
|
||||
"individual_use": individual_use,
|
||||
"free_shipping": free_shipping,
|
||||
}
|
||||
|
||||
# Add optional fields
|
||||
if description:
|
||||
data["description"] = description
|
||||
if date_expires:
|
||||
data["date_expires"] = date_expires
|
||||
if minimum_amount:
|
||||
data["minimum_amount"] = minimum_amount
|
||||
if maximum_amount:
|
||||
data["maximum_amount"] = maximum_amount
|
||||
if product_ids:
|
||||
data["product_ids"] = product_ids
|
||||
if excluded_product_ids:
|
||||
data["excluded_product_ids"] = excluded_product_ids
|
||||
if usage_limit:
|
||||
data["usage_limit"] = usage_limit
|
||||
if usage_limit_per_user:
|
||||
data["usage_limit_per_user"] = usage_limit_per_user
|
||||
if limit_usage_to_x_items:
|
||||
data["limit_usage_to_x_items"] = limit_usage_to_x_items
|
||||
|
||||
coupon = await self.client.post("coupons", json_data=data, use_woocommerce=True)
|
||||
|
||||
result = {
|
||||
"id": coupon["id"],
|
||||
"code": coupon["code"],
|
||||
"discount_type": coupon["discount_type"],
|
||||
"amount": coupon["amount"],
|
||||
"date_expires": coupon.get("date_expires"),
|
||||
"message": f"Coupon '{code}' created successfully with ID {coupon['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create coupon: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_coupon(
|
||||
self,
|
||||
coupon_id: int,
|
||||
code: str | None = None,
|
||||
discount_type: str | None = None,
|
||||
amount: str | None = None,
|
||||
description: str | None = None,
|
||||
date_expires: str | None = None,
|
||||
minimum_amount: str | None = None,
|
||||
maximum_amount: str | None = None,
|
||||
individual_use: bool | None = None,
|
||||
product_ids: list[int] | None = None,
|
||||
excluded_product_ids: list[int] | None = None,
|
||||
usage_limit: int | None = None,
|
||||
usage_limit_per_user: int | None = None,
|
||||
limit_usage_to_x_items: int | None = None,
|
||||
free_shipping: bool | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing WooCommerce coupon.
|
||||
|
||||
Args:
|
||||
coupon_id: Coupon ID to update
|
||||
code: Coupon code
|
||||
discount_type: Type of discount
|
||||
amount: Discount amount
|
||||
description: Coupon description
|
||||
date_expires: Expiration date in ISO 8601 format
|
||||
minimum_amount: Minimum order amount
|
||||
maximum_amount: Maximum order amount
|
||||
individual_use: If true, coupon cannot be combined with others
|
||||
product_ids: Product IDs coupon applies to
|
||||
excluded_product_ids: Product IDs coupon does NOT apply to
|
||||
usage_limit: Maximum number of uses
|
||||
usage_limit_per_user: Maximum uses per user
|
||||
limit_usage_to_x_items: Maximum number of items coupon applies to
|
||||
free_shipping: If true, grants free shipping
|
||||
|
||||
Returns:
|
||||
JSON string with updated coupon data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if code is not None:
|
||||
data["code"] = code
|
||||
if discount_type is not None:
|
||||
data["discount_type"] = discount_type
|
||||
if amount is not None:
|
||||
data["amount"] = amount
|
||||
if description is not None:
|
||||
data["description"] = description
|
||||
if date_expires is not None:
|
||||
data["date_expires"] = date_expires
|
||||
if minimum_amount is not None:
|
||||
data["minimum_amount"] = minimum_amount
|
||||
if maximum_amount is not None:
|
||||
data["maximum_amount"] = maximum_amount
|
||||
if individual_use is not None:
|
||||
data["individual_use"] = individual_use
|
||||
if product_ids is not None:
|
||||
data["product_ids"] = product_ids
|
||||
if excluded_product_ids is not None:
|
||||
data["excluded_product_ids"] = excluded_product_ids
|
||||
if usage_limit is not None:
|
||||
data["usage_limit"] = usage_limit
|
||||
if usage_limit_per_user is not None:
|
||||
data["usage_limit_per_user"] = usage_limit_per_user
|
||||
if limit_usage_to_x_items is not None:
|
||||
data["limit_usage_to_x_items"] = limit_usage_to_x_items
|
||||
if free_shipping is not None:
|
||||
data["free_shipping"] = free_shipping
|
||||
|
||||
if not data:
|
||||
raise Exception("No update data provided")
|
||||
|
||||
coupon = await self.client.put(
|
||||
f"coupons/{coupon_id}", json_data=data, use_woocommerce=True
|
||||
)
|
||||
|
||||
result = {
|
||||
"id": coupon["id"],
|
||||
"code": coupon["code"],
|
||||
"discount_type": coupon["discount_type"],
|
||||
"amount": coupon["amount"],
|
||||
"message": f"Coupon ID {coupon_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update coupon {coupon_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def delete_coupon(self, coupon_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete a WooCommerce coupon.
|
||||
|
||||
Args:
|
||||
coupon_id: Coupon ID to delete
|
||||
force: Force permanent delete (True) or move to trash (False)
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(
|
||||
f"coupons/{coupon_id}", params=params, use_woocommerce=True
|
||||
)
|
||||
|
||||
action = "permanently deleted" if force else "moved to trash"
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"coupon_id": coupon_id,
|
||||
"message": f"Coupon {action} successfully",
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete coupon {coupon_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
373
plugins/wordpress/handlers/customers.py
Normal file
373
plugins/wordpress/handlers/customers.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""Customers Handler - manages WooCommerce customers"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
{
|
||||
"name": "list_customers",
|
||||
"method_name": "list_customers",
|
||||
"description": "List WooCommerce customers. Returns paginated customer list with email, orders count, and total spent.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of customers per page (1-100)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Search by name or email",
|
||||
},
|
||||
"email": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter by specific email address",
|
||||
},
|
||||
"role": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter by role (customer, subscriber, etc.)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_customer",
|
||||
"method_name": "get_customer",
|
||||
"description": "Get detailed information about a specific WooCommerce customer. Returns customer details, billing, shipping, and order history.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customer_id": {
|
||||
"type": "integer",
|
||||
"description": "Customer ID to retrieve",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["customer_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_customer",
|
||||
"method_name": "create_customer",
|
||||
"description": "Create a new WooCommerce customer. Requires email, optionally includes name, username, password, billing, and shipping address.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Customer email address (required)",
|
||||
"minLength": 1,
|
||||
},
|
||||
"first_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Customer first name",
|
||||
},
|
||||
"last_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Customer last name",
|
||||
},
|
||||
"username": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Username (generated from email if not provided)",
|
||||
},
|
||||
"password": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Password (auto-generated if not provided)",
|
||||
},
|
||||
"billing": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Billing address object",
|
||||
},
|
||||
"shipping": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Shipping address object",
|
||||
},
|
||||
},
|
||||
"required": ["email"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_customer",
|
||||
"method_name": "update_customer",
|
||||
"description": "Update an existing WooCommerce customer. Can update name, email, billing, shipping, and other customer fields.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customer_id": {
|
||||
"type": "integer",
|
||||
"description": "Customer ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"first_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Customer first name",
|
||||
},
|
||||
"last_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Customer last name",
|
||||
},
|
||||
"email": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Customer email address",
|
||||
},
|
||||
"billing": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Billing address object",
|
||||
},
|
||||
"shipping": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Shipping address object",
|
||||
},
|
||||
},
|
||||
"required": ["customer_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class CustomersHandler:
|
||||
"""Handle WooCommerce customer operations"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize customers handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def list_customers(
|
||||
self,
|
||||
per_page: int = 10,
|
||||
page: int = 1,
|
||||
search: str | None = None,
|
||||
email: str | None = None,
|
||||
role: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
List WooCommerce customers.
|
||||
|
||||
Args:
|
||||
per_page: Number of customers per page (1-100)
|
||||
page: Page number
|
||||
search: Search by name or email
|
||||
email: Filter by specific email
|
||||
role: Filter by role (customer, subscriber, etc.)
|
||||
|
||||
Returns:
|
||||
JSON string with customers list
|
||||
"""
|
||||
try:
|
||||
# Build query parameters
|
||||
params = {"per_page": per_page, "page": page}
|
||||
|
||||
# Add optional filters
|
||||
if search:
|
||||
params["search"] = search
|
||||
if email:
|
||||
params["email"] = email
|
||||
if role:
|
||||
params["role"] = role
|
||||
|
||||
# Make request to WooCommerce API
|
||||
customers = await self.client.get("customers", params=params, use_woocommerce=True)
|
||||
|
||||
# Format response
|
||||
result = {
|
||||
"total": len(customers),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"customers": [
|
||||
{
|
||||
"id": customer["id"],
|
||||
"email": customer["email"],
|
||||
"first_name": customer.get("first_name", ""),
|
||||
"last_name": customer.get("last_name", ""),
|
||||
"username": customer.get("username", ""),
|
||||
"role": customer.get("role", ""),
|
||||
"date_created": customer.get("date_created", ""),
|
||||
"orders_count": customer.get("orders_count", 0),
|
||||
"total_spent": customer.get("total_spent", "0"),
|
||||
"avatar_url": customer.get("avatar_url", ""),
|
||||
}
|
||||
for customer in customers
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list customers: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_customer(self, customer_id: int) -> str:
|
||||
"""
|
||||
Get detailed information about a specific customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string with customer data
|
||||
"""
|
||||
try:
|
||||
customer = await self.client.get(f"customers/{customer_id}", use_woocommerce=True)
|
||||
|
||||
# Format detailed response
|
||||
result = {
|
||||
"id": customer["id"],
|
||||
"email": customer["email"],
|
||||
"username": customer.get("username", ""),
|
||||
"first_name": customer.get("first_name", ""),
|
||||
"last_name": customer.get("last_name", ""),
|
||||
"role": customer.get("role", ""),
|
||||
"date_created": customer.get("date_created", ""),
|
||||
"date_modified": customer.get("date_modified", ""),
|
||||
"orders_count": customer.get("orders_count", 0),
|
||||
"total_spent": customer.get("total_spent", "0"),
|
||||
"avatar_url": customer.get("avatar_url", ""),
|
||||
"billing": customer.get("billing", {}),
|
||||
"shipping": customer.get("shipping", {}),
|
||||
"is_paying_customer": customer.get("is_paying_customer", False),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get customer {customer_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def create_customer(
|
||||
self,
|
||||
email: str,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
billing: dict | None = None,
|
||||
shipping: dict | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a new WooCommerce customer.
|
||||
|
||||
Args:
|
||||
email: Customer email (required)
|
||||
first_name: First name
|
||||
last_name: Last name
|
||||
username: Username (generated from email if not provided)
|
||||
password: Password (auto-generated if not provided)
|
||||
billing: Billing address dictionary
|
||||
shipping: Shipping address dictionary
|
||||
|
||||
Returns:
|
||||
JSON string with created customer data
|
||||
"""
|
||||
try:
|
||||
# Build customer data
|
||||
data = {"email": email}
|
||||
|
||||
if first_name:
|
||||
data["first_name"] = first_name
|
||||
if last_name:
|
||||
data["last_name"] = last_name
|
||||
if username:
|
||||
data["username"] = username
|
||||
if password:
|
||||
data["password"] = password
|
||||
if billing:
|
||||
data["billing"] = billing
|
||||
if shipping:
|
||||
data["shipping"] = shipping
|
||||
|
||||
# Create customer via WooCommerce API
|
||||
customer = await self.client.post("customers", json_data=data, use_woocommerce=True)
|
||||
|
||||
result = {
|
||||
"id": customer["id"],
|
||||
"email": customer["email"],
|
||||
"username": customer.get("username", ""),
|
||||
"first_name": customer.get("first_name", ""),
|
||||
"last_name": customer.get("last_name", ""),
|
||||
"message": f"Customer created successfully with ID {customer['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create customer: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_customer(
|
||||
self,
|
||||
customer_id: int,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
email: str | None = None,
|
||||
billing: dict | None = None,
|
||||
shipping: dict | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing WooCommerce customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID to update
|
||||
first_name: First name
|
||||
last_name: Last name
|
||||
email: Email address
|
||||
billing: Billing address dictionary
|
||||
shipping: Shipping address dictionary
|
||||
|
||||
Returns:
|
||||
JSON string with updated customer data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if first_name is not None:
|
||||
data["first_name"] = first_name
|
||||
if last_name is not None:
|
||||
data["last_name"] = last_name
|
||||
if email is not None:
|
||||
data["email"] = email
|
||||
if billing is not None:
|
||||
data["billing"] = billing
|
||||
if shipping is not None:
|
||||
data["shipping"] = shipping
|
||||
|
||||
# Update customer via WooCommerce API
|
||||
customer = await self.client.put(
|
||||
f"customers/{customer_id}", json_data=data, use_woocommerce=True
|
||||
)
|
||||
|
||||
result = {
|
||||
"id": customer["id"],
|
||||
"email": customer["email"],
|
||||
"first_name": customer.get("first_name", ""),
|
||||
"last_name": customer.get("last_name", ""),
|
||||
"message": f"Customer {customer_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update customer {customer_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
418
plugins/wordpress/handlers/media.py
Normal file
418
plugins/wordpress/handlers/media.py
Normal file
@@ -0,0 +1,418 @@
|
||||
"""Media Handler - manages WordPress media library operations"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === MEDIA ===
|
||||
{
|
||||
"name": "list_media",
|
||||
"method_name": "list_media",
|
||||
"description": "List media library items. Returns images, videos, documents with URLs and metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of media items per page",
|
||||
"default": 20,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number for pagination",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"media_type": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter by media type (image, video, audio, or application)",
|
||||
"enum": ["image", "video", "audio", "application"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_media",
|
||||
"method_name": "get_media",
|
||||
"description": "Get detailed information about a media item. Returns full metadata including URLs, dimensions, and MIME type.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media_id": {
|
||||
"type": "integer",
|
||||
"description": "Media ID to retrieve",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["media_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "upload_media_from_url",
|
||||
"method_name": "upload_media_from_url",
|
||||
"description": "Upload media from URL to media library (sideload). Downloads file from public URL and uploads to WordPress.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Public URL of the media file to upload (image, video, document, etc.)",
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media title (used in media library)",
|
||||
},
|
||||
"alt_text": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Alternative text for accessibility (important for images)",
|
||||
},
|
||||
"caption": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media caption (displayed below image when inserted into content)",
|
||||
},
|
||||
},
|
||||
"required": ["url"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_media",
|
||||
"method_name": "update_media",
|
||||
"description": "Update media metadata. Supports title, description, slug, alt text, caption, status, and associated post.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media_id": {
|
||||
"type": "integer",
|
||||
"description": "Media ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media title (displayed in media library)",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media description (full text content, displayed in attachment page)",
|
||||
},
|
||||
"slug": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media URL slug (e.g., 'my-image-name')",
|
||||
},
|
||||
"alt_text": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Alternative text for accessibility (important for images)",
|
||||
},
|
||||
"caption": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Media caption (shown below image in content)",
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Publication status of the media",
|
||||
"enum": ["publish", "draft", "private"],
|
||||
},
|
||||
"post": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "ID of the post/page to attach this media to",
|
||||
},
|
||||
},
|
||||
"required": ["media_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_media",
|
||||
"method_name": "delete_media",
|
||||
"description": "Delete media from library. Can permanently delete or move to trash.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media_id": {
|
||||
"type": "integer",
|
||||
"description": "Media ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Permanently delete (true) or move to trash (false)",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["media_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class MediaHandler:
|
||||
"""Handle media-related operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize media handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
# === MEDIA ===
|
||||
|
||||
async def list_media(
|
||||
self, per_page: int = 20, page: int = 1, media_type: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
List media library items.
|
||||
|
||||
Args:
|
||||
per_page: Number of media items per page (1-100)
|
||||
page: Page number
|
||||
media_type: Filter by media type (image, video, audio, application)
|
||||
|
||||
Returns:
|
||||
JSON string with media list
|
||||
"""
|
||||
try:
|
||||
params = {"per_page": per_page, "page": page}
|
||||
if media_type:
|
||||
params["media_type"] = media_type
|
||||
|
||||
media = await self.client.get("media", params=params)
|
||||
|
||||
# Format response
|
||||
result = {
|
||||
"total": len(media),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"media": [
|
||||
{
|
||||
"id": m["id"],
|
||||
"title": m["title"]["rendered"],
|
||||
"mime_type": m["mime_type"],
|
||||
"media_type": m.get("media_type", ""),
|
||||
"url": m["source_url"],
|
||||
"date": m["date"],
|
||||
"alt_text": m.get("alt_text", ""),
|
||||
"link": m.get("link", ""),
|
||||
}
|
||||
for m in media
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list media: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_media(self, media_id: int) -> str:
|
||||
"""
|
||||
Get detailed information about a specific media item.
|
||||
|
||||
Args:
|
||||
media_id: Media ID to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string with media data
|
||||
"""
|
||||
try:
|
||||
media = await self.client.get(f"media/{media_id}")
|
||||
|
||||
result = {
|
||||
"id": media["id"],
|
||||
"title": media["title"]["rendered"],
|
||||
"mime_type": media["mime_type"],
|
||||
"media_type": media.get("media_type", ""),
|
||||
"url": media["source_url"],
|
||||
"alt_text": media.get("alt_text", ""),
|
||||
"caption": media.get("caption", {}).get("rendered", ""),
|
||||
"description": media.get("description", {}).get("rendered", ""),
|
||||
"date": media["date"],
|
||||
"modified": media.get("modified", ""),
|
||||
"link": media.get("link", ""),
|
||||
"media_details": media.get("media_details", {}),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get media {media_id}: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def upload_media_from_url(
|
||||
self,
|
||||
url: str,
|
||||
title: str | None = None,
|
||||
alt_text: str | None = None,
|
||||
caption: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Upload media from URL to media library (sideload).
|
||||
|
||||
Downloads file from a public URL and uploads it to WordPress media library.
|
||||
|
||||
Args:
|
||||
url: Public URL of the media file to upload
|
||||
title: Media title (used in media library)
|
||||
alt_text: Alternative text for accessibility
|
||||
caption: Media caption (displayed below image)
|
||||
|
||||
Returns:
|
||||
JSON string with uploaded media data
|
||||
"""
|
||||
try:
|
||||
# Download file from URL
|
||||
async with aiohttp.ClientSession() as session, session.get(url) as response:
|
||||
if response.status >= 400:
|
||||
raise Exception(f"Failed to download from URL: HTTP {response.status}")
|
||||
|
||||
file_content = await response.read()
|
||||
content_type = response.headers.get("Content-Type", "application/octet-stream")
|
||||
|
||||
# Extract filename from URL
|
||||
filename = url.split("/")[-1].split("?")[0]
|
||||
if not filename:
|
||||
filename = "downloaded_file"
|
||||
|
||||
# Create FormData for upload
|
||||
form = aiohttp.FormData()
|
||||
form.add_field("file", file_content, filename=filename, content_type=content_type)
|
||||
|
||||
# Upload to WordPress using client's upload method
|
||||
# Note: We need to use the client's base_url and auth directly for file upload
|
||||
upload_url = f"{self.client.base_url}/media"
|
||||
headers = {
|
||||
"Authorization": self.client.auth_header,
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(upload_url, data=form, headers=headers) as response:
|
||||
if response.status >= 400:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"Upload failed (HTTP {response.status}): {error_text}")
|
||||
|
||||
media = await response.json()
|
||||
|
||||
# Update metadata if provided
|
||||
if title or alt_text or caption:
|
||||
update_data = {}
|
||||
if title:
|
||||
update_data["title"] = title
|
||||
if alt_text:
|
||||
update_data["alt_text"] = alt_text
|
||||
if caption:
|
||||
update_data["caption"] = caption
|
||||
|
||||
await self.client.post(f"media/{media['id']}", json_data=update_data)
|
||||
|
||||
result = {
|
||||
"id": media["id"],
|
||||
"title": media["title"]["rendered"],
|
||||
"url": media["source_url"],
|
||||
"mime_type": media["mime_type"],
|
||||
"message": f"Media uploaded from URL successfully with ID {media['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to upload media from URL: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_media(
|
||||
self,
|
||||
media_id: int,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
slug: str | None = None,
|
||||
alt_text: str | None = None,
|
||||
caption: str | None = None,
|
||||
status: str | None = None,
|
||||
post: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update media metadata.
|
||||
|
||||
Args:
|
||||
media_id: Media ID to update
|
||||
title: Media title
|
||||
description: Media description
|
||||
slug: Media URL slug
|
||||
alt_text: Alternative text for accessibility
|
||||
caption: Media caption
|
||||
status: Publication status (publish, draft, private)
|
||||
post: ID of post/page to attach media to
|
||||
|
||||
Returns:
|
||||
JSON string with updated media data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if title is not None:
|
||||
data["title"] = title
|
||||
if description is not None:
|
||||
data["description"] = description
|
||||
if slug is not None:
|
||||
data["slug"] = slug
|
||||
if alt_text is not None:
|
||||
data["alt_text"] = alt_text
|
||||
if caption is not None:
|
||||
data["caption"] = caption
|
||||
if status is not None:
|
||||
data["status"] = status
|
||||
if post is not None:
|
||||
data["post"] = post
|
||||
|
||||
media = await self.client.post(f"media/{media_id}", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": media["id"],
|
||||
"title": media["title"]["rendered"],
|
||||
"alt_text": media.get("alt_text", ""),
|
||||
"caption": media.get("caption", {}).get("rendered", ""),
|
||||
"url": media["source_url"],
|
||||
"message": f"Media {media_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update media {media_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def delete_media(self, media_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete or trash media from library.
|
||||
|
||||
Args:
|
||||
media_id: Media ID to delete
|
||||
force: Permanently delete (True) or move to trash (False)
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(f"media/{media_id}", params=params)
|
||||
|
||||
message = f"Media {media_id} {'permanently deleted' if force else 'moved to trash'}"
|
||||
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete media {media_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
401
plugins/wordpress/handlers/menus.py
Normal file
401
plugins/wordpress/handlers/menus.py
Normal file
@@ -0,0 +1,401 @@
|
||||
"""Menus Handler - manages WordPress navigation menus"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
{
|
||||
"name": "list_menus",
|
||||
"method_name": "list_menus",
|
||||
"description": "List all WordPress navigation menus. Returns list of menus with their locations and item counts.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_menu",
|
||||
"method_name": "get_menu",
|
||||
"description": "Get detailed information about a specific menu including all items. Returns menu details with hierarchical structure of menu items.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"menu_id": {
|
||||
"type": "integer",
|
||||
"description": "Menu ID to retrieve",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["menu_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_menu",
|
||||
"method_name": "create_menu",
|
||||
"description": "Create a new navigation menu. Can optionally assign to theme locations.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Menu name (displayed in admin)",
|
||||
"minLength": 1,
|
||||
},
|
||||
"slug": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Menu slug (auto-generated from name if not provided)",
|
||||
},
|
||||
"locations": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Theme locations to assign menu to (e.g., ['primary', 'footer'])",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "list_menu_items",
|
||||
"method_name": "list_menu_items",
|
||||
"description": "List all items in a specific menu. Returns hierarchical list of menu items with links and ordering.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"menu_id": {
|
||||
"type": "integer",
|
||||
"description": "Menu ID to get items from",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["menu_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_menu_item",
|
||||
"method_name": "create_menu_item",
|
||||
"description": "Add a new item to a menu. Supports linking to posts, pages, categories, or custom URLs.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"menu_id": {
|
||||
"type": "integer",
|
||||
"description": "Menu ID to add item to",
|
||||
"minimum": 1,
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Item title/label displayed in menu",
|
||||
"minLength": 1,
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Item type: 'post_type' (post/page), 'taxonomy' (category/tag), or 'custom' (URL)",
|
||||
"enum": ["post_type", "taxonomy", "custom"],
|
||||
},
|
||||
"object_id": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "ID of linked post/page/term (required for post_type/taxonomy)",
|
||||
},
|
||||
"url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Custom URL (required for type=custom)",
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Parent item ID for creating sub-menu items",
|
||||
},
|
||||
},
|
||||
"required": ["menu_id", "title", "type"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_menu_item",
|
||||
"method_name": "update_menu_item",
|
||||
"description": "Update an existing menu item. Can change title, URL, parent, or menu order.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"type": "integer",
|
||||
"description": "Menu item ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New item title",
|
||||
},
|
||||
"url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New URL",
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "New parent item ID (0 for top-level)",
|
||||
},
|
||||
"menu_order": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Position in menu (lower numbers appear first)",
|
||||
},
|
||||
},
|
||||
"required": ["item_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class MenusHandler:
|
||||
"""Handle menu-related operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize menus handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def list_menus(self) -> str:
|
||||
"""
|
||||
List all WordPress navigation menus.
|
||||
|
||||
Returns list of all menus with their locations and item counts.
|
||||
|
||||
Returns:
|
||||
JSON string with total count and menu list
|
||||
|
||||
Example response:
|
||||
{
|
||||
"total": 3,
|
||||
"menus": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Primary Menu",
|
||||
"slug": "primary-menu",
|
||||
"locations": ["primary"],
|
||||
"count": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# WordPress REST API for menus (requires plugin support)
|
||||
# Try custom endpoint first, fallback to standard if not available
|
||||
try:
|
||||
menus = await self.client.get("menus", use_custom_namespace=True)
|
||||
except:
|
||||
# Fallback: use wp/v2/navigation endpoint (WP 5.9+)
|
||||
menus = await self.client.get("navigation")
|
||||
|
||||
result = {"total": len(menus) if isinstance(menus, list) else 0, "menus": menus}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list menus: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_menu(self, menu_id: int) -> str:
|
||||
"""
|
||||
Get detailed information about a specific menu including all items.
|
||||
|
||||
Args:
|
||||
menu_id: Menu ID
|
||||
|
||||
Returns:
|
||||
JSON string with menu details and items
|
||||
"""
|
||||
try:
|
||||
# Get menu details
|
||||
try:
|
||||
menu = await self.client.get(f"menus/{menu_id}", use_custom_namespace=True)
|
||||
except:
|
||||
menu = await self.client.get(f"navigation/{menu_id}")
|
||||
|
||||
# Get menu items
|
||||
menu_items = await self.list_menu_items(menu_id)
|
||||
|
||||
result = {
|
||||
"menu": menu,
|
||||
"items": json.loads(menu_items) if isinstance(menu_items, str) else menu_items,
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get menu {menu_id}: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def create_menu(
|
||||
self, name: str, slug: str | None = None, locations: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Create a new navigation menu.
|
||||
|
||||
Args:
|
||||
name: Menu name
|
||||
slug: Menu slug (auto-generated if not provided)
|
||||
locations: Theme locations to assign menu to
|
||||
|
||||
Returns:
|
||||
JSON string with created menu details
|
||||
"""
|
||||
try:
|
||||
data = {"name": name}
|
||||
if slug:
|
||||
data["slug"] = slug
|
||||
if locations:
|
||||
data["locations"] = locations
|
||||
|
||||
try:
|
||||
menu = await self.client.post("menus", json_data=data, use_custom_namespace=True)
|
||||
except:
|
||||
# Try navigation endpoint
|
||||
menu = await self.client.post("navigation", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": menu.get("id"),
|
||||
"name": menu.get("name"),
|
||||
"slug": menu.get("slug"),
|
||||
"message": f"Menu '{name}' created successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create menu: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def list_menu_items(self, menu_id: int) -> str:
|
||||
"""
|
||||
List all items in a specific menu.
|
||||
|
||||
Args:
|
||||
menu_id: Menu ID
|
||||
|
||||
Returns:
|
||||
JSON string with menu items list
|
||||
"""
|
||||
try:
|
||||
params = {"menus": menu_id, "per_page": 100}
|
||||
items = await self.client.get("menu-items", params=params)
|
||||
|
||||
result = {
|
||||
"total": len(items) if isinstance(items, list) else 0,
|
||||
"menu_id": menu_id,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to list menu items for menu {menu_id}: {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def create_menu_item(
|
||||
self,
|
||||
menu_id: int,
|
||||
title: str,
|
||||
type: str,
|
||||
object_id: int | None = None,
|
||||
url: str | None = None,
|
||||
parent: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Add a new item to a menu.
|
||||
|
||||
Args:
|
||||
menu_id: Menu ID to add item to
|
||||
title: Item title/label
|
||||
type: Item type (post_type, taxonomy, custom)
|
||||
object_id: ID of linked post/term (required for post_type/taxonomy)
|
||||
url: Custom URL (required for type=custom)
|
||||
parent: Parent item ID for creating sub-menu items
|
||||
|
||||
Returns:
|
||||
JSON string with created menu item
|
||||
"""
|
||||
try:
|
||||
data = {"menus": menu_id, "title": title, "type": type}
|
||||
|
||||
if object_id:
|
||||
data["object_id"] = object_id
|
||||
if url:
|
||||
data["url"] = url
|
||||
if parent:
|
||||
data["parent"] = parent
|
||||
|
||||
item = await self.client.post("menu-items", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": item.get("id"),
|
||||
"title": item.get("title", {}).get("rendered", title),
|
||||
"type": item.get("type"),
|
||||
"url": item.get("url"),
|
||||
"message": f"Menu item '{title}' created successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create menu item: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_menu_item(
|
||||
self,
|
||||
item_id: int,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
parent: int | None = None,
|
||||
menu_order: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing menu item.
|
||||
|
||||
Args:
|
||||
item_id: Menu item ID
|
||||
title: New title
|
||||
url: New URL
|
||||
parent: New parent item ID
|
||||
menu_order: Position in menu
|
||||
|
||||
Returns:
|
||||
JSON string with updated menu item
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if title is not None:
|
||||
data["title"] = title
|
||||
if url is not None:
|
||||
data["url"] = url
|
||||
if parent is not None:
|
||||
data["parent"] = parent
|
||||
if menu_order is not None:
|
||||
data["menu_order"] = menu_order
|
||||
|
||||
item = await self.client.put(f"menu-items/{item_id}", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": item.get("id"),
|
||||
"title": item.get("title", {}).get("rendered", title),
|
||||
"url": item.get("url"),
|
||||
"menu_order": item.get("menu_order"),
|
||||
"message": f"Menu item {item_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update menu item {item_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
449
plugins/wordpress/handlers/orders.py
Normal file
449
plugins/wordpress/handlers/orders.py
Normal file
@@ -0,0 +1,449 @@
|
||||
"""Orders Handler - manages WooCommerce orders"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
{
|
||||
"name": "list_orders",
|
||||
"method_name": "list_orders",
|
||||
"description": "List WooCommerce orders. Returns paginated order list with customer details, totals, and status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of orders per page (1-100)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"any",
|
||||
"pending",
|
||||
"processing",
|
||||
"on-hold",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"refunded",
|
||||
"failed",
|
||||
"trash",
|
||||
],
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Filter by order status (optional)",
|
||||
},
|
||||
"customer": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Filter by customer ID",
|
||||
},
|
||||
"after": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter orders after this date (ISO 8601 format)",
|
||||
},
|
||||
"before": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter orders before this date (ISO 8601 format)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_order",
|
||||
"method_name": "get_order",
|
||||
"description": "Get detailed information about a specific WooCommerce order. Returns full order details including line items, totals, billing, and shipping.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order_id": {
|
||||
"type": "integer",
|
||||
"description": "Order ID to retrieve",
|
||||
"minimum": 1,
|
||||
}
|
||||
},
|
||||
"required": ["order_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "update_order_status",
|
||||
"method_name": "update_order_status",
|
||||
"description": "Update WooCommerce order status. Change order status to pending, processing, completed, etc.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order_id": {
|
||||
"type": "integer",
|
||||
"description": "Order ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "New order status",
|
||||
"enum": [
|
||||
"pending",
|
||||
"processing",
|
||||
"on-hold",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"refunded",
|
||||
"failed",
|
||||
],
|
||||
},
|
||||
},
|
||||
"required": ["order_id", "status"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "create_order",
|
||||
"method_name": "create_order",
|
||||
"description": "Create a new WooCommerce order. Supports line items, billing, shipping, and payment method configuration.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customer_id": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Customer ID (optional)",
|
||||
},
|
||||
"line_items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_id": {
|
||||
"type": "integer",
|
||||
"description": "Product ID",
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"description": "Quantity",
|
||||
"minimum": 1,
|
||||
},
|
||||
"variation_id": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Variation ID (for variable products)",
|
||||
},
|
||||
},
|
||||
"required": ["product_id", "quantity"],
|
||||
},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Order line items with product IDs and quantities",
|
||||
},
|
||||
"billing": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Billing address object",
|
||||
},
|
||||
"shipping": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Shipping address object",
|
||||
},
|
||||
"payment_method": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Payment method ID (e.g., 'bacs', 'cod', 'paypal')",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Order status",
|
||||
"enum": ["pending", "processing", "on-hold", "completed"],
|
||||
"default": "pending",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_order",
|
||||
"method_name": "delete_order",
|
||||
"description": "Delete or trash a WooCommerce order. Can permanently delete or move to trash.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"order_id": {
|
||||
"type": "integer",
|
||||
"description": "Order ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Permanently delete (true) or move to trash (false)",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["order_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class OrdersHandler:
|
||||
"""Handle WooCommerce order-related operations"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize orders handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def list_orders(
|
||||
self,
|
||||
per_page: int = 10,
|
||||
page: int = 1,
|
||||
status: str | None = None,
|
||||
customer: int | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
List WooCommerce orders with filters.
|
||||
|
||||
Args:
|
||||
per_page: Number of orders per page (1-100)
|
||||
page: Page number
|
||||
status: Filter by order status (any, pending, processing, on-hold, completed, cancelled, refunded, failed, trash)
|
||||
customer: Filter by customer ID
|
||||
after: Filter orders after this date (ISO 8601 format)
|
||||
before: Filter orders before this date (ISO 8601 format)
|
||||
|
||||
Returns:
|
||||
JSON string with orders list
|
||||
"""
|
||||
try:
|
||||
# Build query parameters
|
||||
params = {"per_page": per_page, "page": page}
|
||||
|
||||
# Add optional filters
|
||||
if status:
|
||||
params["status"] = status
|
||||
if customer is not None:
|
||||
params["customer"] = customer
|
||||
if after:
|
||||
params["after"] = after
|
||||
if before:
|
||||
params["before"] = before
|
||||
|
||||
# Make request to WooCommerce API
|
||||
orders = await self.client.get("orders", params=params, use_woocommerce=True)
|
||||
|
||||
# Format response
|
||||
result = {
|
||||
"total": len(orders),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"orders": [
|
||||
{
|
||||
"id": order["id"],
|
||||
"number": order["number"],
|
||||
"status": order["status"],
|
||||
"date_created": order["date_created"],
|
||||
"date_modified": order.get("date_modified", ""),
|
||||
"total": order["total"],
|
||||
"currency": order["currency"],
|
||||
"customer_id": order["customer_id"],
|
||||
"billing": {
|
||||
"first_name": order["billing"].get("first_name", ""),
|
||||
"last_name": order["billing"].get("last_name", ""),
|
||||
"email": order["billing"].get("email", ""),
|
||||
},
|
||||
"line_items_count": len(order.get("line_items", [])),
|
||||
"payment_method_title": order.get("payment_method_title", ""),
|
||||
"transaction_id": order.get("transaction_id", ""),
|
||||
}
|
||||
for order in orders
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list orders: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_order(self, order_id: int) -> str:
|
||||
"""
|
||||
Get detailed information about a specific order.
|
||||
|
||||
Args:
|
||||
order_id: Order ID to retrieve
|
||||
|
||||
Returns:
|
||||
JSON string with order data
|
||||
"""
|
||||
try:
|
||||
order = await self.client.get(f"orders/{order_id}", use_woocommerce=True)
|
||||
|
||||
# Format detailed response
|
||||
result = {
|
||||
"id": order["id"],
|
||||
"number": order["number"],
|
||||
"status": order["status"],
|
||||
"currency": order["currency"],
|
||||
"date_created": order["date_created"],
|
||||
"date_modified": order.get("date_modified", ""),
|
||||
"discount_total": order["discount_total"],
|
||||
"shipping_total": order["shipping_total"],
|
||||
"total": order["total"],
|
||||
"total_tax": order["total_tax"],
|
||||
"customer_id": order["customer_id"],
|
||||
"customer_note": order.get("customer_note", ""),
|
||||
"billing": order["billing"],
|
||||
"shipping": order["shipping"],
|
||||
"payment_method": order["payment_method"],
|
||||
"payment_method_title": order.get("payment_method_title", ""),
|
||||
"transaction_id": order.get("transaction_id", ""),
|
||||
"line_items": [
|
||||
{
|
||||
"id": item["id"],
|
||||
"name": item["name"],
|
||||
"product_id": item["product_id"],
|
||||
"quantity": item["quantity"],
|
||||
"subtotal": item["subtotal"],
|
||||
"total": item["total"],
|
||||
"sku": item.get("sku", ""),
|
||||
}
|
||||
for item in order.get("line_items", [])
|
||||
],
|
||||
"shipping_lines": order.get("shipping_lines", []),
|
||||
"fee_lines": order.get("fee_lines", []),
|
||||
"coupon_lines": order.get("coupon_lines", []),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get order {order_id}: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_order_status(self, order_id: int, status: str) -> str:
|
||||
"""
|
||||
Update order status.
|
||||
|
||||
Args:
|
||||
order_id: Order ID to update
|
||||
status: New status (pending, processing, on-hold, completed, cancelled, refunded, failed)
|
||||
|
||||
Returns:
|
||||
JSON string with updated order data
|
||||
"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
|
||||
order = await self.client.put(
|
||||
f"orders/{order_id}", json_data=data, use_woocommerce=True
|
||||
)
|
||||
|
||||
result = {
|
||||
"id": order["id"],
|
||||
"number": order["number"],
|
||||
"status": order["status"],
|
||||
"message": f"Order #{order['number']} status updated to '{status}'",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update order {order_id} status: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
customer_id: int | None = None,
|
||||
line_items: list[dict] | None = None,
|
||||
billing: dict | None = None,
|
||||
shipping: dict | None = None,
|
||||
payment_method: str | None = None,
|
||||
status: str = "pending",
|
||||
) -> str:
|
||||
"""
|
||||
Create a new order.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID (optional)
|
||||
line_items: List of line items [{"product_id": 123, "quantity": 1}]
|
||||
billing: Billing address dictionary
|
||||
shipping: Shipping address dictionary
|
||||
payment_method: Payment method ID (e.g., 'bacs', 'cod', 'paypal')
|
||||
status: Order status (default: pending)
|
||||
|
||||
Returns:
|
||||
JSON string with created order data
|
||||
"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
|
||||
if customer_id is not None:
|
||||
data["customer_id"] = customer_id
|
||||
if line_items:
|
||||
data["line_items"] = line_items
|
||||
if billing:
|
||||
data["billing"] = billing
|
||||
if shipping:
|
||||
data["shipping"] = shipping
|
||||
if payment_method:
|
||||
data["payment_method"] = payment_method
|
||||
|
||||
order = await self.client.post("orders", json_data=data, use_woocommerce=True)
|
||||
|
||||
result = {
|
||||
"id": order["id"],
|
||||
"number": order["number"],
|
||||
"status": order["status"],
|
||||
"total": order["total"],
|
||||
"currency": order["currency"],
|
||||
"message": f"Order #{order['number']} created successfully with ID {order['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create order: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def delete_order(self, order_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete or trash an order.
|
||||
|
||||
Args:
|
||||
order_id: Order ID to delete
|
||||
force: Permanently delete (True) or move to trash (False)
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(
|
||||
f"orders/{order_id}", params=params, use_woocommerce=True
|
||||
)
|
||||
|
||||
message = f"Order {order_id} {'permanently deleted' if force else 'moved to trash'}"
|
||||
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete order {order_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
1156
plugins/wordpress/handlers/posts.py
Normal file
1156
plugins/wordpress/handlers/posts.py
Normal file
File diff suppressed because it is too large
Load Diff
1425
plugins/wordpress/handlers/products.py
Normal file
1425
plugins/wordpress/handlers/products.py
Normal file
File diff suppressed because it is too large
Load Diff
299
plugins/wordpress/handlers/reports.py
Normal file
299
plugins/wordpress/handlers/reports.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""Reports Handler - manages WooCommerce reporting and analytics"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === WOOCOMMERCE REPORTS ===
|
||||
{
|
||||
"name": "get_sales_report",
|
||||
"method_name": "get_sales_report",
|
||||
"description": "Get WooCommerce sales report. Returns sales data with totals and date ranges. Note: WooCommerce v3 API has limited reporting capabilities.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"description": "Report period (week, month, last_month, year)",
|
||||
"enum": ["week", "month", "last_month", "year"],
|
||||
"default": "week",
|
||||
},
|
||||
"date_min": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Start date for report (ISO 8601 format, e.g., '2024-01-01')",
|
||||
},
|
||||
"date_max": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "End date for report (ISO 8601 format, e.g., '2024-12-31')",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_top_sellers",
|
||||
"method_name": "get_top_sellers",
|
||||
"description": "Get top selling products report. Returns products with highest sales quantities.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"description": "Report period (week, month, last_month, year)",
|
||||
"enum": ["week", "month", "last_month", "year"],
|
||||
"default": "week",
|
||||
},
|
||||
"date_min": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Start date for report (ISO 8601 format)",
|
||||
},
|
||||
"date_max": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "End date for report (ISO 8601 format)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_customer_report",
|
||||
"method_name": "get_customer_report",
|
||||
"description": "Get customer statistics report. Returns customer count and spending data. Falls back to customer list if reports endpoint unavailable.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"description": "Report period (week, month, last_month, year)",
|
||||
"enum": ["week", "month", "last_month", "year"],
|
||||
"default": "week",
|
||||
},
|
||||
"date_min": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Start date for report (ISO 8601 format)",
|
||||
},
|
||||
"date_max": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "End date for report (ISO 8601 format)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
class ReportsHandler:
|
||||
"""Handle WooCommerce reporting operations"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize reports handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
# === WOOCOMMERCE REPORTS ===
|
||||
|
||||
async def get_sales_report(
|
||||
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Get WooCommerce sales report.
|
||||
|
||||
Args:
|
||||
period: Report period (week, month, last_month, year)
|
||||
date_min: Start date for report (ISO 8601 format, e.g., '2024-01-01')
|
||||
date_max: End date for report (ISO 8601 format, e.g., '2024-12-31')
|
||||
|
||||
Returns:
|
||||
JSON string with sales report data
|
||||
|
||||
Note:
|
||||
WooCommerce v3 API has limited reporting capabilities.
|
||||
For advanced analytics, use WooCommerce Analytics extension.
|
||||
"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
if date_min:
|
||||
params["date_min"] = date_min
|
||||
if date_max:
|
||||
params["date_max"] = date_max
|
||||
|
||||
report_data = await self.client.get(
|
||||
"reports/sales", params=params, use_woocommerce=True
|
||||
)
|
||||
|
||||
# Format the response based on what the API returns
|
||||
result = {
|
||||
"period": period,
|
||||
"sales_data": report_data if isinstance(report_data, list) else [report_data],
|
||||
"note": "WooCommerce v3 API has limited reporting. For advanced analytics, use WooCommerce Analytics extension.",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Check if reports endpoint is not available
|
||||
if "404" in error_msg or "not found" in error_msg.lower():
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "Sales reports endpoint not available",
|
||||
"message": "WooCommerce v3 API has limited reporting capabilities. Consider using WooCommerce Analytics or custom queries.",
|
||||
"details": error_msg,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get sales report: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_top_sellers(
|
||||
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Get top selling products report.
|
||||
|
||||
Args:
|
||||
period: Report period (week, month, last_month, year)
|
||||
date_min: Start date for report (ISO 8601 format)
|
||||
date_max: End date for report (ISO 8601 format)
|
||||
|
||||
Returns:
|
||||
JSON string with top sellers data
|
||||
"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
if date_min:
|
||||
params["date_min"] = date_min
|
||||
if date_max:
|
||||
params["date_max"] = date_max
|
||||
|
||||
top_sellers = await self.client.get(
|
||||
"reports/top_sellers", params=params, use_woocommerce=True
|
||||
)
|
||||
|
||||
result = {
|
||||
"period": period,
|
||||
"total_products": len(top_sellers) if isinstance(top_sellers, list) else 0,
|
||||
"top_sellers": [
|
||||
{
|
||||
"product_id": item.get("product_id"),
|
||||
"title": item.get("title"),
|
||||
"quantity": item.get("quantity", 0),
|
||||
}
|
||||
for item in (top_sellers if isinstance(top_sellers, list) else [])
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "404" in error_msg or "not found" in error_msg.lower():
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "Top sellers endpoint not available",
|
||||
"message": "WooCommerce v3 API has limited reporting capabilities.",
|
||||
"details": error_msg,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get top sellers: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_customer_report(
|
||||
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Get customer statistics report.
|
||||
|
||||
Args:
|
||||
period: Report period (week, month, last_month, year)
|
||||
date_min: Start date for report (ISO 8601 format)
|
||||
date_max: End date for report (ISO 8601 format)
|
||||
|
||||
Returns:
|
||||
JSON string with customer report data
|
||||
|
||||
Note:
|
||||
Falls back to customer list endpoint if reports endpoint is unavailable.
|
||||
"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
if date_min:
|
||||
params["date_min"] = date_min
|
||||
if date_max:
|
||||
params["date_max"] = date_max
|
||||
|
||||
try:
|
||||
customer_data = await self.client.get(
|
||||
"reports/customers", params=params, use_woocommerce=True
|
||||
)
|
||||
|
||||
result = {
|
||||
"period": period,
|
||||
"customer_data": (
|
||||
customer_data if isinstance(customer_data, list) else [customer_data]
|
||||
),
|
||||
"note": "Customer reporting may be limited in WooCommerce v3 API.",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as report_error:
|
||||
# Check if it's a 404 error - use fallback
|
||||
if "404" in str(report_error) or "not found" in str(report_error).lower():
|
||||
return await self._get_customer_report_fallback()
|
||||
raise report_error
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get customer report: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def _get_customer_report_fallback(self) -> str:
|
||||
"""
|
||||
Fallback method when customer reports endpoint is not available.
|
||||
|
||||
Uses the customers list endpoint to generate basic statistics.
|
||||
|
||||
Returns:
|
||||
JSON string with basic customer statistics
|
||||
"""
|
||||
try:
|
||||
# Use customers list to generate basic stats
|
||||
customers = await self.client.get(
|
||||
"customers", params={"per_page": 100}, use_woocommerce=True
|
||||
)
|
||||
|
||||
# Calculate basic stats
|
||||
total_customers = len(customers) if isinstance(customers, list) else 0
|
||||
total_spent = (
|
||||
sum(float(c.get("total_spent", 0)) for c in customers)
|
||||
if isinstance(customers, list)
|
||||
else 0
|
||||
)
|
||||
avg_spent = total_spent / total_customers if total_customers > 0 else 0
|
||||
|
||||
result = {
|
||||
"total_customers": total_customers,
|
||||
"total_spent": f"{total_spent:.2f}",
|
||||
"average_spent_per_customer": f"{avg_spent:.2f}",
|
||||
"note": "Generated from customer list (fallback method). For detailed analytics, use WooCommerce Analytics.",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": "Customer report and fallback both unavailable",
|
||||
"details": str(e),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
617
plugins/wordpress/handlers/seo.py
Normal file
617
plugins/wordpress/handlers/seo.py
Normal file
@@ -0,0 +1,617 @@
|
||||
"""SEO Handler - manages WordPress SEO plugin operations (Yoast/RankMath)"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === SEO (Rank Math / Yoast) ===
|
||||
{
|
||||
"name": "get_post_seo",
|
||||
"method_name": "get_post_seo",
|
||||
"description": "Get SEO metadata for a WordPress post or page. Returns Rank Math or Yoast SEO fields including focus keyword, meta title, description, and social media settings. Requires SEO API Bridge plugin.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"post_id": {"type": "integer", "description": "Post or Page ID", "minimum": 1}
|
||||
},
|
||||
"required": ["post_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_product_seo",
|
||||
"method_name": "get_product_seo",
|
||||
"description": "Get SEO metadata for a WooCommerce product. Returns Rank Math or Yoast SEO fields including focus keyword, meta title, description, and social media settings. Requires SEO API Bridge plugin.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_id": {"type": "integer", "description": "Product ID", "minimum": 1}
|
||||
},
|
||||
"required": ["product_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "update_post_seo",
|
||||
"method_name": "update_post_seo",
|
||||
"description": "Update SEO metadata for a WordPress post or page. Supports both Rank Math and Yoast SEO fields. Automatically detects which plugin is active. Requires SEO API Bridge plugin.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"post_id": {
|
||||
"type": "integer",
|
||||
"description": "Post or Page ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"focus_keyword": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Primary focus keyword for SEO",
|
||||
},
|
||||
"seo_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "SEO meta title (appears in search results)",
|
||||
},
|
||||
"meta_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "SEO meta description (appears in search results)",
|
||||
},
|
||||
"additional_keywords": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Additional keywords (comma-separated)",
|
||||
},
|
||||
"canonical_url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Canonical URL for this content",
|
||||
},
|
||||
"robots": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Robots meta directives (e.g., ['noindex', 'nofollow'])",
|
||||
},
|
||||
"og_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph title for Facebook",
|
||||
},
|
||||
"og_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph description for Facebook",
|
||||
},
|
||||
"og_image": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph image URL for Facebook",
|
||||
},
|
||||
"twitter_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card title",
|
||||
},
|
||||
"twitter_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card description",
|
||||
},
|
||||
"twitter_image": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card image URL",
|
||||
},
|
||||
},
|
||||
"required": ["post_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_product_seo",
|
||||
"method_name": "update_product_seo",
|
||||
"description": "Update SEO metadata for a WooCommerce product. Same as update_post_seo but specifically for products. Requires SEO API Bridge plugin.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_id": {
|
||||
"type": "integer",
|
||||
"description": "Product ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"focus_keyword": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Primary focus keyword for SEO",
|
||||
},
|
||||
"seo_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "SEO meta title (appears in search results)",
|
||||
},
|
||||
"meta_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "SEO meta description (appears in search results)",
|
||||
},
|
||||
"additional_keywords": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Additional keywords (comma-separated)",
|
||||
},
|
||||
"canonical_url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Canonical URL for this product",
|
||||
},
|
||||
"og_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph title for Facebook",
|
||||
},
|
||||
"og_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph description for Facebook",
|
||||
},
|
||||
"og_image": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Open Graph image URL for Facebook",
|
||||
},
|
||||
"twitter_title": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card title",
|
||||
},
|
||||
"twitter_description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card description",
|
||||
},
|
||||
"twitter_image": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Twitter Card image URL",
|
||||
},
|
||||
},
|
||||
"required": ["product_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class SEOHandler:
|
||||
"""Handle SEO-related operations for WordPress (Yoast SEO and RankMath)"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize SEO handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def _check_seo_plugins(self) -> dict[str, Any]:
|
||||
"""
|
||||
Check if Rank Math or Yoast SEO is installed and SEO API Bridge is active.
|
||||
|
||||
Returns:
|
||||
Dict with plugin status information
|
||||
"""
|
||||
try:
|
||||
# First, try to use the new health check endpoint (v1.1.0+)
|
||||
try:
|
||||
status_result = await self.client.get(
|
||||
"seo-api-bridge/v1/status", use_custom_namespace=True
|
||||
)
|
||||
|
||||
if status_result and isinstance(status_result, dict):
|
||||
# Successfully got status from dedicated endpoint
|
||||
rank_math_info = status_result.get("seo_plugins", {}).get("rank_math", {})
|
||||
yoast_info = status_result.get("seo_plugins", {}).get("yoast", {})
|
||||
|
||||
return {
|
||||
"rank_math": {
|
||||
"active": rank_math_info.get("active", False),
|
||||
"version": rank_math_info.get("version"),
|
||||
},
|
||||
"yoast": {
|
||||
"active": yoast_info.get("active", False),
|
||||
"version": yoast_info.get("version"),
|
||||
},
|
||||
"api_bridge_active": True,
|
||||
"api_bridge_version": status_result.get("version"),
|
||||
"message": status_result.get("message", "SEO API Bridge is active"),
|
||||
}
|
||||
except Exception:
|
||||
# Health check endpoint not available, fall back to old method
|
||||
pass
|
||||
|
||||
# Fallback: Try to check posts for SEO meta fields
|
||||
result = await self.client.get("posts", params={"per_page": 1})
|
||||
|
||||
# If no posts, try products
|
||||
if not result or (isinstance(result, list) and len(result) == 0):
|
||||
result = await self.client.get(
|
||||
"products", params={"per_page": 1}, use_woocommerce=True
|
||||
)
|
||||
|
||||
if not result or (isinstance(result, list) and len(result) == 0):
|
||||
return {
|
||||
"rank_math": {"active": False},
|
||||
"yoast": {"active": False},
|
||||
"api_bridge_active": False,
|
||||
"message": "No posts or products available to check SEO plugin status. Please install SEO API Bridge v1.1.0+ or create content with SEO metadata.",
|
||||
}
|
||||
|
||||
# Check if meta fields are present (indicates SEO API Bridge is active)
|
||||
first_item = result[0] if isinstance(result, list) and len(result) > 0 else {}
|
||||
meta = first_item.get("meta", {})
|
||||
|
||||
# Check for Rank Math fields
|
||||
rank_math_active = any(
|
||||
key in meta
|
||||
for key in [
|
||||
"rank_math_focus_keyword",
|
||||
"rank_math_seo_title",
|
||||
"rank_math_description",
|
||||
]
|
||||
)
|
||||
|
||||
# Check for Yoast SEO fields
|
||||
yoast_active = any(
|
||||
key in meta
|
||||
for key in ["_yoast_wpseo_focuskw", "_yoast_wpseo_title", "_yoast_wpseo_metadesc"]
|
||||
)
|
||||
|
||||
api_bridge_active = rank_math_active or yoast_active
|
||||
|
||||
return {
|
||||
"rank_math": {"active": rank_math_active},
|
||||
"yoast": {"active": yoast_active},
|
||||
"api_bridge_active": api_bridge_active,
|
||||
"message": (
|
||||
"SEO API Bridge required. Please install and activate the plugin, then upgrade to v1.1.0+ for better detection."
|
||||
if not api_bridge_active
|
||||
else "SEO fields accessible via meta (legacy detection)"
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"rank_math": {"active": False},
|
||||
"yoast": {"active": False},
|
||||
"api_bridge_active": False,
|
||||
"message": f"SEO plugin check failed: {str(e)}",
|
||||
}
|
||||
|
||||
# === SEO METHODS ===
|
||||
|
||||
async def get_post_seo(self, post_id: int) -> str:
|
||||
"""
|
||||
Get SEO metadata for a post or page.
|
||||
|
||||
Args:
|
||||
post_id: Post or Page ID
|
||||
|
||||
Returns:
|
||||
JSON string with SEO metadata
|
||||
"""
|
||||
try:
|
||||
result = await self.client.get(f"posts/{post_id}")
|
||||
|
||||
# Extract SEO meta fields
|
||||
meta = result.get("meta", {})
|
||||
|
||||
# Check which SEO plugin is active
|
||||
has_rank_math = "rank_math_focus_keyword" in meta
|
||||
has_yoast = "_yoast_wpseo_focuskw" in meta
|
||||
|
||||
if not has_rank_math and not has_yoast:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "SEO API Bridge plugin not detected",
|
||||
"message": "Please install and activate the SEO API Bridge WordPress plugin.",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# Format SEO data
|
||||
seo_data = {
|
||||
"post_id": post_id,
|
||||
"post_title": result.get("title", {}).get("rendered", ""),
|
||||
"plugin_detected": "rank_math" if has_rank_math else "yoast",
|
||||
}
|
||||
|
||||
if has_rank_math:
|
||||
seo_data.update(
|
||||
{
|
||||
"focus_keyword": meta.get("rank_math_focus_keyword", ""),
|
||||
"seo_title": meta.get("rank_math_seo_title", ""),
|
||||
"meta_description": meta.get("rank_math_description", ""),
|
||||
"additional_keywords": meta.get("rank_math_additional_keywords", ""),
|
||||
"canonical_url": meta.get("rank_math_canonical_url", ""),
|
||||
"robots": meta.get("rank_math_robots", []),
|
||||
"breadcrumb_title": meta.get("rank_math_breadcrumb_title", ""),
|
||||
"open_graph": {
|
||||
"title": meta.get("rank_math_facebook_title", ""),
|
||||
"description": meta.get("rank_math_facebook_description", ""),
|
||||
"image": meta.get("rank_math_facebook_image", ""),
|
||||
"image_id": meta.get("rank_math_facebook_image_id", ""),
|
||||
},
|
||||
"twitter": {
|
||||
"title": meta.get("rank_math_twitter_title", ""),
|
||||
"description": meta.get("rank_math_twitter_description", ""),
|
||||
"image": meta.get("rank_math_twitter_image", ""),
|
||||
"image_id": meta.get("rank_math_twitter_image_id", ""),
|
||||
"card_type": meta.get("rank_math_twitter_card_type", ""),
|
||||
},
|
||||
}
|
||||
)
|
||||
elif has_yoast:
|
||||
seo_data.update(
|
||||
{
|
||||
"focus_keyword": meta.get("_yoast_wpseo_focuskw", ""),
|
||||
"seo_title": meta.get("_yoast_wpseo_title", ""),
|
||||
"meta_description": meta.get("_yoast_wpseo_metadesc", ""),
|
||||
"canonical_url": meta.get("_yoast_wpseo_canonical", ""),
|
||||
"noindex": meta.get("_yoast_wpseo_meta-robots-noindex", ""),
|
||||
"nofollow": meta.get("_yoast_wpseo_meta-robots-nofollow", ""),
|
||||
"breadcrumb_title": meta.get("_yoast_wpseo_bctitle", ""),
|
||||
"open_graph": {
|
||||
"title": meta.get("_yoast_wpseo_opengraph-title", ""),
|
||||
"description": meta.get("_yoast_wpseo_opengraph-description", ""),
|
||||
"image": meta.get("_yoast_wpseo_opengraph-image", ""),
|
||||
"image_id": meta.get("_yoast_wpseo_opengraph-image-id", ""),
|
||||
},
|
||||
"twitter": {
|
||||
"title": meta.get("_yoast_wpseo_twitter-title", ""),
|
||||
"description": meta.get("_yoast_wpseo_twitter-description", ""),
|
||||
"image": meta.get("_yoast_wpseo_twitter-image", ""),
|
||||
"image_id": meta.get("_yoast_wpseo_twitter-image-id", ""),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(seo_data, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to get SEO data for post {post_id}: {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def get_product_seo(self, product_id: int) -> str:
|
||||
"""
|
||||
Get SEO metadata for a WooCommerce product.
|
||||
|
||||
Uses SEO API Bridge endpoint for products.
|
||||
|
||||
Args:
|
||||
product_id: Product ID
|
||||
|
||||
Returns:
|
||||
JSON string with SEO metadata
|
||||
"""
|
||||
try:
|
||||
# Use SEO API Bridge endpoint for products (same as update_product_seo)
|
||||
result = await self.client.get(
|
||||
f"seo-api-bridge/v1/products/{product_id}/seo", use_custom_namespace=True
|
||||
)
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to get SEO data for product {product_id}: {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def update_post_seo(
|
||||
self,
|
||||
post_id: int,
|
||||
focus_keyword: str | None = None,
|
||||
seo_title: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
additional_keywords: str | None = None,
|
||||
canonical_url: str | None = None,
|
||||
robots: list[str] | None = None,
|
||||
og_title: str | None = None,
|
||||
og_description: str | None = None,
|
||||
og_image: str | None = None,
|
||||
twitter_title: str | None = None,
|
||||
twitter_description: str | None = None,
|
||||
twitter_image: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update SEO metadata for a post or page.
|
||||
|
||||
Automatically detects whether Rank Math or Yoast is active and uses appropriate field names.
|
||||
|
||||
Args:
|
||||
post_id: Post or Page ID
|
||||
focus_keyword: Primary focus keyword
|
||||
seo_title: Meta title
|
||||
meta_description: Meta description
|
||||
additional_keywords: Additional keywords
|
||||
canonical_url: Canonical URL
|
||||
robots: Robots meta directives
|
||||
og_title: Open Graph title
|
||||
og_description: Open Graph description
|
||||
og_image: Open Graph image URL
|
||||
twitter_title: Twitter Card title
|
||||
twitter_description: Twitter Card description
|
||||
twitter_image: Twitter Card image URL
|
||||
|
||||
Returns:
|
||||
JSON string with update result
|
||||
"""
|
||||
try:
|
||||
# First check which SEO plugin is active
|
||||
seo_check = await self._check_seo_plugins()
|
||||
|
||||
if not seo_check.get("api_bridge_active"):
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "SEO API Bridge plugin not detected",
|
||||
"message": "Please install and activate the SEO API Bridge WordPress plugin.",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# Build meta object based on active plugin
|
||||
meta = {}
|
||||
|
||||
if seo_check.get("rank_math", {}).get("active"):
|
||||
# Use Rank Math field names
|
||||
if focus_keyword is not None:
|
||||
meta["rank_math_focus_keyword"] = focus_keyword
|
||||
if seo_title is not None:
|
||||
meta["rank_math_seo_title"] = seo_title
|
||||
if meta_description is not None:
|
||||
meta["rank_math_description"] = meta_description
|
||||
if additional_keywords is not None:
|
||||
meta["rank_math_additional_keywords"] = additional_keywords
|
||||
if canonical_url is not None:
|
||||
meta["rank_math_canonical_url"] = canonical_url
|
||||
if robots is not None:
|
||||
meta["rank_math_robots"] = robots
|
||||
if og_title is not None:
|
||||
meta["rank_math_facebook_title"] = og_title
|
||||
if og_description is not None:
|
||||
meta["rank_math_facebook_description"] = og_description
|
||||
if og_image is not None:
|
||||
meta["rank_math_facebook_image"] = og_image
|
||||
if twitter_title is not None:
|
||||
meta["rank_math_twitter_title"] = twitter_title
|
||||
if twitter_description is not None:
|
||||
meta["rank_math_twitter_description"] = twitter_description
|
||||
if twitter_image is not None:
|
||||
meta["rank_math_twitter_image"] = twitter_image
|
||||
|
||||
elif seo_check.get("yoast", {}).get("active"):
|
||||
# Use Yoast field names
|
||||
if focus_keyword is not None:
|
||||
meta["_yoast_wpseo_focuskw"] = focus_keyword
|
||||
if seo_title is not None:
|
||||
meta["_yoast_wpseo_title"] = seo_title
|
||||
if meta_description is not None:
|
||||
meta["_yoast_wpseo_metadesc"] = meta_description
|
||||
if canonical_url is not None:
|
||||
meta["_yoast_wpseo_canonical"] = canonical_url
|
||||
if og_title is not None:
|
||||
meta["_yoast_wpseo_opengraph-title"] = og_title
|
||||
if og_description is not None:
|
||||
meta["_yoast_wpseo_opengraph-description"] = og_description
|
||||
if og_image is not None:
|
||||
meta["_yoast_wpseo_opengraph-image"] = og_image
|
||||
if twitter_title is not None:
|
||||
meta["_yoast_wpseo_twitter-title"] = twitter_title
|
||||
if twitter_description is not None:
|
||||
meta["_yoast_wpseo_twitter-description"] = twitter_description
|
||||
if twitter_image is not None:
|
||||
meta["_yoast_wpseo_twitter-image"] = twitter_image
|
||||
|
||||
# Update post with meta fields
|
||||
data = {"meta": meta}
|
||||
await self.client.post(f"posts/{post_id}", json_data=data)
|
||||
|
||||
# Read back saved values for confirmation
|
||||
saved_seo_json = await self.get_post_seo(post_id)
|
||||
saved_seo = json.loads(saved_seo_json)
|
||||
|
||||
response = {
|
||||
"post_id": post_id,
|
||||
"updated_fields": list(meta.keys()),
|
||||
"message": f"SEO metadata updated successfully for post {post_id}",
|
||||
"current_values": saved_seo,
|
||||
}
|
||||
|
||||
return json.dumps(response, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to update SEO data for post {post_id}: {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def update_product_seo(
|
||||
self,
|
||||
product_id: int,
|
||||
focus_keyword: str | None = None,
|
||||
seo_title: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
additional_keywords: str | None = None,
|
||||
canonical_url: str | None = None,
|
||||
og_title: str | None = None,
|
||||
og_description: str | None = None,
|
||||
og_image: str | None = None,
|
||||
twitter_title: str | None = None,
|
||||
twitter_description: str | None = None,
|
||||
twitter_image: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update SEO metadata for a WooCommerce product.
|
||||
|
||||
Uses SEO API Bridge endpoint for products.
|
||||
|
||||
Args:
|
||||
product_id: Product ID
|
||||
focus_keyword: Primary focus keyword
|
||||
seo_title: Meta title
|
||||
meta_description: Meta description
|
||||
additional_keywords: Additional keywords
|
||||
canonical_url: Canonical URL
|
||||
og_title: Open Graph title
|
||||
og_description: Open Graph description
|
||||
og_image: Open Graph image URL
|
||||
twitter_title: Twitter Card title
|
||||
twitter_description: Twitter Card description
|
||||
twitter_image: Twitter Card image URL
|
||||
|
||||
Returns:
|
||||
JSON string with update result
|
||||
"""
|
||||
try:
|
||||
# Build request data with only provided fields
|
||||
data = {}
|
||||
|
||||
if focus_keyword is not None:
|
||||
data["focus_keyword"] = focus_keyword
|
||||
if seo_title is not None:
|
||||
data["seo_title"] = seo_title
|
||||
if meta_description is not None:
|
||||
data["meta_description"] = meta_description
|
||||
if additional_keywords is not None:
|
||||
data["additional_keywords"] = additional_keywords
|
||||
if canonical_url is not None:
|
||||
data["canonical_url"] = canonical_url
|
||||
if og_title is not None:
|
||||
data["og_title"] = og_title
|
||||
if og_description is not None:
|
||||
data["og_description"] = og_description
|
||||
if og_image is not None:
|
||||
data["og_image"] = og_image
|
||||
if twitter_title is not None:
|
||||
data["twitter_title"] = twitter_title
|
||||
if twitter_description is not None:
|
||||
data["twitter_description"] = twitter_description
|
||||
if twitter_image is not None:
|
||||
data["twitter_image"] = twitter_image
|
||||
|
||||
# Use SEO API Bridge endpoint for products
|
||||
await self.client.post(
|
||||
f"seo-api-bridge/v1/products/{product_id}/seo",
|
||||
json_data=data,
|
||||
use_custom_namespace=True,
|
||||
)
|
||||
|
||||
# Read back saved values for confirmation
|
||||
saved_seo_json = await self.get_product_seo(product_id)
|
||||
saved_seo = json.loads(saved_seo_json)
|
||||
|
||||
response = {
|
||||
"product_id": product_id,
|
||||
"updated_fields": list(data.keys()),
|
||||
"message": f"SEO metadata updated successfully for product {product_id}",
|
||||
"current_values": saved_seo,
|
||||
}
|
||||
|
||||
return json.dumps(response, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to update SEO data for product {product_id}: {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
243
plugins/wordpress/handlers/site.py
Normal file
243
plugins/wordpress/handlers/site.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Site Handler - manages WordPress site management operations"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === PLUGINS ===
|
||||
{
|
||||
"name": "list_plugins",
|
||||
"method_name": "list_plugins",
|
||||
"description": "List installed WordPress plugins. Shows plugin status (active/inactive), version, and details.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Filter plugins by status",
|
||||
"enum": ["active", "inactive", "all"],
|
||||
"default": "all",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# === THEMES ===
|
||||
{
|
||||
"name": "list_themes",
|
||||
"method_name": "list_themes",
|
||||
"description": "List installed WordPress themes. Returns all themes with their status and metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Filter themes by status",
|
||||
"enum": ["active", "inactive", "all"],
|
||||
"default": "all",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_active_theme",
|
||||
"method_name": "get_active_theme",
|
||||
"description": "Get information about the currently active WordPress theme including name, version, and author.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
# === SETTINGS ===
|
||||
{
|
||||
"name": "get_settings",
|
||||
"method_name": "get_settings",
|
||||
"description": "Get WordPress site settings. Includes site title, description, URL, email, timezone, and language.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
# === HEALTH ===
|
||||
{
|
||||
"name": "get_site_health",
|
||||
"method_name": "get_site_health",
|
||||
"description": "Check WordPress site health and accessibility. Returns comprehensive health status including WordPress, WooCommerce, and SEO plugin availability.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
class SiteHandler:
|
||||
"""Handle site management operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize site handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
# === PLUGINS ===
|
||||
|
||||
async def list_plugins(self, status: str = "all") -> str:
|
||||
"""
|
||||
List installed WordPress plugins.
|
||||
|
||||
Args:
|
||||
status: Filter by plugin status (active, inactive, all)
|
||||
|
||||
Returns:
|
||||
JSON string with plugins list
|
||||
"""
|
||||
try:
|
||||
# Build endpoint with status filter
|
||||
params = {}
|
||||
if status != "all":
|
||||
params["status"] = status
|
||||
|
||||
plugins = await self.client.get("plugins", params=params)
|
||||
|
||||
result = {
|
||||
"total": len(plugins),
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": p["plugin"],
|
||||
"name": p["name"],
|
||||
"version": p["version"],
|
||||
"status": p["status"],
|
||||
"description": p.get("description", {}).get("raw", "")[:100],
|
||||
}
|
||||
for p in plugins
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list plugins: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === THEMES ===
|
||||
|
||||
async def list_themes(self, status: str = "all") -> str:
|
||||
"""
|
||||
List installed WordPress themes.
|
||||
|
||||
Args:
|
||||
status: Filter by theme status (active, inactive, all)
|
||||
|
||||
Returns:
|
||||
JSON string with themes list
|
||||
"""
|
||||
try:
|
||||
# Build endpoint with status filter
|
||||
params = {}
|
||||
if status != "all":
|
||||
params["status"] = status
|
||||
|
||||
themes = await self.client.get("themes", params=params)
|
||||
|
||||
result = {
|
||||
"total": len(themes),
|
||||
"themes": [
|
||||
{
|
||||
"stylesheet": t["stylesheet"],
|
||||
"name": t["name"]["rendered"],
|
||||
"version": t["version"],
|
||||
"status": t["status"],
|
||||
}
|
||||
for t in themes
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list themes: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_active_theme(self) -> str:
|
||||
"""
|
||||
Get the currently active WordPress theme.
|
||||
|
||||
Returns:
|
||||
JSON string with active theme details
|
||||
"""
|
||||
try:
|
||||
themes = await self.client.get("themes", params={"status": "active"})
|
||||
|
||||
if themes:
|
||||
theme = themes[0]
|
||||
result = {
|
||||
"stylesheet": theme["stylesheet"],
|
||||
"name": theme["name"]["rendered"],
|
||||
"version": theme["version"],
|
||||
"author": theme.get("author", {}).get("raw", "Unknown"),
|
||||
}
|
||||
else:
|
||||
result = {"message": "No active theme found"}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get active theme: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === SETTINGS ===
|
||||
|
||||
async def get_settings(self) -> str:
|
||||
"""
|
||||
Get WordPress site settings.
|
||||
|
||||
Returns:
|
||||
JSON string with site settings including title, description, URL, email, timezone, and language
|
||||
"""
|
||||
try:
|
||||
settings = await self.client.get("settings")
|
||||
|
||||
result = {
|
||||
"title": settings.get("title", ""),
|
||||
"description": settings.get("description", ""),
|
||||
"url": settings.get("url", ""),
|
||||
"email": settings.get("email", ""),
|
||||
"timezone": settings.get("timezone_string", ""),
|
||||
"language": settings.get("language", ""),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get settings: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === HEALTH ===
|
||||
|
||||
async def get_site_health(self) -> str:
|
||||
"""
|
||||
Check WordPress site health and accessibility.
|
||||
|
||||
Returns:
|
||||
JSON string with comprehensive health status
|
||||
"""
|
||||
try:
|
||||
health = await self.client.check_site_health()
|
||||
return json.dumps(health, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get site health: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def health_check(self) -> dict[str, Any]:
|
||||
"""
|
||||
Perform health check using client's check_site_health method.
|
||||
|
||||
This method is used internally and returns a dict instead of JSON string.
|
||||
|
||||
Returns:
|
||||
Dict with health status information
|
||||
"""
|
||||
return await self.client.check_site_health()
|
||||
689
plugins/wordpress/handlers/taxonomy.py
Normal file
689
plugins/wordpress/handlers/taxonomy.py
Normal file
@@ -0,0 +1,689 @@
|
||||
"""Taxonomy Handler - manages WordPress categories, tags, and custom taxonomies"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === CATEGORIES ===
|
||||
{
|
||||
"name": "list_categories",
|
||||
"method_name": "list_categories",
|
||||
"description": "List WordPress post categories. Returns hierarchical list of categories with post counts.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of categories per page (1-100)",
|
||||
"default": 100,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"hide_empty": {
|
||||
"type": "boolean",
|
||||
"description": "Hide categories with no posts",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_category",
|
||||
"method_name": "create_category",
|
||||
"description": "Create a new WordPress category. Supports hierarchical categories with parent relationships.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Category name", "minLength": 1},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Category description",
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Parent category ID for hierarchy",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_category",
|
||||
"method_name": "update_category",
|
||||
"description": "Update an existing WordPress category. Can modify name, description, and parent category.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"description": "Category ID to update",
|
||||
"minimum": 1,
|
||||
},
|
||||
"name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New category name",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New category description",
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "New parent category ID",
|
||||
},
|
||||
},
|
||||
"required": ["category_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_category",
|
||||
"method_name": "delete_category",
|
||||
"description": "Delete a WordPress category. Can permanently delete or reassign posts to another category.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"description": "Category ID to delete",
|
||||
"minimum": 1,
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Force permanent deletion",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["category_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# === TAGS ===
|
||||
{
|
||||
"name": "list_tags",
|
||||
"method_name": "list_tags",
|
||||
"description": "List WordPress post tags. Returns all tags with usage counts and metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of tags per page (1-100)",
|
||||
"default": 100,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"hide_empty": {
|
||||
"type": "boolean",
|
||||
"description": "Hide tags with no posts",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_tag",
|
||||
"method_name": "create_tag",
|
||||
"description": "Create a new WordPress tag. Tag slug is auto-generated from name if not provided.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Tag name", "minLength": 1},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Tag description",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_tag",
|
||||
"method_name": "update_tag",
|
||||
"description": "Update an existing WordPress tag. Can modify name and description.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag_id": {"type": "integer", "description": "Tag ID to update", "minimum": 1},
|
||||
"name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New tag name",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New tag description",
|
||||
},
|
||||
},
|
||||
"required": ["tag_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_tag",
|
||||
"method_name": "delete_tag",
|
||||
"description": "Delete a WordPress tag. Permanently removes the tag from all posts.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag_id": {"type": "integer", "description": "Tag ID to delete", "minimum": 1},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
"description": "Force permanent deletion",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["tag_id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# === CUSTOM TAXONOMIES ===
|
||||
{
|
||||
"name": "list_taxonomies",
|
||||
"method_name": "list_taxonomies",
|
||||
"description": "List all registered taxonomies including built-in (category, post_tag) and custom taxonomies. Returns configuration and post type associations.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_taxonomy_terms",
|
||||
"method_name": "list_taxonomy_terms",
|
||||
"description": "List terms of a specific taxonomy. Works with any registered taxonomy including categories, tags, and custom taxonomies.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"taxonomy": {
|
||||
"type": "string",
|
||||
"description": "Taxonomy slug (e.g., 'category', 'post_tag', 'product_cat')",
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of terms per page",
|
||||
"default": 100,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"hide_empty": {
|
||||
"type": "boolean",
|
||||
"description": "Hide terms with no posts",
|
||||
"default": False,
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Filter by parent term ID (for hierarchical taxonomies)",
|
||||
},
|
||||
},
|
||||
"required": ["taxonomy"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_taxonomy_term",
|
||||
"method_name": "create_taxonomy_term",
|
||||
"description": "Create a new term in a taxonomy. Supports hierarchical taxonomies with parent terms. Works with any registered taxonomy.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"taxonomy": {
|
||||
"type": "string",
|
||||
"description": "Taxonomy slug (e.g., 'category', 'product_cat')",
|
||||
},
|
||||
"name": {"type": "string", "description": "Term name", "minLength": 1},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Term description",
|
||||
},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Parent term ID for hierarchical taxonomies (e.g., subcategories)",
|
||||
},
|
||||
},
|
||||
"required": ["taxonomy", "name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class TaxonomyHandler:
|
||||
"""Handle taxonomy-related operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize taxonomy handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
# === CATEGORIES ===
|
||||
|
||||
async def list_categories(
|
||||
self, per_page: int = 100, page: int = 1, hide_empty: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
List WordPress post categories.
|
||||
|
||||
Args:
|
||||
per_page: Number of categories per page (1-100)
|
||||
page: Page number
|
||||
hide_empty: Hide categories with no posts
|
||||
|
||||
Returns:
|
||||
JSON string with categories list
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
"per_page": per_page,
|
||||
"page": page,
|
||||
"hide_empty": "true" if hide_empty else "false",
|
||||
}
|
||||
|
||||
categories = await self.client.get("categories", params=params)
|
||||
|
||||
result = {
|
||||
"total": len(categories),
|
||||
"categories": [
|
||||
{
|
||||
"id": cat["id"],
|
||||
"name": cat["name"],
|
||||
"slug": cat["slug"],
|
||||
"description": cat.get("description", ""),
|
||||
"count": cat.get("count", 0),
|
||||
"parent": cat.get("parent", 0),
|
||||
}
|
||||
for cat in categories
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list categories: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def create_category(
|
||||
self, name: str, description: str | None = None, parent: int | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Create a new WordPress category.
|
||||
|
||||
Args:
|
||||
name: Category name
|
||||
description: Category description
|
||||
parent: Parent category ID for hierarchy
|
||||
|
||||
Returns:
|
||||
JSON string with created category data
|
||||
"""
|
||||
try:
|
||||
data = {"name": name}
|
||||
if description:
|
||||
data["description"] = description
|
||||
if parent:
|
||||
data["parent"] = parent
|
||||
|
||||
category = await self.client.post("categories", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": category["id"],
|
||||
"name": category["name"],
|
||||
"slug": category["slug"],
|
||||
"message": f"Category '{name}' created successfully with ID {category['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create category: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_category(
|
||||
self,
|
||||
category_id: int,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
parent: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing WordPress category.
|
||||
|
||||
Args:
|
||||
category_id: Category ID to update
|
||||
name: New category name
|
||||
description: New category description
|
||||
parent: New parent category ID
|
||||
|
||||
Returns:
|
||||
JSON string with updated category data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if name is not None:
|
||||
data["name"] = name
|
||||
if description is not None:
|
||||
data["description"] = description
|
||||
if parent is not None:
|
||||
data["parent"] = parent
|
||||
|
||||
category = await self.client.post(f"categories/{category_id}", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": category["id"],
|
||||
"name": category["name"],
|
||||
"slug": category["slug"],
|
||||
"message": f"Category {category_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update category {category_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def delete_category(self, category_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete a WordPress category.
|
||||
|
||||
Args:
|
||||
category_id: Category ID to delete
|
||||
force: Force permanent deletion
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(f"categories/{category_id}", params=params)
|
||||
|
||||
message = f"Category {category_id} deleted successfully"
|
||||
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete category {category_id}: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# === TAGS ===
|
||||
|
||||
async def list_tags(self, per_page: int = 100, page: int = 1, hide_empty: bool = False) -> str:
|
||||
"""
|
||||
List WordPress post tags.
|
||||
|
||||
Args:
|
||||
per_page: Number of tags per page (1-100)
|
||||
page: Page number
|
||||
hide_empty: Hide tags with no posts
|
||||
|
||||
Returns:
|
||||
JSON string with tags list
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
"per_page": per_page,
|
||||
"page": page,
|
||||
"hide_empty": "true" if hide_empty else "false",
|
||||
}
|
||||
|
||||
tags = await self.client.get("tags", params=params)
|
||||
|
||||
result = {
|
||||
"total": len(tags),
|
||||
"tags": [
|
||||
{
|
||||
"id": tag["id"],
|
||||
"name": tag["name"],
|
||||
"slug": tag["slug"],
|
||||
"description": tag.get("description", ""),
|
||||
"count": tag.get("count", 0),
|
||||
}
|
||||
for tag in tags
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list tags: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def create_tag(self, name: str, description: str | None = None) -> str:
|
||||
"""
|
||||
Create a new WordPress tag.
|
||||
|
||||
Args:
|
||||
name: Tag name
|
||||
description: Tag description
|
||||
|
||||
Returns:
|
||||
JSON string with created tag data
|
||||
"""
|
||||
try:
|
||||
data = {"name": name}
|
||||
if description:
|
||||
data["description"] = description
|
||||
|
||||
tag = await self.client.post("tags", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": tag["id"],
|
||||
"name": tag["name"],
|
||||
"slug": tag["slug"],
|
||||
"message": f"Tag '{name}' created successfully with ID {tag['id']}",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to create tag: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def update_tag(
|
||||
self, tag_id: int, name: str | None = None, description: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Update an existing WordPress tag.
|
||||
|
||||
Args:
|
||||
tag_id: Tag ID to update
|
||||
name: New tag name
|
||||
description: New tag description
|
||||
|
||||
Returns:
|
||||
JSON string with updated tag data
|
||||
"""
|
||||
try:
|
||||
# Build data dict with only provided values
|
||||
data = {}
|
||||
if name is not None:
|
||||
data["name"] = name
|
||||
if description is not None:
|
||||
data["description"] = description
|
||||
|
||||
tag = await self.client.post(f"tags/{tag_id}", json_data=data)
|
||||
|
||||
result = {
|
||||
"id": tag["id"],
|
||||
"name": tag["name"],
|
||||
"slug": tag["slug"],
|
||||
"message": f"Tag {tag_id} updated successfully",
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update tag {tag_id}: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def delete_tag(self, tag_id: int, force: bool = False) -> str:
|
||||
"""
|
||||
Delete a WordPress tag.
|
||||
|
||||
Args:
|
||||
tag_id: Tag ID to delete
|
||||
force: Force permanent deletion
|
||||
|
||||
Returns:
|
||||
JSON string with deletion result
|
||||
"""
|
||||
try:
|
||||
params = {"force": "true" if force else "false"}
|
||||
result = await self.client.delete(f"tags/{tag_id}", params=params)
|
||||
|
||||
message = f"Tag {tag_id} deleted successfully"
|
||||
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete tag {tag_id}: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === CUSTOM TAXONOMIES ===
|
||||
|
||||
async def list_taxonomies(self) -> str:
|
||||
"""
|
||||
List all registered taxonomies.
|
||||
|
||||
Returns list of all taxonomies including built-in (category, post_tag)
|
||||
and custom taxonomies (portfolio_category, etc.).
|
||||
|
||||
Returns:
|
||||
JSON string with total count and taxonomies list
|
||||
"""
|
||||
try:
|
||||
taxonomies_data = await self.client.get("taxonomies")
|
||||
|
||||
# Convert dict to list
|
||||
taxonomies = []
|
||||
if isinstance(taxonomies_data, dict):
|
||||
for slug, data in taxonomies_data.items():
|
||||
taxonomies.append(
|
||||
{
|
||||
"slug": slug,
|
||||
"name": data.get("name", slug),
|
||||
"description": data.get("description", ""),
|
||||
"types": data.get("types", []),
|
||||
"hierarchical": data.get("hierarchical", False),
|
||||
"rest_base": data.get("rest_base", slug),
|
||||
}
|
||||
)
|
||||
|
||||
result = {"total": len(taxonomies), "taxonomies": taxonomies}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list taxonomies: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def list_taxonomy_terms(
|
||||
self,
|
||||
taxonomy: str,
|
||||
per_page: int = 100,
|
||||
page: int = 1,
|
||||
hide_empty: bool = False,
|
||||
parent: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
List terms of a specific taxonomy.
|
||||
|
||||
Args:
|
||||
taxonomy: Taxonomy slug (e.g., 'category', 'product_category')
|
||||
per_page: Number of terms per page
|
||||
page: Page number
|
||||
hide_empty: Hide terms with no posts
|
||||
parent: Filter by parent term ID
|
||||
|
||||
Returns:
|
||||
JSON string with terms list
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
"per_page": per_page,
|
||||
"page": page,
|
||||
"hide_empty": "true" if hide_empty else "false",
|
||||
}
|
||||
|
||||
if parent is not None:
|
||||
params["parent"] = parent
|
||||
|
||||
terms = await self.client.get(taxonomy, params=params)
|
||||
|
||||
result = {
|
||||
"total": len(terms) if isinstance(terms, list) else 0,
|
||||
"taxonomy": taxonomy,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"terms": terms,
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to list taxonomy terms for '{taxonomy}': {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def create_taxonomy_term(
|
||||
self, taxonomy: str, name: str, description: str | None = None, parent: int | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Create a new term in a taxonomy.
|
||||
|
||||
Args:
|
||||
taxonomy: Taxonomy slug (e.g., 'category', 'product_category')
|
||||
name: Term name
|
||||
description: Term description
|
||||
parent: Parent term ID for hierarchical taxonomies
|
||||
|
||||
Returns:
|
||||
JSON string with created term details
|
||||
"""
|
||||
try:
|
||||
data = {"name": name}
|
||||
|
||||
if description:
|
||||
data["description"] = description
|
||||
if parent:
|
||||
data["parent"] = parent
|
||||
|
||||
term = await self.client.post(taxonomy, json_data=data)
|
||||
|
||||
return json.dumps(term, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"error": str(e),
|
||||
"message": f"Failed to create taxonomy term in '{taxonomy}': {str(e)}",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
134
plugins/wordpress/handlers/users.py
Normal file
134
plugins/wordpress/handlers/users.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Users Handler - manages WordPress user operations"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === USERS ===
|
||||
{
|
||||
"name": "list_users",
|
||||
"method_name": "list_users",
|
||||
"description": "List WordPress users. Returns paginated list of users with name, username, email, and roles.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of users per page (1-100)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
},
|
||||
"roles": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Filter by user roles (e.g., ['administrator', 'editor', 'author'])",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_current_user",
|
||||
"method_name": "get_current_user",
|
||||
"description": "Get information about the currently authenticated user including ID, name, username, email, and roles.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
class UsersHandler:
|
||||
"""Handle user-related operations for WordPress"""
|
||||
|
||||
def __init__(self, client: WordPressClient):
|
||||
"""
|
||||
Initialize users handler.
|
||||
|
||||
Args:
|
||||
client: WordPress API client instance
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
# === USERS ===
|
||||
|
||||
async def list_users(
|
||||
self, per_page: int = 10, page: int = 1, roles: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
List WordPress users.
|
||||
|
||||
Args:
|
||||
per_page: Number of users per page (1-100)
|
||||
page: Page number
|
||||
roles: Filter by user roles (e.g., ['administrator', 'editor'])
|
||||
|
||||
Returns:
|
||||
JSON string with users list
|
||||
"""
|
||||
try:
|
||||
params = {"per_page": per_page, "page": page}
|
||||
if roles:
|
||||
params["roles"] = ",".join(roles)
|
||||
|
||||
users = await self.client.get("users", params=params)
|
||||
|
||||
# Format response
|
||||
result = {
|
||||
"total": len(users),
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"users": [
|
||||
{
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"username": user["slug"],
|
||||
"email": user.get("email", "N/A"),
|
||||
"roles": user.get("roles", []),
|
||||
"link": user.get("link", ""),
|
||||
}
|
||||
for user in users
|
||||
],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list users: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def get_current_user(self) -> str:
|
||||
"""
|
||||
Get current authenticated user.
|
||||
|
||||
Returns:
|
||||
JSON string with current user data
|
||||
"""
|
||||
try:
|
||||
user = await self.client.get("users/me")
|
||||
|
||||
result = {
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"username": user["slug"],
|
||||
"email": user.get("email", "N/A"),
|
||||
"roles": user.get("roles", []),
|
||||
"capabilities": user.get("capabilities", {}),
|
||||
"description": user.get("description", ""),
|
||||
"link": user.get("link", ""),
|
||||
"registered_date": user.get("registered_date", ""),
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get current user: {str(e)}"}, indent=2
|
||||
)
|
||||
520
plugins/wordpress/handlers/wp_cli.py
Normal file
520
plugins/wordpress/handlers/wp_cli.py
Normal file
@@ -0,0 +1,520 @@
|
||||
"""WP-CLI Handler - manages WordPress WP-CLI operations"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.wp_cli import WPCLIManager
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# === CACHE MANAGEMENT (4 tools) ===
|
||||
{
|
||||
"name": "wp_cache_flush",
|
||||
"method_name": "wp_cache_flush",
|
||||
"description": "Flush WordPress object cache. Clears all cached objects from Redis, Memcached, or file cache. Safe to run anytime.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_cache_type",
|
||||
"method_name": "wp_cache_type",
|
||||
"description": "Get the object cache type being used (Redis, Memcached, file-based, etc.).",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_transient_delete_all",
|
||||
"method_name": "wp_transient_delete_all",
|
||||
"description": "Delete all expired transients from the database. Improves database performance by cleaning up temporary cached data.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_transient_list",
|
||||
"method_name": "wp_transient_list",
|
||||
"description": "List transients in the database (limited to first 100). Shows transient keys with expiration times. Useful for debugging caching issues.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
# === DATABASE OPERATIONS (3 tools) ===
|
||||
{
|
||||
"name": "wp_db_check",
|
||||
"method_name": "wp_db_check",
|
||||
"description": "Check WordPress database for errors. Runs integrity checks to ensure tables are healthy. Read-only operation.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_db_optimize",
|
||||
"method_name": "wp_db_optimize",
|
||||
"description": "Optimize WordPress database tables. Runs OPTIMIZE TABLE on all WordPress tables to reclaim space and improve performance.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_db_export",
|
||||
"method_name": "wp_db_export",
|
||||
"description": "Export WordPress database to SQL file in /tmp directory. Creates timestamped backup file for database recovery.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
# === PLUGIN/THEME INFO (4 tools) ===
|
||||
{
|
||||
"name": "wp_plugin_list_detailed",
|
||||
"method_name": "wp_plugin_list_detailed",
|
||||
"description": "List all WordPress plugins with detailed information including versions, status (active/inactive), and available updates.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_theme_list_detailed",
|
||||
"method_name": "wp_theme_list_detailed",
|
||||
"description": "List all WordPress themes with detailed information including versions, status, and active theme identification.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_plugin_verify_checksums",
|
||||
"method_name": "wp_plugin_verify_checksums",
|
||||
"description": "Verify plugin file integrity against WordPress.org checksums. Detects tampering or corruption. Only works for plugins from WordPress.org repository.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_core_verify_checksums",
|
||||
"method_name": "wp_core_verify_checksums",
|
||||
"description": "Verify WordPress core files against official checksums. Critical security tool for detecting tampering or unauthorized modifications.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "write",
|
||||
},
|
||||
# === SEARCH & REPLACE + UPDATES (4 tools) ===
|
||||
{
|
||||
"name": "wp_search_replace_dry_run",
|
||||
"method_name": "wp_search_replace_dry_run",
|
||||
"description": "Search and replace in database (DRY RUN ONLY). Previews what would be changed. NEVER makes actual changes. Use for migration planning.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "String to search for in database",
|
||||
"minLength": 1,
|
||||
"maxLength": 500,
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "String to replace with",
|
||||
"minLength": 1,
|
||||
"maxLength": 500,
|
||||
},
|
||||
"tables": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Optional list of specific tables to search (default: all tables)",
|
||||
},
|
||||
},
|
||||
"required": ["old_string", "new_string"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_plugin_update",
|
||||
"method_name": "wp_plugin_update",
|
||||
"description": "Update WordPress plugin(s). Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. Always backup first!",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "Plugin slug to update, or 'all' for all plugins",
|
||||
"minLength": 1,
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "If true, only show available updates without applying them (default: true)",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"required": ["plugin_name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_theme_update",
|
||||
"method_name": "wp_theme_update",
|
||||
"description": "Update WordPress theme(s). Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. WARNING: Updating active theme can break site appearance!",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"theme_name": {
|
||||
"type": "string",
|
||||
"description": "Theme slug to update, or 'all' for all themes",
|
||||
"minLength": 1,
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "If true, only show available updates without applying them (default: true)",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"required": ["theme_name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "wp_core_update",
|
||||
"method_name": "wp_core_update",
|
||||
"description": "Update WordPress core. Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. CRITICAL: Always backup before core updates!",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Specific version to update to, or null for latest version",
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "If true, only show available updates without applying them (default: true)",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class WPCLIHandler:
|
||||
"""Handle WP-CLI operations for WordPress"""
|
||||
|
||||
def __init__(self, wp_cli: WPCLIManager):
|
||||
"""
|
||||
Initialize WP-CLI handler.
|
||||
|
||||
Args:
|
||||
wp_cli: WPCLIManager instance for executing WP-CLI commands
|
||||
"""
|
||||
self.wp_cli = wp_cli
|
||||
|
||||
# === CACHE MANAGEMENT ===
|
||||
|
||||
async def wp_cache_flush(self) -> str:
|
||||
"""
|
||||
Flush WordPress object cache.
|
||||
|
||||
Clears all cached objects from the object cache (Redis, Memcached, or file).
|
||||
Safe to run anytime - will not affect database or content.
|
||||
|
||||
Returns:
|
||||
JSON string with flush status and message
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_cache_flush()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to flush cache: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_cache_type(self) -> str:
|
||||
"""
|
||||
Get the object cache type being used.
|
||||
|
||||
Shows which caching backend is active (e.g., Redis, Memcached, file-based).
|
||||
|
||||
Returns:
|
||||
JSON string with cache type information
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_cache_type()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to get cache type: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_transient_delete_all(self) -> str:
|
||||
"""
|
||||
Delete all expired transients from the database.
|
||||
|
||||
Transients are temporary cached data stored in the WordPress database.
|
||||
This command only deletes expired transients, improving database performance.
|
||||
|
||||
Returns:
|
||||
JSON string with count of deleted transients
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_transient_delete_all()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to delete transients: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_transient_list(self) -> str:
|
||||
"""
|
||||
List transients in the database (limited to first 100).
|
||||
|
||||
Shows transient keys with their expiration times.
|
||||
Useful for debugging caching issues.
|
||||
|
||||
Returns:
|
||||
JSON string with total count and list of transients (max 100)
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_transient_list()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list transients: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === DATABASE OPERATIONS ===
|
||||
|
||||
async def wp_db_check(self) -> str:
|
||||
"""
|
||||
Check WordPress database for errors.
|
||||
|
||||
Runs database integrity checks to ensure tables are healthy.
|
||||
Safe to run - read-only operation.
|
||||
|
||||
Returns:
|
||||
JSON string with health status and tables checked
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_db_check()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to check database: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_db_optimize(self) -> str:
|
||||
"""
|
||||
Optimize WordPress database tables.
|
||||
|
||||
Runs OPTIMIZE TABLE on all WordPress tables to reclaim space
|
||||
and improve performance. Safe operation - non-destructive.
|
||||
|
||||
Returns:
|
||||
JSON string with optimization results
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_db_optimize()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to optimize database: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_db_export(self) -> str:
|
||||
"""
|
||||
Export WordPress database to SQL file in /tmp.
|
||||
|
||||
Creates a database backup in the /tmp directory with timestamp.
|
||||
Safe - exports are only saved to /tmp for security.
|
||||
|
||||
Returns:
|
||||
JSON string with export file path and size
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_db_export()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to export database: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === PLUGIN/THEME INFO ===
|
||||
|
||||
async def wp_plugin_list_detailed(self) -> str:
|
||||
"""
|
||||
List all WordPress plugins with detailed information.
|
||||
|
||||
Shows plugin names, versions, status (active/inactive), and available updates.
|
||||
Useful for inventory management and update planning.
|
||||
|
||||
Returns:
|
||||
JSON string with total count and plugin list
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_plugin_list_detailed()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list plugins: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_theme_list_detailed(self) -> str:
|
||||
"""
|
||||
List all WordPress themes with detailed information.
|
||||
|
||||
Shows theme names, versions, status, and identifies the active theme.
|
||||
Useful for theme management and updates.
|
||||
|
||||
Returns:
|
||||
JSON string with total count, theme list, and active theme
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_theme_list_detailed()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to list themes: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_plugin_verify_checksums(self) -> str:
|
||||
"""
|
||||
Verify plugin file integrity against WordPress.org checksums.
|
||||
|
||||
Checks all plugins against official checksums to detect tampering or corruption.
|
||||
Important security tool for detecting malware or unauthorized modifications.
|
||||
|
||||
Note: Only works for plugins from WordPress.org repository.
|
||||
Premium/custom plugins will be skipped.
|
||||
|
||||
Returns:
|
||||
JSON string with verification results
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_plugin_verify_checksums()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to verify plugin checksums: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def wp_core_verify_checksums(self) -> str:
|
||||
"""
|
||||
Verify WordPress core files against official checksums.
|
||||
|
||||
Checks WordPress core files for tampering, corruption, or unauthorized modifications.
|
||||
Critical security tool for ensuring WordPress integrity.
|
||||
|
||||
Returns:
|
||||
JSON string with verification status and any modified files
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_core_verify_checksums()
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to verify core checksums: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
# === SEARCH & REPLACE + UPDATES ===
|
||||
|
||||
async def wp_search_replace_dry_run(
|
||||
self, old_string: str, new_string: str, tables: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
Search and replace in database (DRY RUN ONLY - no actual changes).
|
||||
|
||||
Previews what would be changed if you run search-replace.
|
||||
ALWAYS runs in dry-run mode - never makes actual changes.
|
||||
|
||||
Security: This tool ONLY shows what would be changed. To make actual
|
||||
changes, you must use WP-CLI directly with appropriate backups.
|
||||
|
||||
Args:
|
||||
old_string: String to search for
|
||||
new_string: String to replace with
|
||||
tables: Optional list of specific tables to search (default: all tables)
|
||||
|
||||
Returns:
|
||||
JSON string with preview of changes
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_search_replace_dry_run(
|
||||
old_string=old_string, new_string=new_string, tables=tables
|
||||
)
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to run search-replace dry run: {str(e)}"},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def wp_plugin_update(self, plugin_name: str, dry_run: bool = True) -> str:
|
||||
"""
|
||||
Update WordPress plugin(s) - DRY RUN by default.
|
||||
|
||||
Shows available updates or performs actual update.
|
||||
Default behavior is DRY RUN for safety.
|
||||
|
||||
Security:
|
||||
- Default: dry_run=True (only shows what would be updated)
|
||||
- Before actual update: backup database and files
|
||||
- Check plugin compatibility before major version updates
|
||||
|
||||
Args:
|
||||
plugin_name: Plugin slug or "all" for all plugins
|
||||
dry_run: If True, only show available updates (default: True)
|
||||
|
||||
Returns:
|
||||
JSON string with update information or results
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_plugin_update(plugin_name=plugin_name, dry_run=dry_run)
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update plugin: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_theme_update(self, theme_name: str, dry_run: bool = True) -> str:
|
||||
"""
|
||||
Update WordPress theme(s) - DRY RUN by default.
|
||||
|
||||
Shows available updates or performs actual update.
|
||||
Default behavior is DRY RUN for safety.
|
||||
|
||||
Security:
|
||||
- Default: dry_run=True (only shows what would be updated)
|
||||
- Before actual update: backup database and files
|
||||
- Test theme compatibility after updates
|
||||
- WARNING: Updating active theme can break site appearance
|
||||
|
||||
Args:
|
||||
theme_name: Theme slug or "all" for all themes
|
||||
dry_run: If True, only show available updates (default: True)
|
||||
|
||||
Returns:
|
||||
JSON string with update information or results
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_theme_update(theme_name=theme_name, dry_run=dry_run)
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update theme: {str(e)}"}, indent=2
|
||||
)
|
||||
|
||||
async def wp_core_update(self, version: str | None = None, dry_run: bool = True) -> str:
|
||||
"""
|
||||
Update WordPress core - DRY RUN by default.
|
||||
|
||||
Shows available updates or performs actual core update.
|
||||
Default behavior is DRY RUN for safety.
|
||||
|
||||
Security:
|
||||
- Default: dry_run=True (only shows what would be updated)
|
||||
- CRITICAL: Always backup database and files before core updates
|
||||
- Check plugin/theme compatibility before major version updates
|
||||
- Test thoroughly on staging environment first
|
||||
- Major version updates may have breaking changes
|
||||
|
||||
Args:
|
||||
version: Specific version to update to, or None for latest (default: None)
|
||||
dry_run: If True, only show available updates (default: True)
|
||||
|
||||
Returns:
|
||||
JSON string with update information or results
|
||||
"""
|
||||
try:
|
||||
result = await self.wp_cli.wp_core_update(version=version, dry_run=dry_run)
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"error": str(e), "message": f"Failed to update WordPress core: {str(e)}"}, indent=2
|
||||
)
|
||||
Reference in New Issue
Block a user