Skip to content

Commit

Permalink
Feature/aily support (larksuite#92)
Browse files Browse the repository at this point in the history
* feat: support aily sence & user-access-token manage
  • Loading branch information
mazhe-nerd authored Aug 13, 2024
1 parent 614a776 commit ee01302
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 2 deletions.
5 changes: 5 additions & 0 deletions client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { LoggerProxy } from '@node-sdk/logger/logger-proxy';
import { IRequestOptions, IClientParams, IPayload } from './types';
import { TokenManager } from './token-manager';
import { HttpInstance } from '@node-sdk/typings/http';
import { UserAccessToken } from './user-access-token';

export class Client extends RequestTemplate {
appId: string = '';
Expand All @@ -43,6 +44,8 @@ export class Client extends RequestTemplate {

httpInstance: HttpInstance;

userAccessToken: UserAccessToken;

constructor(params: IClientParams) {
super();

Expand Down Expand Up @@ -78,6 +81,8 @@ export class Client extends RequestTemplate {
httpInstance: this.httpInstance,
});

this.userAccessToken = new UserAccessToken({client: this});

this.logger.info('client ready');
}

Expand Down
142 changes: 142 additions & 0 deletions client/user-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import get from 'lodash.get';
import { CUserAccessToken } from '@node-sdk/consts';
import { mergeObject } from '@node-sdk/utils/merge-object';
import type { Client } from './client';

interface ITokenInfo {
code?: string;
token?: string;
refreshToken?: string;
expiredTime?: number;
}

export class UserAccessToken {
client: Client;

constructor(params: { client: Client }) {
this.client = params.client;
}

private getCacheKey(key: string, options: { namespace?: string }) {
const namespace = get(options, 'namespace', this.client.appId);
return `${namespace}/${CUserAccessToken.toString()}/${key}`;
}

// the unit of time is seconds
private calibrateTime(time?: number) {
// Due to the time-consuming network, the time needs to be 3 minutes earlier
return new Date().getTime() + (time || 0) * 1000 - 3 * 60 * 1000;
}

async initWithCode(key2Code: Record<string, string>, options?: { namespace?: string }) {
const key2Info: Record<string, ITokenInfo> = {};
for (const [key, code] of Object.entries(key2Code)) {
const oidcAccessInfo = await this.client.authen.oidcAccessToken.create({
data: {
grant_type: 'authorization_code',
code
}
});

if (oidcAccessInfo.code !== 0) {
// @ts-ignore
this.client.logger.error('init user access token error', key, oidcAccessInfo.msg || oidcAccessInfo.message);
continue;
}
// code expired
if (!oidcAccessInfo.data) {
this.client.logger.error('user access code expired', key, code);
continue;
}

key2Info[key] = {
code,
token: oidcAccessInfo.data.access_token,
refreshToken: oidcAccessInfo.data.refresh_token,
expiredTime: this.calibrateTime(oidcAccessInfo.data.expires_in)
}
}
await this.update(key2Info, { namespace: get(options, 'namespace')});
return key2Info;
}

async update(key2Info: Record<string, ITokenInfo>, options?: { namespace?: string }) {
for (const [key, info] of Object.entries(key2Info)) {
const cacheKey = this.getCacheKey(key, { namespace: get(options, 'namespace')});
const cacheValue = await this.client.cache.get(cacheKey) || {};

const { code, token, refreshToken, expiredTime } = info;
const targetValue = mergeObject(cacheValue, { code, token, refreshToken, expiredTime });

await this.client.cache.set(cacheKey, targetValue, Infinity);
}
}

async get(key: string, options?: { namespace?: string }) {
const cacheKey = this.getCacheKey(key, { namespace: get(options, 'namespace')});
const cacheInfo = await this.client.cache.get(cacheKey);

// cacheInfo是否存在
if (!cacheInfo) {
this.client.logger.error('user access token needs to be initialized or updated first');
return;
}

const { token, code, refreshToken, expiredTime } = cacheInfo;
// step1 token存在且未过期
if (token && expiredTime && expiredTime - new Date().getTime() > 0) {
return token;
}

// step2 refresh token存在,刷新token
if (refreshToken) {
const refreshAccessInfo = await this.client.authen.oidcRefreshAccessToken.create({
data: {
grant_type: 'refresh_token',
refresh_token: refreshToken
}
});

if (refreshAccessInfo.code === 0 && refreshAccessInfo.data) {
await this.update({
key: {
token: refreshAccessInfo.data.access_token,
refreshToken: refreshAccessInfo.data.refresh_token,
expiredTime: this.calibrateTime(refreshAccessInfo.data.expires_in)
}
});

return refreshAccessInfo.data.access_token;
} else {
this.client.logger.error('get user access token by refresh token failed.', refreshAccessInfo.msg);
return;
}
}

// step3 code存在的话,用code重新获取
if (code) {
const oidcAccessInfo = await this.client.authen.oidcAccessToken.create({
data: {
grant_type: "authorization_code",
code: code
}
});

if (oidcAccessInfo.code === 0 && oidcAccessInfo.data) {
await this.update({
key: {
token: oidcAccessInfo.data.access_token,
refreshToken: oidcAccessInfo.data.refresh_token,
expiredTime: this.calibrateTime(oidcAccessInfo.data.expires_in)
}
})
} else {
this.client.logger.error('get user access token by code failed.', oidcAccessInfo.msg);
}
}

// step4 重试完毕没结果后,返回undefine
return;
}
}

2 changes: 2 additions & 0 deletions consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export const CAppTicket = Symbol('app-ticket');
export const CTenantAccessToken = Symbol('tenant-access-token');
export const CWithHelpdeskAuthorization = Symbol('with-helpdesk-authorization');
export const CWithUserAccessToken = Symbol('with-user-access-token');
export const CUserAccessToken = Symbol('user-access-token');
export const CAilySessionRecord = Symbol('aily-session-record');
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export { AESCipher } from './utils/aes-cipher';
export { default as defaultHttpInstance } from './http';
export { HttpInstance, HttpRequestOptions } from './typings/http';
export * as messageCard from './utils/message-card';
export { WSClient } from './ws-client';
export { WSClient } from './ws-client';
export { Aily } from './scene/aily/client';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"scripts": {
"build": "rm -r lib es types & rollup -c",
"test": "jest",
"test:watch": "jest --watch --testPathPattern=ws-client/__tests__"
"test:watch": "jest --watch --testPathPattern=utils/__tests__"
},
"author": "mazhe.nerd",
"license": "MIT",
Expand Down
16 changes: 16 additions & 0 deletions scene/aily/__tests__/session-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SessionCache } from '../session-cache';

describe('aily session cache', () => {
test('work right', async () => {
const sessionCache = new SessionCache();

await sessionCache.set('test', { a: 1 });
expect(await sessionCache.get('test')).toEqual({a: 1});

await sessionCache.set('test', { a: undefined });
expect(await sessionCache.get('test')).toEqual({a: 1});

await sessionCache.set('test', { a: 2, b: 1 });
expect(await sessionCache.get('test')).toEqual({a: 2, b: 1});
});
})
Loading

0 comments on commit ee01302

Please sign in to comment.