Add Public API
This commit is contained in:
+566
@@ -0,0 +1,566 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "The Meme Protocol API",
|
||||
"version": "1.0.0",
|
||||
"description": "Public API for listing approved memes and submitting PNG/JPEG meme uploads. Uploads follow the same moderation, image validation, normalization, and IP quota rules as the browser form."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "memes",
|
||||
"description": "Approved meme discovery and submission"
|
||||
},
|
||||
{
|
||||
"name": "discovery",
|
||||
"description": "Machine-readable API metadata"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/memes": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"memes"
|
||||
],
|
||||
"summary": "List approved memes",
|
||||
"description": "Returns approved public memes only. Results are paginated and read-limited per IP to reduce abuse.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pageSize",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 48,
|
||||
"default": 12
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Approved meme page",
|
||||
"headers": {
|
||||
"RateLimit-Limit": {
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"RateLimit-Remaining": {
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"RateLimit-Reset": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemeListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"memes"
|
||||
],
|
||||
"summary": "Submit a meme",
|
||||
"description": "Submits one PNG or JPEG image using the same path as the browser upload form. The request must be multipart/form-data with a file field named meme. The server enforces the same size, dimension, pixel, moderation, normalization, and per-IP upload limits as human submissions.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meme"
|
||||
],
|
||||
"properties": {
|
||||
"meme": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "PNG or JPEG image, maximum 5 MB, maximum edge 6000 px, maximum 20 megapixels."
|
||||
}
|
||||
}
|
||||
},
|
||||
"encoding": {
|
||||
"meme": {
|
||||
"contentType": "image/png, image/jpeg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Upload approved and published immediately",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemeSubmitResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Upload accepted and queued for admin review",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemeSubmitResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"411": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"413": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"415": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"422": {
|
||||
"description": "Upload rejected by moderation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ModerationRejected"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/memes/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"memes"
|
||||
],
|
||||
"summary": "Get one approved meme",
|
||||
"description": "Returns public metadata for an approved meme. Pending, rejected, deleted, or unknown IDs return 404.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Approved meme metadata",
|
||||
"headers": {
|
||||
"RateLimit-Limit": {
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"RateLimit-Remaining": {
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"RateLimit-Reset": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meme",
|
||||
"rateLimit"
|
||||
],
|
||||
"properties": {
|
||||
"meme": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
},
|
||||
"rateLimit": {
|
||||
"$ref": "#/components/schemas/RateLimit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/media/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"memes"
|
||||
],
|
||||
"summary": "Fetch approved meme image",
|
||||
"description": "Returns the normalized WebP image for an approved meme. Media reads are rate-limited per IP.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "WebP meme image",
|
||||
"content": {
|
||||
"image/webp": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"429": {
|
||||
"$ref": "#/components/responses/RateLimited"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/openapi.json": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"discovery"
|
||||
],
|
||||
"summary": "OpenAPI document",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OpenAPI 3.1 document",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/meme-api.skill.md": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"discovery"
|
||||
],
|
||||
"summary": "LLM API consumption instructions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Markdown skill instructions for LLM clients",
|
||||
"content": {
|
||||
"text/markdown": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Meme": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"byteSize",
|
||||
"width",
|
||||
"height",
|
||||
"mime",
|
||||
"originalMime",
|
||||
"status",
|
||||
"moderationScore",
|
||||
"moderationReason",
|
||||
"viewCount",
|
||||
"downloadCount",
|
||||
"originalName",
|
||||
"url",
|
||||
"downloadUrl",
|
||||
"demo"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"byteSize": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"mime": {
|
||||
"type": "string",
|
||||
"const": "image/webp"
|
||||
},
|
||||
"originalMime": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"image/png",
|
||||
"image/jpeg"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"approved",
|
||||
"pending"
|
||||
]
|
||||
},
|
||||
"moderationScore": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"moderationReason": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"downloadCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"originalName": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri-reference"
|
||||
},
|
||||
"downloadUrl": {
|
||||
"type": "string",
|
||||
"format": "uri-reference"
|
||||
},
|
||||
"demo": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemeListResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"page",
|
||||
"pageSize",
|
||||
"total",
|
||||
"totalPages",
|
||||
"memes",
|
||||
"rateLimit"
|
||||
],
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"totalPages": {
|
||||
"type": "integer"
|
||||
},
|
||||
"memes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
}
|
||||
},
|
||||
"rateLimit": {
|
||||
"$ref": "#/components/schemas/RateLimit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemeSubmitResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meme",
|
||||
"quota",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"meme": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
},
|
||||
"quota": {
|
||||
"$ref": "#/components/schemas/UploadQuota"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Upload approved.",
|
||||
"Upload queued for admin review."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"UploadQuota": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"remainingHour",
|
||||
"remainingDay",
|
||||
"remainingGlobalDay"
|
||||
],
|
||||
"properties": {
|
||||
"remainingHour": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"remainingDay": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"remainingGlobalDay": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"RateLimit": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"limit",
|
||||
"remaining",
|
||||
"resetAt"
|
||||
],
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remaining": {
|
||||
"type": "integer"
|
||||
},
|
||||
"resetAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"error"
|
||||
],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModerationRejected": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"error",
|
||||
"moderationScore"
|
||||
],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"moderationScore": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"Error": {
|
||||
"description": "Error response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"RateLimited": {
|
||||
"description": "Rate limit exceeded",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user