├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── onPushToMain.yml │ ├── onRelease.yml │ └── test.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc.json ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── package-lock.json ├── package.json ├── src ├── api.ts ├── command.ts ├── commands │ ├── buy │ │ └── index.ts │ ├── login │ │ └── index.ts │ ├── logout │ │ └── index.ts │ ├── offers │ │ └── index.ts │ └── status │ │ └── index.ts ├── config.ts ├── index.ts └── table.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["oclif", "oclif-typescript", "prettier"], 3 | "rules": { 4 | "perfectionist/sort-objects": ["off"], 5 | "perfectionist/sort-interfaces": ["off"], 6 | "perfectionist/sort-imports": ["off"], 7 | "perfectionist/sort-object-types": ["off"], 8 | "perfectionist/sort-classes": ["off"], 9 | "camelcase": ["off"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/onPushToMain.yml: -------------------------------------------------------------------------------- 1 | # test 2 | name: version, tag and github release 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | - name: Check if version already exists 15 | id: version-check 16 | run: | 17 | package_version=$(node -p "require('./package.json').version") 18 | exists=$(gh api repos/${{ github.repository }}/releases/tags/v$package_version >/dev/null 2>&1 && echo "true" || echo "") 19 | 20 | if [ -n "$exists" ]; 21 | then 22 | echo "Version v$package_version already exists" 23 | echo "::warning file=package.json,line=1::Version v$package_version already exists - no release will be created. If you want to create a new release, please update the version in package.json and push again." 24 | echo "skipped=true" >> $GITHUB_OUTPUT 25 | else 26 | echo "Version v$package_version does not exist. Creating release..." 27 | echo "skipped=false" >> $GITHUB_OUTPUT 28 | echo "tag=v$package_version" >> $GITHUB_OUTPUT 29 | fi 30 | env: 31 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 32 | - name: Setup git 33 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 34 | run: | 35 | git config --global user.email ${{ secrets.GH_EMAIL }} 36 | git config --global user.name ${{ secrets.GH_USERNAME }} 37 | - name: Generate oclif README 38 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 39 | id: oclif-readme 40 | run: | 41 | npm install 42 | npm exec oclif readme 43 | if [ -n "$(git status --porcelain)" ]; then 44 | git add . 45 | git commit -am "chore: update README.md" 46 | git push -u origin ${{ github.ref_name }} 47 | fi 48 | - name: Create Github Release 49 | uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 50 | if: ${{ steps.version-check.outputs.skipped == 'false' }} 51 | with: 52 | name: ${{ steps.version-check.outputs.tag }} 53 | tag: ${{ steps.version-check.outputs.tag }} 54 | commit: ${{ github.ref_name }} 55 | token: ${{ secrets.GH_TOKEN }} 56 | skipIfReleaseExists: true 57 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: latest 15 | - run: npm install 16 | - uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c 17 | with: 18 | token: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | unit-tests: 9 | strategy: 10 | matrix: 11 | os: ['ubuntu-latest', 'windows-latest'] 12 | node_version: [lts/*] 13 | fail-fast: false 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node_version }} 20 | cache: npm 21 | - run: npm install 22 | - run: npm run build 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | **/.DS_Store 4 | /.idea 5 | /dist 6 | /tmp 7 | /node_modules 8 | oclif.manifest.json 9 | 10 | 11 | 12 | yarn.lock 13 | pnpm-lock.yaml 14 | 15 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node/register" 4 | ], 5 | "watch-extensions": [ 6 | "ts" 7 | ], 8 | "recursive": true, 9 | "reporter": "spec", 10 | "timeout": 60000, 11 | "node-option": [ 12 | "loader=ts-node/esm", 13 | "experimental-specifier-resolution=node" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach", 8 | "port": 9229, 9 | "skipFiles": ["/**"] 10 | }, 11 | { 12 | "type": "node", 13 | "request": "launch", 14 | "name": "Execute Command", 15 | "skipFiles": ["/**"], 16 | "runtimeExecutable": "node", 17 | "runtimeArgs": ["--loader", "ts-node/esm", "--no-warnings=ExperimentalWarning"], 18 | "program": "${workspaceFolder}/bin/dev.js", 19 | "args": ["hello", "world"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["irancell"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Erfan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | my-irancell-cli 2 | ================= 3 | 4 | Cli for my.irancell.ir. You can get your current status, list offers and buy offers by using this cli. 5 | 6 | ![my-irancell](https://github.com/erfanium/my-irancell-cli/assets/47688578/8a8350b4-96a8-4b84-98d3-c22e6b2a7289) 7 | 8 | 9 | # Installation 10 | ```sh 11 | $ npm install -g my-irancell-cli 12 | ``` 13 | 14 | # Login 15 | You have to login to your irancell account with your phone number and password. 16 | ```sh 17 | $ my-irancell login 18 | ``` 19 | 20 | 21 | * [Installation](#installation) 22 | * [Login](#login) 23 | * [Usage](#usage) 24 | * [Commands](#commands) 25 | 26 | # Usage 27 | 28 | ```sh-session 29 | $ npm install -g my-irancell-cli 30 | $ my-irancell COMMAND 31 | running command... 32 | $ my-irancell (--version) 33 | my-irancell-cli/0.1.1 linux-x64 node-v20.9.0 34 | $ my-irancell --help [COMMAND] 35 | USAGE 36 | $ my-irancell COMMAND 37 | ... 38 | ``` 39 | 40 | # Commands 41 | 42 | * [`my-irancell buy [OFFERID]`](#my-irancell-buy-offerid) 43 | * [`my-irancell help [COMMAND]`](#my-irancell-help-command) 44 | * [`my-irancell login`](#my-irancell-login) 45 | * [`my-irancell logout`](#my-irancell-logout) 46 | * [`my-irancell offers`](#my-irancell-offers) 47 | * [`my-irancell status`](#my-irancell-status) 48 | 49 | ## `my-irancell buy [OFFERID]` 50 | 51 | Buy an offer 52 | 53 | ``` 54 | USAGE 55 | $ my-irancell buy [OFFERID] 56 | 57 | DESCRIPTION 58 | Buy an offer 59 | ``` 60 | 61 | _See code: [src/commands/buy/index.ts](https://github.com/erfanium/my-irancell-cli/blob/v0.1.1/src/commands/buy/index.ts)_ 62 | 63 | ## `my-irancell help [COMMAND]` 64 | 65 | Display help for my-irancell. 66 | 67 | ``` 68 | USAGE 69 | $ my-irancell help [COMMAND...] [-n] 70 | 71 | ARGUMENTS 72 | COMMAND... Command to show help for. 73 | 74 | FLAGS 75 | -n, --nested-commands Include all nested commands in the output. 76 | 77 | DESCRIPTION 78 | Display help for my-irancell. 79 | ``` 80 | 81 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.3/src/commands/help.ts)_ 82 | 83 | ## `my-irancell login` 84 | 85 | Login to a new account 86 | 87 | ``` 88 | USAGE 89 | $ my-irancell login 90 | 91 | DESCRIPTION 92 | Login to a new account 93 | ``` 94 | 95 | _See code: [src/commands/login/index.ts](https://github.com/erfanium/my-irancell-cli/blob/v0.1.1/src/commands/login/index.ts)_ 96 | 97 | ## `my-irancell logout` 98 | 99 | Logout from account 100 | 101 | ``` 102 | USAGE 103 | $ my-irancell logout 104 | 105 | DESCRIPTION 106 | Logout from account 107 | ``` 108 | 109 | _See code: [src/commands/logout/index.ts](https://github.com/erfanium/my-irancell-cli/blob/v0.1.1/src/commands/logout/index.ts)_ 110 | 111 | ## `my-irancell offers` 112 | 113 | List all available offers 114 | 115 | ``` 116 | USAGE 117 | $ my-irancell offers 118 | 119 | DESCRIPTION 120 | List all available offers 121 | ``` 122 | 123 | _See code: [src/commands/offers/index.ts](https://github.com/erfanium/my-irancell-cli/blob/v0.1.1/src/commands/offers/index.ts)_ 124 | 125 | ## `my-irancell status` 126 | 127 | Show account status 128 | 129 | ``` 130 | USAGE 131 | $ my-irancell status 132 | 133 | DESCRIPTION 134 | Show account status 135 | ``` 136 | 137 | _See code: [src/commands/status/index.ts](https://github.com/erfanium/my-irancell-cli/blob/v0.1.1/src/commands/status/index.ts)_ 138 | 139 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | 3 | // eslint-disable-next-line n/shebang 4 | import {execute} from '@oclif/core' 5 | 6 | await execute({development: true, dir: import.meta.url}) 7 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {execute} from '@oclif/core' 4 | 5 | await execute({dir: import.meta.url}) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-irancell-cli", 3 | "description": "cli for my.irancell.ir", 4 | "version": "0.1.1", 5 | "author": "Erfanium", 6 | "bin": { 7 | "my-irancell": "./bin/run.js" 8 | }, 9 | "bugs": "https://github.com/erfanium/my-irancell-cli/issues", 10 | "dependencies": { 11 | "@oclif/core": "^4", 12 | "@oclif/plugin-help": "^6", 13 | "@oclif/plugin-plugins": "^5", 14 | "cli-table3": "^0.6.5", 15 | "fs-extra": "^11.2.0", 16 | "inquirer": "^9.2.23", 17 | "jwt-decode": "^4.0.0", 18 | "open": "^10.1.0" 19 | }, 20 | "devDependencies": { 21 | "@oclif/prettier-config": "^0.2.1", 22 | "@oclif/test": "^4", 23 | "@types/chai": "^4", 24 | "@types/fs-extra": "^11.0.4", 25 | "@types/inquirer": "^9.0.7", 26 | "@types/mocha": "^10", 27 | "@types/node": "^18", 28 | "chai": "^4", 29 | "eslint": "^8", 30 | "eslint-config-oclif": "^5", 31 | "eslint-config-oclif-typescript": "^3", 32 | "eslint-config-prettier": "^9", 33 | "mocha": "^10", 34 | "oclif": "^4", 35 | "shx": "^0.3.3", 36 | "ts-node": "^10", 37 | "typescript": "^5" 38 | }, 39 | "engines": { 40 | "node": ">=18.0.0" 41 | }, 42 | "files": [ 43 | "/bin", 44 | "/dist", 45 | "/oclif.manifest.json" 46 | ], 47 | "homepage": "https://github.com/erfanium/my-irancell-cli", 48 | "keywords": [ 49 | "oclif" 50 | ], 51 | "license": "MIT", 52 | "main": "dist/index.js", 53 | "type": "module", 54 | "oclif": { 55 | "bin": "my-irancell", 56 | "dirname": "my-irancell-cli", 57 | "commands": "./dist/commands", 58 | "plugins": [ 59 | "@oclif/plugin-help" 60 | ] 61 | }, 62 | "repository": "https://github.com/erfanium/my-irancell-cli", 63 | "scripts": { 64 | "build": "shx rm -rf dist && tsc -b", 65 | "lint": "eslint . --ext .ts", 66 | "postpack": "shx rm -f oclif.manifest.json", 67 | "prepack": "oclif manifest && oclif readme", 68 | "test": "tsc --noEmit", 69 | "version": "oclif readme && git add README.md" 70 | }, 71 | "types": "dist/index.d.ts" 72 | } 73 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { ConfigStore, StaticShit } from "./config.js"; 2 | import { jwtDecode } from "jwt-decode"; 3 | import qs from "node:querystring"; 4 | 5 | export interface MyIrancellApiCtorParams { 6 | configStore: ConfigStore; 7 | } 8 | 9 | export interface Account { 10 | active_offers: ActiveOffer[]; 11 | cumulative_amounts: CumulativeAmount[]; 12 | shared_offers: null; 13 | main_account_balance: number; 14 | wow_charge: WowCharge; 15 | } 16 | 17 | export interface ActiveOffer { 18 | calculation_type: string; 19 | expiry_date: string; 20 | global_data_remaining: number; 21 | global_data_used: number; 22 | is_gift: boolean; 23 | is_roaming: boolean; 24 | local_data_remaining: number; 25 | local_data_used: number; 26 | local_messenger_remaining: number; 27 | local_messenger_used: number; 28 | offer_code: string; 29 | remained_amount: number; 30 | start_date: string; 31 | total_amount: number; 32 | type: string; 33 | name: string; 34 | } 35 | 36 | export interface CumulativeAmount { 37 | type?: string; 38 | total?: number; 39 | remained?: number; 40 | shared_data?: null[]; 41 | shared_fund?: null[]; 42 | } 43 | 44 | export interface WowCharge { 45 | main_amount: number; 46 | main_expiry: string; 47 | gifts: unknown[]; 48 | } 49 | 50 | export interface Offer { 51 | upc_code: string; 52 | offer_id: string; 53 | sub_type: string; 54 | sim_type: string; 55 | category: string; 56 | offer_type: string; 57 | profile_type: string; 58 | price: number; 59 | auto_renewal: string; 60 | giftable: boolean; 61 | giftable_code: unknown; 62 | duration: string; 63 | duration_unit: string; 64 | title: string; 65 | description: string; 66 | details: Details; 67 | tags: unknown[]; 68 | cra_info: CraInfo; 69 | volume?: number; 70 | bandwidth: unknown; 71 | source: string; 72 | } 73 | 74 | export interface Details {} 75 | 76 | export interface PaymentMethodsResult { 77 | transaction_id: string; 78 | response_time: number; 79 | command_status: string; 80 | response_msg: string; 81 | order_id: string; 82 | reference_id: string; 83 | amount: number; 84 | available_balance: number; 85 | digital_wallet_balance: number; 86 | service: string; 87 | offer_code: string; 88 | loyalty_points_to_redeem: number; 89 | payment_modes: PaymentMode[]; 90 | } 91 | 92 | export interface PaymentMode { 93 | mode: string; 94 | id: string; 95 | desc: string; 96 | balance: number; 97 | points: number; 98 | discount: number; 99 | rf1: unknown; 100 | rf2: unknown; 101 | available: string; 102 | bank_details: BankDetail[]; 103 | tax: string; 104 | auth_code: string; 105 | } 106 | 107 | export interface BankDetail { 108 | bank_id: number; 109 | bank_name: string; 110 | location: string; 111 | url: string; 112 | created_date: number; 113 | created_user: unknown; 114 | bank_code: string; 115 | status: number; 116 | class_path: unknown; 117 | bank_icon: string; 118 | bank_retry_count: number; 119 | bank_name_farsi: string; 120 | bank_display_order: number; 121 | } 122 | 123 | export interface CraInfo { 124 | description: string; 125 | code: string; 126 | url_link: string; 127 | } 128 | 129 | export class MyIrancellApi { 130 | #configStore: ConfigStore; 131 | constructor(params: MyIrancellApiCtorParams) { 132 | this.#configStore = params.configStore; 133 | } 134 | 135 | async #fetch(params: { 136 | path: string; 137 | method: "GET" | "POST"; 138 | authorization?: string; 139 | body?: unknown; 140 | }): Promise { 141 | const response = await fetch(`https://my.irancell.ir/api${params.path}`, { 142 | method: params.method, 143 | headers: { 144 | "Accept-Language": "en", 145 | ...(params.authorization 146 | ? { Authorization: params.authorization } 147 | : {}), 148 | 149 | ...(params.body ? { "Content-Type": "application/json" } : {}), 150 | }, 151 | body: params.body ? JSON.stringify(params.body) : undefined, 152 | }); 153 | 154 | if (response.status !== 200) { 155 | const error = await response.text(); 156 | throw new Error(`fetch error. status:${response.status} body:${error}`); 157 | } 158 | 159 | return response.json(); 160 | } 161 | 162 | async #getAccessToken(): Promise { 163 | const config = this.#configStore.get(); 164 | if (!config.refresh_token) { 165 | throw new Error("Not logged in"); 166 | } 167 | 168 | if (!config.access_token || isJwtExpired(config.access_token)) { 169 | const { access_token: newAccessToken } = await this.#fetch<{ 170 | access_token: string; 171 | }>({ 172 | method: "POST", 173 | path: "/authorization/v1/token", 174 | authorization: config.access_token, 175 | body: { 176 | grant_type: "refresh_token", 177 | refresh_token: config.refresh_token, 178 | ...StaticShit, 179 | }, 180 | }); 181 | 182 | this.#configStore.set({ 183 | access_token: newAccessToken, 184 | }); 185 | 186 | return newAccessToken; 187 | } 188 | 189 | return config.access_token; 190 | } 191 | 192 | async login(phone: string, password: string) { 193 | const result = await this.#fetch<{ 194 | access_token: string; 195 | refresh_token: string; 196 | }>({ 197 | method: "POST", 198 | path: "/authorization/v1/token", 199 | body: { 200 | grant_type: "password", 201 | phone_number: phone, 202 | password, 203 | ...StaticShit, 204 | }, 205 | }); 206 | 207 | this.#configStore.set({ 208 | access_token: result.access_token, 209 | refresh_token: result.refresh_token, 210 | }); 211 | 212 | return result; 213 | } 214 | 215 | async logout() { 216 | const config = this.#configStore.get(); 217 | 218 | if (config.refresh_token) { 219 | await this.#fetch({ 220 | method: "POST", 221 | path: "/authorization/v1/logout", 222 | authorization: config.access_token, 223 | body: { 224 | refresh_token: config.refresh_token, 225 | }, 226 | }); 227 | } 228 | 229 | this.#configStore.set({ 230 | access_token: undefined, 231 | refresh_token: undefined, 232 | }); 233 | } 234 | 235 | async getAccount() { 236 | const result = await this.#fetch({ 237 | method: "GET", 238 | path: "/sim/v3/account", 239 | authorization: await this.#getAccessToken(), 240 | }); 241 | 242 | return result; 243 | } 244 | 245 | async getOffers() { 246 | const result = await this.#fetch({ 247 | method: "GET", 248 | path: "/catalog/v2/offers?type=data&category=normal", 249 | authorization: await this.#getAccessToken(), 250 | }); 251 | 252 | return result; 253 | } 254 | 255 | async getPaymentMethods(params: { 256 | service: "NormalBolton"; 257 | amount: number; 258 | offer_code: string; 259 | bank_list_required: true; 260 | }) { 261 | const result = await this.#fetch({ 262 | method: "GET", 263 | path: `/payment/v2/methods?${qs.stringify(params)}`, 264 | authorization: await this.#getAccessToken(), 265 | }); 266 | 267 | return result; 268 | } 269 | 270 | async initPayment(params: { 271 | amount: number; 272 | bank_id: number; 273 | callback_type: "web"; 274 | offer_code: string; 275 | order_id: string; 276 | payment_mode_id: string; 277 | reference_id: string; 278 | service: "NormalBolton"; 279 | }) { 280 | const result = await this.#fetch<{ redirection_url: string }>({ 281 | method: "POST", 282 | path: `/payment/v2/initiate`, 283 | authorization: await this.#getAccessToken(), 284 | body: params, 285 | }); 286 | 287 | return result; 288 | } 289 | } 290 | 291 | interface JwtPayload { 292 | exp: number; 293 | } 294 | 295 | const isJwtExpired = (token: string): boolean => { 296 | try { 297 | const decoded: JwtPayload = jwtDecode(token); 298 | const currentTime = Math.floor(Date.now() / 1000); 299 | return decoded.exp < currentTime; 300 | } catch { 301 | return true; 302 | } 303 | }; 304 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | import { MyIrancellApi } from "./api.js"; 3 | import { ConfigStore } from "./config.js"; 4 | 5 | export abstract class AuthorizedCommand extends Command { 6 | api = new MyIrancellApi({ 7 | configStore: new ConfigStore(this.config.configDir), 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/buy/index.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "@oclif/core"; 2 | import { AuthorizedCommand } from "../../command.js"; 3 | import inquirer from "inquirer"; 4 | import open from "open"; 5 | 6 | export default class Buy extends AuthorizedCommand { 7 | static description = "Buy an offer"; 8 | 9 | static args = { 10 | offerId: Args.string(), 11 | }; 12 | 13 | async run(): Promise { 14 | const { 15 | args: { offerId }, 16 | } = await this.parse(Buy); 17 | 18 | if (!offerId) { 19 | this.error("Offer ID is required"); 20 | } 21 | 22 | const offer = (await this.api.getOffers()).find( 23 | (o) => o.offer_id === offerId 24 | ); 25 | 26 | if (!offer) { 27 | this.error(`Offer with ID ${offerId} not found`); 28 | } 29 | 30 | this.log( 31 | `Purchasing offer ${offer.title} for ${Math.round( 32 | offer.price / 10_000 33 | ).toLocaleString()}T ...` 34 | ); 35 | 36 | const paymentMethodsResult = await this.api.getPaymentMethods({ 37 | service: "NormalBolton", 38 | amount: offer.price, 39 | offer_code: offer.upc_code, 40 | bank_list_required: true, 41 | }); 42 | 43 | const firstPaymentMode = paymentMethodsResult.payment_modes[0]; 44 | 45 | const selectedBank: { bank_id: number } = await inquirer.prompt({ 46 | type: "list", 47 | name: "bank_id", 48 | message: "Select a payment gateway:", 49 | choices: firstPaymentMode.bank_details.map((bank) => ({ 50 | name: bank.bank_name, 51 | value: bank.bank_id, 52 | })), 53 | }); 54 | 55 | if (!selectedBank.bank_id) { 56 | this.error("No bank selected"); 57 | } 58 | 59 | const { redirection_url } = await this.api.initPayment({ 60 | amount: offer.price, 61 | bank_id: selectedBank.bank_id, 62 | callback_type: "web", 63 | offer_code: offer.upc_code, 64 | order_id: paymentMethodsResult.order_id, 65 | payment_mode_id: firstPaymentMode.id, 66 | reference_id: paymentMethodsResult.reference_id, 67 | service: "NormalBolton", 68 | }); 69 | 70 | open(redirection_url); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/login/index.ts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import { AuthorizedCommand } from "../../command.js"; 3 | 4 | export default class Login extends AuthorizedCommand { 5 | static description = "Login to a new account"; 6 | 7 | async run() { 8 | const { username } = await inquirer.prompt({ 9 | type: "input", 10 | name: "username", 11 | 12 | message: "Username (phone starting with 98):", 13 | }); 14 | 15 | const { password } = await inquirer.prompt({ 16 | type: "password", 17 | name: "password", 18 | message: "Password:", 19 | }); 20 | 21 | await this.api.login(username, password); 22 | 23 | this.log("login success"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/logout/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizedCommand } from "../../command.js"; 2 | 3 | export default class Logout extends AuthorizedCommand { 4 | static description = "Logout from account"; 5 | 6 | async run() { 7 | await this.api.logout(); 8 | 9 | this.log("Logged out"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/offers/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizedCommand } from "../../command.js"; 2 | import { makeTable } from "../../table.js"; 3 | 4 | export default class Offers extends AuthorizedCommand { 5 | static description = "List all available offers"; 6 | 7 | async run(): Promise { 8 | const data = await this.api.getOffers(); 9 | 10 | const table = makeTable({ 11 | head: ["Offer Code", "Title", "Price", "Per Gig Price"], 12 | colWidths: [15, 30, 15, 15], 13 | rows: data 14 | .filter((item) => item.category !== "smart-bundle") 15 | .sort((a, b) => { 16 | const aDuration = Number(a.duration); 17 | const bDuration = Number(b.duration); 18 | return aDuration - bDuration; 19 | }) 20 | .map((offer) => [ 21 | offer.offer_id, 22 | offer.title, 23 | Math.round(offer.price / 10_000).toLocaleString() + "T", 24 | offer.volume 25 | ? (offer.price / 10_000 / (offer.volume / 1024)).toFixed(1) + "T" 26 | : "-", 27 | ]), 28 | }); 29 | 30 | this.log(table.toString()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/status/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizedCommand } from "../../command.js"; 2 | import { makeTable } from "../../table.js"; 3 | 4 | export const formatSize = (sizeInMegaBytes: number): string => 5 | `${(sizeInMegaBytes / 1024).toFixed(2)} GB`; 6 | 7 | export default class Status extends AuthorizedCommand { 8 | static description = "Show account status"; 9 | 10 | async run(): Promise { 11 | const rtf = new Intl.RelativeTimeFormat("en", { 12 | style: "long", 13 | numeric: "auto", 14 | }); 15 | 16 | const data = await this.api.getAccount(); 17 | 18 | const table = makeTable({ 19 | head: ["Offer Code", "Name", "Expiry", "Data Used / Remaining"], 20 | colWidths: [15, 30, 15, 30], 21 | rows: data.active_offers.map((offer) => [ 22 | offer.offer_code, 23 | offer.name, 24 | rtf.format( 25 | Math.round( 26 | (Date.parse(offer.expiry_date) - Date.now()) / (1000 * 60 * 60 * 24) 27 | ), 28 | "day" 29 | ), 30 | `${formatSize( 31 | offer.total_amount - offer.remained_amount 32 | )} / ${formatSize(offer.total_amount)}`, 33 | ]), 34 | }); 35 | 36 | this.log(table.toString()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { default as fse } from "fs-extra"; 2 | import path from "node:path"; 3 | 4 | export const StaticShit = { 5 | device_name: "Web Linux", 6 | client_id: "4725a997e94b372b1c26e425086f4a17", 7 | client_secret: 8 | "7e9379a4d444a3c21cf28da6a032154dc4b644eba523e7684f71818dec3beeb7", 9 | client_version: "4.3.3", 10 | installation_id: "678c8721-0285-4223-8fbf-d93b378b66e7", 11 | }; 12 | 13 | export interface Config { 14 | access_token?: string; 15 | refresh_token?: string; 16 | } 17 | 18 | export class ConfigStore { 19 | #dir: string; 20 | #path: string; 21 | constructor(configDir: string) { 22 | this.#dir = configDir; 23 | this.#path = path.join(configDir, "config.json"); 24 | } 25 | 26 | get(): Config { 27 | try { 28 | return fse.readJsonSync(this.#path); 29 | } catch { 30 | fse.ensureDirSync(this.#dir); 31 | fse.writeJSONSync(this.#path, {}); 32 | return {}; 33 | } 34 | } 35 | 36 | set(config: Config) { 37 | const current = this.get(); 38 | fse.writeJSONSync(this.#path, { 39 | ...current, 40 | ...config, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core' 2 | -------------------------------------------------------------------------------- /src/table.ts: -------------------------------------------------------------------------------- 1 | import CliTable from "cli-table3"; 2 | 3 | const TableCommon = { 4 | chars: { 5 | top: "", 6 | "top-mid": "", 7 | "top-left": "", 8 | "top-right": "", 9 | bottom: "", 10 | "bottom-mid": "", 11 | "bottom-left": "", 12 | "bottom-right": "", 13 | left: "", 14 | "left-mid": "", 15 | mid: "", 16 | "mid-mid": "", 17 | right: "", 18 | "right-mid": "", 19 | middle: " ", 20 | }, 21 | style: { "padding-left": 0, "padding-right": 0 }, 22 | }; 23 | 24 | export const makeTable = ({ 25 | head, 26 | rows, 27 | colWidths, 28 | }: { 29 | head: string[]; 30 | rows: string[][]; 31 | colWidths: number[]; 32 | }) => { 33 | const t = new CliTable({ 34 | ...TableCommon, 35 | head, 36 | colWidths, 37 | }); 38 | 39 | t.push(...rows); 40 | 41 | return t; 42 | }; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "Node16", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": true, 8 | "target": "es2022", 9 | "moduleResolution": "node16" 10 | }, 11 | "include": ["./src/**/*"], 12 | "ts-node": { 13 | "esm": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------