├── .nvmrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── test-and-release.yml ├── .prettierrc.json ├── tsconfig.build.json ├── test └── testMinimal.js ├── LICENSE ├── package.json ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── README.md └── src └── bring.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: foxriver76 2 | custom: https://www.paypal.me/foxriver76 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | versioning-strategy: increase-if-necessary 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "trailingComma": "none", 7 | "singleQuote": true, 8 | "endOfLine": "lf", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid", 11 | "quoteProps": "as-needed" 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | // Specialized tsconfig to only compile .ts-files in the src dir 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "allowJs": false, 6 | "checkJs": false, 7 | "noEmit": false, 8 | "declaration": true 9 | }, 10 | "include": [ 11 | "src/**/*.ts" 12 | ], 13 | "exclude": [ 14 | "src/**/*.test.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/testMinimal.js: -------------------------------------------------------------------------------- 1 | const bringApi = require(`${__dirname}/../build/bring.js`); 2 | const { describe } = require(`mocha`); 3 | 4 | describe(`Wrong login test`, () => { 5 | let bring; 6 | 7 | before(done => { 8 | bring = new bringApi({ mail: 'example@example.com', password: 'secret' }); 9 | done(); 10 | }); 11 | 12 | it(`init should take less than 2000ms`, async () => { 13 | try { 14 | await bring.login(); 15 | } catch (e) { 16 | if (!e.message.includes(`email password combination not existing`)) { 17 | throw new Error(`Wrong rejection message on login: ${e.message}`); 18 | } 19 | } 20 | }); 21 | 22 | it(`init should throw invalid JWT error`, async () => { 23 | try { 24 | const lists = await bring.loadLists(); 25 | expect(lists).to.be.undefined; 26 | } catch (e) { 27 | if (!e.message.includes(`JWT access token is not valid`)) { 28 | throw new Error(`Wrong rejection message on loading lists: ${e.message}`); 29 | } 30 | } 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Moritz Heusinger 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bring-shopping", 3 | "version": "2.0.1", 4 | "engines": { 5 | "node": ">=18.0.0" 6 | }, 7 | "description": "Nodejs wrapper for the Bring! API", 8 | "author": { 9 | "name": "Moritz Heusinger", 10 | "email": "moritz.heusinger@gmail.com" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Moritz Heusinger", 15 | "email": "moritz.heusinger@gmail.com" 16 | } 17 | ], 18 | "homepage": "https://github.com/foxriver76/bring-api", 19 | "license": "MIT", 20 | "keywords": [ 21 | "bring", 22 | "shopping-list" 23 | ], 24 | "devDependencies": { 25 | "@types/node": "^18.11.8", 26 | "@typescript-eslint/eslint-plugin": "^5.41.0", 27 | "@typescript-eslint/parser": "^5.41.0", 28 | "chai": "^4.3.6", 29 | "eslint": "^8.26.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-prettier": "^4.2.1", 32 | "mocha": "^10.1.0", 33 | "prettier": "^2.7.1", 34 | "typescript": "^5.7.2" 35 | }, 36 | "scripts": { 37 | "test": "node node_modules/mocha/bin/mocha", 38 | "build": "tsc -b tsconfig.build.json" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/foxriver76/node-bring-api.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/foxriver76/node-bring-api/issues" 46 | }, 47 | "files": [ 48 | "build/", 49 | "LICENSE" 50 | ], 51 | "main": "build/bring.js", 52 | "types": "build/bring.d.ts" 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | .eslintrc.json 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # Serverless directories 75 | .serverless 76 | ### Example user template template 77 | ### Example user template 78 | 79 | # IntelliJ project files 80 | .idea 81 | *.iml 82 | out 83 | gen 84 | 85 | *.tsbuildinfo 86 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // Root tsconfig to set the settings and power editor support for all TS files 2 | { 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | // do not compile anything, this file is just to configure type checking 6 | // the compilation is configured in tsconfig.build.json 7 | "noEmit": true, 8 | 9 | // check JS files, but do not compile them => tsconfig.build.json 10 | "allowJs": true, 11 | "checkJs": true, 12 | 13 | "skipLibCheck": true, // Don't report errors in 3rd party definitions 14 | "noEmitOnError": true, 15 | "outDir": "./build/", 16 | "removeComments": false, 17 | "module": "commonjs", 18 | "moduleResolution": "node", 19 | "esModuleInterop": true, 20 | // this is necessary for the automatic typing of the adapter config 21 | "resolveJsonModule": true, 22 | 23 | // Set this to false if you want to disable the very strict rules (not recommended) 24 | "strict": true, 25 | // Or enable some of those features for more fine-grained control 26 | // "strictNullChecks": true, 27 | // "strictPropertyInitialization": true, 28 | // "strictBindCallApply": true, 29 | // "noImplicitAny": true, 30 | // "noUnusedLocals": true, 31 | // "noUnusedParameters": true, 32 | // Uncomment this if you want the old behavior of catch variables being `any` 33 | // "useUnknownInCatchVariables": false, 34 | 35 | // Consider targeting es2019 or higher if you only support Node.js 12+ 36 | "target": "es2018", 37 | 38 | "sourceMap": true, 39 | "inlineSourceMap": false 40 | }, 41 | "include": [ 42 | "src/**/*.ts", 43 | "admin/**/*.ts", 44 | "admin/**/*.tsx" 45 | ], 46 | "exclude": [ 47 | "build/**", 48 | "node_modules/**" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | # Run this job on all pushes and pull requests 4 | # as well as tags with a semantic version 5 | on: 6 | push: 7 | branches: 8 | - "*" 9 | tags: 10 | - "v[0-9]+.[0-9]+.[0-9]+" 11 | pull_request: {} 12 | 13 | jobs: 14 | # Runs tests on all supported node versions and OSes 15 | tests: 16 | if: contains(github.event.head_commit.message, '[skip ci]') == false 17 | 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [18.x, 20.x, 22.x] 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install package 31 | run: npm i 32 | 33 | - name: Run unit tests 34 | run: npm run test 35 | 36 | # Deploys the final package to NPM 37 | deploy: 38 | needs: [tests] 39 | 40 | # Trigger this step only when a commit on master is tagged with a version number 41 | if: | 42 | contains(github.event.head_commit.message, '[skip ci]') == false && 43 | github.event_name == 'push' && 44 | github.event.base_ref == 'refs/heads/master' && 45 | startsWith(github.ref, 'refs/tags/v') 46 | 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v1 50 | - name: Use Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v1 52 | with: 53 | node-version-file: '.nvmrc' 54 | 55 | - name: Install package 56 | run: npm i 57 | 58 | - name: Publish package to npm 59 | run: | 60 | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 61 | npm whoami 62 | npm publish 63 | 64 | # Dummy job for skipped builds - without this, github reports the build as failed 65 | skip-ci: 66 | if: contains(github.event.head_commit.message, '[skip ci]') 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Skip build 70 | run: echo "Build skipped!" 71 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, // Don't look outside this project for inherited configs 3 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 4 | parserOptions: { 5 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 6 | sourceType: 'module', // Allows for the use of imports 7 | project: './tsconfig.json' 8 | }, 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 11 | 'plugin:prettier/recommended' 12 | ], 13 | plugins: [], 14 | rules: { 15 | indent: 'off', 16 | quotes: [ 17 | 'error', 18 | 'single', 19 | { 20 | avoidEscape: true, 21 | allowTemplateLiterals: true 22 | } 23 | ], 24 | '@typescript-eslint/no-parameter-properties': 'off', 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | '@typescript-eslint/no-use-before-define': [ 27 | 'error', 28 | { 29 | functions: false, 30 | typedefs: false, 31 | classes: false 32 | } 33 | ], 34 | '@typescript-eslint/no-unused-vars': [ 35 | 'error', 36 | { 37 | ignoreRestSiblings: true, 38 | argsIgnorePattern: '^_' 39 | } 40 | ], 41 | '@typescript-eslint/explicit-function-return-type': [ 42 | 'warn', 43 | { 44 | allowExpressions: true, 45 | allowTypedFunctionExpressions: true 46 | } 47 | ], 48 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 49 | '@typescript-eslint/interface-name-prefix': 'off', 50 | '@typescript-eslint/no-non-null-assertion': 'off', // This is necessary for Map.has()/get()! 51 | 'no-var': 'error', 52 | 'prefer-const': 'error', 53 | 'no-trailing-spaces': 'error', 54 | curly: 'error', 55 | 'brace-style': 'error', 56 | 'arrow-parens': ['error', 'as-needed'], 57 | 'no-console': 'off', 58 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrors: 'all' }], 59 | 'no-useless-escape': 'warn', 60 | 'no-constant-condition': 'off', 61 | 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], 62 | 'no-throw-literal': 'error', 63 | 'prefer-promise-reject-errors': 'error', 64 | //"require-await": "error", 65 | 'no-return-await': 'error', 66 | eqeqeq: ['error', 'always'], 67 | semi: ['error', 'always'], 68 | 'comma-dangle': [ 69 | 'error', 70 | { 71 | arrays: 'never', 72 | objects: 'never', 73 | imports: 'never', 74 | exports: 'never', 75 | functions: 'ignore' 76 | } 77 | ] 78 | }, 79 | overrides: [ 80 | { 81 | files: ['*.test.ts'], 82 | rules: { 83 | '@typescript-eslint/explicit-function-return-type': 'off' 84 | } 85 | }, 86 | { 87 | files: ['**/*.js'], 88 | parser: 'espree' 89 | } 90 | ] 91 | }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-Bring-Shopping 2 | [![NPM version](http://img.shields.io/npm/v/bring-shopping.svg)](https://www.npmjs.com/package/bring-shopping) 3 | [![Downloads](https://img.shields.io/npm/dm/bring-shopping.svg)](https://www.npmjs.com/package/bring-shopping) 4 | ![Build Status](https://github.com/foxriver76/node-bring-api/workflows/Test%20and%20Release/badge.svg) 5 | 6 | A __zero dependency__ node module for Bring! shopping lists __entirely written in TypeScript__. 7 | 8 | ## Disclaimer 9 | The developers of this module are in no way endorsed by or affiliated with 10 | Bring! Labs AG, or any associated subsidiaries, logos or trademarks. 11 | 12 | ## Installation 13 | ```npm install bring-shopping --production``` 14 | 15 | ## Usage Example 16 | 17 | ```javascript 18 | const bringApi = require(`bring-shopping`); 19 | 20 | main(); 21 | 22 | async function main () { 23 | // provide user and email to login 24 | const bring = new bringApi({mail: `example@example.com`, password: `secret`}); 25 | 26 | // login to get your uuid and Bearer token 27 | try { 28 | await bring.login(); 29 | console.log(`Successfully logged in as ${bring.name}`); 30 | } catch (e) { 31 | console.error(`Error on Login: ${e.message}`); 32 | } 33 | 34 | // get all lists and their listUuid 35 | const lists = await bring.loadLists(); 36 | 37 | // get items of a list by its list uuid 38 | const items = await bring.getItems('9b3ba561-02ad-4744-a737-c43k7e5b93ec'); 39 | 40 | // get translations 41 | const translations = await bring.loadTranslations('de-DE'); 42 | } 43 | ``` 44 | 45 | More important methods are `getItems(listUUID)`, `getItemsDetails(listUUID)`, `saveItem(listUuid, itemName, specificaiton)`, 46 | `moveToRecentList(listUuid, itemName)` and `getAllUsersFromList(listUuid)`. 47 | 48 | ## Changelog 49 | ### 2.0.1 (2025-01-21) 50 | * (@foxriver76) also throw on http errors 51 | 52 | ### 2.0.0 (2024-11-27) 53 | * (@foxriver76) ported to native `fetch` module (BREAKING: Requires Node.js 18 or above) 54 | 55 | ### 1.5.1 (2022-10-31) 56 | * (foxriver76) updated types 57 | * (foxriver76) fixed `removeItemImage` as headers were missing 58 | 59 | ### 1.5.0 (2022-10-31) 60 | * (Aliyss) added methods to link an image to an item (PR #221) 61 | 62 | ### 1.4.3 (2022-05-01) 63 | * (foxriver76) fixed typos in types (thanks to @Squawnchy) 64 | 65 | ### 1.4.2 (2021-08-12) 66 | * (foxriver76) restructure to typescript 67 | 68 | ### 1.3.1 (2021-04-29) 69 | * (foxriver76) fixed issue where error was used instead of the mssage on `getPendingInvitations` 70 | 71 | ### 1.3.0 (2020-10-05) 72 | * (mdhom) added `getItemsDetails` method 73 | * (foxriver76) now reject with real errors instead of strings 74 | 75 | ### 1.2.3 (2019-09-22) 76 | * (foxriver76) on new call of login overwrite bearer header to allow reauth 77 | 78 | ### 1.2.2 79 | * (foxriver76) More information on rejection of getItems 80 | 81 | ### 1.2.1 82 | * (foxriver76) minor fix 83 | 84 | ### 1.2.0 85 | * (foxriver76) new functionalities -> getTranslations, getCatalog and getPendingInvitations 86 | 87 | ### 1.1.0 88 | * (foxriver76) use API version v2 89 | 90 | ### 1.0.2 91 | * (foxriver76) minor code optimization, nothing functional 92 | 93 | ### 1.0.1 94 | * (foxriver76) fix links in package 95 | 96 | ### 1.0.0 97 | * (foxriver76) offical release 98 | 99 | -------------------------------------------------------------------------------- /src/bring.ts: -------------------------------------------------------------------------------- 1 | interface BringOptions { 2 | mail: string; 3 | password: string; 4 | url?: string; 5 | uuid?: string; 6 | } 7 | 8 | interface GetItemsResponseEntry { 9 | specification: string; 10 | name: string; 11 | } 12 | 13 | interface GetItemsResponse { 14 | uuid: string; 15 | status: string; 16 | purchase: GetItemsResponseEntry[]; 17 | recently: GetItemsResponseEntry[]; 18 | } 19 | 20 | interface AuthSuccessResponse { 21 | name: string; 22 | uuid: string; 23 | access_token: string; 24 | refresh_token: string; 25 | } 26 | 27 | interface ErrorResponse { 28 | message: string; 29 | error: string; 30 | error_description: string; 31 | errorcode: number; 32 | } 33 | 34 | type AuthResponse = AuthSuccessResponse | ErrorResponse; 35 | 36 | interface GetAllUsersFromListEntry { 37 | publicUuid: string; 38 | name: string; 39 | email: string; 40 | photoPath: string; 41 | pushEnabled: boolean; 42 | plusTryOut: boolean; 43 | country: string; 44 | language: string; 45 | } 46 | interface GetAllUsersFromListResponse { 47 | users: GetAllUsersFromListEntry[]; 48 | } 49 | 50 | interface LoadListsEntry { 51 | listUuid: string; 52 | name: string; 53 | theme: string; 54 | } 55 | 56 | interface LoadListsResponse { 57 | lists: LoadListsEntry[]; 58 | } 59 | 60 | interface GetItemsDetailsEntry { 61 | uuid: string; 62 | itemId: string; 63 | listUuid: string; 64 | userIconItemId: string; 65 | userSectionId: string; 66 | assignedTo: string; 67 | imageUrl: string; 68 | } 69 | 70 | interface UserSettingsEntry { 71 | key: string; 72 | value: string; 73 | } 74 | 75 | interface UserListSettingsEntry { 76 | listUuid: string; 77 | usersettings: UserSettingsEntry[]; 78 | } 79 | 80 | interface GetUserSettingsResponse { 81 | userSettings: UserSettingsEntry[]; 82 | userlistsettings: UserListSettingsEntry[]; 83 | } 84 | 85 | interface CatalogItemsEntry { 86 | itemId: string; 87 | name: string; 88 | } 89 | 90 | interface CatalogSectionsEntry { 91 | sectionId: string; 92 | name: string; 93 | items: CatalogItemsEntry[]; 94 | } 95 | 96 | interface LoadCatalogResponse { 97 | language: string; 98 | catalog: { 99 | sections: CatalogSectionsEntry[]; 100 | }; 101 | } 102 | 103 | interface GetPendingInvitationsResponse { 104 | invitations: any[]; 105 | } 106 | 107 | interface Image { 108 | /** the image itself */ 109 | imageData: string; 110 | } 111 | 112 | class Bring { 113 | private readonly mail: string; 114 | private readonly password: string; 115 | private readonly url: string; 116 | private uuid: string; 117 | private readonly headers: { 118 | 'X-BRING-CLIENT-SOURCE': string; 119 | 'X-BRING-COUNTRY': string; 120 | 'X-BRING-CLIENT': string; 121 | 'X-BRING-API-KEY': string; 122 | Authorization?: string; 123 | 'X-BRING-USER-UUID'?: string; 124 | }; 125 | public name?: string; 126 | private bearerToken?: string; 127 | private refreshToken?: string; 128 | private putHeaders?: { 129 | Authorization?: string; 130 | 'X-BRING-USER-UUID'?: string; 131 | 'X-BRING-CLIENT-SOURCE': string; 132 | 'X-BRING-COUNTRY': string; 133 | 'X-BRING-CLIENT': string; 134 | 'X-BRING-API-KEY': string; 135 | 'Content-Type': string; 136 | }; 137 | 138 | constructor(options: BringOptions) { 139 | this.mail = options.mail; 140 | this.password = options.password; 141 | this.url = options.url || `https://api.getbring.com/rest/v2/`; 142 | this.uuid = options.uuid || ``; 143 | this.headers = { 144 | 'X-BRING-API-KEY': `cof4Nc6D8saplXjE3h3HXqHH8m7VU2i1Gs0g85Sp`, 145 | 'X-BRING-CLIENT': `webApp`, 146 | 'X-BRING-CLIENT-SOURCE': `webApp`, 147 | 'X-BRING-COUNTRY': `DE` 148 | }; 149 | } 150 | 151 | /** 152 | * Try to log into given account 153 | */ 154 | async login(): Promise { 155 | let data: AuthResponse; 156 | 157 | try { 158 | const resp = await fetch(`${this.url}bringauth`, { 159 | method: 'POST', 160 | body: new URLSearchParams({ email: this.mail, password: this.password }) 161 | }); 162 | 163 | data = await resp.json(); 164 | } catch (e: any) { 165 | throw new Error(`Cannot Login: ${e.message}`); 166 | } 167 | 168 | if ('error' in data) { 169 | throw new Error(`Cannot Login: ${data.message}`); 170 | } 171 | 172 | this.name = data.name; 173 | this.uuid = data.uuid; 174 | this.bearerToken = data.access_token; 175 | this.refreshToken = data.refresh_token; 176 | 177 | this.headers[`X-BRING-USER-UUID`] = this.uuid; 178 | this.headers[`Authorization`] = `Bearer ${this.bearerToken}`; 179 | this.putHeaders = { 180 | ...this.headers, 181 | ...{ 'Content-Type': `application/x-www-form-urlencoded; charset=UTF-8` } 182 | }; 183 | } 184 | 185 | /** 186 | * Loads all shopping lists 187 | */ 188 | async loadLists(): Promise { 189 | try { 190 | const resp = await fetch(`${this.url}bringusers/${this.uuid}/lists`, { headers: this.headers }); 191 | const lists: LoadListsResponse | ErrorResponse = await resp.json(); 192 | 193 | if ('error' in lists) { 194 | throw new Error(lists.message); 195 | } 196 | 197 | return lists; 198 | } catch (e: any) { 199 | throw new Error(`Cannot get lists: ${e.message}`); 200 | } 201 | } 202 | 203 | /** 204 | * Get all items from the current selected shopping list 205 | */ 206 | async getItems(listUuid: string): Promise { 207 | try { 208 | const resp = await fetch(`${this.url}bringlists/${listUuid}`, { headers: this.headers }); 209 | const items: GetItemsResponse | ErrorResponse = await resp.json(); 210 | 211 | if ('error' in items) { 212 | throw new Error(items.message); 213 | } 214 | 215 | return items; 216 | } catch (e: any) { 217 | throw new Error(`Cannot get items for list ${listUuid}: ${e.message}`); 218 | } 219 | } 220 | 221 | /** 222 | * Get detailed information about all items from the current selected shopping list 223 | */ 224 | async getItemsDetails(listUuid: string): Promise { 225 | try { 226 | const resp = await fetch(`${this.url}bringlists/${listUuid}/details`, { headers: this.headers }); 227 | const items: GetItemsDetailsEntry[] | ErrorResponse = await resp.json(); 228 | 229 | if ('error' in items) { 230 | throw new Error(items.message); 231 | } 232 | 233 | return items; 234 | } catch (e: any) { 235 | throw new Error(`Cannot get detailed items for list ${listUuid}: ${e.message}`); 236 | } 237 | } 238 | 239 | /** 240 | * Save an item to your current shopping list 241 | * 242 | * @param itemName The name of the item you want to send to the bring server 243 | * @param specification The little description under the name of the item 244 | * @param listUuid The listUUID you want to receive a list of users from. 245 | * returns an empty string and answerHttpStatus should contain 204. If not -> error 246 | */ 247 | async saveItem(listUuid: string, itemName: string, specification: string): Promise { 248 | try { 249 | const resp = await fetch(`${this.url}bringlists/${listUuid}`, { 250 | method: 'PUT', 251 | headers: this.putHeaders, 252 | body: `&purchase=${itemName}&recently=&specification=${specification}&remove=&sender=null` 253 | }); 254 | return resp.text(); 255 | } catch (e: any) { 256 | throw new Error(`Cannot save item ${itemName} (${specification}) to ${listUuid}: ${e.message}`); 257 | } 258 | } 259 | 260 | /** 261 | * Save an image to an item 262 | * 263 | * @param itemUuid The itemUUID which will be updated 264 | * @param image The image you want to link to the item 265 | * @return returns an imageUrl and answerHttpStatus should contain 204. If not -> error 266 | */ 267 | async saveItemImage(itemUuid: string, image: Image): Promise<{ imageUrl: string }> { 268 | try { 269 | const resp = await fetch(`${this.url}bringlistitemdetails/${itemUuid}/image`, { 270 | method: 'PUT', 271 | headers: this.putHeaders, 272 | body: new URLSearchParams({ ...image }) 273 | }); 274 | const imageObj: { imageUrl: string } | ErrorResponse = await resp.json(); 275 | 276 | if ('error' in imageObj) { 277 | throw new Error(imageObj.message); 278 | } 279 | 280 | return imageObj; 281 | } catch (e: any) { 282 | throw new Error(`Cannot save item image ${itemUuid}: ${e.message}`); 283 | } 284 | } 285 | 286 | /** 287 | * remove an item from your current shopping list 288 | * 289 | * @param listUuid The listUUID you want to remove a item from 290 | * @param itemName Name of the item you want to delete from you shopping list 291 | * @return should return an empty string and $answerHttpStatus should contain 204. If not -> error 292 | */ 293 | async removeItem(listUuid: string, itemName: string): Promise { 294 | try { 295 | const resp = await fetch(`${this.url}bringlists/${listUuid}`, { 296 | method: 'PUT', 297 | headers: this.putHeaders, 298 | body: `&purchase=&recently=&specification=&remove=${itemName}&sender=null` 299 | }); 300 | return resp.text(); 301 | } catch (e: any) { 302 | throw new Error(`Cannot remove item ${itemName} from ${listUuid}: ${e.message}`); 303 | } 304 | } 305 | 306 | /** 307 | * Remove the image from your item 308 | * 309 | * @param itemUuid The itemUUID you want to remove the image from 310 | * @return returns an empty string and answerHttpStatus should contain 204. If not -> error 311 | */ 312 | async removeItemImage(itemUuid: string): Promise { 313 | try { 314 | const resp = await fetch(`${this.url}bringlistitemdetails/${itemUuid}/image`, { 315 | method: 'DELETE', 316 | headers: this.headers 317 | }); 318 | return resp.text(); 319 | } catch (e: any) { 320 | throw new Error(`Cannot remove item image ${itemUuid}: ${e.message}`); 321 | } 322 | } 323 | 324 | /** 325 | * Move an item to recent items list 326 | * 327 | * @param itemName Name of the item you want to delete from you shopping list 328 | * @param listUuid The lisUUID you want to receive a list of users from. 329 | * @return Should return an empty string and $answerHttpStatus should contain 204. If not -> error 330 | */ 331 | async moveToRecentList(listUuid: string, itemName: string): Promise { 332 | try { 333 | const resp = await fetch(`${this.url}bringlists/${listUuid}`, { 334 | method: 'PUT', 335 | headers: this.putHeaders, 336 | body: `&purchase=&recently=${itemName}&specification=&remove=&&sender=null` 337 | }); 338 | return resp.text(); 339 | } catch (e: any) { 340 | throw new Error(`Cannot remove item ${itemName} from ${listUuid}: ${e.message}`); 341 | } 342 | } 343 | 344 | /** 345 | * Get all users from a shopping list 346 | * 347 | * @param listUuid The listUUID you want to receive a list of users from 348 | */ 349 | async getAllUsersFromList(listUuid: string): Promise { 350 | try { 351 | const resp = await fetch(`${this.url}bringlists/${listUuid}/users`, { headers: this.headers }); 352 | const users: GetAllUsersFromListResponse | ErrorResponse = await resp.json(); 353 | 354 | if ('error' in users) { 355 | throw new Error(users.message); 356 | } 357 | 358 | return users; 359 | } catch (e: any) { 360 | throw new Error(`Cannot get users from list: ${e.message}`); 361 | } 362 | } 363 | 364 | /** 365 | * Get the user settings 366 | */ 367 | async getUserSettings(): Promise { 368 | try { 369 | const resp = await fetch(`${this.url}bringusersettings/${this.uuid}`, { headers: this.headers }); 370 | const settings: GetUserSettingsResponse | ErrorResponse = await resp.json(); 371 | 372 | if ('error' in settings) { 373 | throw new Error(settings.message); 374 | } 375 | 376 | return settings; 377 | } catch (e: any) { 378 | throw new Error(`Cannot get user settings: ${e.message}`); 379 | } 380 | } 381 | 382 | /** 383 | * Load translation file e. g. via 'de-DE' 384 | * @param locale from which country translations will be loaded 385 | */ 386 | async loadTranslations(locale: string): Promise> { 387 | try { 388 | const resp = await fetch(`https://web.getbring.com/locale/articles.${locale}.json`); 389 | const translations: Record | ErrorResponse = await resp.json(); 390 | 391 | if ('error' in translations) { 392 | throw new Error(translations.message); 393 | } 394 | 395 | return translations; 396 | } catch (e: any) { 397 | throw new Error(`Cannot get translations: ${e.message}`); 398 | } 399 | } 400 | 401 | /** 402 | * Load translation file e.g. via 'de-DE' 403 | * @param locale from which country translations will be loaded 404 | */ 405 | async loadCatalog(locale: string): Promise { 406 | try { 407 | const resp = await fetch(`https://web.getbring.com/locale/catalog.${locale}.json`); 408 | const catalog: LoadCatalogResponse | ErrorResponse = await resp.json(); 409 | 410 | if ('error' in catalog) { 411 | throw new Error(catalog.message); 412 | } 413 | 414 | return catalog; 415 | } catch (e: any) { 416 | throw new Error(`Cannot get catalog: ${e.message}`); 417 | } 418 | } 419 | 420 | /** 421 | * Get pending invitations 422 | */ 423 | async getPendingInvitations(): Promise { 424 | try { 425 | const resp = await fetch(`${this.url}bringusers/${this.uuid}/invitations?status=pending`, { 426 | headers: this.headers 427 | }); 428 | const invites: GetPendingInvitationsResponse | ErrorResponse = await resp.json(); 429 | 430 | if ('error' in invites) { 431 | throw new Error(invites.message); 432 | } 433 | 434 | return invites; 435 | } catch (e: any) { 436 | throw new Error(`Cannot get pending invitations: ${e.message}`); 437 | } 438 | } 439 | } 440 | 441 | export = Bring; 442 | --------------------------------------------------------------------------------