feat: TikTok and Snapchat integrations
TikTok: getUserProfile, getUserVideos, createVideo (PULL_FROM_URL), getVideoStatus via Content Posting API v2. Full multi-tenant credential isolation and audit logging on write operations. Snapchat: getMe (Login Kit), getAdAccounts (Marketing API). createSnap throws with a clear explanation that Creative Kit is mobile-only — no server-side posting API exists. Platform type, validPlatforms list, and /api/connections endpoint all updated to include tiktok and snapchat. Architecture diagram updated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,8 @@ package "hermes-mcp | Node.js / TypeScript | hermes.squaremcp.com" {
|
|||||||
[discord.ts] as c_dc
|
[discord.ts] as c_dc
|
||||||
[instagram.ts] as c_ig
|
[instagram.ts] as c_ig
|
||||||
[twitter.ts] as c_tw
|
[twitter.ts] as c_tw
|
||||||
|
[tiktok.ts] as c_tt
|
||||||
|
[snapchat.ts] as c_sc
|
||||||
[obsidian.ts] as c_ob
|
[obsidian.ts] as c_ob
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +111,8 @@ cloud "External Platform APIs" {
|
|||||||
[Telegram Bot API] as tg_api
|
[Telegram Bot API] as tg_api
|
||||||
[Discord API v10] as dc_api
|
[Discord API v10] as dc_api
|
||||||
[Twitter API v2] as tw_api
|
[Twitter API v2] as tw_api
|
||||||
|
[TikTok Content\nPosting API v2] as tt_api
|
||||||
|
[Snapchat\nMarketing API] as sc_api
|
||||||
[Obsidian Vault\nfilesystem] as ob_vault
|
[Obsidian Vault\nfilesystem] as ob_vault
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +138,8 @@ dispatch --> c_tg
|
|||||||
dispatch --> c_dc
|
dispatch --> c_dc
|
||||||
dispatch --> c_ig
|
dispatch --> c_ig
|
||||||
dispatch --> c_tw
|
dispatch --> c_tw
|
||||||
|
dispatch --> c_tt
|
||||||
|
dispatch --> c_sc
|
||||||
dispatch --> c_ob
|
dispatch --> c_ob
|
||||||
|
|
||||||
c_wa --> cred_store : getCredential
|
c_wa --> cred_store : getCredential
|
||||||
@@ -142,6 +148,8 @@ c_tg --> cred_store : getCredential
|
|||||||
c_dc --> cred_store : getCredential
|
c_dc --> cred_store : getCredential
|
||||||
c_ig --> cred_store : getCredential
|
c_ig --> cred_store : getCredential
|
||||||
c_tw --> cred_store : getCredential
|
c_tw --> cred_store : getCredential
|
||||||
|
c_tt --> cred_store : getCredential
|
||||||
|
c_sc --> cred_store : getCredential
|
||||||
|
|
||||||
cred_store --> redis
|
cred_store --> redis
|
||||||
wh_router --> redis
|
wh_router --> redis
|
||||||
@@ -153,6 +161,8 @@ c_li --> li_api
|
|||||||
c_tg --> tg_api
|
c_tg --> tg_api
|
||||||
c_dc --> dc_api
|
c_dc --> dc_api
|
||||||
c_tw --> tw_api
|
c_tw --> tw_api
|
||||||
|
c_tt --> tt_api
|
||||||
|
c_sc --> sc_api
|
||||||
c_ob --> ob_vault
|
c_ob --> ob_vault
|
||||||
|
|
||||||
oauth --> mysql
|
oauth --> mysql
|
||||||
|
|||||||
83
src/clients/snapchat.ts
Normal file
83
src/clients/snapchat.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { Customer } from '../billing/middleware.js';
|
||||||
|
import type { OAuthCredentials } from '../multitenancy/credential-store.js';
|
||||||
|
|
||||||
|
const SNAPCHAT_API_BASE = 'https://api.snapchat.com/v1';
|
||||||
|
|
||||||
|
function getEnvToken(account: string): string {
|
||||||
|
const envKey = `SNAPCHAT_${account.toUpperCase()}_ACCESS_TOKEN`;
|
||||||
|
return process.env[envKey] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveToken(args: { account?: string }, customer?: Customer): Promise<string> {
|
||||||
|
if (customer) {
|
||||||
|
const creds = await customer.getCredential<OAuthCredentials>('snapchat');
|
||||||
|
if (!creds) throw new Error('Snapchat not connected for this account');
|
||||||
|
return creds.accessToken;
|
||||||
|
}
|
||||||
|
const token = getEnvToken(args.account ?? 'default');
|
||||||
|
if (!token) throw new Error('Missing Snapchat credentials. Set SNAPCHAT_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function snapchatRequest(endpoint: string, accessToken: string) {
|
||||||
|
const res = await fetch(`${SNAPCHAT_API_BASE}${endpoint}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${accessToken}` },
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(`Snapchat API error (${res.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMe(
|
||||||
|
args: { account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
bitmoji_avatar_url?: string;
|
||||||
|
}> {
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
const data = await snapchatRequest('/me', accessToken);
|
||||||
|
const me = data.data?.me ?? data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: me.externalId ?? me.id ?? '',
|
||||||
|
display_name: me.displayName ?? me.display_name ?? '',
|
||||||
|
bitmoji_avatar_url: me.bitmoji?.avatarUrl ?? me.bitmoji_avatar_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapchat Creative Kit is a mobile-only SDK — there is no server-side
|
||||||
|
// endpoint for publishing Snaps or Stories. This stub documents that
|
||||||
|
// constraint rather than silently doing nothing.
|
||||||
|
export async function createSnap(
|
||||||
|
_args: { image_url?: string; video_url?: string; caption?: string; account?: string },
|
||||||
|
_customer?: Customer
|
||||||
|
): Promise<never> {
|
||||||
|
throw new Error(
|
||||||
|
'Snapchat Creative Kit is a mobile-only SDK — it cannot post Snaps from a server. ' +
|
||||||
|
'To share content to Snapchat, integrate the Creative Kit iOS/Android SDK in a mobile app. ' +
|
||||||
|
'See https://kit.snapchat.com/creative-kit for docs.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAdAccounts(
|
||||||
|
args: { account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<Array<{ id: string; name: string; status: string; currency: string }>> {
|
||||||
|
// Snapchat Marketing API — available for ad account management
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
const data = await snapchatRequest('/me/adaccounts', accessToken);
|
||||||
|
|
||||||
|
return (data.adaccounts ?? []).map((a: Record<string, unknown>) => ({
|
||||||
|
id: String((a.adaccount as Record<string, unknown>)?.id ?? a.id ?? ''),
|
||||||
|
name: String((a.adaccount as Record<string, unknown>)?.name ?? a.name ?? ''),
|
||||||
|
status: String((a.adaccount as Record<string, unknown>)?.status ?? a.status ?? ''),
|
||||||
|
currency: String((a.adaccount as Record<string, unknown>)?.currency_code ?? a.currency ?? ''),
|
||||||
|
}));
|
||||||
|
}
|
||||||
175
src/clients/tiktok.ts
Normal file
175
src/clients/tiktok.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import type { Customer } from '../billing/middleware.js';
|
||||||
|
import type { OAuthCredentials } from '../multitenancy/credential-store.js';
|
||||||
|
import { createToolAudit } from '../multitenancy/audit-log.js';
|
||||||
|
|
||||||
|
const TIKTOK_API_BASE = 'https://open.tiktokapis.com/v2';
|
||||||
|
|
||||||
|
function getEnvToken(account: string): string {
|
||||||
|
const envKey = `TIKTOK_${account.toUpperCase()}_ACCESS_TOKEN`;
|
||||||
|
return process.env[envKey] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveToken(args: { account?: string }, customer?: Customer): Promise<string> {
|
||||||
|
if (customer) {
|
||||||
|
const creds = await customer.getCredential<OAuthCredentials>('tiktok');
|
||||||
|
if (!creds) throw new Error('TikTok not connected for this account');
|
||||||
|
return creds.accessToken;
|
||||||
|
}
|
||||||
|
const token = getEnvToken(args.account ?? 'default');
|
||||||
|
if (!token) throw new Error('Missing TikTok credentials. Set TIKTOK_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tiktokRequest(
|
||||||
|
endpoint: string,
|
||||||
|
accessToken: string,
|
||||||
|
method: 'GET' | 'POST' = 'GET',
|
||||||
|
body?: unknown,
|
||||||
|
params?: Record<string, string>
|
||||||
|
) {
|
||||||
|
const url = new URL(`${TIKTOK_API_BASE}${endpoint}`);
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error?.code && data.error.code !== 'ok') {
|
||||||
|
throw new Error(`TikTok API error: ${data.error.message ?? data.error.code}`);
|
||||||
|
}
|
||||||
|
return data.data ?? data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserProfile(
|
||||||
|
args: { account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{
|
||||||
|
open_id: string;
|
||||||
|
display_name: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
bio?: string;
|
||||||
|
is_verified: boolean;
|
||||||
|
follower_count: number;
|
||||||
|
following_count: number;
|
||||||
|
likes_count: number;
|
||||||
|
video_count: number;
|
||||||
|
}> {
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
|
||||||
|
const fields = 'open_id,union_id,avatar_url,display_name,bio_description,is_verified,follower_count,following_count,likes_count,video_count';
|
||||||
|
const data = await tiktokRequest('/user/info/', accessToken, 'GET', undefined, { fields });
|
||||||
|
const u = data.user ?? data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
open_id: u.open_id ?? '',
|
||||||
|
display_name: u.display_name ?? '',
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
|
bio: u.bio_description,
|
||||||
|
is_verified: u.is_verified ?? false,
|
||||||
|
follower_count: u.follower_count ?? 0,
|
||||||
|
following_count: u.following_count ?? 0,
|
||||||
|
likes_count: u.likes_count ?? 0,
|
||||||
|
video_count: u.video_count ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserVideos(
|
||||||
|
args: { max_count?: number; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<Array<{
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
video_description?: string;
|
||||||
|
duration: number;
|
||||||
|
cover_image_url?: string;
|
||||||
|
share_url?: string;
|
||||||
|
view_count: number;
|
||||||
|
like_count: number;
|
||||||
|
comment_count: number;
|
||||||
|
share_count: number;
|
||||||
|
create_time: number;
|
||||||
|
}>> {
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
|
||||||
|
const fields = 'id,title,video_description,duration,cover_image_url,share_url,view_count,like_count,comment_count,share_count,create_time';
|
||||||
|
const data = await tiktokRequest('/video/list/', accessToken, 'POST', {
|
||||||
|
max_count: Math.min(args.max_count ?? 10, 20),
|
||||||
|
fields: fields.split(','),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (data.videos ?? []).map((v: Record<string, unknown>) => ({
|
||||||
|
id: String(v.id ?? ''),
|
||||||
|
title: v.title as string | undefined,
|
||||||
|
video_description: v.video_description as string | undefined,
|
||||||
|
duration: Number(v.duration ?? 0),
|
||||||
|
cover_image_url: v.cover_image_url as string | undefined,
|
||||||
|
share_url: v.share_url as string | undefined,
|
||||||
|
view_count: Number(v.view_count ?? 0),
|
||||||
|
like_count: Number(v.like_count ?? 0),
|
||||||
|
comment_count: Number(v.comment_count ?? 0),
|
||||||
|
share_count: Number(v.share_count ?? 0),
|
||||||
|
create_time: Number(v.create_time ?? 0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createVideo(
|
||||||
|
args: { video_url: string; title?: string; description?: string; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{ publish_id: string; status: string }> {
|
||||||
|
const audit = customer ? createToolAudit(customer.id, 'tiktok:createVideo') : null;
|
||||||
|
const auditArgs = { title: args.title };
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
|
||||||
|
// Step 1: initialise upload
|
||||||
|
const init = await tiktokRequest('/post/video/init/', accessToken, 'POST', {
|
||||||
|
post_info: {
|
||||||
|
title: args.title ?? '',
|
||||||
|
description: args.description ?? '',
|
||||||
|
disable_duet: false,
|
||||||
|
disable_comment: false,
|
||||||
|
disable_stitch: false,
|
||||||
|
privacy_level: 'PUBLIC_TO_EVERYONE',
|
||||||
|
},
|
||||||
|
source_info: {
|
||||||
|
source: 'PULL_FROM_URL',
|
||||||
|
video_url: args.video_url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const publishId = init.publish_id ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = { publish_id: publishId, status: 'processing' };
|
||||||
|
if (audit) await audit.success(auditArgs);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
if (audit) await audit.error(auditArgs, String(err));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVideoStatus(
|
||||||
|
args: { publish_id: string; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{ publish_id: string; status: string; fail_reason?: string }> {
|
||||||
|
const accessToken = await resolveToken(args, customer);
|
||||||
|
|
||||||
|
const data = await tiktokRequest('/post/publish/status/fetch/', accessToken, 'POST', {
|
||||||
|
publish_id: args.publish_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
publish_id: args.publish_id,
|
||||||
|
status: String(data.status ?? 'unknown'),
|
||||||
|
fail_reason: data.fail_reason as string | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -663,7 +663,7 @@ app.post('/api/connect/:platform', meterMiddleware, async (req, res) => {
|
|||||||
const platform = req.params.platform as Platform;
|
const platform = req.params.platform as Platform;
|
||||||
const { accessToken, refreshToken, expiresAt, scope } = req.body as Record<string, string>;
|
const { accessToken, refreshToken, expiresAt, scope } = req.body as Record<string, string>;
|
||||||
|
|
||||||
const validPlatforms: Platform[] = ['linkedin', 'telegram', 'discord', 'instagram', 'twitter'];
|
const validPlatforms: Platform[] = ['linkedin', 'telegram', 'discord', 'instagram', 'twitter', 'tiktok', 'snapchat'];
|
||||||
if (!validPlatforms.includes(platform)) {
|
if (!validPlatforms.includes(platform)) {
|
||||||
res.status(400).json({ error: 'unknown_platform' });
|
res.status(400).json({ error: 'unknown_platform' });
|
||||||
return;
|
return;
|
||||||
@@ -687,7 +687,7 @@ app.post('/api/connect/:platform', meterMiddleware, async (req, res) => {
|
|||||||
// Get connection status for a customer
|
// Get connection status for a customer
|
||||||
app.get('/api/connections', meterMiddleware, async (req, res) => {
|
app.get('/api/connections', meterMiddleware, async (req, res) => {
|
||||||
const customer = (req as unknown as { customer: Customer }).customer;
|
const customer = (req as unknown as { customer: Customer }).customer;
|
||||||
const platforms: Platform[] = ['email', 'whatsapp', 'linkedin', 'telegram', 'discord', 'instagram', 'twitter', 'obsidian'];
|
const platforms: Platform[] = ['email', 'whatsapp', 'linkedin', 'telegram', 'discord', 'instagram', 'twitter', 'tiktok', 'snapchat', 'obsidian'];
|
||||||
|
|
||||||
const status: Record<string, boolean> = {};
|
const status: Record<string, boolean> = {};
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function decrypt(ciphertext: string): string {
|
|||||||
return decipher.update(encrypted) + decipher.final('utf8');
|
return decipher.update(encrypted) + decipher.final('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'obsidian';
|
export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'tiktok' | 'snapchat' | 'obsidian';
|
||||||
|
|
||||||
export interface EmailCredentials {
|
export interface EmailCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
145
src/tools.ts
145
src/tools.ts
@@ -9,6 +9,8 @@ import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto a
|
|||||||
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
|
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
|
||||||
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createPost as createInstagramPost } from './clients/instagram.js';
|
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createPost as createInstagramPost } from './clients/instagram.js';
|
||||||
import { searchTweets, getUserProfile, getUserTweets, createTweet } from './clients/twitter.js';
|
import { searchTweets, getUserProfile, getUserTweets, createTweet } from './clients/twitter.js';
|
||||||
|
import { getUserProfile as getTikTokProfile, getUserVideos, createVideo, getVideoStatus } from './clients/tiktok.js';
|
||||||
|
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
|
||||||
|
|
||||||
const ACCOUNT_PARAM = {
|
const ACCOUNT_PARAM = {
|
||||||
account: {
|
account: {
|
||||||
@@ -517,6 +519,97 @@ export const tools: Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── TikTok tools ─────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'tiktok_get_profile',
|
||||||
|
description:
|
||||||
|
'Get the TikTok user profile including follower count, following count, likes, and video count.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tiktok_get_videos',
|
||||||
|
description:
|
||||||
|
'List recent videos from the authenticated TikTok account with view, like, comment, and share counts.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' },
|
||||||
|
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tiktok_create_video',
|
||||||
|
description:
|
||||||
|
'Post a video to TikTok by providing a publicly accessible video URL. Returns a publish_id to check status.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['video_url'],
|
||||||
|
properties: {
|
||||||
|
video_url: { type: 'string', description: 'Publicly accessible URL of the video to post' },
|
||||||
|
title: { type: 'string', description: 'Video title (max 150 chars)' },
|
||||||
|
description: { type: 'string', description: 'Video description / caption' },
|
||||||
|
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tiktok_get_video_status',
|
||||||
|
description:
|
||||||
|
'Check the processing status of a TikTok video upload using the publish_id returned by tiktok_create_video.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['publish_id'],
|
||||||
|
properties: {
|
||||||
|
publish_id: { type: 'string', description: 'Publish ID returned by tiktok_create_video' },
|
||||||
|
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Snapchat tools ───────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'snapchat_get_me',
|
||||||
|
description:
|
||||||
|
'Get the authenticated Snapchat user profile (display name, bitmoji avatar). Uses Snapchat Login Kit.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'snapchat_create_snap',
|
||||||
|
description:
|
||||||
|
'[NOT SUPPORTED] Snapchat Creative Kit is a mobile-only SDK — posting Snaps from a server is not possible. Use the iOS/Android Creative Kit SDK instead.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
image_url: { type: 'string', description: 'Image URL (not usable server-side)' },
|
||||||
|
video_url: { type: 'string', description: 'Video URL (not usable server-side)' },
|
||||||
|
caption: { type: 'string', description: 'Caption text (not usable server-side)' },
|
||||||
|
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'snapchat_get_ad_accounts',
|
||||||
|
description:
|
||||||
|
'List Snapchat Ads Manager ad accounts. Use when the user wants to manage Snapchat advertising campaigns.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function acct(args: Record<string, unknown>): Account {
|
function acct(args: Record<string, unknown>): Account {
|
||||||
@@ -779,6 +872,58 @@ export async function handleToolCall(
|
|||||||
}, customer);
|
}, customer);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ── TikTok ─────────────────────────────────────────────────
|
||||||
|
case 'tiktok_get_profile':
|
||||||
|
result = await getTikTokProfile({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tiktok_get_videos':
|
||||||
|
result = await getUserVideos({
|
||||||
|
max_count: (args.max_count as number) ?? 10,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tiktok_create_video':
|
||||||
|
result = await createVideo({
|
||||||
|
video_url: args.video_url as string,
|
||||||
|
title: args.title as string | undefined,
|
||||||
|
description: args.description as string | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tiktok_get_video_status':
|
||||||
|
result = await getVideoStatus({
|
||||||
|
publish_id: args.publish_id as string,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ── Snapchat ────────────────────────────────────────────────
|
||||||
|
case 'snapchat_get_me':
|
||||||
|
result = await getSnapchatMe({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'snapchat_create_snap':
|
||||||
|
result = await createSnap({
|
||||||
|
image_url: args.image_url as string | undefined,
|
||||||
|
video_url: args.video_url as string | undefined,
|
||||||
|
caption: args.caption as string | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'snapchat_get_ad_accounts':
|
||||||
|
result = await getAdAccounts({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
||||||
case 'yahoo_get_profile':
|
case 'yahoo_get_profile':
|
||||||
result = await getProfile('yahoo');
|
result = await getProfile('yahoo');
|
||||||
|
|||||||
Reference in New Issue
Block a user