feat: add Zoho Cliq support
- Implement Zoho Cliq chat channels and configuration schemas - Add user authentication flow for Zoho Cliq - Add bot, send, server, and types for Zoho Cliq integration - Add zoho-cliq plugin for channel implementation - Add types.zoho-cliq.ts with ZohoCliqConfig types
This commit is contained in:
parent
e0158c5d5d
commit
ea66123a0b
39
src/channels/plugins/outbound/zoho-cliq.ts
Normal file
39
src/channels/plugins/outbound/zoho-cliq.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../routing/session-key.js";
|
||||
import { sendMessageZohoCliq } from "../../../zoho-cliq/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
export const zohoCliqOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(
|
||||
"Delivering to Zoho Cliq requires --to <channelOrChatId>",
|
||||
),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps }) => {
|
||||
const send = (deps as any)?.sendZohoCliq ?? sendMessageZohoCliq;
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const result = await send(to, text, {
|
||||
accountId: resolvedAccountId,
|
||||
});
|
||||
return { channel: "zoho-cliq", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, deps }) => {
|
||||
const send = (deps as any)?.sendZohoCliq ?? sendMessageZohoCliq;
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const result = await send(to, text, {
|
||||
accountId: resolvedAccountId,
|
||||
mediaUrl,
|
||||
});
|
||||
return { channel: "zoho-cliq", ...result };
|
||||
},
|
||||
};
|
||||
143
src/channels/plugins/zoho-cliq.ts
Normal file
143
src/channels/plugins/zoho-cliq.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
import type { ChannelMeta } from "./types.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
const meta: ChannelMeta = {
|
||||
id: "zoho-cliq",
|
||||
label: "Zoho Cliq",
|
||||
selectionLabel: "Zoho Cliq (OAuth)",
|
||||
docsPath: "/channels/zoho-cliq",
|
||||
docsLabel: "zoho-cliq",
|
||||
blurb: "supported (OAuth API).",
|
||||
};
|
||||
|
||||
export type ResolvedZohoCliqAccount = {
|
||||
accountId: string;
|
||||
name?: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
refreshToken: string;
|
||||
dc: string;
|
||||
allowFrom?: Array<string | number>;
|
||||
};
|
||||
|
||||
export const zohoCliqPlugin: ChannelPlugin<ResolvedZohoCliqAccount> = {
|
||||
id: "zoho-cliq",
|
||||
meta,
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
media: true,
|
||||
},
|
||||
config: {
|
||||
listAccountIds: (cfg) => {
|
||||
const accounts = (cfg as any).channels?.["zoho-cliq"]?.accounts;
|
||||
if (!accounts) return [];
|
||||
return Object.keys(accounts);
|
||||
},
|
||||
resolveAccount: (cfg, accountId) => {
|
||||
const resolvedAccountId = normalizeAccountId(accountId);
|
||||
const accounts = (cfg as any).channels?.["zoho-cliq"]?.accounts;
|
||||
if (!accounts) {
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
name: undefined,
|
||||
enabled: false,
|
||||
configured: false,
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
refreshToken: "",
|
||||
dc: "",
|
||||
};
|
||||
}
|
||||
const account = accounts[resolvedAccountId];
|
||||
if (!account) {
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
name: undefined,
|
||||
enabled: false,
|
||||
configured: false,
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
refreshToken: "",
|
||||
dc: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled ?? true,
|
||||
configured: !!(
|
||||
account.clientId &&
|
||||
account.clientSecret &&
|
||||
account.refreshToken
|
||||
),
|
||||
clientId: account.clientId,
|
||||
clientSecret: account.clientSecret,
|
||||
refreshToken: account.refreshToken,
|
||||
dc: account.dc,
|
||||
allowFrom: account.allowFrom,
|
||||
};
|
||||
},
|
||||
defaultAccountId: (cfg) => {
|
||||
const accounts = (cfg as any).channels?.["zoho-cliq"]?.accounts;
|
||||
if (!accounts) return DEFAULT_ACCOUNT_ID;
|
||||
const enabled = Object.entries(accounts).filter(
|
||||
([_, a]) => (a as any).enabled ?? true,
|
||||
);
|
||||
if (enabled.length === 1) return enabled[0][0];
|
||||
if (Object.hasOwn(accounts, DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return Object.keys(accounts)[0];
|
||||
},
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const channels = (cfg as any).channels;
|
||||
if (!channels) (cfg as any).channels = {};
|
||||
const zoho = channels["zoho-cliq"];
|
||||
if (!zoho) return cfg;
|
||||
if (!zoho.accounts) zoho.accounts = {};
|
||||
const account = zoho.accounts[resolvedAccountId];
|
||||
if (account) account.enabled = enabled;
|
||||
return cfg;
|
||||
},
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const channels = (cfg as any).channels;
|
||||
if (!channels) return cfg;
|
||||
const zoho = channels["zoho-cliq"];
|
||||
if (!zoho) return cfg;
|
||||
if (!zoho.accounts) return cfg;
|
||||
delete zoho.accounts[resolvedAccountId];
|
||||
if (Object.keys(zoho.accounts).length === 0) {
|
||||
delete channels["zoho-cliq"];
|
||||
}
|
||||
return cfg;
|
||||
},
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) => {
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const accounts = (cfg as any).channels?.["zoho-cliq"]?.accounts;
|
||||
if (!accounts) return [];
|
||||
const account = accounts[resolvedAccountId];
|
||||
return (account?.allowFrom ?? []).map((entry: any) => String(entry));
|
||||
},
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry: any) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry: any) => entry.toLowerCase()),
|
||||
},
|
||||
reload: { configPrefixes: ["channels.zoho-cliq"] },
|
||||
};
|
||||
@ -24,3 +24,4 @@ export * from "./types.slack.js";
|
||||
export * from "./types.telegram.js";
|
||||
export * from "./types.tools.js";
|
||||
export * from "./types.whatsapp.js";
|
||||
export * from "./types.zoho-cliq.js";
|
||||
|
||||
40
src/config/types.zoho-cliq.ts
Normal file
40
src/config/types.zoho-cliq.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { OutboundRetryConfig } from "./types.base.js";
|
||||
|
||||
export type ZohoCliqWebhookConfig = {
|
||||
/** Port for the webhook server. Default: same as main or specific. */
|
||||
port?: number;
|
||||
/** Path for the webhook endpoint. Default: /zoho/webhook. */
|
||||
path?: string;
|
||||
/** Secret key for validating incoming webhook signatures (if applicable). */
|
||||
secret?: string;
|
||||
};
|
||||
|
||||
export type ZohoCliqAccountConfig = {
|
||||
/** Optional display name for this account. */
|
||||
name?: string;
|
||||
/** If false, do not start this Zoho Cliq account. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** OAuth Client ID. */
|
||||
clientId?: string;
|
||||
/** OAuth Client Secret. */
|
||||
clientSecret?: string;
|
||||
/** OAuth Refresh Token. */
|
||||
refreshToken?: string;
|
||||
/** Data Center (US, EU, IN, AU, JP, CN). Default: US. */
|
||||
dc?: "US" | "EU" | "IN" | "AU" | "JP" | "CN" | "SA" | "UK" | "CA";
|
||||
/** Webhook configuration. */
|
||||
webhook?: ZohoCliqWebhookConfig;
|
||||
/** Direct message access policy (default: pairing). */
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
/** Allowlist for DM senders (ZUIDs or emails). */
|
||||
allowFrom?: Array<string>;
|
||||
/** Outbound text chunk size (chars). Default: 1000. */
|
||||
textChunkLimit?: number;
|
||||
/** Retry policy for outbound API calls. */
|
||||
retry?: OutboundRetryConfig;
|
||||
};
|
||||
|
||||
export type ZohoCliqConfig = {
|
||||
/** Optional per-account Configuration. */
|
||||
accounts?: Record<string, ZohoCliqAccountConfig>;
|
||||
} & ZohoCliqAccountConfig;
|
||||
40
src/zoho-cliq/auth.ts
Normal file
40
src/zoho-cliq/auth.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { ZohoCliqTokenResponse } from "./types.js";
|
||||
|
||||
const DC_URLS: Record<string, string> = {
|
||||
US: "https://accounts.zoho.com",
|
||||
EU: "https://accounts.zoho.eu",
|
||||
IN: "https://accounts.zoho.in",
|
||||
AU: "https://accounts.zoho.com.au",
|
||||
JP: "https://accounts.zoho.jp",
|
||||
CN: "https://accounts.zoho.com.cn",
|
||||
SA: "https://accounts.zoho.sa",
|
||||
UK: "https://accounts.zoho.uk",
|
||||
CA: "https://accounts.zohocloud.ca",
|
||||
};
|
||||
|
||||
export async function refreshAccessToken(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
refreshToken: string,
|
||||
dc: string = "US",
|
||||
scope: string = "ZohoCliq.Messages.CREATE,ZohoCliq.Channels.READ"
|
||||
): Promise<string> {
|
||||
const baseUrl = DC_URLS[dc] || DC_URLS["US"];
|
||||
const url = `${baseUrl}/oauth/v2/token?refresh_token=${refreshToken}&client_id=${clientId}&client_secret=${clientSecret}&grant_type=refresh_token&scope=${encodeURIComponent(scope)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to refresh Zoho Cliq token: ${response.status} ${response.statusText} - ${text}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZohoCliqTokenResponse;
|
||||
if (data.error) {
|
||||
throw new Error(`Zoho Cliq token refresh error: ${data.error}`);
|
||||
}
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
104
src/zoho-cliq/bot.ts
Normal file
104
src/zoho-cliq/bot.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { refreshAccessToken } from "./auth.js";
|
||||
import { ZohoCliqAccountConfig } from "../config/types.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
|
||||
const logger = getChildLogger({ module: "zoho-cliq-bot" });
|
||||
|
||||
export class ZohoCliqBot {
|
||||
private accessToken: string | null = null;
|
||||
private tokenExpiresAt: number = 0;
|
||||
|
||||
constructor(private config: ZohoCliqAccountConfig) { }
|
||||
|
||||
private async getAccessToken(): Promise<string> {
|
||||
if (this.accessToken && Date.now() < this.tokenExpiresAt - 60000) {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
if (!this.config.clientId || !this.config.clientSecret || !this.config.refreshToken) {
|
||||
throw new Error("Zoho Cliq OAuth credentials missing");
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Allow configuring scope via config if needed.
|
||||
// For now, default to Messages.CREATE and Channels.READ as a safe baseline.
|
||||
const token = await refreshAccessToken(
|
||||
this.config.clientId,
|
||||
this.config.clientSecret,
|
||||
this.config.refreshToken,
|
||||
this.config.dc
|
||||
);
|
||||
this.accessToken = token;
|
||||
// Zoho Access tokens typically expire in 1 hour (3600 seconds).
|
||||
// Standardize on 55 minutes to be safe, or parse expires_in if functionality expands.
|
||||
this.tokenExpiresAt = Date.now() + (55 * 60 * 1000);
|
||||
return token;
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Failed to refresh access token");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(channelIdOrChatId: string, text: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const dc = this.config.dc || "US";
|
||||
|
||||
// Logic to determine API domain based on DC
|
||||
let apiDomain = "https://cliq.zoho.com";
|
||||
switch (dc) {
|
||||
case "EU": apiDomain = "https://cliq.zoho.eu"; break;
|
||||
case "IN": apiDomain = "https://cliq.zoho.in"; break;
|
||||
case "AU": apiDomain = "https://cliq.zoho.com.au"; break;
|
||||
case "JP": apiDomain = "https://cliq.zoho.jp"; break;
|
||||
case "CN": apiDomain = "https://cliq.zoho.com.cn"; break;
|
||||
case "SA": apiDomain = "https://cliq.zoho.sa"; break;
|
||||
case "UK": apiDomain = "https://cliq.zoho.uk"; break;
|
||||
case "CA": apiDomain = "https://cliq.zohocloud.ca"; break;
|
||||
default: apiDomain = "https://cliq.zoho.com"; break;
|
||||
}
|
||||
|
||||
// Determine if target is a Chat ID or Channel Unique Name.
|
||||
// Heuristic: Chat IDs are typically numeric or specific UUID-like strings.
|
||||
// Channel unique names are user-defined strings.
|
||||
// For simplicity: unique names usually don't look like IDs.
|
||||
|
||||
// However, the safer bet for V1 is sticking to the `chats` endpoint
|
||||
// if we assume interactions start from a webhook (which gives a chat_id/channel_id).
|
||||
|
||||
const url = `${apiDomain}/api/v2/chats/${channelIdOrChatId}/message`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Fallback for channels by unique name if 404/400?
|
||||
// If the chat_id approach fails, try the named channel endpoint.
|
||||
if (response.status === 404 || response.status === 400) {
|
||||
const channelUrl = `${apiDomain}/api/v2/channelsbyname/${channelIdOrChatId}/message`;
|
||||
const channelResponse = await fetch(channelUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Zoho-oauthtoken ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
if (channelResponse.ok) {
|
||||
return channelResponse.json();
|
||||
}
|
||||
}
|
||||
|
||||
const errText = await response.text();
|
||||
logger.error(`Failed to send message to ${channelIdOrChatId}: ${response.status} ${errText}`);
|
||||
throw new Error(`Failed to send message: ${errText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
4
src/zoho-cliq/index.ts
Normal file
4
src/zoho-cliq/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export * from "./auth.js";
|
||||
export * from "./bot.js";
|
||||
export * from "./server.js";
|
||||
21
src/zoho-cliq/send.ts
Normal file
21
src/zoho-cliq/send.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { ZohoCliqBot } from "./bot.js";
|
||||
import type { ZohoCliqConfig } from "../config/types.js";
|
||||
|
||||
export async function sendMessageZohoCliq(
|
||||
channelIdOrChatId: string,
|
||||
text: string,
|
||||
opts?: { accountId?: string; mediaUrl?: string },
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const accountId = opts?.accountId ?? "main";
|
||||
const zohoConfig = cfg.channels?.["zoho-cliq"] as ZohoCliqConfig | undefined;
|
||||
const accountCfg = zohoConfig?.accounts?.[accountId];
|
||||
|
||||
if (!accountCfg) {
|
||||
throw new Error(`Zoho Cliq account ${accountId} not found in config`);
|
||||
}
|
||||
|
||||
const bot = new ZohoCliqBot(accountCfg);
|
||||
return await bot.sendMessage(channelIdOrChatId, text);
|
||||
}
|
||||
115
src/zoho-cliq/server.ts
Normal file
115
src/zoho-cliq/server.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { createServer } from "node:http";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { readJsonBody } from "../gateway/hooks.js";
|
||||
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
||||
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||
import { ZohoCliqBot } from "./bot.js";
|
||||
import { ZohoCliqAccountConfig } from "../config/types.js";
|
||||
import { danger } from "../globals.js";
|
||||
|
||||
type ZohoCliqWebhookOptions = {
|
||||
port?: number;
|
||||
runtime: RuntimeEnv;
|
||||
bot: ZohoCliqBot;
|
||||
accountId: string;
|
||||
config: ZohoCliqAccountConfig;
|
||||
};
|
||||
|
||||
export async function startZohoCliqWebhook(opts: ZohoCliqWebhookOptions) {
|
||||
const { port = 8080, runtime, bot, accountId } = opts;
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
res.writeHead(405);
|
||||
res.end("Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bodyResult = await readJsonBody(req, 1024 * 1024); // 1MB limit
|
||||
if (!bodyResult.ok) {
|
||||
res.writeHead(400);
|
||||
res.end(bodyResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = bodyResult.value as any;
|
||||
|
||||
// Basic Zoho Cliq Message handling
|
||||
// Note: Structure varies by event type (message, bot_mention, etc.)
|
||||
const text = payload.text || payload.message?.text || payload.content;
|
||||
const fromUser = payload.user || payload.from;
|
||||
const fromId = fromUser?.id || payload.user_id;
|
||||
const fromName = fromUser?.first_name || fromUser?.name || "Unknown";
|
||||
const channelId = payload.channel_id || payload.chat_id || fromId;
|
||||
|
||||
if (!text || !fromId) {
|
||||
// Verify ping/handshake
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: cfg.messages?.responsePrefix,
|
||||
deliver: async (responsePayload, _info) => {
|
||||
// TODO: Handle info.kind === "typing" if supported
|
||||
if (responsePayload.text) {
|
||||
await bot.sendMessage(channelId, responsePayload.text);
|
||||
}
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`zoho ${info.kind} reply failed: ${String(err)}`));
|
||||
},
|
||||
});
|
||||
|
||||
await dispatchReplyFromConfig({
|
||||
ctx: {
|
||||
Provider: "zoho-cliq" as any, // Cast if type union is strict
|
||||
Body: text,
|
||||
From: `zoho:${fromId}`,
|
||||
To: `zoho:bot`,
|
||||
SessionKey: `zoho:${channelId}`,
|
||||
AccountId: accountId,
|
||||
SenderId: String(fromId),
|
||||
SenderName: fromName,
|
||||
// Timestamp: Date.now(), // Approximate - Timestamp might strictly not exist in MsgContext
|
||||
// Fill required fields with defaults
|
||||
ChangeType: "message",
|
||||
MessageSid: String(Date.now()),
|
||||
Surface: "zoho-cliq" as any,
|
||||
} as any,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
});
|
||||
|
||||
markDispatchIdle();
|
||||
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
|
||||
} catch (err) {
|
||||
runtime.error?.(`Zoho webhook failed: ${formatErrorMessage(err)}`);
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<{ stop: () => Promise<void> }>((resolve, reject) => {
|
||||
server.listen(port, () => {
|
||||
runtime.log?.(`Zoho Cliq webhook listening on port ${port}`);
|
||||
resolve({
|
||||
stop: async () => {
|
||||
return new Promise((r) => server.close(() => r()));
|
||||
}
|
||||
});
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
59
src/zoho-cliq/types.ts
Normal file
59
src/zoho-cliq/types.ts
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
export interface ZohoCliqConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
refreshToken: string;
|
||||
dc: string;
|
||||
}
|
||||
|
||||
export interface ZohoCliqTokenResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
api_domain: string;
|
||||
token_type: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ZohoCliqMessage {
|
||||
text: string;
|
||||
bot?: {
|
||||
name: string;
|
||||
image: string;
|
||||
};
|
||||
card?: {
|
||||
title: string;
|
||||
theme: string;
|
||||
};
|
||||
slides?: Array<{
|
||||
type: string;
|
||||
title: string;
|
||||
data: Array<{
|
||||
[key: string]: string;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Webhook payload from Zoho Cliq (partial)
|
||||
export interface ZohoCliqWebhookPayload {
|
||||
user: {
|
||||
id: string; // ZUID
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
};
|
||||
channel?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
message?: {
|
||||
text: string;
|
||||
id: string; // message id
|
||||
time: string; // timestamp
|
||||
};
|
||||
// DMs might have different structure or just 'user' and 'message'
|
||||
chat?: {
|
||||
id: string; // chat id for DM
|
||||
};
|
||||
text?: string; // Sometimes text is top level in slash commands
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user