Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement chats API client #26

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Loading