Servidor MCP para el asistente del equipo y API para idioma, traducciones y necesidades
- mcp/: servidor MCP (stdio) con list/get/create/update animal, set_status, upload_photo, get/set_needs, sobre la API REST con contraseña de aplicación. - REST: campos language y translation_of (crear la versión en euskera enlazada y heredar los datos comunes); endpoint bilagun/v1/needs. - README: herramientas del equipo y tareas pendientes fuera del código. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
.git
|
||||
*.md
|
||||
docker-compose*.yml
|
||||
mcp/
|
||||
|
||||
22
README.md
22
README.md
@@ -46,9 +46,31 @@ Cada ficha tiene su propio enlace y botones para compartirla por WhatsApp o Face
|
||||
| 3 | Plugins recomendados |
|
||||
| 4 | APIs (WPGraphQL, Swagger) |
|
||||
| 5 | Indicaciones del cliente: páginas Acoge y Preguntas frecuentes, menú con submenús, retirada del hotel, estados nuevos y sin animaciones |
|
||||
| 6 | Preguntas frecuentes con perdidos/encontrados; páginas del cuestionario y de finales felices (borrador) |
|
||||
|
||||
`docker/setup-content.php` es idempotente. **Las páginas que el cliente haya editado nunca se sobrescriben**; se detecta con una huella del contenido original.
|
||||
|
||||
## Herramientas para el equipo
|
||||
|
||||
- **Escritorio → «Fichas por revisar»**: animales sin foto, sin euskera, sin «Cómo es», sin especie o en proceso desde hace demasiado.
|
||||
- **Animales (listado)**: estrella ☆/★ para destacar con un clic; acciones en bloque (estado, destacar, **imprimir carteles con QR**).
|
||||
- **Ficha del animal → caja «Compartir»**: imagen para redes generada automáticamente (la que sale al compartir el enlace) y cartel A4 con QR.
|
||||
- **Finales felices** (Animales → Finales felices): historias enviadas por las familias desde la página «Finales felices» (borrador hasta que queráis publicarla); se publican solo tras revisarlas.
|
||||
- **Cuestionario de adopción** online: página fuera del menú y sin indexar; enviad el enlace tras el primer contacto (`?adoptar=ID` lo rellena con el animal). Preguntas en *Personalizar → Adopción y colabora → Cuestionario*.
|
||||
- **Mensajes**: se borran solos a los 12 meses (configurable).
|
||||
- **Asistente (MCP)**: ver [`mcp/README.md`](mcp/README.md).
|
||||
|
||||
### Pendiente fuera del código (requiere cuentas del cliente)
|
||||
|
||||
| Tarea | Cómo |
|
||||
|---|---|
|
||||
| Copias fuera del servidor | UpdraftPlus → Ajustes → Google Drive o Dropbox |
|
||||
| Aviso si la web se cae | UptimeRobot o Better Stack (gratis) apuntando a la portada |
|
||||
| Caché y cortafuegos | Cloudflare delante del dominio (proxy naranja) |
|
||||
| Google Maps / búsqueda local | Ficha de Google Business con la misma dirección, horario y web |
|
||||
| Aviso de «Nuevos animales» | Activar MailPoet, crear un formulario y un email de tipo «Aviso de nuevas publicaciones» para *Animales*; pegar el shortcode en *Personalizar → Pie* |
|
||||
| Apadrinamiento con pago | Confirmar medios de pago, activar GiveWP y enlazarlo desde la sección «Apadrina» |
|
||||
|
||||
## Logo y colores
|
||||
|
||||
- El logo original se ha vectorizado en `wp-content/themes/gure-ametsa/assets/img/`:
|
||||
|
||||
1
mcp/.gitignore
vendored
Normal file
1
mcp/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
60
mcp/README.md
Normal file
60
mcp/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Bilagun MCP server
|
||||
|
||||
Lets an AI assistant (Claude Desktop, Claude Code…) manage the Bilagun website in plain language,
|
||||
through the website's REST API. Examples:
|
||||
|
||||
- "Crea la ficha de Kira, perra de unos 2 años, tamaño mediano, muy cariñosa. Déjala en borrador."
|
||||
- "Marca a Txuri como adoptada."
|
||||
- "Sube esta foto como principal de Hartz."
|
||||
- "Añade 'arena para gatos' a lo que necesitamos, en castellano y en euskera."
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | What it does |
|
||||
|---|---|
|
||||
| `list_animals` | List animals (filter by language, status, species, text) |
|
||||
| `get_animal` | Full record: story, profile, translation |
|
||||
| `create_animal` | New animal (draft by default). With `language: "eu"` + `translationOf` it creates the Basque version and links it |
|
||||
| `update_animal` | Change name, story, texts or profile fields |
|
||||
| `set_status` | available / pending / adopted (both languages) |
|
||||
| `upload_photo` | Upload a JPG/PNG/WebP (local file or URL) as main photo or to the gallery |
|
||||
| `get_needs` / `set_needs` | Material needs list shown on the website (es / eu) |
|
||||
|
||||
It can't read the contact messages, delete content or change settings: that stays in the dashboard.
|
||||
|
||||
## Setup
|
||||
|
||||
1. In WordPress create a dedicated user (role **Editor**) and, in *Users → Profile → Application passwords*,
|
||||
create one called "Asistente". Copy it (it is shown only once). It can be revoked at any time.
|
||||
2. Install: `cd mcp && npm install` (Node 20+).
|
||||
3. Add the server to your assistant.
|
||||
|
||||
**Claude Code**
|
||||
|
||||
```bash
|
||||
claude mcp add bilagun \
|
||||
--env BILAGUN_URL=https://gureametsabilagun.pireon.dev \
|
||||
--env BILAGUN_USER=asistente \
|
||||
--env "BILAGUN_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx" \
|
||||
-- node /absolute/path/to/mcp/index.js
|
||||
```
|
||||
|
||||
**Claude Desktop** (`claude_desktop_config.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bilagun": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/mcp/index.js"],
|
||||
"env": {
|
||||
"BILAGUN_URL": "https://gureametsabilagun.pireon.dev",
|
||||
"BILAGUN_USER": "asistente",
|
||||
"BILAGUN_APP_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Application passwords only work over HTTPS (the production site already is).
|
||||
220
mcp/index.js
Normal file
220
mcp/index.js
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bilagun MCP server.
|
||||
*
|
||||
* Lets an assistant (Claude Desktop, Claude Code…) manage the website through its REST API:
|
||||
* list / read / create / update animals, change status, feature, upload photos and edit the
|
||||
* material needs list. Authentication: WordPress application password (Users → Profile).
|
||||
*
|
||||
* Env: BILAGUN_URL (https://…), BILAGUN_USER, BILAGUN_APP_PASSWORD
|
||||
*/
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { basename, extname } from 'node:path';
|
||||
|
||||
const BASE = (process.env.BILAGUN_URL || '').replace(/\/+$/, '');
|
||||
const USER = process.env.BILAGUN_USER || '';
|
||||
const PASS = process.env.BILAGUN_APP_PASSWORD || '';
|
||||
if (!BASE || !USER || !PASS) {
|
||||
console.error('Set BILAGUN_URL, BILAGUN_USER and BILAGUN_APP_PASSWORD.');
|
||||
process.exit(1);
|
||||
}
|
||||
const AUTH = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
|
||||
|
||||
async function api(path, { method = 'GET', body, headers = {}, raw = false } = {}) {
|
||||
const res = await fetch(`${BASE}/wp-json${path}`, {
|
||||
method,
|
||||
headers: { Authorization: AUTH, ...(body && !raw ? { 'Content-Type': 'application/json' } : {}), ...headers },
|
||||
body: body ? (raw ? body : JSON.stringify(body)) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try { data = JSON.parse(text); } catch { data = text; }
|
||||
if (!res.ok) {
|
||||
const msg = (data && data.message) || text.slice(0, 300);
|
||||
throw new Error(`${method} ${path} → ${res.status}: ${msg}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const ok = (data) => ({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] });
|
||||
const fail = (e) => ({ isError: true, content: [{ type: 'text', text: String(e.message || e) }] });
|
||||
const tool = (fn) => async (args) => { try { return ok(await fn(args)); } catch (e) { return fail(e); } };
|
||||
|
||||
const SPECIES = { dog: { es: 'perro', eu: 'txakurra' }, cat: { es: 'gato', eu: 'katua' } };
|
||||
async function speciesId(kind, lang) {
|
||||
const slug = SPECIES[kind][lang];
|
||||
const terms = await api(`/wp/v2/species?slug=${slug}&lang=${lang}&_fields=id`);
|
||||
if (!terms.length) throw new Error(`Species "${slug}" not found.`);
|
||||
return terms[0].id;
|
||||
}
|
||||
|
||||
/** Compact view of an animal for the assistant. */
|
||||
const summary = (a) => ({
|
||||
id: a.id,
|
||||
name: a.title?.rendered ?? a.title?.raw,
|
||||
wpStatus: a.status,
|
||||
language: a.language,
|
||||
translationOf: a.translation_of,
|
||||
link: a.link,
|
||||
profile: a.profile,
|
||||
});
|
||||
|
||||
const Profile = {
|
||||
status: z.enum(['available', 'pending', 'adopted']).optional().describe('Adoption status. Adopted animals disappear from public listings.'),
|
||||
featured: z.boolean().optional().describe('Show on the home page.'),
|
||||
sponsorable: z.boolean().optional().describe('Looking for a sponsor.'),
|
||||
sex: z.enum(['male', 'female']).optional(),
|
||||
birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('YYYY-MM-DD'),
|
||||
age: z.number().nonnegative().optional().describe('Approximate age when the birth date is unknown.'),
|
||||
ageUnit: z.enum(['years', 'months']).optional(),
|
||||
size: z.enum(['small', 'medium', 'large']).optional().describe('Dogs only.'),
|
||||
arrivalDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Date it arrived at the shelter (only if confirmed).'),
|
||||
video: z.string().url().optional(),
|
||||
personality: z.string().optional().describe('What it is like (in the language of this record).'),
|
||||
needs: z.string().optional().describe('What it needs.'),
|
||||
compatibility: z.string().optional().describe('Observed behaviour with children and other animals, with context. Never claim what is not known.'),
|
||||
idealHome: z.string().optional().describe('The home we are looking for.'),
|
||||
};
|
||||
const pickProfile = (args) => Object.fromEntries(Object.keys(Profile).filter((k) => args[k] !== undefined).map((k) => [k, args[k]]));
|
||||
|
||||
const server = new McpServer({ name: 'bilagun', version: '1.0.0' });
|
||||
|
||||
server.registerTool('list_animals', {
|
||||
title: 'List animals',
|
||||
description: 'List animals of the website. Filters are optional.',
|
||||
inputSchema: {
|
||||
language: z.enum(['es', 'eu']).default('es'),
|
||||
status: z.enum(['available', 'pending', 'adopted']).optional(),
|
||||
species: z.enum(['dog', 'cat']).optional(),
|
||||
search: z.string().optional().describe('Text to search in the name or story.'),
|
||||
includeDrafts: z.boolean().default(false),
|
||||
},
|
||||
}, tool(async ({ language, status, species, search, includeDrafts }) => {
|
||||
const params = new URLSearchParams({ lang: language, per_page: '100', context: 'edit', _fields: 'id,title,status,link,language,translation_of,profile' });
|
||||
params.set('status', includeDrafts ? 'publish,draft,pending' : 'publish');
|
||||
if (search) params.set('search', search);
|
||||
if (species) params.set('species', String(await speciesId(species, language)));
|
||||
let animals = (await api(`/wp/v2/animals?${params}`)).map(summary);
|
||||
if (status) animals = animals.filter((a) => a.profile?.status === status);
|
||||
return animals.map(({ id, name, wpStatus, profile, translationOf }) => ({ id, name, wpStatus, status: profile?.status, featured: profile?.featured, age: profile?.labels?.age, translationOf }));
|
||||
}));
|
||||
|
||||
server.registerTool('get_animal', {
|
||||
title: 'Get animal',
|
||||
description: 'Full record of one animal, including its story and profile.',
|
||||
inputSchema: { id: z.number().int() },
|
||||
}, tool(async ({ id }) => {
|
||||
const a = await api(`/wp/v2/animals/${id}?context=edit&_fields=id,title,content,excerpt,status,link,language,translation_of,featured_media,profile`);
|
||||
return { ...summary(a), story: a.content?.raw, shortPhrase: a.excerpt?.raw, featuredMedia: a.featured_media };
|
||||
}));
|
||||
|
||||
server.registerTool('create_animal', {
|
||||
title: 'Create animal',
|
||||
description: 'Create an animal record. It is saved as a draft unless publish=true. To create the Basque version of an existing animal, pass language="eu" and translationOf=<id of the Spanish record>: shared data (status, dates, photos…) is copied automatically; write the texts in Basque.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1),
|
||||
species: z.enum(['dog', 'cat']),
|
||||
language: z.enum(['es', 'eu']).default('es'),
|
||||
translationOf: z.number().int().optional(),
|
||||
story: z.string().optional().describe('Main story (plain text or HTML).'),
|
||||
shortPhrase: z.string().optional().describe('Optional one-line phrase.'),
|
||||
publish: z.boolean().default(false),
|
||||
...Profile,
|
||||
},
|
||||
}, tool(async (args) => {
|
||||
const body = {
|
||||
title: args.name,
|
||||
content: args.story ?? '',
|
||||
excerpt: args.shortPhrase ?? '',
|
||||
status: args.publish ? 'publish' : 'draft',
|
||||
species: [await speciesId(args.species, args.language)],
|
||||
language: args.language,
|
||||
profile: { status: 'available', ...pickProfile(args) },
|
||||
};
|
||||
if (args.translationOf) body.translation_of = args.translationOf;
|
||||
const created = await api('/wp/v2/animals', { method: 'POST', body });
|
||||
return { created: summary(created), editInDashboard: `${BASE}/wp-admin/post.php?post=${created.id}&action=edit` };
|
||||
}));
|
||||
|
||||
server.registerTool('update_animal', {
|
||||
title: 'Update animal',
|
||||
description: 'Update an animal. Only the fields provided change. Shared fields (status, featured, dates, size, photos) are applied to both languages automatically; texts only to this record.',
|
||||
inputSchema: {
|
||||
id: z.number().int(),
|
||||
name: z.string().optional(),
|
||||
story: z.string().optional(),
|
||||
shortPhrase: z.string().optional(),
|
||||
publish: z.boolean().optional().describe('true = publish, false = back to draft.'),
|
||||
...Profile,
|
||||
},
|
||||
}, tool(async (args) => {
|
||||
const body = {};
|
||||
if (args.name !== undefined) body.title = args.name;
|
||||
if (args.story !== undefined) body.content = args.story;
|
||||
if (args.shortPhrase !== undefined) body.excerpt = args.shortPhrase;
|
||||
if (args.publish !== undefined) body.status = args.publish ? 'publish' : 'draft';
|
||||
const profile = pickProfile(args);
|
||||
if (Object.keys(profile).length) body.profile = profile;
|
||||
return summary(await api(`/wp/v2/animals/${args.id}`, { method: 'POST', body }));
|
||||
}));
|
||||
|
||||
server.registerTool('set_status', {
|
||||
title: 'Set adoption status',
|
||||
description: 'Shortcut to mark an animal as available, pending (in process) or adopted, in both languages.',
|
||||
inputSchema: { id: z.number().int(), status: z.enum(['available', 'pending', 'adopted']) },
|
||||
}, tool(async ({ id, status }) => summary(await api(`/wp/v2/animals/${id}`, { method: 'POST', body: { profile: { status } } }))));
|
||||
|
||||
server.registerTool('upload_photo', {
|
||||
title: 'Upload photo',
|
||||
description: 'Upload a photo (local file path or https URL) and attach it to an animal, as the main photo or added to its gallery.',
|
||||
inputSchema: {
|
||||
animalId: z.number().int(),
|
||||
source: z.string().describe('Local file path or https URL of a JPG, PNG or WebP image.'),
|
||||
asMain: z.boolean().default(false).describe('true = main photo; false = add to the gallery.'),
|
||||
altText: z.string().optional(),
|
||||
},
|
||||
}, tool(async ({ animalId, source, asMain, altText }) => {
|
||||
let bytes, name;
|
||||
if (/^https?:\/\//.test(source)) {
|
||||
const res = await fetch(source);
|
||||
if (!res.ok) throw new Error(`Could not download ${source}: ${res.status}`);
|
||||
bytes = Buffer.from(await res.arrayBuffer());
|
||||
name = basename(new URL(source).pathname) || 'photo.jpg';
|
||||
} else {
|
||||
bytes = await readFile(source);
|
||||
name = basename(source);
|
||||
}
|
||||
const ext = extname(name).toLowerCase();
|
||||
const type = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp' }[ext];
|
||||
if (!type) throw new Error('Only JPG, PNG or WebP images are allowed.');
|
||||
const media = await api('/wp/v2/media', {
|
||||
method: 'POST', raw: true, body: bytes,
|
||||
headers: { 'Content-Type': type, 'Content-Disposition': `attachment; filename="${name.replace(/"/g, '')}"` },
|
||||
});
|
||||
if (altText) await api(`/wp/v2/media/${media.id}`, { method: 'POST', body: { alt_text: altText } });
|
||||
if (asMain) {
|
||||
await api(`/wp/v2/animals/${animalId}`, { method: 'POST', body: { featured_media: media.id } });
|
||||
} else {
|
||||
const a = await api(`/wp/v2/animals/${animalId}?context=edit&_fields=profile`);
|
||||
const ids = [...new Set([...(a.profile?.galleryIds || []), media.id])];
|
||||
await api(`/wp/v2/animals/${animalId}`, { method: 'POST', body: { profile: { galleryIds: ids } } });
|
||||
}
|
||||
return { mediaId: media.id, url: media.source_url, attachedTo: animalId, as: asMain ? 'main photo' : 'gallery' };
|
||||
}));
|
||||
|
||||
server.registerTool('get_needs', {
|
||||
title: 'Get material needs',
|
||||
description: 'Current list of material needs shown on the website, in Spanish and Basque.',
|
||||
inputSchema: {},
|
||||
}, tool(async () => api('/bilagun/v1/needs')));
|
||||
|
||||
server.registerTool('set_needs', {
|
||||
title: 'Set material needs',
|
||||
description: 'Replace the list of material needs. Provide both languages when possible (es = Spanish, eu = Basque).',
|
||||
inputSchema: { es: z.array(z.string()).optional(), eu: z.array(z.string()).optional() },
|
||||
}, tool(async ({ es, eu }) => api('/bilagun/v1/needs', { method: 'PUT', body: { ...(es ? { es } : {}), ...(eu ? { eu } : {}) } })));
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
1210
mcp/package-lock.json
generated
Normal file
1210
mcp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
mcp/package.json
Normal file
15
mcp/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "bilagun-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server to manage the Bilagun website (animals, photos, material needs) through its REST API.",
|
||||
"type": "module",
|
||||
"bin": { "bilagun-mcp": "index.js" },
|
||||
"main": "index.js",
|
||||
"engines": { "node": ">=20" },
|
||||
"scripts": { "start": "node index.js" },
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.1",
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"license": "GPL-2.0-or-later"
|
||||
}
|
||||
@@ -149,6 +149,18 @@ function ga_animal_api_update( array $profile, $post_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function ga_lines_of( $text ) {
|
||||
return array_values( array_filter( array_map( 'trim', explode( "\n", (string) $text ) ) ) );
|
||||
}
|
||||
|
||||
/** Default needs list (same as the theme registry), used when nothing has been saved yet. */
|
||||
function ga_default_needs( $lang ) {
|
||||
if ( function_exists( 'ga_customizer_registry' ) ) {
|
||||
return ga_customizer_registry()['material']['fields']['list'][ $lang ] ?? '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** JSON schema of the profile (used by REST validation and the OpenAPI docs). */
|
||||
function ga_animal_api_schema() {
|
||||
$string_or_null = array( 'type' => array( 'string', 'null' ) );
|
||||
@@ -181,6 +193,79 @@ function ga_animal_api_schema() {
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', function () {
|
||||
// Language of the animal and link to its translation (write), e.g. {"language":"eu","translation_of":123}.
|
||||
register_rest_field( 'animal', 'language', array(
|
||||
'get_callback' => fn( $post ) => function_exists( 'pll_get_post_language' ) ? pll_get_post_language( $post['id'] ) : 'es',
|
||||
'update_callback' => function ( $value, $post ) {
|
||||
if ( function_exists( 'pll_set_post_language' ) ) {
|
||||
pll_set_post_language( $post->ID, $value );
|
||||
}
|
||||
return true;
|
||||
},
|
||||
'schema' => array( 'type' => 'string', 'enum' => array( 'es', 'eu' ), 'description' => 'Language of this translation.', 'context' => array( 'view', 'edit' ) ),
|
||||
) );
|
||||
register_rest_field( 'animal', 'translation_of', array(
|
||||
'get_callback' => function ( $post ) {
|
||||
$translations = function_exists( 'pll_get_post_translations' ) ? pll_get_post_translations( $post['id'] ) : array();
|
||||
unset( $translations[ pll_get_post_language( $post['id'] ) ?: '' ] );
|
||||
return $translations ? (int) reset( $translations ) : null;
|
||||
},
|
||||
'update_callback' => function ( $value, $post ) {
|
||||
if ( ! $value || ! function_exists( 'pll_save_post_translations' ) || 'animal' !== get_post_type( $value ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( ! current_user_can( 'edit_post', $value ) ) {
|
||||
return new WP_Error( 'rest_forbidden', 'Cannot edit the linked animal.', array( 'status' => 403 ) );
|
||||
}
|
||||
$translations = pll_get_post_translations( $value );
|
||||
$translations[ pll_get_post_language( $post->ID ) ] = $post->ID;
|
||||
pll_save_post_translations( $translations );
|
||||
// The new translation inherits the shared data (status, dates, photos…).
|
||||
foreach ( ga_fields() as $key => $field ) {
|
||||
if ( ! empty( $field['shared'] ) && '' === ga_get( $post->ID, $key ) && '' !== ga_get( $value, $key ) ) {
|
||||
update_post_meta( $post->ID, "_ga_$key", ga_get( $value, $key ) );
|
||||
}
|
||||
}
|
||||
if ( ! has_post_thumbnail( $post->ID ) && has_post_thumbnail( $value ) ) {
|
||||
set_post_thumbnail( $post->ID, get_post_thumbnail_id( $value ) );
|
||||
}
|
||||
return true;
|
||||
},
|
||||
'schema' => array( 'type' => array( 'integer', 'null' ), 'description' => 'ID of the same animal in the other language.', 'context' => array( 'view', 'edit' ) ),
|
||||
) );
|
||||
|
||||
// Material needs list (Personalizar → Ayuda material), per language.
|
||||
register_rest_route( 'bilagun/v1', '/needs', array(
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'permission_callback' => '__return_true',
|
||||
'callback' => fn() => array(
|
||||
'es' => ga_lines_of( get_theme_mod( 'ga_material_list', ga_default_needs( 'es' ) ) ),
|
||||
'eu' => ga_lines_of( get_theme_mod( 'ga_material_list_eu', ga_default_needs( 'eu' ) ) ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => 'PUT',
|
||||
'permission_callback' => fn() => current_user_can( 'edit_others_posts' ),
|
||||
'args' => array(
|
||||
'es' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
|
||||
'eu' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
|
||||
),
|
||||
'callback' => function ( WP_REST_Request $req ) {
|
||||
foreach ( array( 'es' => 'ga_material_list', 'eu' => 'ga_material_list_eu' ) as $lang => $mod ) {
|
||||
if ( null !== $req->get_param( $lang ) ) {
|
||||
$items = array_filter( array_map( 'sanitize_text_field', (array) $req->get_param( $lang ) ) );
|
||||
set_theme_mod( $mod, implode( "\n", $items ) );
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'es' => ga_lines_of( get_theme_mod( 'ga_material_list', '' ) ),
|
||||
'eu' => ga_lines_of( get_theme_mod( 'ga_material_list_eu', '' ) ),
|
||||
);
|
||||
},
|
||||
),
|
||||
) );
|
||||
|
||||
register_rest_field( 'animal', 'profile', array(
|
||||
'get_callback' => fn( $post ) => ga_animal_api_data( $post['id'] ),
|
||||
'update_callback' => fn( $value, $post ) => ga_animal_api_update( (array) $value, $post->ID ),
|
||||
|
||||
Reference in New Issue
Block a user