WhatsApp Business API (Meta Cloud API)
- New client: src/clients/whatsapp.ts
- Tools: whatsapp_send_message, whatsapp_send_template, whatsapp_list_templates
- REST endpoints: POST /api/whatsapp/send, POST /api/whatsapp/template, GET /api/whatsapp/templates
- Multi-account env var pattern: WHATSAPP_{ACCOUNT}_*
LinkedIn API (OpenID Connect)
- New client: src/clients/linkedin.ts
- Tools: linkedin_get_profile, linkedin_create_post, linkedin_search_connections, linkedin_send_message
- REST endpoints: GET /api/linkedin/profile, POST /api/linkedin/post, POST /api/linkedin/search-connections, POST /api/linkedin/message
- Multi-account env var pattern: LINKEDIN_{ACCOUNT}_*
- Uses /v2/userinfo (OpenID Connect) for profile reads
Domain migration
- hermes.fetcherpay.com -> hermes.squaremcp.com
- Updated K8s ingress, TLS cert, SERVER_URL env var
- Updated OPENCODE.md and opencode.json references
SquareMCP site
- Added logo assets (SVG, LinkedIn variants)
- Added terms.html
- Updated Dockerfile, nginx config, styles, index, privacy pages
Docs
- Added OPENCODE.md for opencode AI integration setup
- Updated .env.example with WhatsApp and LinkedIn credentials
- Added opencode.json to .gitignore (contains live API key)
Total tools: 19 (email 6, obsidian 5, whatsapp 4, linkedin 4)
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
|
|
|
|
const WHATSAPP_API_VERSION = 'v18.0';
|
|
const WHATSAPP_BASE_URL = process.env['WHATSAPP_API_BASE_URL'] ?? 'https://graph.facebook.com';
|
|
|
|
function getEnvVar(prefix: string, account: string, key: string): string {
|
|
const envKey = `WHATSAPP_${account.toUpperCase()}_${key}`;
|
|
return process.env[envKey] ?? '';
|
|
}
|
|
|
|
function getPhoneNumberId(account: string): string {
|
|
return getEnvVar('WHATSAPP', account, 'PHONE_NUMBER_ID');
|
|
}
|
|
|
|
function getAccessToken(account: string): string {
|
|
return getEnvVar('WHATSAPP', account, 'ACCESS_TOKEN');
|
|
}
|
|
|
|
function getBusinessAccountId(account: string): string {
|
|
return getEnvVar('WHATSAPP', account, 'BUSINESS_ACCOUNT_ID');
|
|
}
|
|
|
|
interface WhatsAppMessageResponse {
|
|
messaging_product: string;
|
|
contacts?: Array<{ wa_id: string; input: string }>;
|
|
messages?: Array<{ id: string }>;
|
|
}
|
|
|
|
async function whatsappApiRequest(
|
|
phoneId: string,
|
|
accessToken: string,
|
|
endpoint: string,
|
|
method: 'GET' | 'POST' = 'POST',
|
|
body?: unknown
|
|
) {
|
|
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${phoneId}/${endpoint}`;
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.text();
|
|
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export async function sendMessage(args: { to: string; message: string; account?: string }): Promise<{ success: boolean; message_id: string }> {
|
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
|
const accessToken = getAccessToken(args.account ?? 'default');
|
|
|
|
if (!phoneId || !accessToken) {
|
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
|
}
|
|
|
|
const body = {
|
|
messaging_product: 'whatsapp',
|
|
to: args.to,
|
|
type: 'text',
|
|
text: { body: args.message },
|
|
};
|
|
|
|
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
|
|
const response = data as WhatsAppMessageResponse;
|
|
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
|
|
}
|
|
|
|
export async function sendTemplate(args: { to: string; template_name: string; language?: string; components?: unknown[]; account?: string }): Promise<{ success: boolean; message_id: string }> {
|
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
|
const accessToken = getAccessToken(args.account ?? 'default');
|
|
|
|
if (!phoneId || !accessToken) {
|
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
|
}
|
|
|
|
const body: Record<string, unknown> = {
|
|
messaging_product: 'whatsapp',
|
|
to: args.to,
|
|
type: 'template',
|
|
template: {
|
|
name: args.template_name,
|
|
language: { code: args.language ?? 'en' },
|
|
},
|
|
};
|
|
|
|
if (args.components) {
|
|
(body.template as Record<string, unknown>).components = args.components;
|
|
}
|
|
|
|
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
|
|
const response = data as WhatsAppMessageResponse;
|
|
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
|
|
}
|
|
|
|
export async function getMessageStatus(args: { message_id: string; account?: string }): Promise<{ message_id: string; status: string; timestamp?: string }> {
|
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
|
const accessToken = getAccessToken(args.account ?? 'default');
|
|
|
|
if (!phoneId || !accessToken) {
|
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
|
}
|
|
|
|
// Note: Meta Cloud API doesn't support polling message status via GET
|
|
// Status updates are only available via webhooks (push-based)
|
|
throw new Error('whatsapp_get_message_status is not supported. Meta Cloud API only provides delivery status via webhooks. Use POST /api/whatsapp/webhook to receive status updates.');
|
|
}
|
|
|
|
export async function listTemplates(args: { account?: string }): Promise<{ templates: Array<{ name: string; language: string; status: string }> }> {
|
|
const account = args.account ?? 'default';
|
|
const businessAccountId = getBusinessAccountId(account);
|
|
const accessToken = getAccessToken(account);
|
|
|
|
if (!businessAccountId || !accessToken) {
|
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_BUSINESS_ACCOUNT_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
|
}
|
|
|
|
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${businessAccountId}/message_templates?fields=name,language,status`;
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.text();
|
|
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
|
|
}
|
|
|
|
const data = await res.json();
|
|
return {
|
|
templates: (data.data ?? []).map((t: { name: string; language: string | { code: string }; status: string }) => ({
|
|
name: t.name,
|
|
language: typeof t.language === 'string' ? t.language : t.language?.code ?? 'en',
|
|
status: t.status,
|
|
})),
|
|
};
|
|
}
|