Files

137 lines
3.6 KiB
JavaScript

/**
* Lark (Feishu) API Wrapper
* Handles authentication and API calls with automatic token refresh
*/
import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// Load secrets
const __dirname = dirname(fileURLToPath(import.meta.url));
config({ path: join(__dirname, '../../../../.secrets.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
// Use larksuite.com for international Lark
const BASE_URL = 'https://open.larksuite.com/open-apis';
let accessToken = null;
let tokenExpiry = 0;
/**
* Get or refresh access token
*/
async function getAccessToken() {
// Return cached token if still valid (with 5 min buffer)
if (accessToken && Date.now() < tokenExpiry - 300000) {
return accessToken;
}
const response = await fetch(`${BASE_URL}/auth/v3/tenant_access_token/internal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: APP_ID,
app_secret: APP_SECRET
})
});
const data = await response.json();
if (data.code !== 0) {
throw new Error(`Failed to get access token: ${JSON.stringify(data)}`);
}
accessToken = `Bearer ${data.tenant_access_token}`;
// Token expires in ~2 hours, we store expiry time
tokenExpiry = Date.now() + (data.expire * 1000);
return accessToken;
}
/**
* Make authenticated API request
* @param {string} method - HTTP method
* @param {string} endpoint - API endpoint (without base URL)
* @param {object} options - { params, data }
* @returns {object} - Response data
*/
export async function larkApi(method, endpoint, { params = null, data = null } = {}) {
const token = await getAccessToken();
let url = `${BASE_URL}${endpoint}`;
// Add query params
if (params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, value);
}
}
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
const fetchOptions = {
method,
headers: {
'Authorization': token,
'Content-Type': 'application/json'
}
};
if (data && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
fetchOptions.body = JSON.stringify(data);
}
const response = await fetch(url, fetchOptions);
const result = await response.json();
// Check for token expiry error
if (result.code === 99991663) {
// Token expired, clear cache and retry once
accessToken = null;
tokenExpiry = 0;
return larkApi(method, endpoint, { params, data });
}
if (result.code !== 0) {
const error = new Error(`Lark API error: ${JSON.stringify(result)}`);
error.code = result.code;
error.larkResponse = result;
throw error;
}
return result.data;
}
/**
* Reply to a message
* @param {string} messageId - Message ID to reply to
* @param {object} content - Message content
*/
export async function replyMessage(messageId, content) {
return larkApi('POST', `/im/v1/messages/${messageId}/reply`, { data: content });
}
/**
* Send message to a chat
* @param {string} receiveId - Chat ID or user ID
* @param {string} receiveIdType - 'chat_id' | 'user_id' | 'open_id'
* @param {object} content - Message content
*/
export async function sendMessage(receiveId, receiveIdType, content) {
return larkApi('POST', '/im/v1/messages', {
params: { receive_id_type: receiveIdType },
data: {
receive_id: receiveId,
...content
}
});
}