Skip to content

Commit

Permalink
feat: implement chats API client
Browse files Browse the repository at this point in the history
  • Loading branch information
belyaev-dev committed Jun 29, 2024
1 parent 08f6966 commit a275d4d
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 25 deletions.
22 changes: 21 additions & 1 deletion src/amo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Options } from "./typings/lib.ts";
import type { ChatOptions, Options } from "./typings/lib.ts";
import type { OAuth, OAuthCode, OAuthRefresh } from "./typings/auth.ts";
import type {
Catalog,
Expand Down Expand Up @@ -43,9 +43,12 @@ import { ShortLinkApi } from "./api/short-link/client.ts";
import { ChatTemplateApi } from "./api/chat-template/client.ts";
import { SalesBotApi } from "./api/salesbot/client.ts";
import { FileApi } from "./api/file/client.ts";
import { ChatApi } from "./api/chat/client.ts";
import { ChatsRestClient } from "./core/chats/chats-rest-client.ts";

export class Amo extends EventEmitter<WebhookEventMap> {
private rest: RestClient;
private chat_rest?: ChatsRestClient;

/** Свойства акканта */
readonly account: AccountApi;
Expand Down Expand Up @@ -100,15 +103,21 @@ export class Amo extends EventEmitter<WebhookEventMap> {
/** Salesbot Api */
readonly salesbot: SalesBotApi;
private _file?: FileApi;
private _chat?: ChatApi;

constructor(
base_url: string,
auth: OAuthCode | (OAuth & Pick<OAuthRefresh, "client_id" | "client_secret" | "redirect_uri">),
options?: Options,
private chat_options?: ChatOptions,
) {
super();
this.rest = new RestClient(base_url, auth, options);

if(chat_options) {
this.chat_rest = new ChatsRestClient(chat_options?.amojo_base_url, chat_options?.amojo_secret, options);
}

this.account = new AccountApi(this.rest);
this.lead = new LeadApi(this.rest);
this.unsorted = new UnsortedApi(this.rest);
Expand Down Expand Up @@ -155,6 +164,17 @@ export class Amo extends EventEmitter<WebhookEventMap> {
return this._file;
}

chat(): ChatApi {
if(this.chat_options === undefined || this.chat_rest === undefined) {
throw new Error("API чатов не инициализировано используя chat_options");
}

if (this._chat === undefined) {
this._chat = new ChatApi(this.chat_rest!, this.chat_options!.amojo_account_id, this.chat_options!.amojo_id, this.chat_options!.amojo_bot_id, this.chat_options!.amojo_channel_title);
}
return this._chat;
}

webhookHandler(): (request: Request) => Promise<Response> {
return async (request: Request) => {
try {
Expand Down
56 changes: 32 additions & 24 deletions src/api/chat/client.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { createHash, createHmac } from "node:crypto";
import { Endpoint } from "../../core/endpoint.ts";
import type { ChatsRestClient } from "../../core/chats/chats-rest-client.ts";
import { ChatsEndpoint } from "../../core/chats/chats-endpoint.ts";
import type { JSONValue } from "../../typings/utility.ts";

export class ChatApi extends Endpoint {
headers(
url: string,
method: string,
secret: string,
body: JSONValue = "",
): Record<string, string | number | boolean> {
const date = (new Date()).toUTCString();
const body_hash = createHash("md5")
.update(JSON.stringify(body))
.digest("hex")
.toLowerCase();
const signature = createHmac("sha1", secret)
.update([method.toUpperCase(), body_hash, "application/json", date, url].join("n"))
.digest("hex")
.toLowerCase();
return {
"Date": date,
"Content-Type": "application/json",
"Content-MD5": body_hash.toLowerCase(),
"X-Signature": signature.toLowerCase(),
};
export class ChatApi extends ChatsEndpoint {
private scope_id: string;

constructor(
rest: ChatsRestClient,
private amojo_account_id: string,
private amojo_id: string,
private amojo_bot_id: string,
private amojo_channel_title: string,
) {
super(rest);
this.scope_id = `${amojo_id}_${amojo_account_id}`;
}

connectChannel() {
return this.rest.post({
url: `/v2/origin/custom/${this.amojo_id}/connect`,
payload: {
account_id: this.amojo_account_id,
title: this.amojo_channel_title,
hook_api_version: "v2",
},
});
}

createChat(body: JSONValue) {
return this.rest.post({
url: `/v2/origin/custom/${this.scope_id}/chats`,
payload: body,
});
}
}
5 changes: 5 additions & 0 deletions src/core/chats/chats-endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ChatsRestClient } from "./chats-rest-client.ts";

export class ChatsEndpoint {
constructor(protected rest: ChatsRestClient) {}
}
112 changes: 112 additions & 0 deletions src/core/chats/chats-rest-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { createHash, createHmac } from "node:crypto";
import { ApiError } from "../../errors/api.ts";
import { HttpError } from "../../errors/http.ts";
import { NoContentError } from "../../errors/no-content.ts";
import { ConcurrentPool, DelayQueue } from "../async-queue.ts";

import type { HttpMethod, Options, RequestInit } from "../../typings/lib.ts";
import type { JSONValue } from "../../typings/utility.ts";

export class ChatsRestClient {
private url_base: string;
private queue: DelayQueue<Response> | ConcurrentPool<Response>;

constructor(
private base_url: string,
private secretKey: string,
private options?: Options,
) {
this.url_base = `https://${this.base_url}`; //amojo.amocrm.ru | amojo.kommo.com

if (options?.request_delay) {
this.queue = new DelayQueue<Response>(options.request_delay);
} else {
this.queue = new ConcurrentPool<Response>(
options?.concurrent_request ?? 7,
options?.concurrent_timeframe ?? 1000,
);
}
}

private getHeaders(body: JSONValue): Headers {
const contentType = "application/json";
const date = new Date().toUTCString().replace(
/(\w+), (\d+) (\w+) (\d+) (\d+):(\d+):(\d+) GMT/,
"$1, $2 $3 $4 $5:$6:$7 +0000",
);

const checkSum = createHash("md5")
.update(JSON.stringify(body))
.digest("hex")
.toLowerCase();
const signature = createHmac("sha1", this.secretKey)
.update(JSON.stringify(body))
.digest("hex")
.toLowerCase();

const headers = new Headers();
headers.append("Date", date);
headers.append("Content-Type", contentType);
headers.append("Content-MD5", checkSum);
headers.append("X-Signature", signature);

return headers;
}

private async checkError(res: Response, method: HttpMethod): Promise<void> {
if (res.ok !== false && res.status !== 204) return;
if (res.status === 204 && method === "DELETE") return;
if (res.headers.get("Content-Type") === "application/problem+json") {
throw new ApiError(res.body ? await res.json() : "Error", `${res.status} ${res.statusText}, ${res.url}`);
} else if (res.status === 204) {
throw new NoContentError(`${res.status} ${res.statusText}, ${res.url}`);
} else {
throw new HttpError(res.body ? await res.text() : `${res.status} ${res.statusText}, ${res.url}`);
}
}

async request<T>(method: HttpMethod, init: RequestInit): Promise<T> {
try {
const target = `${this.url_base}${init.url}${init.query ? "?" + init.query : ""}`;
const headers = this.getHeaders(init.payload || {});

const res = await this.queue.push(() =>
fetch(target, {
method: method,
headers: headers,
body: init.payload ? JSON.stringify(init.payload) : undefined,
})
);

await this.checkError(res, method);
return res.body ? (await res.json()) as T : null as T;
} catch (err) {
if (this.options?.on_error) {
this.options.on_error(err);
return null as T;
} else {
throw err;
}
}
}

get<T>(init: RequestInit): Promise<T> {
return this.request<T>("GET", init);
}

post<T>(init: RequestInit): Promise<T> {
return this.request<T>("POST", init);
}

patch<T>(init: RequestInit): Promise<T> {
return this.request<T>("PATCH", init);
}

delete<T>(init: RequestInit): Promise<T> {
return this.request<T>("DELETE", init);
}

put<T>(init: RequestInit): Promise<T> {
return this.request<T>("PUT", init);
}
}
9 changes: 9 additions & 0 deletions src/typings/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ export type RequestInit = {
payload?: JSONValue;
headers?: Record<string, string | number | boolean>;
};

export type ChatOptions = {
amojo_base_url: string;
amojo_account_id: string;
amojo_id: string;
amojo_secret: string;
amojo_bot_id: string;
amojo_channel_title: string;
}

0 comments on commit a275d4d

Please sign in to comment.