├── _config.yml ├── .gitignore ├── src ├── index.ts ├── models │ ├── APIResponse.ts │ ├── CreditCard.ts │ ├── User.ts │ ├── index.ts │ ├── Region.ts │ ├── Order.ts │ ├── Cart.ts │ ├── Login.ts │ ├── Address.ts │ ├── Store.ts │ └── Menu.ts ├── commands │ ├── user.ts │ ├── logout.ts │ ├── lsaddr.ts │ ├── validate.ts │ ├── menu.ts │ ├── ls.ts │ ├── lscart.ts │ ├── login.ts │ ├── mkorder.ts │ ├── setaddr.ts │ ├── setstore.ts │ ├── payment.ts │ └── addcart.ts ├── fixtures │ └── loginResponse.js ├── classes │ ├── SessionStore.ts │ ├── Session.test.ts │ └── Session.ts └── CLI.ts ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── bin └── efood.js ├── .npmignore ├── test └── mocha.opts ├── tslint.json ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-merlot 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | .nyc_output 4 | coverage 5 | dist 6 | test-results.xml -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models/index'; 2 | export * from './classes/Session'; 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kpapadatos 4 | -------------------------------------------------------------------------------- /bin/efood.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const CLI = require('../dist/CLI').default; 3 | 4 | new CLI(process.argv); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | src 4 | node_modules 5 | .nyc_output 6 | coverage 7 | tsconfig.json 8 | package-lock.json 9 | test-results.xml -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --reporter mocha-junit-reporter 4 | --recursive 5 | --full-trace 6 | --bail 7 | --exit 8 | src/**/*.test.ts -------------------------------------------------------------------------------- /src/models/APIResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IAPIResponse { 2 | data: any; 3 | provisioning: {}; 4 | status: 'ok'; 5 | error_code: 'success'; 6 | message: 'Επιτυχής κλήση'; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/CreditCard.ts: -------------------------------------------------------------------------------- 1 | export interface ICreditCard { 2 | id: string; 3 | hashcode: string; 4 | card_number: string; 5 | expiration_date: string; 6 | is_active: boolean; 7 | card_type: string; 8 | card_logo: string; 9 | is_default: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an e-food.gr user as returned by the login call. 3 | */ 4 | export interface IUser { 5 | id: number; 6 | cellphone: string; 7 | email: string; 8 | first_name: string; 9 | last_name: string; 10 | user_name: string; 11 | verified: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './User'; 2 | export * from './Address'; 3 | export * from './Store'; 4 | export * from './Region'; 5 | export * from './APIResponse'; 6 | export * from './Menu'; 7 | export * from './Cart'; 8 | export * from './Order'; 9 | export * from './CreditCard'; 10 | export * from './Login'; 11 | -------------------------------------------------------------------------------- /src/models/Region.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the location of an address with information 3 | * that is possibly provided by Google Maps. 4 | */ 5 | export interface IRegion { 6 | area: string; 7 | area_slug: string; 8 | city: string; 9 | is_served: boolean; 10 | latitude: number; 11 | longitude: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/models/Order.ts: -------------------------------------------------------------------------------- 1 | import { ICartProduct } from '.'; 2 | 3 | export interface IOrder { 4 | created: string; 5 | payment_method: string; 6 | discount: any[]; 7 | delivery_type: string; 8 | restaurant_id: number; 9 | coupons: any[]; 10 | amount: number; 11 | address_id: number; 12 | products: ICartProduct[]; 13 | payment_token?: string; 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [ 9 | true, 10 | "single" 11 | ], 12 | "space-before-function-paren": false, 13 | "no-console": false, 14 | "no-unused-expression": false, 15 | "trailing-comma": false 16 | }, 17 | "rulesDirectory": [] 18 | } -------------------------------------------------------------------------------- /src/models/Cart.ts: -------------------------------------------------------------------------------- 1 | export class Cart { 2 | public coupons: string[]; 3 | public deliveryType: string; 4 | public paymentMethod: string; 5 | public products: ICartProduct[] = []; 6 | } 7 | 8 | export interface ICartProduct { 9 | product_id: string; 10 | quantity: number; 11 | price: number; 12 | offer: number; 13 | total: number; 14 | materials: string[]; 15 | description: string; 16 | comment: string; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "types": [ 11 | "node", 12 | "mocha" 13 | ] 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "**/*.test.ts" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/commands/user.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | 9 | session = s; 10 | 11 | program 12 | .command('user') 13 | .alias('u') 14 | .description('Shows current user info.') 15 | .action(handler); 16 | 17 | } 18 | 19 | async function handler() { 20 | const u = await session.getUser(); 21 | console.log(`Logged in as ${c.cyan(`[${u.id}] ${u.first_name} ${u.last_name} (${u.email})`)}.`); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Use Node.js 10.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 10.x 18 | - name: npm install, build, and test 19 | run: | 20 | npm ci 21 | npm run build --if-present 22 | npm test 23 | npm run upload-coverage 24 | env: 25 | CI: true 26 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 27 | -------------------------------------------------------------------------------- /src/commands/logout.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('logout') 12 | .alias('lo') 13 | .description('Removes all local data.') 14 | .action(handler) 15 | .consoleHandler = handler; 16 | } 17 | 18 | async function handler(addressId: string) { 19 | console.log(`Deleting all local data...`); 20 | await session.logout(); 21 | console.log(c.green('Success!')); 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/lsaddr.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('lsaddr') 12 | .description('Lists the current user\'s addresses.') 13 | .action(handler); 14 | } 15 | 16 | async function handler() { 17 | console.log('Getting addresses...'); 18 | const addresses = await session.getUserAddresses(); 19 | 20 | for (const address of addresses) { 21 | console.log(c.cyan(`[${address.id}] ${address.description}`)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/models/Login.ts: -------------------------------------------------------------------------------- 1 | import { IAPIResponse } from './APIResponse'; 2 | 3 | export interface ILoginResponse extends IAPIResponse { 4 | data: 5 | { 6 | session_id: string; 7 | user: { 8 | cellphone: string; 9 | consents: Array<{ 10 | timestamp: number; 11 | type: 'is_adult' | 'is_dob_verified'; 12 | value: boolean; 13 | }>; 14 | date_of_birth: string; 15 | email: string; 16 | first_name: string; 17 | first_name_in_vocative: string; 18 | id: number; 19 | last_name: string; 20 | user_name: string; 21 | verified: true; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/validate.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | const { green, red } = chalk; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('validate') 13 | .description('Validates your current cart.') 14 | .action(handler) 15 | .consoleHandler = handler; 16 | } 17 | 18 | async function handler() { 19 | console.log(`Validating...`); 20 | 21 | const success = await session.validateOrder(); 22 | 23 | if (success) { 24 | console.log(green(`Success!`)); 25 | } else { 26 | console.log(red(`Validation failed.`)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "google" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | "linebreak-style": "off", 23 | "comma-dangle": 0, 24 | "indent": [ 25 | "error", 26 | 4 27 | ], 28 | "max-len": [ 29 | "error", 30 | 120 31 | ], 32 | "object-curly-spacing": [ 33 | "error", 34 | "always" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /src/commands/menu.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('menu') 12 | .description('Gets the menu of the selected store.') 13 | .action(handler); 14 | } 15 | 16 | async function handler(cmd: any) { 17 | console.log(`Getting menu for ${c.cyan(session.store.storeId.toString())} ...`); 18 | 19 | const store = await session.getStore(); 20 | 21 | const items = store.menu.categories as EFood.IMenuCategories[]; 22 | 23 | for (const item of items) { 24 | for (const product of item.items) { 25 | console.log(c.cyan(`[${product.code}] [${product.price}€] ${product.name}`)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/fixtures/loginResponse.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | data: 3 | { 4 | session_id: '6e08582dff0a4bdf62c5c333ee956f64', 5 | user: { 6 | cellphone: '+301234567890', 7 | consents: [ 8 | { 9 | timestamp: 1572810334569, 10 | type: 'is_adult', 11 | value: false 12 | }, { 13 | timestamp: 1572810334569, 14 | type: 'is_dob_verified', 15 | value: false 16 | } 17 | ], 18 | date_of_birth: null, 19 | email: 'test@example.com', 20 | first_name: 'Κοσμάς', 21 | first_name_in_vocative: 'Κοσμά', 22 | id: 123456, 23 | last_name: 'Παπαδάτος', 24 | user_name: 'test@example.com', 25 | verified: true, 26 | } 27 | }, 28 | error_code: 'success', 29 | message: 'Επιτυχής κλήση', 30 | provisioning: {}, 31 | status: 'ok' 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Kosmas Papadatos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/classes/SessionStore.ts: -------------------------------------------------------------------------------- 1 | import * as Models from '../models/index'; 2 | 3 | export class SessionStore { 4 | /** 5 | * Primary authentication identifier. 6 | */ 7 | public sessionId: string; 8 | /** 9 | * The authenticated user's information. 10 | */ 11 | public user: Models.IUser; 12 | /** 13 | * The selected address for this session. 14 | */ 15 | public addressId: number; 16 | /** 17 | * The currently selected store. 18 | */ 19 | public storeId: number; 20 | /** 21 | * The current cart. 22 | */ 23 | public cart: Models.Cart = new Models.Cart(); 24 | /** 25 | * Payment method selected. 26 | */ 27 | public paymentMethod: string = 'cash'; 28 | /** 29 | * Delivery type. 30 | */ 31 | public deliveryType: string = 'delivery'; 32 | /** 33 | * Payment token used for credit card payments. 34 | */ 35 | public paymentToken: string; 36 | /** 37 | * A unique identifier for the credit card to be used. 38 | */ 39 | public paymentHashcode: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/ls.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '../index'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('ls') 12 | .description('Lists stores for current address.') 13 | .action(handler) 14 | .consoleHandler = handler; 15 | } 16 | 17 | async function handler() { 18 | console.log(`Getting stores for address ${c.cyan(session.store.addressId.toString())} ...`); 19 | const addresses = await session.getUserAddresses(); 20 | 21 | const address = addresses.filter((a) => a.id === session.store.addressId)[0]; 22 | 23 | const shops = await session.getStores({ 24 | latitude: address.latitude, 25 | longitude: address.longitude, 26 | onlyOpen: true 27 | }); 28 | 29 | for (const shop of shops) { 30 | console.log(c.cyan(`[${shop.id}] [${shop.average_rating}*] [${shop.minimum_order}€] [${shop.delivery_eta}min] ${shop.title}`)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/lscart.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '..'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('lscart') 12 | .description('Lists all cart items.') 13 | .action(handler) 14 | .consoleHandler = handler; 15 | } 16 | 17 | async function handler(cmd: any) { 18 | console.log('Getting cart contents...\n'); 19 | 20 | const store = await session.getStore(); 21 | const cartItems = 22 | (await Promise.all( 23 | session.store.cart.products 24 | .map((p) => 25 | session.getMenuItemOptions(p.product_id) 26 | .then((r) => `[${p.total}€] ${r.data.name}`)))); 27 | 28 | console.log(`Store: ${c.cyan(store.information.title)}`); 29 | 30 | console.log('\nCart contents'); 31 | 32 | for (const cartItem of cartItems) { 33 | console.log(c.cyan(cartItem)); 34 | } 35 | 36 | console.log(); 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as inquirer from 'inquirer'; 4 | import * as EFood from '../index'; 5 | 6 | let session: EFood.Session; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('login') 13 | .alias('l') 14 | .description('Log in with your efood.gr account.') 15 | .option('-u, --username ', 'user identification') 16 | .option('-p, --password ', 'user password') 17 | .action(handler) 18 | .consoleHandler = async () => { 19 | const { username, password } = await inquirer.prompt([ 20 | { 21 | message: 'Enter the email address of your account:', 22 | name: 'username' 23 | }, 24 | { 25 | message: 'Enter your password:', 26 | name: 'password', 27 | type: 'password' 28 | } 29 | ]); 30 | 31 | await handler({ username, password }); 32 | }; 33 | } 34 | 35 | async function handler(cmd: any) { 36 | console.log(`Logging in as ${c.cyan(cmd.username)} ...`); 37 | 38 | const success = await session.login(cmd.username, cmd.password); 39 | 40 | if (success) { 41 | console.log(c.green(`Success!`)); 42 | } else { 43 | console.log(c.red(`Login failed.`)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/mkorder.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as EFood from '..'; 4 | 5 | let session: EFood.Session; 6 | 7 | export default function (program: CommanderStatic, s: EFood.Session) { 8 | session = s; 9 | 10 | program 11 | .command('mkorder') 12 | .description('Places the order.') 13 | .action(handler) 14 | .consoleHandler = handler; 15 | } 16 | 17 | async function handler(cmd: any) { 18 | console.log('Placing order...'); 19 | 20 | if (session.store.paymentMethod === 'piraeus.creditcard') { 21 | const cards = await session.getCreditCards(); 22 | 23 | session.store.paymentToken = 24 | cards.filter((card) => card.hashcode === session.store.paymentHashcode)[0].id; 25 | } 26 | 27 | await session.validateOrder(); 28 | 29 | const orderId = await session.submitOrder(); 30 | 31 | if (orderId) { 32 | let orderStatus: any; 33 | 34 | do { 35 | orderStatus = await session.getOrderStatus(orderId); 36 | await new Promise((r) => setTimeout(r, 3e3)); 37 | } while (orderStatus.status === 'submitted'); 38 | 39 | if (orderStatus.status === 'accepted') { 40 | console.log(c.green(`Order complete! Delivery time: ${orderStatus.delivery_time}'`)); 41 | } else { 42 | console.log(c.red('Order failed.')); 43 | } 44 | } else { 45 | console.log(c.red('Order failed. No orderId.')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/setaddr.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as inquirer from 'inquirer'; 4 | import * as EFood from '../index'; 5 | 6 | let session: EFood.Session; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('setaddr [addressId]') 13 | .description('Sets the current address.') 14 | .action(handler) 15 | .consoleHandler = async () => { 16 | console.log(`Getting user addresses ...`); 17 | 18 | const addresses = await session.getUserAddresses(); 19 | const choices = []; 20 | 21 | for (const address of addresses) { 22 | choices.push(`[${address.id}] ${address.description}`); 23 | } 24 | 25 | const input = await inquirer.prompt([{ 26 | choices, 27 | message: 'Select current address', 28 | name: 'setaddr', 29 | type: 'list', 30 | }]); 31 | 32 | const addressId = addresses[choices.indexOf(input.setaddr)].id; 33 | 34 | console.log(`Setting address to ${c.cyan(addressId.toString())} ...`); 35 | await session.setAddress(addressId); 36 | console.log(c.green(`Success!`)); 37 | }; 38 | } 39 | 40 | async function handler(addressId: number) { 41 | console.log(`Setting user address to ${c.cyan(addressId.toString())} ...`); 42 | await session.setAddress(addressId); 43 | console.log(c.green(`Success!`)); 44 | } 45 | -------------------------------------------------------------------------------- /src/models/Address.ts: -------------------------------------------------------------------------------- 1 | import { IRegion } from './Region'; 2 | 3 | /** 4 | * Represents an e-food.gr user address. 5 | */ 6 | export interface IAddress { 7 | /** 8 | * This is a string with the entire address' info as a title. 9 | */ 10 | description: string; 11 | 12 | /** 13 | * Extra info that goes with every order to this address (e.g. change 14 | * for 50 euro notes) 15 | */ 16 | details: string; 17 | 18 | /** 19 | * Doorbell name. 20 | */ 21 | doorbell_name: string; 22 | 23 | /** 24 | * Floor number. 25 | */ 26 | floor: string; 27 | 28 | /** 29 | * Unique ID for this address entry. 30 | */ 31 | id: number; 32 | 33 | /** 34 | * Whether or not this is the default address for this account. 35 | */ 36 | is_default: boolean; 37 | 38 | /** 39 | * This field seems to always be false. 40 | */ 41 | is_served: boolean; 42 | 43 | /** 44 | * The phone provided for this address. 45 | */ 46 | landphone: string; 47 | 48 | /** 49 | * Same latitude as `Region.latitude`. 50 | */ 51 | latitude: number; 52 | 53 | /** 54 | * Same longitude as `Region.longitude`. 55 | */ 56 | longitude: number; 57 | 58 | /** 59 | * Street name. 60 | */ 61 | street: string; 62 | 63 | /** 64 | * Street number. 65 | */ 66 | street_number: string; 67 | 68 | /** 69 | * ZIP postal code. 70 | */ 71 | zip: string; 72 | 73 | /** 74 | * A region descriptor, possibly provided by 75 | * Google Maps. 76 | */ 77 | region: IRegion; 78 | } 79 | -------------------------------------------------------------------------------- /src/models/Store.ts: -------------------------------------------------------------------------------- 1 | import { IMenuCategories } from '.'; 2 | import { IOffer } from './Menu'; 3 | 4 | /** 5 | * Represents a store listing. 6 | */ 7 | export interface IStore extends IStoreInformation { 8 | information?: IStoreInformation; 9 | 10 | offers: IOffer[]; 11 | 12 | menu: { 13 | categories: IMenuCategories[]; 14 | }; 15 | } 16 | 17 | export interface IStoreInformation { 18 | id: number; 19 | title: string; 20 | average_rating: number; 21 | delivery_eta: number; 22 | minimum_order: number; 23 | workphone: string; 24 | delivery_cost: number; 25 | has_pickup: boolean; 26 | has_credit: boolean; 27 | has_delivery: boolean; 28 | has_cash_on_delivery: boolean; 29 | is_open: boolean; 30 | auto_closed: { status: string }; 31 | chain_id: number; 32 | message: string; 33 | logo: string; 34 | num_ratings: number; 35 | basic_cuisine: string; 36 | cuisines: string[]; 37 | is_new: boolean; 38 | address: { 39 | street: string; 40 | zip: string; 41 | longitude: number; 42 | latitude: number; 43 | city: string; 44 | area: string; 45 | slug: string; 46 | description: string; 47 | }; 48 | has_offers: boolean; 49 | is_favorite: boolean; 50 | description: string; 51 | distance: number; 52 | timetable: Array<{ 53 | day: string; 54 | times: string; 55 | }>; 56 | is_promoted: boolean; 57 | offer_tags: string[]; 58 | has_discounts: boolean; 59 | slug: string; 60 | rank: number; 61 | is_chain_model: boolean; 62 | youtube_embed_url: string; 63 | cover: string; 64 | offer: { 65 | id: number; 66 | title: string; 67 | description: string; 68 | tag: string; 69 | icon: string; 70 | logo: string; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/classes/Session.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as request from 'request-promise-native'; 3 | import { restore, stub } from 'sinon'; 4 | import { IAddress, IStore } from '../models'; 5 | import { Session } from './Session'; 6 | 7 | describe('Session', () => { 8 | let session: Session; 9 | let addresses: IAddress[]; 10 | let address: IAddress; 11 | let stores: IStore[]; 12 | let store: IStore; 13 | let storeWithMenu: IStore; 14 | 15 | beforeEach(() => restore()); 16 | 17 | it('should instantiate', () => { 18 | session = new Session(); 19 | }); 20 | 21 | it('should log in', async () => { 22 | stub(request, 'post').callsFake((options: any) => { 23 | return Promise.resolve(require('../fixtures/loginResponse')) as request.RequestPromise; 24 | }); 25 | 26 | const isSuccess = await session.login('kosmas.papadatos@gmail.com', '1234567890'); 27 | 28 | expect(isSuccess).to.be.true; 29 | }); 30 | 31 | it('should get addresses', async () => { 32 | addresses = await session.getUserAddresses(); 33 | 34 | expect(addresses).to.not.be.empty; 35 | }); 36 | 37 | it('should set address', async () => { 38 | address = addresses[0]; 39 | await session.setAddress(address.id); 40 | }); 41 | 42 | it('should get stores', async () => { 43 | stores = await session.getStores({ 44 | latitude: address.latitude, 45 | longitude: address.longitude, 46 | onlyOpen: true 47 | }); 48 | 49 | expect(stores).to.not.be.empty; 50 | }); 51 | 52 | it('should set store', async () => { 53 | store = stores[0]; 54 | await session.setStore(store.id); 55 | }); 56 | 57 | it('should get store with menu', async () => { 58 | storeWithMenu = await session.getStore(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/commands/setstore.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as inquirer from 'inquirer'; 4 | import * as EFood from '../index'; 5 | 6 | let session: EFood.Session; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('setstore [storeId]') 13 | .description('Sets the store.') 14 | .action(handler) 15 | .consoleHandler = async () => { 16 | console.log(`Getting stores for address ${c.cyan(session.store.addressId.toString())} ...`); 17 | 18 | const addresses = await session.getUserAddresses(); 19 | 20 | const address = addresses.find((a) => a.id === session.store.addressId); 21 | 22 | const shops = await session.getStores({ 23 | latitude: address.latitude, 24 | longitude: address.longitude, 25 | onlyOpen: true 26 | }); 27 | 28 | const listOptions: string[] = []; 29 | 30 | for (const shop of shops) { 31 | listOptions.push(`[${shop.average_rating}*] [${shop.minimum_order}€] [${shop.delivery_eta}min] ${shop.title}`); 32 | } 33 | 34 | await new Promise((resolve) => inquirer.prompt([{ 35 | choices: listOptions, 36 | message: 'Select a store', 37 | name: 'setstore', 38 | type: 'list' 39 | }]).then(async (input) => { 40 | const storeId = shops[listOptions.indexOf(input.setstore)].id; 41 | 42 | console.log(`Setting store to ${c.cyan(storeId.toString())} ...`); 43 | await session.setStore(storeId); 44 | console.log(c.green(`Success!`)); 45 | 46 | resolve(); 47 | })); 48 | }; 49 | } 50 | 51 | async function handler(storeId: number) { 52 | console.log(`Setting store to ${c.cyan(storeId.toString())} ...`); 53 | await session.setStore(storeId); 54 | console.log(c.green(`Success!`)); 55 | } 56 | -------------------------------------------------------------------------------- /src/models/Menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Menu items of a store. 3 | */ 4 | export interface IMenuCategories { 5 | code: string; 6 | name: string; 7 | description: string; 8 | order: number; 9 | items: IProduct[]; 10 | } 11 | 12 | export interface IProduct { 13 | id: number; 14 | code: string; 15 | name: string; 16 | price: number; 17 | description: string; 18 | order: number; 19 | shortage: boolean; 20 | image: string; 21 | tags: string[]; 22 | quick_add: boolean; 23 | personalized_options: any[]; 24 | offer_line?: number; 25 | } 26 | 27 | export interface IOffer { 28 | id: number; 29 | title: string; 30 | description: string; 31 | price: number; 32 | tag: string; 33 | mode: string; 34 | tiers: IOfferTier[]; 35 | } 36 | 37 | export interface IOfferTier { 38 | offer_line: number; 39 | title: string; 40 | quantity: number; 41 | items: Array<{ code: string; name: string; }>; 42 | } 43 | 44 | export interface IMenuItemOptions { 45 | id: number; 46 | code: string; 47 | restaurant_id: number; 48 | category_code: string; 49 | description: string; 50 | name: string; 51 | tags: string[]; 52 | price: number; 53 | pickup_only: boolean; 54 | has_discount: boolean; 55 | is_available: boolean; 56 | need_server_calculation: boolean; 57 | image: string; 58 | allow_comments: boolean; 59 | special_instructions: string; 60 | excluded_from_minimum_order: boolean; 61 | personalized_options: any[]; 62 | tiers: IOptionTier[]; 63 | } 64 | 65 | export interface IOptionTier { 66 | code: number; 67 | name: string; 68 | order: number; 69 | type: string; 70 | maximum_selections: number; 71 | shortage: boolean; 72 | dependent_options: string[]; 73 | free_options: number; 74 | options: IOption[]; 75 | } 76 | 77 | export interface IOption { 78 | 79 | code: string; 80 | name: string; 81 | price: number; 82 | selected: boolean; 83 | shortage: boolean; 84 | extra: string; 85 | dependencies: Array<{ 86 | code: string; 87 | price: number; 88 | enabled: boolean; 89 | }>; 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "efoodgr", 3 | "version": "2.0.7", 4 | "description": "An unofficial tool to manage your efood.gr account.", 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "types": "./src/index.d.ts", 8 | "readme": "README.md", 9 | "author": { 10 | "email": "k.papadatos@pobuca.com", 11 | "name": "Kosmas Papadatos", 12 | "url": "https://github.com/kpapadatos" 13 | }, 14 | "keywords": [ 15 | "cli", 16 | "efood", 17 | "efoodgr", 18 | "order", 19 | "food" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/kpapadatos/efoodgr.git" 24 | }, 25 | "bin": { 26 | "efood": "./bin/efood.js" 27 | }, 28 | "scripts": { 29 | "dev": "npm link && concurrently -r npm:build:watch npm:test:watch", 30 | "test": "nyc mocha", 31 | "test:watch": "nyc mocha --reporter spec --watch", 32 | "build": "tsc --declaration", 33 | "upload-coverage": "cat ./coverage/lcov.info | codacy-coverage", 34 | "build:watch": "tsc -w --declaration", 35 | "patch": "xcommit Package && npm version patch && git push && npm publish" 36 | }, 37 | "dependencies": { 38 | "@pobuca/xsc": "^1.13.1", 39 | "@types/eventemitter2": "^4.1.0", 40 | "@types/inquirer": "^6.5.0", 41 | "@types/node": "^12.11.7", 42 | "@types/request-promise-native": "^1.0.17", 43 | "chalk": "^2.4.2", 44 | "commander": "^2.14.1", 45 | "eventemitter2": "^5.0.1", 46 | "inquirer": "^1.1.2", 47 | "request": "^2.83.0", 48 | "request-promise-native": "^1.0.5", 49 | "semver": "^6.3.0" 50 | }, 51 | "devDependencies": { 52 | "@istanbuljs/nyc-config-typescript": "^0.1.3", 53 | "@types/chai": "^4.2.4", 54 | "@types/chalk": "^2.2.0", 55 | "@types/mocha": "^5.2.7", 56 | "@types/sinon": "^7.5.0", 57 | "@typescript-eslint/eslint-plugin": "^2.6.0", 58 | "@typescript-eslint/parser": "^2.6.0", 59 | "cat": "^0.2.0", 60 | "chai": "^4.2.0", 61 | "codacy-coverage": "^3.4.0", 62 | "concurrently": "^5.0.0", 63 | "eslint": "^6.6.0", 64 | "eslint-config-google": "^0.14.0", 65 | "mocha": "^6.2.2", 66 | "mocha-junit-reporter": "^1.23.1", 67 | "mocha-lcov-reporter": "^1.3.0", 68 | "nyc": "^14.1.1", 69 | "sinon": "^7.5.0", 70 | "ts-node": "^8.4.1", 71 | "typescript": "^3.6.4" 72 | }, 73 | "nyc": { 74 | "extends": "@istanbuljs/nyc-config-typescript", 75 | "reporter": [ 76 | "text-summary", 77 | "cobertura", 78 | "lcov" 79 | ], 80 | "all": true, 81 | "extension": [ 82 | ".ts" 83 | ], 84 | "require": [ 85 | "ts-node/register" 86 | ], 87 | "plugins": [], 88 | "sourceMap": true, 89 | "instrument": true, 90 | "include": [ 91 | "src/**/*.ts" 92 | ], 93 | "exclude": [ 94 | "src/**/*.test.ts" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/commands/payment.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as inquirer from 'inquirer'; 4 | import * as EFood from '../index'; 5 | 6 | let session: EFood.Session; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('payment') 13 | .description('Sets the payment.') 14 | .option('-t, --type [paymentTime]', 'Payment type.') 15 | .option('--token [paymentToken]', 'Payment token, if not paying with cash.') 16 | .option('--hash [paymentHashcode]', 'Payment hashcode.') 17 | .action(handler) 18 | .consoleHandler = async () => { 19 | console.log(`Getting payment methods ...`); 20 | 21 | const store = await session.getStore(); 22 | const choices = []; 23 | 24 | if (store.information.has_cash_on_delivery) { 25 | choices.push('Cash'); 26 | } 27 | 28 | if (store.information.has_credit) { 29 | choices.push('Credit card'); 30 | } 31 | 32 | const input = await inquirer.prompt([{ 33 | choices, 34 | message: 'Select a payment method', 35 | name: 'method', 36 | type: 'list' 37 | }]); 38 | 39 | if (input.method === 'Cash') { 40 | session.setPayment({ 41 | paymentHashcode: null, 42 | paymentMethod: 'cash', 43 | paymentToken: null 44 | }); 45 | } 46 | 47 | if (input.method === 'Credit card') { 48 | const cards = await session.getCreditCards(); 49 | 50 | const cardInput = await inquirer.prompt([{ 51 | choices: cards.map((card) => `[${card.card_type}] ${card.card_number}`), 52 | message: 'Select a card', 53 | name: 'card', 54 | type: 'list' 55 | }]); 56 | 57 | const selectedCard = cards.find((card) => `[${card.card_type}] ${card.card_number}` === cardInput.card); 58 | 59 | session.setPayment({ 60 | paymentHashcode: selectedCard.hashcode, 61 | paymentMethod: 'piraeus.creditcard', 62 | paymentToken: selectedCard.id 63 | }); 64 | } 65 | 66 | console.log(c.green(`Done.`)); 67 | }; 68 | } 69 | 70 | async function handler(cmd: any) { 71 | console.log(`Setting payment method...`); 72 | session.store.paymentMethod = cmd.type; 73 | session.store.paymentToken = cmd.token; 74 | session.store.paymentHashcode = cmd.hash; 75 | console.log(c.green(`Done.`)); 76 | } 77 | -------------------------------------------------------------------------------- /src/CLI.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as program from 'commander'; 3 | import { EventEmitter2 } from 'eventemitter2'; 4 | import * as inquirer from 'inquirer'; 5 | import * as EFood from './index'; 6 | 7 | import addcart from './commands/addcart'; 8 | import login from './commands/login'; 9 | import logout from './commands/logout'; 10 | import ls from './commands/ls'; 11 | import lsaddr from './commands/lsaddr'; 12 | import lscart from './commands/lscart'; 13 | import menu from './commands/menu'; 14 | import mkorder from './commands/mkorder'; 15 | import payment from './commands/payment'; 16 | import setaddr from './commands/setaddr'; 17 | import setstore from './commands/setstore'; 18 | import user from './commands/user'; 19 | import validate from './commands/validate'; 20 | 21 | export default class CLI extends EventEmitter2 { 22 | private static CACHE_FILE = `${process.env.USERPROFILE}/.cache.efoodgr.json`; 23 | 24 | constructor(private args: string[]) { 25 | super(); 26 | 27 | const { red, cyan, white, green } = chalk; 28 | const { version } = require('../package'); 29 | 30 | const session = new EFood.Session(); 31 | 32 | program.usage(' [options]'); 33 | 34 | program.version(version); 35 | 36 | addcart(program, session); 37 | login(program, session); 38 | logout(program, session); 39 | ls(program, session); 40 | lsaddr(program, session); 41 | lscart(program, session); 42 | menu(program, session); 43 | mkorder(program, session); 44 | payment(program, session); 45 | setaddr(program, session); 46 | setstore(program, session); 47 | user(program, session); 48 | validate(program, session); 49 | 50 | program.parse(process.argv); 51 | 52 | const consoleCommandIndex: any = {}; 53 | 54 | for (const command of program.commands) { 55 | if (!command.consoleHandler) { 56 | continue; 57 | } 58 | 59 | consoleCommandIndex[command._name] = command; 60 | 61 | if (command._alias) { 62 | consoleCommandIndex[command._alias] = command; 63 | } 64 | } 65 | 66 | consoleCommandIndex.debug = { async consoleHandler() { console.log(session.store); } }; 67 | consoleCommandIndex.exit = { async consoleHandler() { process.exit(); } }; 68 | consoleCommandIndex.help = { 69 | async consoleHandler() { 70 | console.log(` 71 | 72 | Commands: 73 | 74 | login|l Login with your efood.gr account. 75 | logout|lo Removes all local data. 76 | setaddress|setaddr Sets the default address. 77 | payment Sets the payment method. 78 | ls Lists the stores for the current address. 79 | setstore Sets the store. 80 | addcart|ac Selects, configures and adds an item to the cart. 81 | lscart Lists your cart's contents. 82 | mkorder Places the order. 83 | 84 | `); 85 | } 86 | }; 87 | 88 | if (!process.argv[2]) { 89 | initEfoodConsole(); 90 | } 91 | 92 | function initEfoodConsole() { 93 | console.log( 94 | red('\n e-food.gr\n') + 95 | white(` ${version} \n\n`) + 96 | green( 97 | ' _........_ |\\||-|\\||-/|/|\n' + 98 | ' .\' o \'. \\\\|\\|//||///\n' + 99 | ' / o o \\ |\\/\\||//||||\n' + 100 | ' |o o o| |||\\\\|/\\\\ ||\n' + 101 | ' /\'-.._o __.-\'\\ | \'./\\_/.\' |\n' + 102 | ' \\ \`\`\`\`\` \\ | .:. .:. |\n' + 103 | ' |\`\`............\'\`| | : :: : |\n' + 104 | ' \\ / | : \'\' : |\n' + 105 | ' \`.__________.\` \'.______.\'\n' + 106 | '\n') + 107 | white(` This is an unofficial tool! Tread lightly :)\n`) 108 | ); 109 | 110 | (function listen() { 111 | const prefix = session.store.user ? `${cyan(session.store.user.first_name)}@` : ''; 112 | inquirer.prompt([ 113 | { 114 | message: prefix + red('efood') + '> ', 115 | name: 'cmd', 116 | validate: (input) => { 117 | const command = input.match(/^([^ ]*)/)[1]; 118 | 119 | if (command in consoleCommandIndex) { 120 | return true; 121 | } else { 122 | return `Unknown command: '${command}'`; 123 | } 124 | } 125 | } 126 | ]).then((input) => consoleCommandIndex[input.cmd].consoleHandler().then(listen)); 127 | })(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Google Cloud Platform logo 2 | 3 | # 🍔 efoodgr ![npm](https://img.shields.io/npm/v/efoodgr) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/efoodgr) ![npm collaborators](https://img.shields.io/npm/collaborators/efoodgr) [![Downloads](https://img.shields.io/npm/dt/efoodgr)](https://www.npmjs.com/package/efoodgr) 4 | ![Build](https://github.com/kpapadatos/efoodgr/workflows/build/badge.svg) 5 | [![Codacy Badge](https://img.shields.io/codacy/grade/3845ebde324f49f3853d56750a473236)](https://www.codacy.com/manual/kpapadatos/efoodgr) 6 | [![Maintainability](https://img.shields.io/codeclimate/maintainability-percentage/kpapadatos/efoodgr)](https://codeclimate.com/github/kpapadatos/efoodgr/maintainability) 7 | [![Codacy Badge](https://img.shields.io/codacy/coverage/3845ebde324f49f3853d56750a473236/master)](https://www.codacy.com/manual/kpapadatos/efoodgr) 8 | [![License: MPL 2.0](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) 9 | 10 | An unofficial tool to manage your [e-food.gr](https://e-food.gr) account and place orders. 11 | 12 | ![demo](https://user-images.githubusercontent.com/3382344/36356704-2f057266-14fe-11e8-94eb-07a30f1157f4.gif) 13 | 14 | ### Buy me coffee ☕ 15 | ![Keybase XLM](https://img.shields.io/keybase/xlm/kpapadatos) 16 | 17 | ## Contents 18 | * [Installation](#installation) 19 | * [EFood.Session class](#efoodsession-class) 20 | * [CLI usage](#cli-usage) 21 | * [Console usage](#console-usage) 22 | * [Build](#build) 23 | * [Contribute](#contribute) 24 | * [License](#license) 25 | 26 | ### Installation 27 | For the CLI: 28 | ```sh 29 | npm i -g efoodgr 30 | ``` 31 | 32 | For the SDK: 33 | ```sh 34 | npm i -S efoodgr 35 | ``` 36 | 37 | ### EFood.Session class 38 | Usage: 39 | ```ts 40 | import * as EFood from 'efoodgr'; 41 | 42 | let session = new EFood.Session(); 43 | 44 | (async function main() { 45 | 46 | await session.login('your.email@efood.gr', 'your-password-or-pat'); 47 | 48 | let addresses = await session.getUserAddresses(); 49 | 50 | await session.setAddress(addresses[0].id); 51 | 52 | let nearbyStores = await session.getStores({ 53 | latitude: address.latitude, 54 | longitude: address.longitude, 55 | onlyOpen: true 56 | }); 57 | 58 | await session.setStore(nearbyStores[0].id); 59 | 60 | let menu = await session.getStore(); 61 | 62 | let menuItem = menu[0]; 63 | 64 | // @todo Document the rest of this process... 65 | // ... add items with session.addToCart(itemOptions) 66 | // See how it's done in src/commands/addcart.ts 67 | 68 | await session.validateOrder(); 69 | 70 | let orderId = await session.submitOrder(); 71 | 72 | // Do this a couple of times until it is 73 | // accepted or rejected 74 | await session.getOrderStatus(orderId); 75 | 76 | })(); 77 | 78 | ``` 79 | 80 | ### CLI Usage 81 | `efood [options]` 82 | 83 | Get help for each command with 84 | `efood --help` 85 | 86 | Commands: 87 | 88 | login|l [options] Log in with your efood.gr account. 89 | menu Gets the menu of the selected store. 90 | setstore [storeId] Sets the store. 91 | addcart|ac [options] Adds cart entry. 92 | mkorder Places the order. 93 | lscart Lists all cart items. 94 | ls Lists stores for current address. 95 | logout|lo Removes all local data. 96 | setaddr [addressId] Sets the current address. 97 | lsaddr Lists the current user's addresses. 98 | user|u Shows current user info. 99 | 100 | Options: 101 | 102 | -h, --help output usage information 103 | -V, --version output the version number 104 | 105 | ### Console usage 106 | Just type `efood` to enter the console and then `help` to see available commands. Not all commands exist/behave the same in the console environment. 107 | 108 | ### Build 109 | If you want to build this package into a standalone binary for your OS, you can use [nexe](https://github.com/jaredallard/nexe) 110 | ```sh 111 | npm i -g nexe 112 | git clone https://github.com/kpapadatos/efoodgr 113 | cd efoodgr 114 | npm i 115 | nexe -i bin/efood.js -o efood.exe 116 | ``` 117 | 118 | #### Notes 119 | - It may take some time as it downloads the latest NodeJS source and builds it. 120 | - If you get errors about `try-thread-sleep` and `thread-sleep` modules missing, you may need to create their folders in `node_modules` with a dummy `package.json` to fool `browserify` that they exist. 121 | 122 | ### Contribute 123 | Feel free to propose changes and/or add features. Future plans include: 124 | 125 | - Paypal support 126 | - Order presets 127 | - Tests 128 | 129 | ### License 130 | (The MIT License) 131 | 132 | Copyright (c) 2015 Kosmas Papadatos 133 | 134 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 135 | 136 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 139 | -------------------------------------------------------------------------------- /src/commands/addcart.ts: -------------------------------------------------------------------------------- 1 | import c from 'chalk'; 2 | import { CommanderStatic } from 'commander'; 3 | import * as inquirer from 'inquirer'; 4 | import * as EFood from '../index'; 5 | 6 | let session: EFood.Session; 7 | 8 | export default function (program: CommanderStatic, s: EFood.Session) { 9 | session = s; 10 | 11 | program 12 | .command('addcart') 13 | .alias('ac') 14 | .description('Adds cart entry.') 15 | .option('-i, --item [itemCode]', 'Item code.') 16 | .option('-c, --config [config]', 'Item options. ') 17 | .option('-q, --quantity [number]', 'Item quantity.') 18 | .option('--comment [comment]', 'A comment for this item.') 19 | .option('--offer [offer]', 'The offer line Id for this item.') 20 | .action(handler) 21 | .consoleHandler = async () => { 22 | console.log(`Getting menu items ...`); 23 | 24 | const store = await session.getStore(); 25 | const categories = store.menu.categories; 26 | const offers = store.offers; 27 | 28 | let itemGroup = 'Menu'; 29 | 30 | if (offers.length) { 31 | itemGroup = (await inquirer.prompt([{ 32 | choices: ['Offers', 'Menu'], 33 | message: 'Select an item group', 34 | name: 'itemGroup', 35 | type: 'list' 36 | }])).itemGroup; 37 | } 38 | 39 | const choices = []; 40 | let itemSets: EFood.IProduct[][] = []; 41 | const items: EFood.IProduct[] = []; 42 | 43 | if (itemGroup === 'Menu') { 44 | const input = (await inquirer.prompt([{ 45 | choices: categories.map((o) => o.name), 46 | message: 'Select a category', 47 | name: 'category', 48 | type: 'list' 49 | }])); 50 | 51 | const category = categories.filter((cat) => cat.name === input.category)[0]; 52 | 53 | for (const product of category.items) { 54 | choices.push(`[${product.price}€] ${product.name}`); 55 | items.push(product); 56 | } 57 | 58 | itemSets = [items]; 59 | } else { 60 | const input = (await inquirer.prompt([{ 61 | choices: offers.map((o) => `[${o.price}€] ${o.description}`), 62 | message: 'Select an offer', 63 | name: 'offer', 64 | type: 'list' 65 | }])); 66 | 67 | const offer = offers.filter((o) => `[${o.price}€] ${o.description}` === input.offer)[0]; 68 | 69 | for (const tier of offer.tiers) { 70 | tier.items.forEach((i: EFood.IProduct) => i.offer_line = tier.offer_line); 71 | itemSets.push(tier.items as EFood.IProduct[]); 72 | } 73 | } 74 | 75 | for (const itemSet of itemSets) { 76 | const priceNameTemplate = (i: any) => i.price ? `[${i.price}€] ${i.name}` : i.name; 77 | const choiceSet = itemSet.map(priceNameTemplate); 78 | const input = await inquirer.prompt([{ 79 | choices: choiceSet, 80 | message: 'Select an item', 81 | name: 'selectedItem', 82 | type: 'list' 83 | }]); 84 | 85 | const selectedItem = itemSet[choiceSet.indexOf(input.selectedItem)]; 86 | const itemCode = selectedItem.code; 87 | const offer = selectedItem.offer_line; 88 | 89 | console.log(`Getting options for ${c.cyan(selectedItem.name)} ...`); 90 | 91 | const menuItemResponse = await session.getMenuItemOptions(itemCode); 92 | 93 | const itemOptions = menuItemResponse.data.tiers as EFood.IOptionTier[]; 94 | 95 | let itemConfig: any = ''; 96 | let price = menuItemResponse.data.price as number; 97 | 98 | if (itemOptions.length) { 99 | itemConfig = []; 100 | for (const tier of itemOptions) { 101 | const availableChoices = tier.options.map(priceNameTemplate); 102 | 103 | const inp = await inquirer.prompt([{ 104 | choices: availableChoices, 105 | message: tier.name, 106 | name: 'opt', 107 | type: tier.type === 'radio' ? 'list' : 'checkbox' 108 | }]); 109 | 110 | if (typeof inp.opt === 'string') { 111 | inp.opt = [inp.opt]; 112 | } 113 | 114 | if (inp.opt.length) { 115 | itemConfig.push( 116 | inp.opt 117 | .map((chosenItem: any) => { 118 | const option = tier.options[availableChoices.indexOf(chosenItem)]; 119 | price += option.price; 120 | return option.code; 121 | }) 122 | ); 123 | } 124 | } 125 | 126 | itemConfig = itemConfig.join(','); 127 | } 128 | 129 | const { comment }: { comment: string } = await inquirer.prompt([{ 130 | message: 'Comment', 131 | name: 'comment' 132 | }]); 133 | 134 | const { quantity }: { quantity: number } = await inquirer.prompt([{ 135 | default: 1, 136 | message: 'Quantity', 137 | name: 'quantity' 138 | }]); 139 | 140 | console.log(`Adding item to cart...`); 141 | 142 | session.addToCart({ 143 | comment, 144 | config: itemConfig, 145 | item: itemCode, 146 | offer, 147 | price, 148 | quantity: quantity || 1 149 | }); 150 | } 151 | 152 | console.log(c.green(`Done.`)); 153 | }; 154 | } 155 | 156 | async function handler(cmd: any) { 157 | console.log(`Adding item to cart...`); 158 | await session.addToCart(cmd); 159 | console.log(c.green(`Done.`)); 160 | } 161 | -------------------------------------------------------------------------------- /src/classes/Session.ts: -------------------------------------------------------------------------------- 1 | import { get, post } from 'request-promise-native'; 2 | import * as Models from '../models/index'; 3 | import { ICreditCard } from '../models/index'; 4 | import { SessionStore } from './SessionStore'; 5 | 6 | export class Session { 7 | public APIEndpoint = 'https://api.e-food.gr'; 8 | 9 | public store: SessionStore = new SessionStore(); 10 | 11 | public setPayment(paymentOptions: { paymentToken: string; paymentHashcode: string; paymentMethod: string; }): void { 12 | this.store.paymentHashcode = paymentOptions.paymentHashcode; 13 | this.store.paymentToken = paymentOptions.paymentToken; 14 | this.store.paymentMethod = paymentOptions.paymentMethod; 15 | } 16 | 17 | public async login(email: string, password: string): Promise { 18 | const response = await post(`${this.APIEndpoint}/api/v1/user/login`, { 19 | body: { email, password }, 20 | json: true 21 | }) as Models.ILoginResponse; 22 | 23 | if (response.status === 'ok') { 24 | this.store.sessionId = response.data.session_id; 25 | this.store.user = response.data.user as Models.IUser; 26 | } 27 | 28 | return response.status === 'ok'; 29 | } 30 | 31 | public get isAuthenticated(): boolean { 32 | return Boolean(this.store.sessionId); 33 | } 34 | 35 | /** 36 | * Validates an authenticated session by testing its `sessionId` 37 | * with a statistics call. 38 | */ 39 | public async validate(): Promise { 40 | const response = await get({ 41 | headers: { 'x-core-session-id': this.store.sessionId }, 42 | json: true, 43 | uri: `${this.APIEndpoint}/api/v1/user/statistics` 44 | }); 45 | 46 | return response.status === 'ok'; 47 | } 48 | 49 | public async getUserAddresses(): Promise { 50 | const response: Models.IAPIResponse = await get({ 51 | headers: { 'x-core-session-id': this.store.sessionId }, 52 | json: true, 53 | uri: `${this.APIEndpoint}/api/v1/user/clients/address` 54 | }) as Models.IAPIResponse; 55 | 56 | return response.data as Models.IAddress[]; 57 | } 58 | 59 | public setAddress(addressId: number): void { 60 | this.store.addressId = addressId; 61 | } 62 | 63 | public async getStores(parameters: { 64 | latitude: number; 65 | longitude: number; 66 | onlyOpen: boolean; 67 | }): Promise { 68 | const response: Models.IAPIResponse = await get({ 69 | headers: { 'x-core-session-id': this.store.sessionId }, 70 | json: true, 71 | uri: `${this.APIEndpoint}/api/v1/restaurants/?filters=%7B%0A%0A%7D&latitude=${parameters.latitude}&longitude=${parameters.longitude}&mode=filter&version=3` 72 | }) as Models.IAPIResponse; 73 | 74 | return response.data.restaurants as Models.IStore[]; 75 | } 76 | 77 | /** 78 | * Submits the order and returns an order number. 79 | */ 80 | public async submitOrder(): Promise { 81 | const response: Models.IAPIResponse = await post({ 82 | body: this.compileOrder(), 83 | headers: { 'x-core-session-id': this.store.sessionId }, 84 | json: true, 85 | uri: `${this.APIEndpoint}/api/v1/order/submit/` 86 | }) as Models.IAPIResponse; 87 | 88 | return response.status === 'ok' && response.data.order_id; 89 | } 90 | 91 | /** 92 | * Gets an order status by Id. 93 | */ 94 | public async getOrderStatus(orderId: string): Promise { 95 | const response: Models.IAPIResponse = await get({ 96 | headers: { 'x-core-session-id': this.store.sessionId }, 97 | json: true, 98 | uri: `${this.APIEndpoint}/api/v1/order/status/${orderId}/` 99 | }) as Models.IAPIResponse; 100 | 101 | return response.data; 102 | } 103 | 104 | /** 105 | * Sets the `storeId` for the current store. 106 | * @param storeId The store's Id. 107 | */ 108 | public setStore(storeId: number) { 109 | if (storeId !== this.store.storeId) { 110 | this.store.cart = new Models.Cart(); 111 | } 112 | 113 | this.store.storeId = storeId; 114 | } 115 | 116 | /** 117 | * Gets the menu of a store. 118 | */ 119 | public async getStore(): Promise { 120 | const response: Models.IAPIResponse = await get({ 121 | headers: { 'x-core-session-id': this.store.sessionId }, 122 | json: true, 123 | uri: `${this.APIEndpoint}/api/v1/restaurants/${this.store.storeId}` 124 | }) as Models.IAPIResponse; 125 | 126 | return response.data as Models.IStore; 127 | } 128 | 129 | /** 130 | * Returns the cached user object of this session. 131 | */ 132 | public getUser(): Models.IUser { 133 | return this.store.user; 134 | } 135 | 136 | /** 137 | * Returns options for an item. 138 | * @param itemCode Item code (not to be confused with Id). 139 | */ 140 | public async getMenuItemOptions(itemCode: string): Promise { 141 | const response: Models.IAPIResponse = await get({ 142 | headers: { 'x-core-session-id': this.store.sessionId }, 143 | json: true, 144 | uri: `${this.APIEndpoint}/api/v1/restaurants/menuitem/?item_code=${itemCode}&restaurant_id=${this.store.storeId}` 145 | }) as Models.IAPIResponse; 146 | 147 | return response; 148 | } 149 | 150 | /** 151 | * Adds a menu item to the cart. 152 | * @param itemOptions Menu item configuration. 153 | */ 154 | public addToCart(itemOptions: { 155 | offer: number; 156 | comment: string; 157 | price: number; 158 | quantity: number; 159 | item: string; 160 | config: string; 161 | }): void { 162 | this.store.cart.products.push({ 163 | comment: itemOptions.comment, 164 | description: '', 165 | materials: itemOptions.config ? itemOptions.config.split(',') : [], 166 | offer: itemOptions.offer || null, 167 | price: itemOptions.price || 0, 168 | product_id: itemOptions.item, 169 | quantity: itemOptions.quantity || 1, 170 | total: itemOptions.price || 0 171 | }); 172 | } 173 | 174 | /** 175 | * Makes a `validate` request with the current 176 | * cart and settings 177 | */ 178 | public async validateOrder(): Promise { 179 | const response: Models.IAPIResponse = await post({ 180 | body: this.compileOrder(), 181 | headers: { 'x-core-session-id': this.store.sessionId }, 182 | json: true, 183 | uri: `${this.APIEndpoint}/api/v1/order/validate/` 184 | }) as Models.IAPIResponse; 185 | 186 | return response.status === 'ok'; 187 | } 188 | 189 | /** 190 | * Compiles a submittable `Order` object from the cart 191 | * cart and configuration. 192 | */ 193 | public compileOrder(): Models.IOrder { 194 | const order: Models.IOrder = { 195 | address_id: this.store.addressId, 196 | amount: this.store.cart.products.map((p) => p.total).reduce((c, e, i) => c += e, 0), 197 | coupons: [], 198 | created: '', 199 | delivery_type: this.store.deliveryType, 200 | discount: [], 201 | payment_method: this.store.paymentMethod, 202 | products: this.store.cart.products, 203 | restaurant_id: this.store.storeId 204 | }; 205 | 206 | if (this.store.paymentToken) { 207 | order.payment_token = this.store.paymentToken; 208 | } 209 | 210 | return order; 211 | } 212 | 213 | public async getCreditCards(): Promise { 214 | const response: Models.IAPIResponse = await get({ 215 | headers: { 'x-core-session-id': this.store.sessionId }, 216 | json: true, 217 | uri: `${this.APIEndpoint}/api/v1/ext/piraeus/methods/` 218 | }) as Models.IAPIResponse; 219 | 220 | return response.data as ICreditCard[]; 221 | } 222 | 223 | /** 224 | * Logs the user out by deleting any local data. 225 | */ 226 | public async logout() { 227 | this.store = new SessionStore(); 228 | } 229 | } 230 | --------------------------------------------------------------------------------