137 lines
3.6 KiB
JavaScript
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
|
|
}
|
|
});
|
|
}
|