From ca411c3c7aeeb5005620ddf6d150a31e28984607 Mon Sep 17 00:00:00 2001 From: Dinesh Bhattarai Date: Wed, 3 Jul 2024 17:12:00 +0545 Subject: [PATCH] feat: initial commit --- .github/workflows/publish.yaml | 19 +++++++++++ .gitignore | 2 ++ README.md | 7 ++++ deno.json | 11 +++++++ deno.lock | 19 +++++++++++ main.ts | 58 ++++++++++++++++++++++++++++++++++ mod.ts | 10 ++++++ src/config.ts | 43 +++++++++++++++++++++++++ src/fetch.ts | 36 +++++++++++++++++++++ src/getCapitals.ts | 11 +++++++ src/getOwnDetail.ts | 40 +++++++++++++++++++++++ src/getPortfolio.ts | 41 ++++++++++++++++++++++++ src/getPortfolioCsv.ts | 22 +++++++++++++ src/login.ts | 22 +++++++++++++ 14 files changed, 341 insertions(+) create mode 100644 .github/workflows/publish.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 main.ts create mode 100644 mod.ts create mode 100644 src/config.ts create mode 100644 src/fetch.ts create mode 100644 src/getCapitals.ts create mode 100644 src/getOwnDetail.ts create mode 100644 src/getPortfolio.ts create mode 100644 src/getPortfolioCsv.ts create mode 100644 src/login.ts diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..32a4f04 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,19 @@ +name: Publish +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + - name: Publish package + run: npx jsr publish + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ebf887 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +.env.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a106fe --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# README + +CLI and API for accessing [MeroShare](https://meroshare.cdsc.com.np/) + +## LICENSE + +MIT diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..e2ab9d2 --- /dev/null +++ b/deno.json @@ -0,0 +1,11 @@ +{ + "name": "@util/meroshare", + "version": "0.1.2", + "exports": "./mod.ts", + "tasks": { + "dev": "deno run --watch main.ts" + }, + "imports": { + "@es-toolkit/es-toolkit": "jsr:@es-toolkit/es-toolkit@^1.7.0" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..9aa2f1a --- /dev/null +++ b/deno.lock @@ -0,0 +1,19 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@es-toolkit/es-toolkit@^1.7.0": "jsr:@es-toolkit/es-toolkit@1.7.0" + }, + "jsr": { + "@es-toolkit/es-toolkit@1.7.0": { + "integrity": "d4ab3223789af5c7adfe603ba963e829cb6ddb655a4a797b61dfb4a64ad8c174" + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@es-toolkit/es-toolkit@^1.7.0" + ] + } +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..45482bf --- /dev/null +++ b/main.ts @@ -0,0 +1,58 @@ +import { loadConfig } from "./src/config.ts"; +import { getPortfolioCsv } from "./src/getPortfolioCsv.ts"; +import { getPortfolio } from "./src/getPortfolio.ts"; +import { login } from "./src/login.ts"; +import { getOwnDetail } from "./src/getOwnDetail.ts"; + +type Args = { + command: string; + subcommand?: string; +}; + +const commands: Record void> = { + "portfolio csv": async () => { + const loginDetails = await loadConfig(); + const { authorization } = await login(loginDetails); + loginDetails.authorization = authorization!; + const myPortfolio = await getPortfolioCsv(loginDetails); + console.log(myPortfolio); + }, + "portfolio json": async () => { + const loginDetails = await loadConfig(); + const { authorization } = await login(loginDetails); + loginDetails.authorization = authorization!; + const portfolio = await getPortfolio(loginDetails); + console.log(portfolio); + }, + info: async () => { + const loginDetails = await loadConfig(); + const { authorization } = await login(loginDetails); + loginDetails.authorization = authorization!; + const info = await getOwnDetail(loginDetails.authorization); + console.log(info); + }, +}; + +export default function main() { + const [command, subcommand] = Deno.args; + const cmd = commands[`${command} ${subcommand}`] ?? commands[command]; + const args = { + command, + subcommand, + }; + if (cmd) { + return cmd(args); + } else { + if (Deno.args.length > 0) { + console.error("ERROR: command not found:", command, subcommand); + } + console.info("Available commands:"); + Object.keys(commands).forEach((cmd) => { + console.info(` - ${cmd}`); + }); + } +} + +if (import.meta.main) { + main(); +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..2881ab8 --- /dev/null +++ b/mod.ts @@ -0,0 +1,10 @@ +export * from "./src/getPortfolioCsv.ts"; +export * from "./src/getPortfolio.ts"; +export * from "./src/login.ts"; +export * from "./src/getOwnDetail.ts"; +export * from "./src/config.ts"; +export * from "./src/fetch.ts"; + +if (import.meta.main) { + import("./main.ts").then((main) => main.default()); +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..5e57ead --- /dev/null +++ b/src/config.ts @@ -0,0 +1,43 @@ +import { getCapitals } from "./getCapitals.ts"; +import { isNotNil, keyBy } from "@es-toolkit/es-toolkit"; + +export type UserDetails = { + demat: string; + username: string; + clientId: number; + clientCode: string; +}; + +export type LoginDetails = UserDetails & { + password: string; + authorization?: string; +}; + +function getUsernamePassword(): { demat: string; password: string } { + const demat = Deno.env.get("DEMAT_ACCOUNT"); + const password = Deno.env.get("DEMAT_PASSWORD"); + if (demat?.length !== 16) { + throw new Error("valid demat account should be provided"); + } + if (!isNotNil(password)) { + throw new Error("password is needed"); + } + return { demat, password }; +} + +export async function loadConfig(): Promise { + const { demat, password } = getUsernamePassword(); + + const capitals = await getCapitals(); + const capitalsMap = keyBy(capitals, (capital) => capital.code); + + const clientCode = demat.substring(3, 8); + const username = demat.substring(8, demat.length); + return { + clientId: capitalsMap[clientCode].id, + password, + username, + clientCode, + demat, + }; +} diff --git a/src/fetch.ts b/src/fetch.ts new file mode 100644 index 0000000..155b0ca --- /dev/null +++ b/src/fetch.ts @@ -0,0 +1,36 @@ +const commonHeaders = { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US", + "Content-Type": "application/json", + "Sec-GPC": "1", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Referer": "https://meroshare.cdsc.com.np/", +}; + +export async function fetchMeroshare( + path: string, + init: RequestInit = {}, +): Promise { + const res = await fetch( + `https://webbackend.cdsc.com.np/api/meroShare${path}`, + { + credentials: "include", + mode: "cors", + ...init, + headers: { + ...commonHeaders, + ...init.headers, + }, + }, + ); + + if (!res.ok) { + throw new Error(`Failed to fetch ${path}`); + } + + return res; +} diff --git a/src/getCapitals.ts b/src/getCapitals.ts new file mode 100644 index 0000000..a41ba65 --- /dev/null +++ b/src/getCapitals.ts @@ -0,0 +1,11 @@ +import { fetchMeroshare } from "./fetch.ts"; + +type Capital = { + id: number; + code: string; + name: string; +}; +export async function getCapitals(): Promise { + const res = await fetchMeroshare("/capital/"); + return await res.json(); +} diff --git a/src/getOwnDetail.ts b/src/getOwnDetail.ts new file mode 100644 index 0000000..a9332f8 --- /dev/null +++ b/src/getOwnDetail.ts @@ -0,0 +1,40 @@ +import { fetchMeroshare } from "./fetch.ts"; + +export type OwnDetail = { + address: string; + boid: string; + clientCode: string; + contact: string; + createdApproveDate: string; + createdApproveDateStr: string; + customerTypeCode: string; + demat: string; + dematExpiryDate: string; + email: string; + expiredDate: string; + expiredDateStr: string; + gender: string; + id: number; + imagePath: string; + meroShareEmail: string; + name: string; + panNumber: string; + passwordChangeDate: string; + passwordChangedDateStr: string; + passwordExpiryDate: string; + passwordExpiryDateStr: string; + profileName: string; + renderDashboard: boolean; + renewedDate: string; + renewedDateStr: string; + username: string; +}; + +export async function getOwnDetail(authorization: string): Promise { + const res = await fetchMeroshare("/ownDetail/", { + "headers": { + "Authorization": authorization, + }, + }); + return await res.json(); +} diff --git a/src/getPortfolio.ts b/src/getPortfolio.ts new file mode 100644 index 0000000..159f8ce --- /dev/null +++ b/src/getPortfolio.ts @@ -0,0 +1,41 @@ +import type { LoginDetails } from "./config.ts"; +import { fetchMeroshare } from "./fetch.ts"; + +export type Portfolio = { + meroShareMyPortfolio: { + currentBalance: number; + lastTransactionPrice: string; + previousClosingPrice: string; + script: string; + scriptDesc: string; + valueAsOfLastTransactionPrice: string; + valueAsOfPreviousClosingPrice: string; + valueOfLastTransPrice: number; + valueOfPrevClosingPrice: number; + }[]; + totalItems: number; + totalValueAsOfLastTransactionPrice: string; + totalValueAsOfPreviousClosingPrice: string; + totalValueOfLastTransPrice: number; + totalValueOfPrevClosingPrice: number; +}; + +export async function getPortfolio( + { authorization, demat, clientCode }: LoginDetails, +): Promise { + const res = await fetchMeroshare("View/myPortfolio/", { + headers: { + authorization: authorization!, + }, + body: JSON.stringify({ + sortBy: "script", + demat: [demat], + clientCode, + page: 1, + size: 200, + sortAsc: true, + }), + method: "POST", + }); + return await res.json(); +} diff --git a/src/getPortfolioCsv.ts b/src/getPortfolioCsv.ts new file mode 100644 index 0000000..7852ea9 --- /dev/null +++ b/src/getPortfolioCsv.ts @@ -0,0 +1,22 @@ +import type { LoginDetails } from "./config.ts"; +import { fetchMeroshare } from "./fetch.ts"; + +export async function getPortfolioCsv( + { authorization, demat, clientCode }: LoginDetails, +): Promise { + const res = await fetchMeroshare("View/report/myPortfolio/csv", { + "headers": { + "Authorization": authorization!, + }, + body: JSON.stringify({ + sortBy: "script", + demat: [demat], + clientCode, + page: 1, + size: 200, + sortAsc: true, + }), + method: "POST", + }); + return await res.text(); +} diff --git a/src/login.ts b/src/login.ts new file mode 100644 index 0000000..bce71bc --- /dev/null +++ b/src/login.ts @@ -0,0 +1,22 @@ +import type { LoginDetails } from "./config.ts"; +import { fetchMeroshare } from "./fetch.ts"; + +export async function login( + { clientId, username, password }: LoginDetails, +): Promise<{ authorization: string }> { + const res = await fetchMeroshare("/auth/", { + method: "POST", + body: JSON.stringify({ + clientId, + username, + password, + }), + }); + const authorization = res.headers.get("authorization"); + if (!authorization) { + throw new Error("Authorization token not found"); + } + return { + authorization, + }; +}