├── .babelrc ├── .editorconfig ├── .env-example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── api.spec.js ├── completed.spec.js ├── filter.spec.js ├── helpers.js ├── item.spec.js ├── label.spec.js ├── live_notifications.spec.js ├── locations.spec.js ├── note.spec.js ├── project.spec.js ├── project_note.spec.js ├── reminder.spec.js ├── share.spec.js ├── template.spec.js └── templates │ └── example.csv ├── package.json └── todoist ├── Api.js ├── Session.js ├── index.js ├── managers ├── ActivityManager.js ├── BackupsManager.js ├── BizInvitationsManager.js ├── BusinessUsersManager.js ├── CollaboratorStatesManager.js ├── CollaboratorsManager.js ├── CompletedManager.js ├── FiltersManager.js ├── GenericNotesManager.js ├── InvitationsManager.js ├── ItemsManager.js ├── LabelsManager.js ├── LiveNotificationsManager.js ├── LocationsManager.js ├── Manager.js ├── NotesManager.js ├── ProjectNotesManager.js ├── ProjectsManager.js ├── RemindersManager.js ├── TemplatesManager.js ├── UploadsManager.js └── UserManager.js ├── models ├── Collaborator.js ├── CollaboratorState.js ├── Filter.js ├── GenericNote.js ├── Item.js ├── Label.js ├── LiveNotification.js ├── Model.js ├── Note.js ├── Project.js ├── ProjectNote.js └── Reminder.js └── utils └── uuid.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "babili"], 3 | "plugins": ["transform-async-to-generator", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*.js] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = LF 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*] 14 | charset = utf-8 15 | end_of_line = LF 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | APP_TOKEN= 2 | CLIENT_ID= 3 | CLIENT_SECRET= 4 | ACCESS_TOKEN= 5 | ALTERNATIVE_ACCOUNT_ACCESS_TOKEN= 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | ecmaFeatures: { 6 | "modules": true, 7 | "jsx": true 8 | }, 9 | "globals": { 10 | }, 11 | "plugins": [ 12 | "react" 13 | ], 14 | "extends": "rackt", 15 | "parser": "babel-eslint", 16 | "rules": { 17 | // Possible Errors 18 | "comma-dangle": [2, "never"], 19 | "no-cond-assign": 2, 20 | "no-console": 2, 21 | "no-constant-condition": 2, 22 | "no-control-regex": 2, 23 | "no-debugger": 2, 24 | "no-dupe-keys": 2, 25 | "no-empty": 2, 26 | "no-empty-character-class": 2, 27 | "no-ex-assign": 2, 28 | "no-extra-boolean-cast": 2, 29 | "no-extra-parens": 0, 30 | "no-extra-semi": 2, 31 | "no-func-assign": 2, 32 | "no-inner-declarations": 2, 33 | "no-invalid-regexp": 2, 34 | "no-irregular-whitespace": 2, 35 | "no-negated-in-lhs": 2, 36 | "no-obj-calls": 2, 37 | "no-regex-spaces": 2, 38 | "no-reserved-keys": 0, 39 | "no-sparse-arrays": 2, 40 | "no-unreachable": 2, 41 | "use-isnan": 2, 42 | "valid-jsdoc": 0, 43 | "valid-typeof": 2, 44 | // Best Practices 45 | "block-scoped-var": 2, 46 | "complexity": 0, 47 | "consistent-return": 2, 48 | "curly": 2, 49 | "default-case": 2, 50 | "dot-notation": 2, 51 | "eqeqeq": 2, 52 | "guard-for-in": 2, 53 | "no-alert": 2, 54 | "no-caller": 2, 55 | "no-div-regex": 2, 56 | "no-else-return": 2, 57 | "no-empty-label": 2, 58 | "no-eq-null": 2, 59 | "no-eval": 2, 60 | "no-extend-native": 2, 61 | "no-extra-bind": 2, 62 | "no-fallthrough": 2, 63 | "no-floating-decimal": 2, 64 | "no-implied-eval": 2, 65 | "no-iterator": 2, 66 | "no-labels": 2, 67 | "no-lone-blocks": 2, 68 | "no-loop-func": 2, 69 | "no-multi-spaces": 2, 70 | "no-multi-str": 0, 71 | "no-native-reassign": 2, 72 | "no-new": 2, 73 | "no-new-func": 2, 74 | "no-new-wrappers": 2, 75 | "no-octal": 2, 76 | "no-octal-escape": 2, 77 | "no-process-env": 2, 78 | "no-proto": 2, 79 | "no-redeclare": 2, 80 | "no-return-assign": 2, 81 | "no-script-url": 2, 82 | "no-self-compare": 2, 83 | "no-sequences": 2, 84 | "no-unused-expressions": 2, 85 | "no-void": 0, 86 | "no-warning-comments": 2, 87 | "no-with": 2, 88 | "radix": 2, 89 | "vars-on-top": 0, 90 | "wrap-iife": 2, 91 | "yoda": 2, 92 | // Strict Mode 93 | "strict": [2, "function"], 94 | // Variables 95 | "no-catch-shadow": 2, 96 | "no-delete-var": 2, 97 | "no-label-var": 2, 98 | "no-shadow": 2, 99 | "no-shadow-restricted-names": 2, 100 | "no-undef": 2, 101 | "no-undef-init": 2, 102 | "no-undefined": 2, 103 | "no-unused-vars": 2, 104 | "no-use-before-define": 2, 105 | // Stylistic Issues 106 | "indent": [2, 2, { 107 | "SwitchCase": 1 108 | }], 109 | "brace-style": 2, 110 | "camelcase": 0, 111 | "comma-spacing": 2, 112 | "comma-style": 2, 113 | "consistent-this": 0, 114 | "eol-last": 2, 115 | "func-names": 0, 116 | "func-style": 0, 117 | "key-spacing": [2, { 118 | "beforeColon": false, 119 | "afterColon": true 120 | }], 121 | "max-nested-callbacks": 0, 122 | "new-cap": 2, 123 | "new-parens": 2, 124 | "no-array-constructor": 2, 125 | "no-inline-comments": 0, 126 | "no-lonely-if": 2, 127 | "no-mixed-spaces-and-tabs": 2, 128 | "no-nested-ternary": 2, 129 | "no-new-object": 2, 130 | "semi-spacing": [2, { 131 | "before": false, 132 | "after": true 133 | }], 134 | "no-spaced-func": 2, 135 | "no-ternary": 0, 136 | "no-trailing-spaces": 2, 137 | "no-multiple-empty-lines": 2, 138 | "no-underscore-dangle": 0, 139 | "one-var": 0, 140 | "operator-assignment": [2, "always"], 141 | "padded-blocks": 0, 142 | "quotes": [2, "single"], 143 | "quote-props": [2, "as-needed"], 144 | "semi": [2, "always"], 145 | "sort-vars": [2, {"ignoreCase": true}], 146 | "space-after-keywords": 2, 147 | "space-before-blocks": 2, 148 | "object-curly-spacing": [2, "always", { 149 | "objectsInObjects": false, 150 | "arraysInObjects": false 151 | }], 152 | "array-bracket-spacing": [2, "never"], 153 | "space-in-parens": 2, 154 | "space-infix-ops": 2, 155 | "space-return-throw-case": 2, 156 | "space-unary-ops": 2, 157 | "spaced-comment": 2, 158 | "wrap-regex": 0, 159 | // Legacy 160 | "max-depth": 0, 161 | "max-len": [2, 120], 162 | "max-params": 0, 163 | "max-statements": 0, 164 | "no-plusplus": 0, 165 | } 166 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.sublime-* 4 | .env 5 | .DS_Store 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | todoist/ 2 | __tests__ 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Doist 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 | # OBSOLETE 2 | ## Todoist V7 endpoint is deprecated 3 | 4 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 5 | 6 | 7 | # todoist-js ![NPM version](https://img.shields.io/npm/v/todoist-js.svg) 8 | ## The (un)official Todoist javascript API library 9 | A javascript client for Todoist Sync API with full support of endpoint resources. 10 | This is an adaptation from Todoist official [Python lib](https://github.com/Doist/todoist-python). 11 | 12 | ## How to start 13 | install the package 14 | `npm install todoist-js --save` 15 | into your project 16 | 17 | ## Usage 18 | import the API 19 | ```javascript 20 | import TodoistAPI from 'todoist-js' 21 | ``` 22 | Create an instance providing an access token (how to [get an access token](https://github.com/Cosmitar/todoist-js/wiki/Getting-access-token)?) 23 | ```javascript 24 | const todoist = new TodoistAPI('xxxxxxxxxx'); 25 | ``` 26 | 27 | Get productivity stats 28 | ```javascript 29 | todoist.completed.get_stats().then(stats => { 30 | console.log(stats.karma_trend); 31 | }); 32 | ``` 33 | 🚀 You can see full list of capabilities in action into [tests folder](https://github.com/Cosmitar/todoist-js/tree/master/__tests__) 34 | 35 | Try out the lib by cloning [this Runkit notebook](https://runkit.com/58a79f5f18a61500140b4f19/58af1ca45b8f4a001496241f) 36 | 37 | ## Implementation opportunities 38 | - Web apps or sites integration. 39 | - Web plugins for content managers like Wordpress, Joomla, etc. 40 | - Browsers add-ons. 41 | - Mobile world with hybrid apps frameworks like react-native, Ionic and others. 42 | - Node.js on server side and universal javascript apps. 43 | - Web components for libs like Reactjs, Angular and more. 44 | - Integration with desktop applications, applets, widgets and all those that support javascript. 45 | - _Can you think of any other?_ 46 | 47 | ## Documentation 48 | Official API [Docs](https://developer.todoist.com/?python#update-multiple-ordersindents) for developers 49 | 50 | ## What's next 51 | - [x] ~~Implement a demo app using this library.~~✅ : this is [Asist](https://github.com/fusenlabs/asist), it can autenticate, sync, fetch and complete tasks for Todoist. 52 | - [x] ~~Implement a web oAuth2 process and document it.~~✅ : [OAuth process](https://github.com/Cosmitar/todoist-js/wiki/OAuth2-with-todoist-js) 53 | - [ ] Test browsers compatibility. 54 | - [ ] Test compatibility with Node. 55 | 56 | ## Development / Testing 57 | Clone this repo `git clone git@github.com:Cosmitar/todoist-js.git`. 58 | 59 | This repo includes a Jest suite of tests, used for TDD. 60 | Before start, make sure you create a `.env` file (you can use `.env-example` as template) and complete, as minimum requirement, the variable `ACCESS_TOKEN` with a valid user access token (how to [get an access token](https://github.com/Cosmitar/todoist-js/wiki/Getting-access-token)?). 61 | Then, install all dev dependencies by running `npm install` 62 | 63 | ❌ Do not run all tests together with `npm run test` or you'll get a _max request limit per seconds_ error. 64 | 65 | Run each suite independently like: 66 | 67 | `npm run test -t api.spec.js` 68 | 69 | `npm run test -t completed.spec.js` 70 | 71 | `npm run test -t filter.spec.js` 72 | and so on. 73 | 74 | ❗ Some tests can fail due to restrictions in your account if you're not premium. 75 | 76 | If you want to test `share.spec.js` you need first to include a 2nd access token (from a different user) into `.env` file, using variable `ALTERNATIVE_ACCOUNT_ACCESS_TOKEN` 77 | 78 | ## Contributing 79 | Pull requests and issues are welcome. If you've found a bug, please open an issue. 80 | 81 | ## License 82 | MIT 83 | 84 | -------------------------------------------------------------------------------- /__tests__/api.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | test('Should get api url', () => { 8 | const resource = 'test'; 9 | expect(api.get_api_url(resource)).toBe(`${api.api_endpoint}/API/v7/${resource}`); 10 | }); 11 | 12 | test('Should make a valid request (getting productivity stats)', async () => { 13 | const response = await api.session.request(api.get_api_url('completed/get_stats'), 'POST'); 14 | expect(response.karma_trend).toBeDefined(); 15 | }); 16 | 17 | test('Should sync', async () => { 18 | const response = await api.sync(); 19 | expect(response.sync_token).toBeDefined(); 20 | }); 21 | 22 | test('Should update user profile. (test_user)', async () => { 23 | await api.sync(); 24 | const date_format = api.state.user.date_format; 25 | const date_format_new = 1 - date_format; 26 | api.user.update({ date_format: date_format_new }); 27 | await api.commit(); 28 | expect(date_format_new).toBe(api.state.user.date_format); 29 | api.user.update_goals({ vacation_mode: 1 }); 30 | await api.commit(); 31 | api.user.update_goals({ vacation_mode: 0 }); 32 | await api.commit(); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/completed.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | test('Should get productivity stats)', async () => { 8 | const response = await api.completed.get_stats(); 9 | expect(response.days_items).toBeDefined(); 10 | expect(response.week_items).toBeDefined(); 11 | expect(response.karma_trend).toBeDefined(); 12 | expect(response.karma_last_update).toBeDefined(); 13 | }); 14 | 15 | // 403 Forbidden, seems to be an "only premium" restriction 16 | test('Should get all completed items)', async () => { 17 | const response = await api.completed.get_all(); 18 | expect(response.items).toBeDefined(); 19 | expect(response.projects).toBeDefined(); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/filter.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | let filter1; 8 | let filter2; 9 | 10 | test('Manager should add a filter', async () => { 11 | await api.sync(); 12 | 13 | filter1 = api.filters.add('Filter1', 'no due date'); 14 | const response = await api.commit(); 15 | 16 | expect(response.filters.some(n => n.name === 'Filter1')).toBe(true); 17 | expect(api.state.filters.some(n => n.name === 'Filter1')).toBe(true); 18 | expect(await api.filters.get_by_id(filter1.id)).toEqual(filter1); 19 | }); 20 | 21 | test('Filter should update itself', async () => { 22 | filter1.update({ name: 'UpdatedFilter1' }); 23 | const response = await api.commit(); 24 | 25 | expect(response.filters.some(n => n.name === 'UpdatedFilter1')).toBe(true); 26 | expect(api.state.filters.some(n => n.name === 'UpdatedFilter1')).toBe(true); 27 | expect(await api.filters.get_by_id(filter1.id)).toEqual(filter1); 28 | }); 29 | 30 | test('Manager should update filter order', async () => { 31 | filter2 = api.filters.add('Filter2', 'today'); 32 | await api.commit(); 33 | 34 | api.filters.update_orders({ [filter1.id]: 2, [filter2.id]: 1 }); 35 | const response = await api.commit(); 36 | 37 | response.filters.forEach((filter) => { 38 | if (filter.id === filter1.id) { 39 | expect(filter1.item_order).toBe(2); 40 | } 41 | if (filter.id === filter2.id) { 42 | expect(filter2.item_order).toBe(1); 43 | } 44 | }); 45 | 46 | expect(api.state.filters.find(f => f.id === filter1.id).item_order).toBe(2); 47 | expect(api.state.filters.find(f => f.id === filter2.id).item_order).toBe(1); 48 | }); 49 | 50 | test('Filter should delete itself', async () => { 51 | filter1.delete(); 52 | const name = filter1.name; 53 | const response = await api.commit(); 54 | 55 | expect(response.filters.some(f => f.id === filter1.id)).toBe(false); 56 | expect(filter1.is_deleted).toBe(1); 57 | expect(api.state.filters.some(f => f.name === name)).toBe(false); 58 | }); 59 | 60 | test('Manager should update a filter', async () => { 61 | api.filters.update(filter2.id, { name: 'UpdatedFilter2' }); 62 | const response = await api.commit(); 63 | 64 | expect(response.filters.some(n => n.name === 'UpdatedFilter2')).toBe(true); 65 | expect(api.state.filters.some(n => n.name === 'UpdatedFilter2')).toBe(true); 66 | }); 67 | 68 | test('Manager should delete a filter', async () => { 69 | api.filters.delete(filter2.id); 70 | const name = filter2.name; 71 | const response = await api.commit(); 72 | 73 | expect(response.filters.some(f => f.id === filter2.id)).toBe(false); 74 | expect(filter2.is_deleted).toBe(1); 75 | expect(api.state.filters.some(f => f.name === name)).toBe(false); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/helpers.js: -------------------------------------------------------------------------------- 1 | export const getDateString = (d) => { 2 | const two = v => v <= 9 ? `0${v}` : v; 3 | return `${d.getFullYear()}-${two(d.getMonth()+1)}-${two(d.getDate())}T${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`; 4 | }; 5 | 6 | export const getLongDateString = (dt) => { 7 | const [a, b, d, Y, HMS, ...args] = dt.toString().split(' '); 8 | return `${a} ${d} ${b} ${Y} ${HMS} +0000`; 9 | }; 10 | -------------------------------------------------------------------------------- /__tests__/item.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | import { 4 | getDateString, 5 | } from './helpers'; 6 | 7 | import API from './../todoist/Api'; 8 | const api = new API(process.env.ACCESS_TOKEN); 9 | 10 | 11 | afterAll(async () => { 12 | // Cleaning the room 13 | project1.delete(); 14 | await api.commit(); 15 | }); 16 | 17 | let item1; 18 | let item2; 19 | let inbox; 20 | let project1; 21 | test('API should add an item', async () => { 22 | await api.sync(); 23 | const response = await api.add_item('Item1'); 24 | expect(response.content).toBe('Item1'); 25 | 26 | await api.sync(); 27 | expect(api.state.items.some(i => i.content === 'Item1')).toBe(true); 28 | item1 = api.state.items.find(i => i.content === 'Item1'); 29 | expect(await api.items.get_by_id(item1.id)).toEqual(item1); 30 | item1.delete(); 31 | await api.commit(); 32 | }); 33 | 34 | test('Manager should add an item', async () => { 35 | await api.sync(); 36 | inbox = api.state.projects.find(project => project.name === 'Inbox'); 37 | item1 = api.items.add('Item1', inbox.id); 38 | const response = await api.commit(); 39 | expect(response.items.some(i => i.content === 'Item1')).toBe(true); 40 | expect(api.state.items.some(i => i.content === 'Item1')).toBe(true); 41 | expect(await api.items.get_by_id(item1.id)).toEqual(item1); 42 | }); 43 | 44 | test('Item should complete itself', async () => { 45 | item1.complete(); 46 | const response = await api.commit(); 47 | expect(api.state.items.find(i => i.id === item1.id).checked).toBeTruthy(); 48 | expect(response.items.some(i => i.id === item1.id)).toBe(false); 49 | }); 50 | 51 | test('Item should uncomplete itself', async () => { 52 | item1.uncomplete(); 53 | const response = await api.commit(); 54 | expect(response.items.some(i => i.content === 'Item1')).toBe(true); 55 | expect(response.items.find(i => i.content === 'Item1').checked).toBeFalsy(); 56 | expect(api.state.items.find(i => i.id === item1.id).checked).toBeFalsy(); 57 | }); 58 | 59 | test('Item should move itself into a project', async () => { 60 | project1 = api.projects.add('Project1_items'); 61 | await api.commit(); 62 | 63 | item1.move(project1.id); 64 | const response = await api.commit(); 65 | expect(response.items.some(i => i.content === 'Item1')).toBe(true); 66 | expect(response.items.find(i => i.content === 'Item1').project_id).toBe(project1.id); 67 | expect(api.state.items.find(i => i.id === item1.id).project_id).toBe(project1.id); 68 | }); 69 | 70 | test('Item should update its content', async () => { 71 | item1.update({ content: 'UpdatedItem1' }); 72 | const response = await api.commit(); 73 | expect(api.state.items.some(i => i.content === 'UpdatedItem1')).toBe(true); 74 | expect(await api.items.get_by_id(item1.id)).toEqual(item1); 75 | }); 76 | 77 | test('Item should update its date info', async () => { 78 | await api.sync(); 79 | inbox = api.state.projects.find(project => project.name === 'Inbox'); 80 | const date = new Date(2038, 1, 19, 3, 14, 7); 81 | const date_string = getDateString(date); 82 | 83 | item2 = api.items.add('Item2', inbox.id, { date_string }); 84 | let response = await api.commit(); 85 | 86 | const tomorrow = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); 87 | const new_date_utc = getDateString(tomorrow); 88 | api.items.update_date_complete(item1.id, new_date_utc, 'every day', 0); 89 | response = await api.commit(); 90 | // Note: in Python's test date_string is expected to be 'every day' but we're recieving the date in string format YYYY-MM-DDTHH:MM:SS 91 | expect(response.items.find(i => i.id === item2.id).date_string).toBe(date_string); 92 | expect(api.state.items.find(i => i.id === item2.id).date_string).toBe(date_string); 93 | }); 94 | 95 | test('Manager should update items order and indent', async () => { 96 | api.items.update_orders_indents({ 97 | [item1.id]: [2, 2], 98 | [item2.id]: [1, 3], 99 | }); 100 | 101 | let response = await api.commit(); 102 | await api.sync(); 103 | 104 | response.items.forEach((item) => { 105 | if (item.id === item1.id) { 106 | expect(item.item_order).toBe(2); 107 | expect(item.indent).toBe(2); 108 | } 109 | 110 | if (item.id === item2.id) { 111 | expect(item.item_order).toBe(1); 112 | expect(item.indent).toBe(3); 113 | } 114 | }); 115 | 116 | expect(api.state.items.find(i => i.id === item1.id).item_order).toBe(2); 117 | expect(api.state.items.find(i => i.id === item1.id).indent).toBe(2); 118 | expect(api.state.items.find(i => i.id === item2.id).item_order).toBe(1); 119 | expect(api.state.items.find(i => i.id === item2.id).indent).toBe(3); 120 | }); 121 | 122 | test('Manager should update items day orders', async () => { 123 | api.items.update_day_orders({ [item1.id]: 1, [item2.id]: 2 }); 124 | const response = await api.commit(); 125 | response.items.forEach((item) => { 126 | if (item.id === item1.id) { 127 | expect(item1.day_order).toBe(1); 128 | } 129 | if (item.id === item2.id) { 130 | expect(item2.day_order).toBe(2); 131 | } 132 | }); 133 | 134 | expect(api.state.day_orders[item1.id]).toBe(1); 135 | expect(api.state.day_orders[item2.id]).toBe(2); 136 | }); 137 | 138 | test('Item should delete itself', async () => { 139 | const content = item1.content; 140 | item1.delete(); 141 | const response = await api.commit(); 142 | expect(response.items.some(i => i.id === item1.id)).toBe(false); 143 | expect(item1.is_deleted).toBe(1); 144 | expect(api.state.items.some(i => i.content === content)).toBe(false); 145 | }); 146 | 147 | test('Manager should complete an item', async () => { 148 | api.items.complete([item2.id]); 149 | const response = await api.commit(); 150 | expect(response.items.some(i => i.content === 'Item2')).toBe(true); 151 | expect(response.items.find(i => i.content === 'Item2').checked).toBeTruthy(); 152 | expect(api.state.items.find(i => i.content === 'Item2').checked).toBeTruthy(); 153 | }); 154 | 155 | test('Manager should uncomplete an item', async () => { 156 | api.items.uncomplete([item2.id]); 157 | const response = await api.commit(); 158 | expect(response.items.some(i => i.content === 'Item2')).toBe(true); 159 | expect(response.items.find(i => i.content === 'Item2').checked).toBeFalsy(); 160 | expect(api.state.items.find(i => i.content === 'Item2').checked).toBeFalsy(); 161 | }); 162 | 163 | test('Manager should move an item into a project', async () => { 164 | api.items.move({ [item2.project_id]: [item2.id] }, project1.id); 165 | const response = await api.commit(); 166 | expect(response.items.some(i => i.content === 'Item2')).toBe(true); 167 | expect(response.items.find(i => i.content === 'Item2').project_id).toBe(project1.id); 168 | expect(api.state.items.find(i => i.id === item2.id).project_id).toBe(project1.id); 169 | }); 170 | 171 | test('Manager should update an item', async () => { 172 | api.items.update(item2.id, { content: 'UpdatedItem2' }); 173 | const response = await api.commit(); 174 | expect(response.items.some(i => i.content === 'UpdatedItem2')).toBe(true); 175 | expect(api.state.items.some(i => i.content === 'UpdatedItem2')).toBe(true); 176 | }); 177 | 178 | test('Manager should delete an item', async () => { 179 | const content = item2.content; 180 | api.items.delete([item2.id]); 181 | const response = await api.commit(); 182 | 183 | expect(response.items.some(i => i.id === item2.id)).toBe(false); 184 | expect(item2.is_deleted).toBe(1); 185 | expect(api.state.items.some(i => i.content === content)).toBe(false); 186 | }); 187 | -------------------------------------------------------------------------------- /__tests__/label.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | let label1; 8 | let label2; 9 | 10 | test('Manager should add a label', async () => { 11 | await api.sync(); 12 | label1 = api.labels.add('Label1'); 13 | const response = await api.commit(); 14 | 15 | expect(response.labels.some(l => l.name === 'Label1')).toBe(true); 16 | expect(api.state.labels.some(l => l.name === 'Label1')).toBe(true); 17 | expect(await api.labels.get_by_id(label1.id)).toEqual(label1); 18 | }); 19 | 20 | test('Label should update itself', async () => { 21 | label1.update({ name: 'UpdatedLabel1' }); 22 | const response = await api.commit(); 23 | 24 | expect(response.labels.some(l => l.name === 'UpdatedLabel1')).toBe(true); 25 | expect(api.state.labels.some(l => l.name === 'UpdatedLabel1')).toBe(true); 26 | expect(await api.labels.get_by_id(label1.id)).toEqual(label1); 27 | }); 28 | 29 | test('Manager should update label order', async () => { 30 | label2 = api.labels.add('Label2'); 31 | await api.commit(); 32 | 33 | api.labels.update_orders({ [label1.id]: 1, [label2.id]: 2 }); 34 | const response = await api.commit(); 35 | 36 | response.labels.forEach((label) => { 37 | if (label.id === label1.id) { 38 | expect(label.item_order).toBe(1); 39 | } 40 | 41 | if (label.id === label2.id) { 42 | expect(label.item_order).toBe(2); 43 | } 44 | }); 45 | 46 | expect(api.state.labels.find(l => l.id === label1.id).item_order).toBe(1); 47 | expect(api.state.labels.find(l => l.id === label2.id).item_order).toBe(2); 48 | }); 49 | 50 | test('Label should delete itself', async () => { 51 | const name = label1.name; 52 | label1.delete(); 53 | const response = await api.commit(); 54 | 55 | expect(response.labels.some(l => l.id === label1.id)).toBe(false); 56 | expect(label1.is_deleted).toBe(1); 57 | expect(api.state.labels.some(l => l.name === name)).toBe(false); 58 | }); 59 | 60 | test('Manager should update a label', async () => { 61 | api.labels.update(label2.id, { name: 'UpdatedLabel2' }); 62 | const response = await api.commit(); 63 | 64 | expect(response.labels.some(l => l.name === 'UpdatedLabel2')).toBe(true); 65 | expect(api.state.labels.some(l => l.name === 'UpdatedLabel2')).toBe(true); 66 | }); 67 | 68 | test('Manager should delete a label', async () => { 69 | const name = label2.name; 70 | api.labels.delete(label2.id); 71 | const response = await api.commit(); 72 | 73 | expect(response.labels.some(l => l.id === label2.id)).toBe(false); 74 | expect(label2.is_deleted).toBe(1); 75 | expect(api.state.labels.some(l => l.name === name)).toBe(false); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/live_notifications.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | test('Manager should set last notification', async () => { 8 | await api.sync(); 9 | api.live_notifications.set_last_read(api.state.live_notifications_last_read_id); 10 | const response = await api.commit(); 11 | 12 | expect(response.live_notifications_last_read_id).toBe(api.state.live_notifications_last_read_id); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/locations.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | test('Manager should clear locations', async () => { 8 | await api.sync(); 9 | api.locations.clear(); 10 | await api.commit(); 11 | 12 | expect(api.state.locations).toEqual([]); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/note.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | afterAll(async () => { 8 | item1.delete(); 9 | await api.commit(); 10 | }); 11 | 12 | let note1; 13 | let item1; 14 | 15 | // only premium 16 | test('Manager should add a note', async () => { 17 | await api.sync(); 18 | const inbox = api.state.projects.find(p => p.name === 'Inbox'); 19 | item1 = api.items.add('Item1_notes', inbox.id); 20 | await api.commit(); 21 | 22 | note1 = api.notes.add(item1.id, 'Note1'); 23 | const response = await api.commit(); 24 | expect(response.notes.some(n => n.content === 'Note1')).toBe(true); 25 | expect(api.state.notes.some(n => n.content === 'Note1')).toBe(true); 26 | expect(await api.notes.get_by_id(note1.id)).toEqual(note1); 27 | }); 28 | 29 | test('Note should update itself', async () => { 30 | note1.update({ content: 'UpdatedNote1' }); 31 | const response = await api.commit(); 32 | 33 | expect(response.notes.some(n => n.content === 'UpdatedNote1')).toBe(true); 34 | expect(api.state.notes.some(n => n.content === 'UpdatedNote1')).toBe(true); 35 | expect(await api.notes.get_by_id(note1.id)).toEqual(note1); 36 | }); 37 | 38 | test('Note should delete itself', async () => { 39 | const content = note1.content; 40 | note1.delete(); 41 | const response = await api.commit(); 42 | 43 | expect(response.notes.some(n => n.id === note1.id)).toBe(false); 44 | expect(note1.is_deleted).toBe(1); 45 | expect(api.state.notes.some(n => n.content === content)).toBe(false); 46 | }); 47 | 48 | test('Manager should delete a note', async () => { 49 | // but first we need to create it 50 | const note2 = api.notes.add(item1.id, 'Note2'); 51 | let response = await api.commit(); 52 | 53 | expect(response.notes.some(n => n.content === 'Note2')).toBe(true); 54 | expect(api.state.notes.some(n => n.content === 'Note2')).toBe(true); 55 | 56 | // now lets delete it 57 | const content = note2.content; 58 | api.notes.delete(note2.id); 59 | response = await api.commit(); 60 | 61 | expect(response.notes.some(n => n.id === note2.id)).toBe(false); 62 | expect(note2.is_deleted).toBe(1); 63 | expect(api.state.notes.some(n => n.content === content)).toBe(false); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/project.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | let project1 = null; 8 | let project2 = null; 9 | test('Manager should create a project', async () => { 10 | await api.sync(); 11 | project1 = api.projects.add('Project1'); 12 | const response = await api.commit(); 13 | expect(response.projects.some(p => p.name === 'Project1')).toBe(true); 14 | expect(api.state.projects.some(p => p.name === 'Project1')).toBe(true); 15 | expect(await api.projects.get_by_id(project1.id)).toEqual(project1); 16 | }); 17 | 18 | test('Project should archive itself', async () => { 19 | project1.archive(); 20 | const response = await api.commit(); 21 | expect(api.state.projects.find(p => p.name === 'Project1').is_archived).toBeTruthy(); 22 | expect(response.projects.some(p => p.name === 'Project1')).toBe(false); 23 | }); 24 | 25 | test('Project should unarchive itself', async () => { 26 | project1.unarchive(); 27 | const response = await api.commit(); 28 | expect(response.projects.some(p => p.name === 'Project1')).toBe(true); 29 | expect(api.state.projects.find(p => p.name === 'Project1').is_archived).toBeFalsy(); 30 | }); 31 | 32 | test('Project should update itself', async () => { 33 | project1.update({ name: 'UpdatedProject1' }); 34 | const response = await api.commit(); 35 | expect(response.projects.some(p => p.name === 'UpdatedProject1')).toBe(true); 36 | expect(api.state.projects.some(p => p.name === 'UpdatedProject1')).toBe(true); 37 | expect(await api.projects.get_by_id(project1.id)).toEqual(project1); 38 | }); 39 | 40 | test('Manager should update projects order and indent', async () => { 41 | project2 = api.projects.add('Project2'); 42 | let response = await api.commit(); 43 | expect(response.projects.some(p => p.name === 'Project2')).toBe(true); 44 | api.projects.update_orders_indents({ 45 | [project1.id]: [1, 2], 46 | [project2.id]: [2, 3] 47 | }); 48 | response = await api.commit(); 49 | 50 | response.projects.forEach((project) => { 51 | if (project.id === project1.id) { 52 | expect(project.item_order).toBe(1); 53 | expect(project.indent).toBe(2); 54 | } 55 | 56 | if (project.id === project2.id) { 57 | expect(project.item_order).toBe(2); 58 | expect(project.indent).toBe(3); 59 | } 60 | }); 61 | 62 | expect(api.state.projects.find(p => p.id === project1.id).item_order).toBe(1); 63 | expect(api.state.projects.find(p => p.id === project1.id).indent).toBe(2); 64 | expect(api.state.projects.find(p => p.id === project2.id).item_order).toBe(2); 65 | expect(api.state.projects.find(p => p.id === project2.id).indent).toBe(3); 66 | }); 67 | 68 | test('Project should delete itself', async () => { 69 | project1.delete(); 70 | const response = await api.commit(); 71 | 72 | expect(response.projects.some(p => p.id === project1.id)).toBe(false); 73 | expect(project1.is_deleted).toBeTruthy(); 74 | expect(api.state.projects.some(p => p.name === 'Project1')).toBe(false); 75 | }); 76 | 77 | test('Manager should archive a project', async () => { 78 | api.projects.archive(project2.id); 79 | const response = await api.commit(); 80 | expect(api.state.projects.find(p => p.id === project2.id).is_archived).toBeTruthy(); 81 | expect(response.projects.some(p => p.id === project2.id)).toBe(false); 82 | }); 83 | 84 | test('Manager should unarchive a project', async () => { 85 | api.projects.unarchive(project2.id); 86 | const response = await api.commit(); 87 | expect(api.state.projects.find(p => p.name === 'Project2').is_archived).toBeFalsy(); 88 | expect(response.projects.some(p => p.name === 'Project2')).toBe(true); 89 | }); 90 | 91 | test('Manager should update a project', async () => { 92 | api.projects.update(project2.id, { name: 'UpdatedProject2' }); 93 | const response = await api.commit(); 94 | expect(response.projects.some(p => p.name === 'UpdatedProject2')).toBe(true); 95 | expect(api.state.projects.some(p => p.name === 'UpdatedProject2')).toBe(true); 96 | expect(await api.projects.get_by_id(project2.id)).toEqual(project2); 97 | }); 98 | 99 | test('Manager should delete a project', async () => { 100 | api.projects.delete([project2.id]); 101 | const response = await api.commit(); 102 | 103 | expect(response.projects.some(p => p.id === project2.id)).toBe(false); 104 | expect(project2.is_deleted).toBeTruthy(); 105 | expect(api.state.projects.some(p => p.id === project2.id)).toBe(false); 106 | }); 107 | -------------------------------------------------------------------------------- /__tests__/project_note.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | 7 | afterAll(async () => { 8 | project.delete(); 9 | await api.commit(); 10 | }); 11 | 12 | let note1; 13 | let project; 14 | 15 | test('Manager should add a project note', async () => { 16 | await api.sync(); 17 | 18 | project = api.projects.add('Project1_notes'); 19 | await api.commit(); 20 | 21 | note1 = api.project_notes.add(project.id, 'ProjectNote1'); 22 | const response = await api.commit(); 23 | 24 | expect(response.project_notes.some(n => n.content === 'ProjectNote1')).toBe(true); 25 | expect(api.state.project_notes.some(n => n.content === 'ProjectNote1')).toBe(true); 26 | expect(await api.project_notes.get_by_id(note1.id)).toEqual(note1); 27 | }); 28 | 29 | test('Project note should update itself', async () => { 30 | note1.update({ content: 'UpdatedProjectNote1' }); 31 | const response = await api.commit(); 32 | 33 | expect(response.project_notes.some(n => n.content === 'UpdatedProjectNote1')).toBe(true); 34 | expect(api.state.project_notes.some(n => n.content === 'UpdatedProjectNote1')).toBe(true); 35 | expect(await api.project_notes.get_by_id(note1.id)).toEqual(note1); 36 | }); 37 | 38 | test('Project note should delete itself', async () => { 39 | note1.delete(); 40 | const content = note1.content; 41 | const response = await api.commit(); 42 | 43 | expect(response.project_notes.some(n => n.id === note1.id)).toBe(false); 44 | expect(note1.is_deleted).toBe(1); 45 | expect(api.state.project_notes.some(n => n.content === content)).toBe(false); 46 | }); 47 | 48 | test('Manager should delete a project note', async () => { 49 | // but first we need to create it 50 | const note2 = api.project_notes.add(project.id, 'ProjectNote2'); 51 | let response = await api.commit(); 52 | 53 | expect(response.project_notes.some(n => n.content === 'ProjectNote2')).toBe(true); 54 | expect(api.state.project_notes.some(n => n.content === 'ProjectNote2')).toBe(true); 55 | 56 | // now lets delete it 57 | const content = note2.content; 58 | api.project_notes.delete(note2.id); 59 | response = await api.commit(); 60 | 61 | expect(response.project_notes.some(n => n.id === note2.id)).toBe(false); 62 | expect(note2.is_deleted).toBe(1); 63 | expect(api.state.project_notes.some(n => n.content === content)).toBe(false); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/reminder.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | import { 4 | getDateString, 5 | getLongDateString 6 | } from './helpers'; 7 | 8 | import API from './../todoist/Api'; 9 | const api = new API(process.env.ACCESS_TOKEN); 10 | 11 | 12 | 13 | afterAll(async () => { 14 | item1.delete(); 15 | await api.commit(); 16 | }); 17 | 18 | let reminder1; 19 | let reminder2; 20 | let item1; 21 | let inbox; 22 | 23 | test('Manager should add a reminder (relative)', async () => { 24 | await api.sync(); 25 | const inbox = api.state.projects.find(p => p.name === 'Inbox'); 26 | item1 = api.items.add('Item1_reminder', inbox.id, { date_string: 'tomorrow' }); 27 | await api.commit(); 28 | 29 | // relative 30 | reminder1 = api.reminders.add(item1.id, { minute_offset: 30 }); 31 | const response = await api.commit(); 32 | 33 | expect(response.reminders.find(r => r.id === reminder1.id).minute_offset).toBe(30); 34 | expect(api.state.reminders.some(r => r.id === reminder1.id)).toBe(true); 35 | expect(await api.reminders.get_by_id(reminder1.id)).toEqual(reminder1); 36 | }); 37 | 38 | test('Reminder should update itself', async () => { 39 | reminder1.update({ minute_offset: 15 }); 40 | const response = await api.commit(); 41 | 42 | expect(response.reminders.find(r => r.id === reminder1.id).minute_offset).toBe(15); 43 | expect(api.state.reminders.find(r => r.id === reminder1.id).minute_offset).toBe(15); 44 | }); 45 | 46 | test('Reminder should delete itself', async () => { 47 | reminder1.delete(); 48 | const response = await api.commit(); 49 | 50 | expect(response.reminders.some(r => r.id === reminder1.id)).toBe(false); 51 | expect(reminder1.is_deleted).toBe(1); 52 | expect(api.state.reminders.some(r => r.id === reminder1.id)).toBe(false); 53 | }); 54 | 55 | test('Manager should add a reminder (absolute)', async () => { 56 | // absolute 57 | let tomorrow = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); 58 | const due_date_utc = getDateString(tomorrow); 59 | const due_date_utc_long = getLongDateString(tomorrow); 60 | reminder2 = api.reminders.add(item1.id, { due_date_utc }); 61 | const response = await api.commit(); 62 | 63 | expect(response.reminders.find(r => r.id === reminder2.id).due_date_utc).toBe(due_date_utc_long); 64 | expect(api.state.reminders.find(r => r.id === reminder2.id).due_date_utc).toBe(due_date_utc_long); 65 | }); 66 | 67 | test('Manager should remove a reminder', async () => { 68 | api.reminders.delete(reminder2.id); 69 | const response = await api.commit(); 70 | 71 | expect(response.reminders.some(r => r.id === reminder2.id)).toBe(false); 72 | expect(reminder2.is_deleted).toBe(1); 73 | expect(api.state.reminders.some(r => r.id === reminder2.id)).toBe(false); 74 | }); 75 | -------------------------------------------------------------------------------- /__tests__/share.spec.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require("babel-polyfill"); 3 | 4 | import API from './../todoist/Api'; 5 | const api = new API(process.env.ACCESS_TOKEN); 6 | const api2 = new API(process.env.ALTERNATIVE_ACCOUNT_ACCESS_TOKEN); 7 | 8 | afterAll(async () => { 9 | // from api 10 | project1.delete(); 11 | project2.delete(); 12 | await api.commit(); 13 | // from api2 14 | api2.projects.delete([api2_project1_id, api2_project2_id]); 15 | await api2.commit(); 16 | }); 17 | 18 | let project1; 19 | let project2; 20 | let response; 21 | let response2; 22 | let invitation1; 23 | let invitation2; 24 | let invitation1resp; 25 | let api2_project1_id; 26 | let api2_project2_id; 27 | test('Manager should share a project', async () => { 28 | await api.sync(); 29 | await api2.sync(); 30 | 31 | // accept 32 | project1 = api.projects.add('Project1_share'); 33 | await api.commit(); 34 | 35 | api.projects.share(project1.id, api2.state.user.email); 36 | response = await api.commit(); 37 | 38 | expect(response.projects.find(p => p.id === project1.id).name).toBe(project1.name); 39 | expect(response.projects.find(p => p.id === project1.id).shared).toBeTruthy(); 40 | 41 | response2 = await api2.sync(); 42 | // can't compare response2.live_notification[i].project_id against project1.id 43 | // because 2 different projects are created on each users and they have individual ids 44 | invitation1 = response2.live_notifications.find( 45 | ln => ln.project_name === project1.name && ln.notification_type === 'share_invitation_sent' 46 | ); 47 | api2_project1_id = invitation1.project_id; 48 | 49 | expect(invitation1.project_name).toBe(project1.name); 50 | expect(invitation1.from_user.email).toBe(api.state.user.email); 51 | }); 52 | 53 | test('Manager should accept an invitation', async () => { 54 | // auto accepted? 55 | if (invitation1.state !== 'accepted') { 56 | // this wasn't tested due to API auto accept my invitations 57 | api2.invitations.accept(invitation1.id, invitation1.invitation_secret); 58 | response2 = await api2.commit(); 59 | 60 | invitation1resp = response2.live_notifications.find(ln => ln.id === invitation1.id); 61 | expect(invitation1resp.id).toBe(invitation1.id); 62 | expect(invitation1resp.state).toBe('accepted'); 63 | expect(response2.projects[0].shared).toBeTruthy(); 64 | expect(response2.collaborator_states.some(c => c.user_id === api.state.user.id)).toBe(true); 65 | expect(response2.collaborator_states.some(c => c.user_id === api2.state.user.id)).toBe(true); 66 | 67 | response = await api.sync(); 68 | invitation1resp = response2.live_notifications.find(ln => ln.id === invitation1.id); 69 | expect(invitation1resp.id).toBe(invitation1.id); 70 | expect(invitation1resp.notification_type).toBe('share_invitation_accepted'); 71 | expect(response.projects.find(p => p.id === project1.id).shared).toBeTruthy(); 72 | } 73 | }); 74 | 75 | test('Manager should reject an invitation', async () => { 76 | // reject 77 | project2 = api.projects.add('Project2_share'); 78 | await api.commit(); 79 | 80 | api.projects.share(project2.id, api2.state.user.email); 81 | response = await api.commit(); 82 | 83 | expect(response.projects.find(p => p.id === project2.id).name).toBe(project2.name); 84 | expect(response.projects.find(p => p.id === project2.id).shared).toBeTruthy(); 85 | 86 | response2 = await api2.sync(); 87 | 88 | invitation2 = response2.live_notifications.find( 89 | ln => ln.project_name === project2.name && ln.notification_type === 'share_invitation_sent' 90 | ); 91 | api2_project2_id = invitation2.project_id; 92 | expect(invitation2.project_name).toBe(project2.name); 93 | expect(invitation2.from_user.email).toBe(api.state.user.email); 94 | 95 | // auto accepted? 96 | if (invitation1.state !== 'accepted') { 97 | // this wasn't tested due to API auto accept my invitations 98 | api2.invitations.reject(invitation2.id, invitation2.invitation_secret); 99 | response2 = await api2.commit(); 100 | 101 | expect(response2.projects.length).toBe(0); 102 | expect(response2.collaborator_states.length).toBe(0); 103 | } 104 | }); 105 | 106 | test('Manager should delete an invitation', async () => { 107 | // delete 108 | api.invitations.delete(invitation1.id); 109 | await api.commit(); 110 | }); 111 | -------------------------------------------------------------------------------- /__tests__/template.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | require('dotenv').config(); 3 | require('babel-polyfill'); 4 | // increase timeout for remote response delay 5 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 6 | 7 | import API from './../todoist/Api'; 8 | const api = new API(process.env.ACCESS_TOKEN); 9 | 10 | let item1; 11 | let project1; 12 | 13 | afterAll(async () => { 14 | item1.delete(); 15 | project1.delete(); 16 | await api.commit(); 17 | }); 18 | 19 | test('Manager should export project as file', async () => { 20 | await api.sync(); 21 | project1 = api.projects.add('Project1_template'); 22 | await api.commit(); 23 | 24 | item1 = api.items.add('Item1_template', project1.id); 25 | await api.commit(); 26 | 27 | const template_file = await api.templates.export_as_file(project1.id); 28 | const fileContent = await template_file.text(); 29 | 30 | expect(fileContent).toEqual(expect.stringMatching(/task,Item1_template,4,1,/)); 31 | }); 32 | 33 | test('Manager should export project as url', async () => { 34 | // requires project1 and item1 35 | 36 | const template_url = await api.templates.export_as_url(project1.id); 37 | // validates returned object structure and data 38 | expect(template_url).toHaveProperty('file_name', expect.stringMatching(/_Project1_template\.csv$/)); 39 | expect(template_url).toHaveProperty('file_url', expect.stringMatching(/(http(s?):)|([\/|.|\w|\s])*_Project1_template\.(?:csv)/)); 40 | 41 | // tests service by requesting file and checking its content. 42 | const getFile = () => { 43 | return new Promise((resolve, reject) => { 44 | request.get(template_url.file_url, (error, response, body) => { 45 | if (!error && response.statusCode === 200) { 46 | resolve(body); 47 | } else { 48 | reject(error); 49 | } 50 | }); 51 | }); 52 | }; 53 | const fileResponse = await getFile(); 54 | 55 | expect(fileResponse).toEqual(expect.stringMatching(/task,Item1_template,4,1,/)); 56 | }); 57 | -------------------------------------------------------------------------------- /__tests__/templates/example.csv: -------------------------------------------------------------------------------- 1 | TYPE,CONTENT,PRIORITY,INDENT,AUTHOR,RESPONSIBLE,DATE,DATE_LANG,TIMEZONE 2 | task,Task1,4,1,Rick (9999999),,today,en,America/Argentina/Cordoba 3 | ,,,,,,,, 4 | task,Task2,4,1,Rick (9999999),,tomorrow,en,America/Argentina/Cordoba 5 | ,,,,,,,, 6 | task,Task3,4,1,Rick (9999999),,today,en,America/Argentina/Cordoba 7 | ,,,,,,,, 8 | task,Task4,4,1,Rick (9999999),,today,en,America/Argentina/Cordoba 9 | ,,,,,,,, 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todoist-js", 3 | "version": "0.3.2", 4 | "description": "The (un)official Todoist javascript API library", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "eslint": "eslint todoist", 9 | "build": "node ./node_modules/babel-cli/bin/babel.js -d dist/ todoist/ --source-maps", 10 | "prepublish": "npm run build" 11 | }, 12 | "jest": { 13 | "transform": { 14 | ".*": "/node_modules/babel-jest" 15 | } 16 | }, 17 | "keywords": [ 18 | "todoist", 19 | "todoist-api", 20 | "javascript" 21 | ], 22 | "author": "cosmitar", 23 | "license": "ISC", 24 | "babel": { 25 | "presets": [ 26 | "env", 27 | "babili" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "babel": "^6.20.0", 32 | "babel-cli": "^6.23.0", 33 | "babel-eslint": "^7.1.1", 34 | "babel-jest": "^18.0.0", 35 | "babel-plugin-transform-async-to-generator": "^6.22.0", 36 | "babel-plugin-transform-object-rest-spread": "^6.22.0", 37 | "babel-preset-babili": "0.0.11", 38 | "babel-preset-env": "^1.1.8", 39 | "dotenv": "^4.0.0", 40 | "eslint": "^3.14.1", 41 | "eslint-config-rackt": "^1.1.1", 42 | "jest": "^18.1.0" 43 | }, 44 | "dependencies": { 45 | "babel-polyfill": "^6.22.0", 46 | "fetch-everywhere": "^1.0.5" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/Cosmitar/todoist-js.git" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /todoist/Api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Implements the API that makes it possible to interact with a Todoist user 3 | * account and its data. 4 | * @author Cosmitar (JS Version) 5 | */ 6 | import Session from './Session'; 7 | // managers 8 | import ActivityManager from './managers/ActivityManager'; 9 | import BackupsManager from './managers/BackupsManager'; 10 | import BizInvitationsManager from './managers/BizInvitationsManager'; 11 | import BusinessUsersManager from './managers/BusinessUsersManager'; 12 | import CollaboratorsManager from './managers/CollaboratorsManager'; 13 | import CollaboratorStatesManager from './managers/CollaboratorStatesManager'; 14 | import CompletedManager from './managers/CompletedManager'; 15 | import FiltersManager from './managers/FiltersManager'; 16 | import InvitationsManager from './managers/InvitationsManager'; 17 | import ItemsManager from './managers/ItemsManager'; 18 | import LabelsManager from './managers/LabelsManager'; 19 | import LiveNotificationsManager from './managers/LiveNotificationsManager'; 20 | import LocationsManager from './managers/LocationsManager'; 21 | import NotesManager from './managers/NotesManager'; 22 | import ProjectNotesManager from './managers/ProjectNotesManager'; 23 | import ProjectsManager from './managers/ProjectsManager'; 24 | import RemindersManager from './managers/RemindersManager'; 25 | import TemplatesManager from './managers/TemplatesManager'; 26 | import UploadsManager from './managers/UploadsManager'; 27 | import UserManager from './managers/UserManager'; 28 | // models 29 | import Collaborator from './models/Collaborator'; 30 | import CollaboratorState from './models/CollaboratorState'; 31 | import Filter from './models/Filter'; 32 | import Item from './models/Item'; 33 | import Label from './models/Label'; 34 | import LiveNotification from './models/LiveNotification'; 35 | import Note from './models/Note'; 36 | import Project from './models/Project'; 37 | import ProjectNote from './models/ProjectNote'; 38 | import Reminder from './models/Reminder'; 39 | 40 | import { generate_uuid } from './utils/uuid'; 41 | 42 | require("babel-polyfill"); 43 | /** 44 | * @class Session 45 | */ 46 | class API { 47 | constructor(token) { 48 | this.api_endpoint = 'https://todoist.com'; 49 | // Session instance for requests 50 | this.session = new Session({ token }); 51 | // Requests to be sent are appended here 52 | this.queue = []; 53 | // Mapping of temporary ids to real ids 54 | this.temp_ids = {}; 55 | 56 | // managers 57 | this.projects = new ProjectsManager(this); 58 | this.project_notes = new ProjectNotesManager(this); 59 | this.items = new ItemsManager(this); 60 | this.labels = new LabelsManager(this); 61 | this.filters = new FiltersManager(this); 62 | this.notes = new NotesManager(this); 63 | this.live_notifications = new LiveNotificationsManager(this); 64 | this.reminders = new RemindersManager(this); 65 | this.locations = new LocationsManager(this); 66 | this.invitations = new InvitationsManager(this); 67 | this.biz_invitations = new BizInvitationsManager(this); 68 | this.user = new UserManager(this); 69 | this.collaborators = new CollaboratorsManager(this); 70 | this.collaborator_states = new CollaboratorStatesManager(this); 71 | this.completed = new CompletedManager(this); 72 | this.uploads = new UploadsManager(this); 73 | this.activity = new ActivityManager(this); 74 | this.business_users = new BusinessUsersManager(this); 75 | this.templates = new TemplatesManager(this); 76 | this.backups = new BackupsManager(this); 77 | // Local copy of all of the user's objects 78 | this.state = { 79 | collaborator_states: [], 80 | collaborators: [], 81 | day_orders: {}, 82 | day_orders_timestamp: '', 83 | filters: [], 84 | items: [], 85 | labels: [], 86 | live_notifications: [], 87 | live_notifications_last_read_id: -1, 88 | locations: [], 89 | notes: [], 90 | project_notes: [], 91 | projects: [], 92 | reminders: [], 93 | settings_notifications: {}, 94 | user: {}, 95 | }; 96 | } 97 | 98 | /** 99 | * Performs a GET request prepending the API endpoint. 100 | * @param {string} resource Requested resource 101 | * @param {Object} params 102 | * @return {Promise} 103 | */ 104 | get(resource, params) { 105 | return this.session.get( 106 | this.get_api_url(resource), 107 | params 108 | ); 109 | } 110 | 111 | /** 112 | * Performs a POST request prepending the API endpoint. 113 | * @param {string} resource Requested resource 114 | * @param {Object} params 115 | * @return {Promise} 116 | */ 117 | post(resource, params, headers) { 118 | return this.session.post( 119 | this.get_api_url(resource), 120 | params, 121 | headers 122 | ); 123 | } 124 | 125 | /** 126 | * Sends to the server the changes that were made locally, and also 127 | * fetches the latest updated data from the server. 128 | * @param {Array.} commands List of commands to be processed. 129 | * @param {Object} params 130 | * @return {Object} Server response 131 | */ 132 | async sync(commands = []) { 133 | const response = await this.session.get( 134 | this.get_api_url('sync'), 135 | { 136 | day_orders_timestamp: this.state.day_orders_timestamp, 137 | include_notification_settings: 1, 138 | resource_types: JSON.stringify(['all']), 139 | commands: JSON.stringify(commands), 140 | } 141 | ); 142 | 143 | const temp_keys = Object.keys(response.temp_id_mapping || {}); 144 | if (temp_keys.length > 0) { 145 | temp_keys.forEach((temp_id) => { 146 | const new_id = response.temp_id_mapping[temp_id]; 147 | this.temp_ids[temp_id] = new_id; 148 | this.replace_temp_id(temp_id, new_id); 149 | }); 150 | } 151 | await this.update_state(response); 152 | 153 | return response; 154 | } 155 | 156 | /** 157 | * Performs a server query 158 | * @deprecated 159 | * @param {Array.} params List of parameters to query 160 | * @return {Promise} 161 | */ 162 | query(params = []) { 163 | console.warning('You are using a deprecated method "query". Unexpected behaviors might occur. See: https://github.com/Doist/todoist-api/issues/22'); 164 | return this.session.get( 165 | this.get_api_url('query'), 166 | { queries: JSON.stringify(params) } 167 | ); 168 | } 169 | 170 | /** 171 | * Updates the local state, with the data returned by the server after a 172 | * sync. 173 | * @param {Object} syncdata Data returned by {@code this.sync}. 174 | */ 175 | async update_state(syncdata) { 176 | // It is straightforward to update these type of data, since it is 177 | // enough to just see if they are present in the sync data, and then 178 | // either replace the local values or update them. 179 | const keys = ['day_orders', 'day_orders_timestamp', 'live_notifications_last_read_id', 'locations', 'settings_notifications', 'user']; 180 | keys.map((key) => { 181 | if (syncdata[key]) { 182 | this.state[key] = syncdata[key]; 183 | } 184 | }); 185 | 186 | const resp_models_mapping = { 187 | collaborator: Collaborator, 188 | collaborator_states: CollaboratorState, 189 | filters: Filter, 190 | items: Item, 191 | labels: Label, 192 | live_notifications: LiveNotification, 193 | notes: Note, 194 | project_notes: ProjectNote, 195 | projects: Project, 196 | reminders: Reminder, 197 | }; 198 | 199 | // Updating these type of data is a bit more complicated, since it is 200 | // necessary to find out whether an object in the sync data is new, 201 | // updates an existing object, or marks an object to be deleted. But 202 | // the same procedure takes place for each of these types of data. 203 | let promises = []; 204 | Object.keys(resp_models_mapping).forEach((datatype) => { 205 | // Process each object of this specific type in the sync data. 206 | // Collect a promise for each object due to some this.find_object are asynchronous 207 | // since they hit the server looking for remote objects 208 | const typePromises = (syncdata[datatype] || []).map((remoteObj) => { 209 | return Promise.resolve().then(async() => { 210 | // Find out whether the object already exists in the local state. 211 | const localObj = await this.find_object(datatype, remoteObj); 212 | if (localObj) { 213 | // If the object is already present in the local state, then 214 | // we either update it 215 | Object.assign(localObj.data, remoteObj); 216 | } else { 217 | // If not, then the object is new and it should be added 218 | const newobj = new resp_models_mapping[datatype](remoteObj, this); 219 | this.state[datatype].push(newobj); 220 | } 221 | }); 222 | }); 223 | 224 | promises = [...promises, ...typePromises]; 225 | }); 226 | // await for all promises to resolve and continue. 227 | await Promise.all(promises); 228 | 229 | // since sync response isn't including deleted objects, we'll rid of from state 230 | // all those items marked as to be deleted 231 | Object.keys(resp_models_mapping).forEach((datatype) => { 232 | if (this.state[datatype]) { 233 | this.state[datatype] = this.state[datatype].filter(stateObj => stateObj.is_deleted !== 1); 234 | } 235 | }); 236 | } 237 | 238 | /** 239 | * Searches for an object in the local state, depending on the type of object, and then on its primary key is. 240 | * If the object is found it is returned, and if not, then null is returned. 241 | * @param {string} objtype Name for the type of the searching object. 242 | * @param {Object} obj Object from where to take search paramters. 243 | * @return {Object|null} Depending on search result. 244 | */ 245 | find_object(objtype, obj) { 246 | if (objtype === 'collaborators') { 247 | return this.collaborators.get_by_id(obj.id); 248 | } else if (objtype === 'collaborator_states') { 249 | return this.collaborator_states.get_by_ids(obj.project_id, obj.user_id); 250 | } else if (objtype === 'filters') { 251 | return this.filters.get_by_id(obj.id, true); 252 | } else if (objtype === 'items') { 253 | return this.items.get_by_id(obj.id, true); 254 | } else if (objtype === 'labels') { 255 | return this.labels.get_by_id(obj.id, true); 256 | } else if (objtype === 'live_notifications') { 257 | return this.live_notifications.get_by_id(obj.id); 258 | } else if (objtype === 'notes') { 259 | return this.notes.get_by_id(obj.id, true); 260 | } else if (objtype === 'project_notes') { 261 | return this.project_notes.get_by_id(obj.id, true); 262 | } else if (objtype === 'projects') { 263 | return this.projects.get_by_id(obj.id, true); 264 | } else if (objtype === 'reminders') { 265 | return this.reminders.get_by_id(obj.id, true); 266 | } else { 267 | return null; 268 | } 269 | } 270 | 271 | /** 272 | * Replaces the temporary id generated locally when an object was first 273 | * created, with a real Id supplied by the server. True is returned if 274 | * the temporary id was found and replaced, and false otherwise. 275 | * @param {string} temp_id Temporary item id. 276 | * @param {number} new_id New item id. 277 | * @return {boolean} Whether temporary id was found or not. 278 | */ 279 | replace_temp_id(temp_id, new_id) { 280 | const datatypes = ['filters', 'items', 'labels', 'notes', 'project_notes', 'projects', 'reminders']; 281 | datatypes.forEach((type) => { 282 | this.state[type].forEach((obj, objIndex) => { 283 | if (obj.temp_id === temp_id) { 284 | this.state[type][objIndex].id = new_id; 285 | } 286 | }); 287 | }); 288 | } 289 | 290 | /** 291 | * Generates a uuid. 292 | * @return {string} 293 | */ 294 | generate_uuid() { 295 | return generate_uuid(); 296 | } 297 | 298 | /** 299 | * Returns the full API url to hit. 300 | * @param {string} resource The API resource. 301 | * @return {string} 302 | */ 303 | get_api_url(resource = '') { 304 | return `${this.api_endpoint}/API/v7/${resource}`; 305 | } 306 | 307 | /** 308 | * Adds a new task. 309 | * @param {string} content The description of the task. 310 | * @param {Object} params All other paramters to set in the new task. 311 | * @return {Promise} 312 | */ 313 | add_item(content, params = {}) { 314 | Object.assign(params, { content }); 315 | if (params.labels) { 316 | params.labels = JSON.stringify(params.labels); 317 | } 318 | return this.get('add_item', params); 319 | } 320 | 321 | /** 322 | * Commits all requests that are queued. Note that, without calling this 323 | * method none of the changes that are made to the objects are actually 324 | * synchronized to the server, unless one of the aforementioned Sync API 325 | * calls are called directly. 326 | */ 327 | async commit(raise_on_error = true) { 328 | if (!this.queue.length) return; 329 | 330 | const response = await this.sync(this.queue); 331 | this.queue = []; 332 | if (response.sync_status) { 333 | if (raise_on_error) { 334 | Object.keys(response.sync_status).forEach((key) => { 335 | if (response.sync_status[key] != 'ok') { 336 | throw new Error(`sync fail (${key}, ${JSON.stringify(response.sync_status[key])})`); 337 | } 338 | }); 339 | } 340 | } 341 | return response; 342 | } 343 | }; 344 | 345 | 346 | export default API; 347 | -------------------------------------------------------------------------------- /todoist/Session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handles session related actions like configuration, 3 | * requests, tokens and responses. 4 | * @author Cosmitar 5 | */ 6 | import 'fetch-everywhere'; 7 | import { generate_uuid } from './utils/uuid'; 8 | /** 9 | * @class Session 10 | */ 11 | class Session { 12 | /** 13 | * @param {Object} config Configuration object with optional params: 14 | * app_token 15 | * client_id 16 | * scope 17 | * state 18 | * client_secret 19 | * token <- this is the access token 20 | * @constructor 21 | */ 22 | constructor(config = {}) { 23 | this._app_token = config.app_token || ''; 24 | this._client = config.client_id || ''; 25 | this._scope = config.scope || 'data:read_write,data:delete,project:delete'; 26 | this._state = config.state || generate_uuid(); 27 | this._secret = config.client_secret || ''; 28 | this._token = config.token || ''; // access token 29 | this._sync_token = '*'; 30 | this._auth_url = 'https://todoist.com/oauth/authorize'; 31 | this._exchange_token_url = 'https://todoist.com/oauth/access_token'; 32 | } 33 | 34 | /** 35 | * Simplifies deferred config after creating an instance 36 | * of a session. 37 | * @param {Object} config An object that can contain 38 | * app_token 39 | * client_id 40 | * scope 41 | * state 42 | * client_secret 43 | */ 44 | config(config = {}) { 45 | this._app_token = config.app_token || this._app_token; 46 | this._client = config.client_id || this._client; 47 | this._scope = config.scope || this._scope; 48 | this._state = config.state || this._state; 49 | this._secret = config.client_secret || this._secret; 50 | } 51 | 52 | /** 53 | * Sets an access token for current session. 54 | * @param {string} token 55 | */ 56 | set accessToken(token) { 57 | this._token = token; 58 | } 59 | 60 | /** 61 | * Sets the authorization code needed later for access token exchange. 62 | * @param {string} token 63 | */ 64 | set code(code) { 65 | this._code = code; 66 | } 67 | 68 | /** 69 | * Returns the authorization url based on configurations. 70 | * @return string The full authorization url. 71 | */ 72 | requestAuthorizationUrl() { 73 | const query = this._dataToQueryString({ 74 | client_id: this._client, 75 | scope: this._scope, 76 | state: this._state, 77 | }); 78 | return `${this._auth_url}?${query}`; 79 | } 80 | 81 | /** 82 | * Requests an access token to the server. 83 | * @return {Promise} 84 | */ 85 | getAccessToken() { 86 | return this.request(this._exchange_token_url, 'POST', { 87 | client_id: this._client, 88 | client_secret: this._secret, 89 | code: this._code, 90 | }); 91 | } 92 | 93 | 94 | /** 95 | * Performs a GET request for the given url and parameters. 96 | * @param {string} url 97 | * @param {Object} data 98 | * @return {Promise} 99 | */ 100 | get(url, data = {}) { 101 | return this.request(url, 'GET', data); 102 | } 103 | 104 | /** 105 | * Performs a POST request for the given url and parameters. 106 | * @param {string} url 107 | * @param {Object} data 108 | * @return {Promise} 109 | */ 110 | post(url, data = {}, headers) { 111 | return this.request(url, 'POST', data, headers); 112 | } 113 | 114 | _dataToQueryString(data) { 115 | return Object.keys(data) 116 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])) 117 | .join('&'); 118 | } 119 | 120 | /** 121 | * Executes a request, handling headers, tokens and response. 122 | * @param {string} url The URL to fetch. 123 | * @param {string} method An http verb, for this API only GET or POST. 124 | * @param {Object} data 125 | */ 126 | request(url, method = 'GET', data = {}, customHeaders = {}) { 127 | let headers = Object.assign({}, { 128 | Accept: 'application/json, text/plain, */*', 129 | // content type text/plain avoid preflight request not supported 130 | // by API server 131 | 'Content-Type': 'text/plain', 132 | }, customHeaders); 133 | 134 | if (this._token) { 135 | data.token = this._token; 136 | } 137 | 138 | if (method === 'POST') { 139 | data.sync_token = this._sync_token; 140 | } 141 | 142 | const query = this._dataToQueryString(data); 143 | const request_url = `${url}?${query}`; 144 | return fetch(request_url, { 145 | method: method, 146 | headers: headers, 147 | body: /GET|HEAD/.test(method) ? null : JSON.stringify(data), 148 | }).then(response => { 149 | if (response.error_code) { 150 | throw new Error(`(cod: ${response.error_code}) ${response.error}`); 151 | } 152 | return response; 153 | }).then((response) => { 154 | if (!response.ok) { 155 | throw new Error(`(${response.status}) ${response.statusText}`); 156 | } 157 | 158 | return response; 159 | }).then(response => { 160 | if (response.sync_token) { 161 | this._sync_token = response.sync_token; 162 | } 163 | 164 | // Todoist API always returns a JSON, even on error (except on templates as files) 165 | if (/attachment/.test(response.headers.get('content-disposition'))) { 166 | return response; 167 | } else { 168 | return response.json(); 169 | } 170 | }); 171 | } 172 | } 173 | 174 | export default Session; 175 | -------------------------------------------------------------------------------- /todoist/index.js: -------------------------------------------------------------------------------- 1 | import { default as TodoistAPI } from './Api'; 2 | 3 | export default TodoistAPI; 4 | -------------------------------------------------------------------------------- /todoist/managers/ActivityManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class ActivityManager extends Manager { 4 | /** 5 | * Get events from the activity log. 6 | * @param {object} params 7 | * @return {Promise} 8 | */ 9 | get(params) { 10 | return this.api.get('activity/get', params); 11 | } 12 | } 13 | 14 | export default ActivityManager; 15 | -------------------------------------------------------------------------------- /todoist/managers/BackupsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class BackupsManager extends Manager { 4 | /** 5 | * Get backups. 6 | * @param {object} params 7 | * @return {Promise} 8 | */ 9 | get(params) { 10 | return this.api.get('backups/get', params); 11 | } 12 | } 13 | 14 | export default BackupsManager; 15 | -------------------------------------------------------------------------------- /todoist/managers/BizInvitationsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class BizInvitationsManager extends Manager { 4 | /** 5 | * Accepts a business invitation to share a project. 6 | * @param {number} invitation_id 7 | * @param {number} invitation_secret 8 | */ 9 | accept(invitation_id, invitation_secret) { 10 | this.queueCmd('biz_accept_invitation', { invitation_id, invitation_secret }); 11 | } 12 | 13 | /** 14 | * Rejects a business invitation to share a project. 15 | * @param {number} invitation_id 16 | * @param {number} invitation_secret 17 | */ 18 | reject(invitation_id, invitation_secret) { 19 | this.queueCmd('biz_reject_invitation', { invitation_id, invitation_secret }); 20 | } 21 | } 22 | 23 | export default BizInvitationsManager; 24 | -------------------------------------------------------------------------------- /todoist/managers/BusinessUsersManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class BusinessUsersManager extends Manager { 4 | /** 5 | * Sends a business user invitation. 6 | * @param {string} email_list 7 | * @return {Promise} 8 | */ 9 | invite(email_list) { 10 | return this.api.get('business/users/invite', { email_list }); 11 | } 12 | 13 | /** 14 | * Accepts a business user invitation. 15 | * @param {number} id 16 | * @param {string} secret 17 | * @return {Promise} 18 | */ 19 | accept_invitation(id, secret) { 20 | const params = { id, secret }; 21 | return this.api.get('business/users/accept_invitation', params); 22 | } 23 | 24 | /** 25 | * Rejects a business user invitation. 26 | * @param {number} id 27 | * @param {string} secret 28 | * @return {Promise} 29 | */ 30 | reject_invitation(id, secret) { 31 | const params = { id, secret }; 32 | return this.api.get('business/users/reject_invitation', params); 33 | } 34 | } 35 | 36 | export default BusinessUsersManager; 37 | -------------------------------------------------------------------------------- /todoist/managers/CollaboratorStatesManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class CollaboratorStatesManager extends Manager { 4 | 5 | get state_name() { return 'collaborator_states'; } 6 | 7 | /** 8 | * Finds and returns the collaborator state based on the project and user 9 | * ids. 10 | * @param {number} project_id 11 | * @param {number} user_id 12 | */ 13 | get_by_ids(project_id, user_id) { 14 | const obj = this.api.state[this.state_name].find( 15 | c => c.project_id === project_id && c.user_id === user_id 16 | ); 17 | return Promise.resolve(obj); 18 | } 19 | } 20 | 21 | export default CollaboratorStatesManager; 22 | -------------------------------------------------------------------------------- /todoist/managers/CollaboratorsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class CollaboratorsManager extends Manager { 4 | 5 | get state_name() { return 'collaborators'; } 6 | 7 | /** 8 | * Deletes a collaborator from a shared project. 9 | * @param {number} project_id 10 | * @param {string} email 11 | */ 12 | delete(project_id, email) { 13 | this.queueCmd('delete_collaborator', { 14 | project_id: project_id, 15 | email: email, 16 | }); 17 | } 18 | } 19 | 20 | export default CollaboratorsManager; 21 | -------------------------------------------------------------------------------- /todoist/managers/CompletedManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class CompletedManager extends Manager{ 4 | /** 5 | * Returns the user's recent productivity stats. 6 | * @return {Promise} 7 | */ 8 | get_stats() { 9 | return this.api.get('completed/get_stats'); 10 | } 11 | 12 | /** 13 | * Returns all user's completed items. 14 | * @return {Promise} 15 | */ 16 | get_all(params = {}) { 17 | return this.api.get('completed/get_all', params); 18 | } 19 | } 20 | 21 | export default CompletedManager; 22 | -------------------------------------------------------------------------------- /todoist/managers/FiltersManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | import Filter from './../models/Filter'; 3 | 4 | class FiltersManager extends Manager { 5 | 6 | get state_name() { return 'filters'; } 7 | get object_type() { return 'filter'; } 8 | 9 | /** 10 | * Creates a local filter object. 11 | * @param {string} name 12 | * @param {string} query 13 | * @param {Object} params 14 | * @return {Filter} 15 | */ 16 | add(name, query, params) { 17 | const obj = new Filter({ name, query }, this.api); 18 | obj.temp_id = obj['id'] = this.api.generate_uuid(); 19 | Object.assign(obj.data, params); 20 | this.api.state[this.state_name].push(obj); 21 | 22 | // get obj data w/o id attribute 23 | const { id, ...args } = obj.data; 24 | 25 | this.queueCmd({ 26 | type: 'filter_add', 27 | temp_id: obj.temp_id, 28 | }, args); 29 | return obj; 30 | } 31 | 32 | /** 33 | * Updates a filter remotely. 34 | * @param {number} filter_id 35 | * @param {Object} params 36 | */ 37 | update(filter_id, params) { 38 | const args = Object.assign({}, params, { id: filter_id }); 39 | this.queueCmd('filter_update', args); 40 | } 41 | 42 | /** 43 | * Deletes a filter remotely. 44 | * note: since api response isn't including deleted objects 45 | * must flag as deleted by received id. 46 | * @param {number} filter_id 47 | * @param {Object} params 48 | */ 49 | delete(filter_id) { 50 | this.queueCmd('filter_delete', { id: filter_id }); 51 | this.get_by_id(filter_id, true).then(f => { 52 | if (f) { 53 | f.is_deleted = 1; 54 | } 55 | }); 56 | } 57 | 58 | /** 59 | * Updates the orders of multiple filters remotely. 60 | * @param {Object} id_order_mapping 61 | */ 62 | update_orders(id_order_mapping) { 63 | this.queueCmd('filter_update_orders', { id_order_mapping }); 64 | } 65 | 66 | /** 67 | * Gets an existing filter. 68 | * @param {number} filter_id 69 | * @return {Promise} 70 | */ 71 | get(filter_id) { 72 | const params = { 73 | filter_id: filter_id, 74 | }; 75 | return this.api.get('filters/get', params).then((response) => { 76 | if (response.error) { 77 | return null; 78 | } 79 | const data = { 80 | filters: response.filters ? [filters] : [] 81 | }; 82 | this.api.update_state(data); 83 | 84 | return response; 85 | }); 86 | } 87 | } 88 | 89 | export default FiltersManager; 90 | -------------------------------------------------------------------------------- /todoist/managers/GenericNotesManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class GenericNotesManager extends Manager { 4 | 5 | get object_type() { return 'note'; } 6 | 7 | /** 8 | * Updates an note remotely. 9 | * @param {number} note_id 10 | * @param {Object} params 11 | */ 12 | update(note_id, params) { 13 | const args = Object.assign({}, params, { id: note_id }); 14 | this.queueCmd('note_update', args); 15 | } 16 | 17 | /** 18 | * Deletes an note remotely. 19 | * @param {number} note_id 20 | */ 21 | delete(note_id) { 22 | this.queueCmd('note_delete', { id: note_id }); 23 | this.get_by_id(note_id, true).then(n => { 24 | if (n) { 25 | n.is_deleted = 1; 26 | } 27 | }); 28 | } 29 | } 30 | 31 | export default GenericNotesManager; 32 | -------------------------------------------------------------------------------- /todoist/managers/InvitationsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class InvitationsManager extends Manager { 4 | get object_type() { return 'share_invitation'; } 5 | 6 | /** 7 | * Accepts an invitation to share a project. 8 | * @param {number} invitation_id 9 | * @param {string} invitation_secret 10 | */ 11 | accept(invitation_id, invitation_secret) { 12 | this.queueCmd('accept_invitation', { invitation_id, invitation_secret }); 13 | } 14 | 15 | /** 16 | * Rejets an invitation to share a project. 17 | * @param {number} invitation_id 18 | * @param {string} invitation_secret 19 | */ 20 | reject(invitation_id, invitation_secret) { 21 | this.queueCmd('reject_invitation', { invitation_id, invitation_secret }); 22 | } 23 | 24 | /** 25 | * Delete an invitation to share a project. 26 | * @param {number} invitation_id 27 | */ 28 | delete(invitation_id) { 29 | this.queueCmd('delete_invitation', { invitation_id }); 30 | } 31 | } 32 | 33 | export default InvitationsManager; 34 | -------------------------------------------------------------------------------- /todoist/managers/ItemsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | import Item from './../models/Item'; 3 | 4 | class ItemsManager extends Manager { 5 | 6 | get state_name() { return 'items'; } 7 | get object_type() { return 'item'; } 8 | 9 | /** 10 | * Creates a local item object. 11 | * @param {string} content 12 | * @param {number} project_id 13 | * @param {Object} params 14 | * @return {Item} 15 | */ 16 | add(content, project_id, params) { 17 | const obj = new Item({ content, project_id }, this.api); 18 | obj.temp_id = obj.id = this.api.generate_uuid(); 19 | Object.assign(obj.data, params); 20 | this.api.state[this.state_name].push(obj); 21 | 22 | // get obj data w/o id attribute 23 | const { id, ...args } = obj.data; 24 | 25 | this.queueCmd({ 26 | type: 'item_add', 27 | temp_id: obj.temp_id, 28 | }, args); 29 | return obj; 30 | } 31 | 32 | /** 33 | * Updates an item remotely. 34 | * @param {number} item_id 35 | * @param {Object} params 36 | */ 37 | update(item_id, params) { 38 | const args = Object.assign( {}, params, { id: item_id }); 39 | this.queueCmd('item_update', args); 40 | } 41 | 42 | /** 43 | * Deletes items remotely. 44 | * @param {Array.} item_ids 45 | */ 46 | delete(item_ids) { 47 | this.queueCmd('item_delete', { ids: item_ids }); 48 | item_ids.forEach(id => { 49 | this.get_by_id(id, true).then(i => { 50 | if (i) { 51 | i.is_deleted = 1; 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | /** 58 | * Moves items to another project remotely. 59 | * @param {Object} project_items Mapping object with project 60 | * ids as keys and Array. as list of item ids. 61 | * @param {number} to_project Destination project id. 62 | */ 63 | move(project_items, to_project) { 64 | this.queueCmd('item_move', { 65 | project_items, 66 | to_project, 67 | }); 68 | } 69 | 70 | /** 71 | * Marks item as done. 72 | * @param {number} item_id 73 | */ 74 | close(item_id) { 75 | this.queueCmd('item_close', { id: item_id }); 76 | } 77 | 78 | /** 79 | * Marks items as completed remotely. 80 | * @param {Array.} item_ids 81 | * @param {boolean} force_history 82 | */ 83 | complete(item_ids, force_history) { 84 | this.queueCmd('item_complete', { 85 | ids: item_ids, 86 | force_history, 87 | }); 88 | } 89 | 90 | /** 91 | * Marks items as not completed remotely. 92 | * @param {Array.} item_ids 93 | * @param {boolean} update_item_orders 94 | * @param {boolean} restore_state 95 | */ 96 | uncomplete(item_ids, update_item_orders, restore_state) { 97 | const args = { 98 | ids: item_ids, 99 | update_item_orders, 100 | }; 101 | 102 | if (restore_state) { 103 | args['restore_state'] = restore_state; 104 | } 105 | 106 | this.queueCmd('item_uncomplete', args); 107 | } 108 | 109 | /** 110 | * Completes a recurring task remotely. 111 | * @param {number} item_id 112 | * @param {string} new_date_utc 113 | * @param {string} date_string 114 | * @param {boolean} is_forward 115 | */ 116 | update_date_complete(item_id, new_date_utc, date_string, is_forward) { 117 | const args = { 118 | 'id': item_id, 119 | }; 120 | 121 | if (new_date_utc) { 122 | args.new_date_utc = new_date_utc; 123 | } 124 | 125 | if (date_string) { 126 | args.date_string = date_string; 127 | } 128 | 129 | if (!isNaN(is_forward)) { 130 | args.is_forward = is_forward; 131 | } 132 | 133 | this.queueCmd('item_update_date_complete', args); 134 | } 135 | 136 | /** 137 | * Updates the order and indents of multiple items remotely. 138 | * @param {object} ids_to_orders_indents Mapping object with item ids as 139 | * keys and values with Array. length 2 where 1st element is order and 2nd indent. 140 | */ 141 | update_orders_indents(ids_to_orders_indents) { 142 | this.queueCmd('item_update_orders_indents', { ids_to_orders_indents }); 143 | } 144 | 145 | /** 146 | * Updates in the local state the day orders of multiple items remotely. 147 | * @param {object} ids_to_orders Mapping object with item ids as keys 148 | * and number values for order. 149 | */ 150 | update_day_orders(ids_to_orders) { 151 | this.queueCmd('item_update_day_orders', { ids_to_orders }); 152 | } 153 | 154 | /** 155 | * Returns a project's completed items. 156 | * @param {number} project_id 157 | * @param {Object} params 158 | * @return {Promise} 159 | */ 160 | get_completed(project_id, params) { 161 | const args = Object.assign({}, params, { project_id }); 162 | return this.api.get('items/get_completed', args); 163 | } 164 | 165 | /** 166 | * Gets an existing item. 167 | * @param {number} item_id 168 | * @return {Promise} 169 | */ 170 | get(item_id) { 171 | const args = { item_id }; 172 | return this.api.get('items/get', args).then((response) => { 173 | if (response.error) { 174 | return null; 175 | } 176 | const data = { 177 | projects: response.project ? [response.project] : [], 178 | items: response.item ? [response.item] : [], 179 | // @TODO check how to assign notes here 180 | notes: response.note ? [...response.notes] :[], 181 | }; 182 | this.api.update_state(data); 183 | 184 | return response; 185 | }); 186 | 187 | } 188 | } 189 | 190 | export default ItemsManager; 191 | -------------------------------------------------------------------------------- /todoist/managers/LabelsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | import Label from './../models/Label'; 3 | 4 | class LabelsManager extends Manager { 5 | 6 | get state_name() { return 'labels'; } 7 | get object_type() { return 'label'; } 8 | 9 | /** 10 | * Creates a local label object. 11 | * @param {string} name 12 | * @param {Object} params 13 | * @return {Label} 14 | */ 15 | add(name, params) { 16 | const obj = new Label({ name }, this.api); 17 | obj.temp_id = obj.id = this.api.generate_uuid(); 18 | Object.assign(obj.data, params); 19 | this.api.state[this.state_name].push(obj); 20 | 21 | // get obj data w/o id attribute 22 | const { id, ...args } = obj.data; 23 | 24 | this.queueCmd({ 25 | type: 'label_add', 26 | temp_id: obj.temp_id, 27 | }, args); 28 | 29 | return obj; 30 | } 31 | 32 | /** 33 | * Updates a label remotely. 34 | * @param {number} label_id 35 | * @param {Objec} params 36 | */ 37 | update(label_id, params) { 38 | const args = Object.assign({}, params, { id: label_id }); 39 | this.queueCmd('label_update', args); 40 | } 41 | 42 | /** 43 | * Deletes a label remotely. 44 | * @param {number} label_id 45 | */ 46 | delete(label_id) { 47 | this.queueCmd('label_delete', { id: label_id }); 48 | this.get_by_id(label_id, true).then(l => { 49 | if (l) { 50 | l.is_deleted = 1; 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * Updates the orders of multiple labels remotely. 57 | * @param {Objec} id_order_mapping Mapping object with label ids 58 | * as keys and Array. as order. 59 | */ 60 | update_orders(id_order_mapping) { 61 | this.queueCmd('label_update_orders', { id_order_mapping }); 62 | } 63 | 64 | /** 65 | * Gets an existing label. 66 | * @param {number} label_id 67 | * @return {Promise} 68 | */ 69 | get(label_id) { 70 | const params = { label_id }; 71 | return this.api.get('labels/get', params).then((response) => { 72 | if (response.error) { 73 | return null; 74 | } 75 | const data = { 76 | labels: response.label ? [response.label] : [], 77 | }; 78 | 79 | this.api.update_state(data); 80 | return response; 81 | }); 82 | } 83 | } 84 | 85 | export default LabelsManager; 86 | -------------------------------------------------------------------------------- /todoist/managers/LiveNotificationsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class LiveNotificationsManager extends Manager { 4 | 5 | get state_name() { return 'live_notifications'; } 6 | 7 | /** 8 | * Sets in the local state the last notification read. 9 | * @param {number} id 10 | */ 11 | set_last_read(id){ 12 | this.queueCmd('live_notifications_set_last_read', { id }); 13 | } 14 | } 15 | 16 | export default LiveNotificationsManager; 17 | -------------------------------------------------------------------------------- /todoist/managers/LocationsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class LocationsManager extends Manager { 4 | 5 | get state_name() { return 'locations'; } 6 | 7 | /** 8 | * Clears the locations. 9 | */ 10 | clear() { 11 | this.queueCmd('clear_locations'); 12 | } 13 | } 14 | 15 | export default LocationsManager; 16 | -------------------------------------------------------------------------------- /todoist/managers/Manager.js: -------------------------------------------------------------------------------- 1 | class Manager { 2 | 3 | constructor(api) { 4 | this.api = api; 5 | } 6 | 7 | // should be re-defined in a subclass 8 | get state_name() { return ''; } 9 | get object_type() { return ''; } 10 | 11 | /** 12 | * Finds and returns the object based on its id. 13 | * @param {number} obj_id 14 | * @param {boolean} only_local 15 | * @return {Promise} 16 | */ 17 | get_by_id(obj_id, only_local = false) { 18 | let response = null; 19 | this.api.state[this.state_name].find((obj) => { 20 | // 2nd term has weak comparison for num-str match. 21 | if (obj.id === obj_id || obj.temp_id == obj_id) { 22 | response = obj; 23 | } 24 | }); 25 | 26 | if (!response && !only_local && this.object_type) { 27 | // this isn't matching with Python code 28 | response = this.api[this.state_name].get(obj_id); 29 | } 30 | 31 | return Promise.resolve(response); 32 | } 33 | 34 | /** 35 | * Shorcut to add commands to the queue. 36 | * @param {string|Object} cmdDef The definition of the command, 37 | * can be a string used as type or an object with desired params. 38 | * @param {Object} cmdArgs The arguments for the command. 39 | */ 40 | queueCmd( cmdDef, cmdArgs = {} ) { 41 | const cmd = Object.assign( 42 | { 43 | uuid: this.api.generate_uuid(), 44 | }, 45 | ( 46 | typeof cmdDef === 'string' ? { type: cmdDef } : cmdDef 47 | ), 48 | { 49 | args: cmdArgs, 50 | } 51 | ); 52 | this.api.queue.push(cmd); 53 | return cmd; 54 | } 55 | } 56 | 57 | export default Manager; 58 | -------------------------------------------------------------------------------- /todoist/managers/NotesManager.js: -------------------------------------------------------------------------------- 1 | import GenericNotesManager from './GenericNotesManager'; 2 | import Note from './../models/Note'; 3 | 4 | class NotesManager extends GenericNotesManager { 5 | 6 | get state_name() { return 'notes'; } 7 | 8 | /** 9 | * Creates a local item note object. 10 | * @param {number} item_id 11 | * @param {string} content 12 | * @param {Object} params 13 | * @return {Note} 14 | */ 15 | add(item_id, content, params) { 16 | const obj = new Note({ item_id, content }, this.api); 17 | obj.temp_id = obj.id = this.api.generate_uuid(); 18 | Object.assign(obj.data, params); 19 | this.api.state[this.state_name].push(obj); 20 | 21 | // get obj data w/o id attribute 22 | const { id, ...args } = obj.data; 23 | 24 | this.queueCmd({ 25 | type: 'note_add', 26 | temp_id: obj.temp_id, 27 | }, args); 28 | return obj; 29 | } 30 | 31 | /** 32 | * Gets an existing note. 33 | * @param {number} note_id 34 | * @return {Promise} 35 | */ 36 | get(note_id) { 37 | const args = { note_id }; 38 | return this.api.get('notes/get', args).then((response) => { 39 | if (response.error) { 40 | return null; 41 | } 42 | const data = { 43 | notes: response.note ? [response.note] : [], 44 | }; 45 | 46 | this.api.update_state(data); 47 | return response; 48 | }); 49 | } 50 | } 51 | 52 | export default NotesManager; 53 | -------------------------------------------------------------------------------- /todoist/managers/ProjectNotesManager.js: -------------------------------------------------------------------------------- 1 | import GenericNotesManager from './GenericNotesManager'; 2 | import ProjectNote from './../models/ProjectNote'; 3 | 4 | class ProjectNotesManager extends GenericNotesManager { 5 | 6 | get state_name() { return 'project_notes'; } 7 | 8 | /** 9 | * Creates a local project note object. 10 | * @param {number} project_id 11 | * @param {string} content 12 | * @param {Object} params 13 | * @return {ProjectNote} 14 | */ 15 | add(project_id, content, params) { 16 | const obj = new ProjectNote({ project_id, content }, this.api); 17 | obj.temp_id = obj.id = this.api.generate_uuid(); 18 | Object.assign(obj.data, params); 19 | this.api.state[this.state_name].push(obj); 20 | 21 | // get obj data w/o id attribute 22 | const { id, ...args } = obj.data; 23 | 24 | this.queueCmd({ 25 | type: 'note_add', 26 | temp_id: obj.temp_id, 27 | }, args); 28 | return obj; 29 | } 30 | } 31 | 32 | export default ProjectNotesManager; 33 | -------------------------------------------------------------------------------- /todoist/managers/ProjectsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | import Project from './../models/Project'; 3 | 4 | class ProjectsManager extends Manager { 5 | get state_name() { return 'projects'; } 6 | get object_type() { return 'project'; } 7 | 8 | /** 9 | * Creates a local project object. 10 | * @param {string} name 11 | * @param {Object} params 12 | * @return {Project} 13 | */ 14 | add(name, params) { 15 | const obj = new Project({ name }, this.api); 16 | obj.temp_id = obj.id = `$${this.api.generate_uuid()}`; 17 | Object.assign(obj.data, params); 18 | this.api.state[this.state_name].push(obj); 19 | 20 | // get obj data w/o id attribute 21 | const { id, ...args } = obj.data; 22 | 23 | this.queueCmd({ 24 | type: 'project_add', 25 | temp_id: obj.temp_id, 26 | }, args); 27 | return obj; 28 | } 29 | 30 | /** 31 | * Updates a project remotely. 32 | * @param {number} project_id 33 | * @param {Object} params 34 | */ 35 | update(project_id, params) { 36 | (async () => { 37 | const obj = await this.get_by_id(project_id); 38 | if (obj) { 39 | Object.assign(obj.data, params); 40 | } 41 | })(); 42 | 43 | const args = Object.assign({}, params, { id: project_id }); 44 | this.queueCmd('project_update', args); 45 | } 46 | 47 | /** 48 | * Deletes a project remotely. 49 | * @param {Array.} project_ids 50 | */ 51 | delete(project_ids) { 52 | this.queueCmd('project_delete', { ids: project_ids }); 53 | project_ids.forEach(id => { 54 | this.get_by_id(id, true).then(p => { 55 | if (p) { 56 | p.is_deleted = 1; 57 | } 58 | }); 59 | }); 60 | } 61 | 62 | /** 63 | * Marks project as archived remotely. 64 | * @param {number} project_id 65 | */ 66 | archive(project_id) { 67 | this.queueCmd('project_archive', { id: project_id }); 68 | this.get_by_id(project_id, true).then(p => { 69 | p.is_archived = 1; 70 | }); 71 | } 72 | 73 | /** 74 | * Marks project as not archived remotely. 75 | * @param {number} project_id 76 | */ 77 | unarchive(project_id) { 78 | this.queueCmd('project_unarchive', { id: project_id }); 79 | } 80 | 81 | /** 82 | * Updates the orders and indents of multiple projects remotely. 83 | * @param {Object} ids_to_orders_indents Mapping object with project ids as 84 | * keys and values with Array. length 2 where 1st element is order and 2nd indent. 85 | */ 86 | update_orders_indents(ids_to_orders_indents) { 87 | this.queueCmd('project_update_orders_indents', { ids_to_orders_indents }); 88 | } 89 | 90 | /** 91 | * Shares a project with a user. 92 | * @param {number} project_id 93 | * @param {string} email 94 | * @param {string} message 95 | */ 96 | share(project_id, email, message) { 97 | this.queueCmd('share_project', { 98 | project_id, 99 | email, 100 | }); 101 | } 102 | 103 | /** 104 | * Returns archived projects. 105 | * @return {Promise} 106 | */ 107 | get_archived() { 108 | return this.api.get('projects/get_archived'); 109 | } 110 | 111 | /** 112 | * Returns a project's uncompleted items. 113 | * @param {number} project_id 114 | * @return {Promise} 115 | */ 116 | get_data(project_id) { 117 | const params = { project_id }; 118 | return this.api.get('projects/get_data', params); 119 | } 120 | 121 | /** 122 | * Gets an existing project. 123 | * @param {number} project_id 124 | * @return {Promise} 125 | */ 126 | get(project_id) { 127 | const params = { project_id }; 128 | return this.api.get('projects/get', params).then((response) => { 129 | if (response.error) { 130 | return null; 131 | } 132 | 133 | const data = { 134 | projects: response.project ? [response.project] : [], 135 | project_notes: response.notes ? [response.notes] : [], 136 | }; 137 | 138 | this.api.update_state(data); 139 | return response; 140 | }); 141 | } 142 | } 143 | 144 | export default ProjectsManager; 145 | -------------------------------------------------------------------------------- /todoist/managers/RemindersManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | import Reminder from './../models/Reminder'; 3 | 4 | class RemindersManager extends Manager { 5 | 6 | get state_name() { return 'reminders'; } 7 | get object_type() { return 'reminder'; } 8 | 9 | /** 10 | * Creates a local reminder object. 11 | * @param {number} item_id 12 | * @param {Object} params 13 | * @return {Reminder} 14 | */ 15 | add(item_id, params) { 16 | const obj = new Reminder({ item_id }, this.api); 17 | obj.temp_id = obj.id = this.api.generate_uuid(); 18 | Object.assign(obj.data, params); 19 | this.api.state[this.state_name].push(obj); 20 | 21 | // get obj data w/o id attribute 22 | const { id, ...args } = obj.data; 23 | 24 | this.queueCmd({ 25 | type: 'reminder_add', 26 | temp_id: obj.temp_id, 27 | }, args); 28 | 29 | return obj; 30 | } 31 | 32 | /** 33 | * Updates a reminder remotely. 34 | * @param {number} reminder_id 35 | * @param {Object} params 36 | */ 37 | update(reminder_id, params) { 38 | const args = Object.assign( {}, params, { id: reminder_id }); 39 | this.queueCmd('reminder_update', args); 40 | } 41 | 42 | /** 43 | * Deletes a reminder remotely. 44 | * @param {number} reminder_id 45 | */ 46 | delete(reminder_id) { 47 | this.queueCmd('reminder_delete', { id: reminder_id }); 48 | this.get_by_id(reminder_id, true).then(r => { 49 | if (r) { 50 | r.is_deleted = 1; 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * Gets an existing reminder. 57 | * @param {number} reminder_id 58 | * @return {Promise} 59 | */ 60 | get(reminder_id) { 61 | const args = { reminder_id }; 62 | return this.api.get('reminders/get', args).then((response) => { 63 | if (response.error) { 64 | return null; 65 | } 66 | const data = { 67 | reminders: response.reminder ? [response.reminder] : [], 68 | }; 69 | this.api.update_state(data); 70 | 71 | return response; 72 | }); 73 | } 74 | } 75 | 76 | export default RemindersManager; 77 | -------------------------------------------------------------------------------- /todoist/managers/TemplatesManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class TemplatesManager extends Manager { 4 | /** 5 | * Imports a template into a project. 6 | * @param {number} project_id 7 | * @param {???} file 8 | * @param {Object} params 9 | * @return {Promise} 10 | */ 11 | import_into_project(project_id, file, params) { 12 | const args = Object.assign( {}, params, { project_id, file }); 13 | const headers = { 'Content-Type': 'application/x-www-form-urlencoded' } 14 | return this.api.post('templates/import_into_project', args, files, headers); 15 | } 16 | 17 | /** 18 | * Exports a template as a file. 19 | * @param {number} project_id 20 | * @param {Object} params 21 | * @return {Promise} 22 | */ 23 | export_as_file(project_id, params) { 24 | const args = Object.assign( {}, params, { project_id }); 25 | return this.api.post('templates/export_as_file', args); 26 | } 27 | 28 | /** 29 | * Exports a template as a URL. 30 | * @param {number} project_id 31 | * @param {Object} params 32 | * @return {Promise} 33 | */ 34 | export_as_url(project_id, params) { 35 | const args = Object.assign( {}, params, { project_id }); 36 | return this.api.post('templates/export_as_url', args); 37 | } 38 | } 39 | 40 | export default TemplatesManager; 41 | -------------------------------------------------------------------------------- /todoist/managers/UploadsManager.js: -------------------------------------------------------------------------------- 1 | import Manager from './Manager'; 2 | 3 | class UploadsManager extends Manager { 4 | /** 5 | * Uploads a file. (NOT TESTED). 6 | * @param {???} file File to upload. 7 | * @param {Object} params 8 | * @return {Promise} 9 | */ 10 | add(files, params) { 11 | const args = Object.assign( {}, params, { project_id }); 12 | // should get a file, maybe file should be a file handler 13 | // @TODO make API.post to manage files 14 | return this.api.post('uploads/add', args, files); 15 | } 16 | 17 | /** 18 | * Returns all user's uploads. 19 | * @param {Object} params 20 | * limit: (int, optional) number of results (1-50) 21 | * last_id: (int, optional) return results with id { 43 | if (response.token) { 44 | this.api.session.accessToken = response.token; 45 | } 46 | 47 | return response; 48 | }); 49 | } 50 | 51 | /** 52 | * Logins user with Google account, and returns the response received by 53 | * the server. 54 | * Note: this method was migrated from Python but is useless 55 | * for 3rd party apps. 56 | * @param {string} email 57 | * @param {string} oauth2_token 58 | * @param {Object} params 59 | * @return {Promise} 60 | */ 61 | login_with_google(email, oauth2_token, params) { 62 | const args = Object.assign({}, params, { email, oauth2_token }); 63 | return this.api.post('user/login_with_google', args).then((response) => { 64 | if (response.token) { 65 | this.api.session.accessToken = response.token; 66 | } 67 | 68 | return response; 69 | }); 70 | } 71 | 72 | /** 73 | * Registers a new user. 74 | * Note: this method was migrated from Python but is useless 75 | * for 3rd party apps. 76 | * @param {string} email 77 | * @param {string} full_name 78 | * @param {string} password 79 | * @param {Object} params 80 | * @return {Promise} 81 | */ 82 | register(email, full_name, password, params) { 83 | const args = Object.assign({}, params, { email, full_name, password }); 84 | return this.api.post('user/register', args).then((response) => { 85 | if (response.token) { 86 | this.api.session.accessToken = response.token; 87 | } 88 | 89 | return response; 90 | }); 91 | } 92 | 93 | /** 94 | * Updates the user's notification settings. 95 | * @param {string} notification_type 96 | * @param {string} service 97 | * @param {boolen} dont_notify 98 | * @return {Promise} 99 | */ 100 | update_notification_setting(notification_type, service, dont_notify) { 101 | return this.api.post('user/update_notification_setting', { 102 | notification_type, 103 | service, 104 | dont_notify, 105 | }); 106 | } 107 | } 108 | 109 | export default UserManager; 110 | -------------------------------------------------------------------------------- /todoist/models/Collaborator.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a collaborator. 5 | */ 6 | class Collaborator extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | email: '', 12 | full_name: '', 13 | timezone: '', 14 | image_id: null, 15 | }; 16 | } 17 | 18 | /** 19 | * Deletes a collaborator from a shared project. 20 | * @param {number} project_id 21 | */ 22 | delete(project_id) { 23 | this.api.collaborators.delete(project_id, this.email); 24 | } 25 | } 26 | 27 | export default Collaborator; 28 | -------------------------------------------------------------------------------- /todoist/models/CollaboratorState.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | /** 3 | * Implements a collaborator state. 4 | */ 5 | class CollaboratorState extends Model { 6 | // 7 | } 8 | 9 | export default CollaboratorState; 10 | -------------------------------------------------------------------------------- /todoist/models/Filter.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a filter. 5 | */ 6 | class Filter extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | name: '', 12 | query: '', 13 | color: 0, 14 | item_order: 0, 15 | is_deleted: 0, 16 | }; 17 | } 18 | 19 | /** 20 | * Updates filter. 21 | * @param {Object} params 22 | */ 23 | update(params) { 24 | this.api.filters.update(this.id, params); 25 | Object.assign(this.data, params); 26 | } 27 | 28 | /** 29 | * Deletes filter. 30 | */ 31 | delete() { 32 | this.api.filters.delete(this.id); 33 | this.is_deleted = 1; 34 | } 35 | }; 36 | 37 | export default Filter; 38 | -------------------------------------------------------------------------------- /todoist/models/GenericNote.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a note. 5 | */ 6 | class GenericNote extends Model { 7 | constructor(data, api) { 8 | super(data, api); 9 | // has to be defined in subclasses 10 | this.local_manager = null; 11 | } 12 | 13 | /** 14 | * Updates note. 15 | * @param {Object} params 16 | */ 17 | update(params) { 18 | this.local_manager.update(this.id, params); 19 | Object.assign(this.data, params); 20 | } 21 | 22 | /** 23 | * Deletes note. 24 | */ 25 | delete() { 26 | this.local_manager.delete(this.id); 27 | this.data.is_deleted = 1; 28 | } 29 | } 30 | 31 | export default GenericNote; 32 | -------------------------------------------------------------------------------- /todoist/models/Item.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements an Item. 5 | */ 6 | class Item extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | user_id: 0, 12 | project_id: 0, 13 | content: '', 14 | date_string: '', 15 | date_lang: '', 16 | due_date_utc: null, 17 | indent: 0, 18 | priority: 0, 19 | item_order: 0, 20 | day_order: 0, 21 | collapsed: 0, 22 | children: null, 23 | labels: [], 24 | assigned_by_uid: 0, 25 | responsible_uid: null, 26 | checked: 0, 27 | in_history: 0, 28 | is_deleted: 0, 29 | is_archived: 0, 30 | sync_id: null, 31 | date_added: '', 32 | }; 33 | } 34 | 35 | /** 36 | * Updates item. 37 | * @param {Object} params 38 | */ 39 | update(params) { 40 | this.api.items.update(this.id, params); 41 | Object.assign(this.data, params); 42 | } 43 | 44 | /** 45 | * Deletes item. 46 | */ 47 | delete() { 48 | this.api.items.delete([this.id]); 49 | this.is_deleted = 1; 50 | } 51 | 52 | /** 53 | * Moves item to another project. 54 | * @param {number} to_project 55 | */ 56 | move(to_project) { 57 | this.api.items.move({ [this.project_id]: [this.id] }, to_project); 58 | this.project_id = to_project; 59 | } 60 | 61 | /** 62 | * Marks item as closed. 63 | */ 64 | close() { 65 | this.api.items.close(this.id); 66 | } 67 | 68 | /** 69 | * Marks item as completed. 70 | * @param {boolean} force_history 71 | */ 72 | complete(force_history = 0) { 73 | this.api.items.complete([this.id], force_history); 74 | this.checked = 1; 75 | this.in_history = force_history; 76 | } 77 | 78 | /** 79 | * Marks item as not completed. 80 | * @param {boolean} update_item_orders 81 | * @param {Object} restore_state 82 | */ 83 | uncomplete(update_item_orders = 1, restore_state = {}){ 84 | this.api.items.uncomplete([this.id], update_item_orders, restore_state); 85 | this.checked = 0; 86 | this.in_history = 0; 87 | 88 | if (restore_state[this.id]) { 89 | [ 90 | this.in_history, 91 | this.checked, 92 | this.item_order, 93 | this.indent 94 | ] = restore_state[this.id]; 95 | } 96 | } 97 | /** 98 | * Completes a recurring task. 99 | * @param {string} new_date_utc 100 | * @param {string} date_string 101 | * @param {boolean} is_forward 102 | */ 103 | update_date_complete(new_date_utc = '', date_string = '', is_forward = 0) { 104 | this.api.items.update_date_complete(this.id, new_date_utc, date_string, is_forward); 105 | this.due_date_utc = new_date_utc || this.due_date_utc; 106 | this.date_string = date_string || this.date_string; 107 | } 108 | }; 109 | 110 | export default Item; 111 | -------------------------------------------------------------------------------- /todoist/models/Label.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a filter. 5 | */ 6 | class Label extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | name: '', 12 | color: 0, 13 | item_order: 0, 14 | is_deleted: 0, 15 | }; 16 | } 17 | 18 | /** 19 | * Updates label. 20 | * @param {Object} params 21 | */ 22 | update(params) { 23 | this.api.labels.update(this.id, params); 24 | Object.assign(this.data, params); 25 | } 26 | 27 | /** 28 | * Deletes label. 29 | */ 30 | delete() { 31 | this.api.labels.delete(this.id); 32 | this.is_deleted = 1; 33 | } 34 | }; 35 | 36 | export default Label; 37 | -------------------------------------------------------------------------------- /todoist/models/LiveNotification.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a live notification. 5 | */ 6 | class LiveNotification extends Model { 7 | 8 | get definition() { 9 | return { 10 | created: 0, 11 | from_uid: 0, 12 | id: 0, 13 | invitation_id: 0, 14 | invitation_secret: '', 15 | notification_key: '', 16 | notification_type: '', 17 | seq_no: 0, 18 | state: '', 19 | }; 20 | } 21 | 22 | } 23 | 24 | export default LiveNotification; 25 | -------------------------------------------------------------------------------- /todoist/models/Model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements a generic object. 3 | */ 4 | class Model { 5 | constructor(data, api) { 6 | this.data = data; 7 | this.api = api; 8 | this.temp_id = ''; 9 | 10 | // Until we decide to imlpement Proxies (lack of browsers/platforms support) lets 11 | // generate each setter/getter based on subclass definition, received data and temp_id. 12 | Object.keys(Object.assign({ temp_id: '' }, this.definition, data)).map((k) => { 13 | Object.defineProperty(this, k, { 14 | get: () => this.data[k], 15 | set: (val) => this.data[k] = val, 16 | }) 17 | }); 18 | } 19 | 20 | toString() { 21 | const data = JSON.stringify(this.data); 22 | return `${this.constructor.name}(${data})`; 23 | } 24 | } 25 | 26 | export default Model; 27 | -------------------------------------------------------------------------------- /todoist/models/Note.js: -------------------------------------------------------------------------------- 1 | import GenericNote from './GenericNote'; 2 | 3 | /** 4 | * Implement an item note. 5 | */ 6 | class Note extends GenericNote { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | posted_uid: 0, 12 | project_id: 0, 13 | item_id: 0, 14 | content: '', 15 | file_attachment: null, 16 | uids_to_notify: null, 17 | is_deleted: 0, 18 | is_archived: 0, 19 | posted: '' 20 | }; 21 | } 22 | 23 | constructor(data, api) { 24 | super(data, api); 25 | this.local_manager = api.notes; 26 | } 27 | } 28 | 29 | export default Note; 30 | -------------------------------------------------------------------------------- /todoist/models/Project.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a project. 5 | */ 6 | class Project extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | name: '', 12 | color: 0, 13 | indent: 0, 14 | item_order: 0, 15 | collapsed: 0, 16 | shared: 0, 17 | is_deleted: 0, 18 | is_archived: 0, 19 | }; 20 | } 21 | 22 | /** 23 | * Updates project. 24 | * @param {Object} params 25 | */ 26 | update(params) { 27 | this.api.projects.update(this.id, params); 28 | Object.assign(this.data, params); 29 | } 30 | 31 | /** 32 | * Deletes project. 33 | */ 34 | delete() { 35 | this.api.projects.delete([this.id]); 36 | this.is_deleted = 1; 37 | } 38 | 39 | /** 40 | * Marks project as archived. 41 | */ 42 | archive() { 43 | this.api.projects.archive(this.id); 44 | this.is_archived = 1; 45 | } 46 | 47 | /** 48 | * Marks project as not archived. 49 | */ 50 | unarchive() { 51 | this.api.projects.unarchive(this.id); 52 | this.is_archived = 0; 53 | } 54 | 55 | /** 56 | * Shares projects with a user. 57 | * @param {string} email 58 | * @param {string} message 59 | */ 60 | share(email, message = '') { 61 | this.api.projects.share(this.id, email, message); 62 | } 63 | 64 | /** 65 | * Takes ownership of a shared project. 66 | */ 67 | take_ownership() { 68 | this.api.projects.take_ownership(this.id); 69 | } 70 | } 71 | 72 | export default Project; 73 | -------------------------------------------------------------------------------- /todoist/models/ProjectNote.js: -------------------------------------------------------------------------------- 1 | import GenericNote from './GenericNote'; 2 | 3 | /** 4 | * Implement a project note. 5 | */ 6 | class ProjectNote extends GenericNote { 7 | constructor(data, api) { 8 | super(data, api); 9 | this.local_manager = api.project_notes; 10 | } 11 | } 12 | 13 | export default ProjectNote; 14 | -------------------------------------------------------------------------------- /todoist/models/Reminder.js: -------------------------------------------------------------------------------- 1 | import Model from './Model'; 2 | 3 | /** 4 | * Implements a reminder. 5 | */ 6 | class Reminder extends Model { 7 | 8 | get definition() { 9 | return { 10 | id: 0, 11 | notify_uid: 0, 12 | item_id: 0, 13 | service: '', 14 | type: '', 15 | date_string: '', 16 | date_lang: '', 17 | due_date_utc: '', 18 | minute_offset: 0, 19 | is_deleted: 0, 20 | }; 21 | } 22 | 23 | /** 24 | * Updates reminder. 25 | * @param {Object} params 26 | */ 27 | update(params) { 28 | this.api.reminders.update(this.id, params); 29 | Object.assign(this.data, params); 30 | } 31 | 32 | /** 33 | * Deletes reminder. 34 | */ 35 | delete() { 36 | this.api.reminders.delete(this.id); 37 | this.is_deleted = 1; 38 | } 39 | } 40 | 41 | export default Reminder; 42 | -------------------------------------------------------------------------------- /todoist/utils/uuid.js: -------------------------------------------------------------------------------- 1 | export const generate_uuid = () => { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 3 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 4 | return v.toString(16); 5 | }); 6 | } 7 | --------------------------------------------------------------------------------