Skip to content

Commit

Permalink
feat(trello): Command to get the next step of each card (#24)
Browse files Browse the repository at this point in the history
* refacto: get rid of the wrap() function

* fix: throw error instead of returning text as usual

* fix: export TrelloOptions

* move test files next to their implementation file

* test: getNextTrelloTasks returns the first task of the only card of a board

* docs: add "How to add a command" tutorial (wip)

* implement dummy getNextTrelloTasks() => test passes

* write a small tool to examine the response from the 3rd-party API

* docs: update steps 3 and 4 with links to examples

* rename previous tool

* add getNextTodoItem() + call it from tools/trello-checklist-get.ts

* fix(vscode): exclude lib/ directory, especially for file search

* make _getNextTrelloTasks() return actual results from Trello's API

* docs: add step 5 with link to example

* Oops, I had forgotten a hard-coded value

* Make the automated test mock the API requests

* docs: add step 6 with link to example

* refactor: simplify nock code

* refacto: extract mock functions for re-use

* refactor: re-use API mocks

* add traps in the test

* ❌ test: "returns the first tasks of both cards of a board"

* feat: return next steps for more than 1 card

* update doc

* ❌ test: it skips cards that don't have a checklist

* fix test

* ✅ implement: it skips cards that don't have a checklist

* fix: .env is only used for firebase & telegram creds

* test new command => document and ease debugging

* fix: skip empty checklists

* stricter assertions + remote TODOs

* ❌ test:it skips cards that don't have a hashtag

* ease debugging

* _getNextTrelloTasks() fetches cards with tags => allow cards without description

* fix error handling for addAsTrelloComment()

* reduce redundant errors logging in tests

* fix test "returns the first incomplete task of the only card of a board"
=> split extractCardFromTags() into fetchCardsWithTags() and fetchTargetedCards()

* fix tests by adding tags to dummy cards

* fix: make _getNextTrelloTasks() consider just cards that have tags

* docs: move `/next #tag` command to TODO section

* make tools runnable directly => remove them from package.json
  • Loading branch information
adrienjoly authored Dec 6, 2020
1 parent e908471 commit ece3b67
Show file tree
Hide file tree
Showing 19 changed files with 621 additions and 291 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The first version of this bot was developed by following the steps provided in [

- `/todo <task> [#tag [#...]]` will add a ToDo/task to TickTick's inbox, for sorting
- `/today <task> [#tag [#...]]` will add a ToDo/task to TickTick, due today
- `/next` will list the next `task` for each Trello card
- `/next <task> [#tag]` will add a `task` to the top of the check-list of the Trello card associated with `#tag`
- `/note <text> [#card [#...]]` will add a comment to the specified Trello card(s), for journaling
- `/shelf <spotify_album_url>` will propose the addition of an album to the [adrienjoly/album-shelf](https://github.com/adrienjoly/album-shelf) GitHub repository (requires options: `spotify.clientid`, `spotify.secret` and `github.token` with "public repo" permissions)
Expand Down Expand Up @@ -115,10 +116,33 @@ You can troubleshoot your bot using [your firebase console](https://console.fire

Set `telegram.onlyfromuserid` in your `.config.json` file and call `$ npm run deploy` again if you want the bot to only respond to that Telegram user identifier.

## How to add a command

The steps are listed in the order I usually follow:

1. In the `commandHandlers` array of `src/messageHandler.ts`, add an entry for your command. At first, make it return a simple `string`, like we did for the `/version` command. Deploy it and test it in production, just to make sure that you won't be blocked later at that critical step.

2. Write an automated test in `src/use-cases/`, to define the expected reponse for a sample command. (see [example](https://github.com/adrienjoly/telegram-scribe-bot/pull/24/commits/d52320b905ad9392472dd28f26abbb4fdc07ee8e))

3. Write a minimal `CommandHandler`, just to make the test pass, without calling any 3rd-party API yet. (see [example](https://github.com/adrienjoly/telegram-scribe-bot/pull/24/commits/cfc22c626b58c5e268d825aa1c2fff691ff16228))

4. Write a small tool to examine the response from the 3rd-party API. (see [example](https://github.com/adrienjoly/telegram-scribe-bot/pull/24/commits/792fbf7d669e8386d5e17c8f50b23623156b99f9))

5. Update the implementation of your `CommandHandler`, so it relies on the actual API response. Make sure that the test passes, when you provide your API credentials. (see [example](https://github.com/adrienjoly/telegram-scribe-bot/pull/24/commits/565cb21a10b8cfd1e44390227976541e62439d2c))

6. Make the automated test mock the API request(s) so that it doesn't require API credentials to run. (see [example](https://github.com/adrienjoly/telegram-scribe-bot/pull/24/commits/b3f4a23a375c49fe152735df41bafef880b77abc))

> In that step, you can leverage the `⚠ no match for [...]` logs displayed when running your test from step 5, in order to know which URL(s) to mock.
7. Test your command locally, using `$ npm run test:bot`.

8. Deploy and test your command in production, as explained above.

## ToDo / Next steps

- Make setup easier and faster, e.g. by automatizing some of the steps
- ideas of "command" use cases to implement:
- `/next [#tag]` will list the next `task` for each Trello card associated with `#tag`
- `/search <text> [#tag [#...]]` will search occurrences of `text` in comments of Trello cards, optionally filtered by `#tags`
- `/openwhyd <track> [#tag] [desc]` will add a music track (e.g. YouTube URL) to Openwhyd.org, in a playlist corresponding to the `tag`, and may add a `desc`ription if provided
- `/issue <repo>` will create a github issue on the provided repo
Expand Down
10 changes: 9 additions & 1 deletion functions/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,13 @@
"source.fixAll.eslint": true
},
"prettier.configPath": ".prettierrc.js",
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"lib/": true,
}
}
10 changes: 2 additions & 8 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
"test": "ts-mocha -p ./tsconfig.json ./test/*.test.ts",
"test": "ts-mocha -p ./tsconfig.json './**/*.test.ts'",
"test:bot": "ts-node tools/bot-cli.ts",
"start": "ts-node tools/start.ts",
"clean": "rm -rf lib",
Expand All @@ -18,13 +18,7 @@
"deploy:test": "source ../.env && curl -X POST -H \"Content-Type:application/json\" ${ROUTER_URL} -d '{}'",
"webhook:bind": "source ../.env && curl https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=${ROUTER_URL}",
"webhook:test": "source ../.env && curl https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo",
"logs": "firebase functions:log",
"spotify:test": "ts-node tools/spotify-album.ts",
"github:pr:test": "ts-node tools/github-pr.ts",
"ticktick:test": "ts-node tools/ticktick.ts",
"trello:test": "source ../.env && curl -LI \"https://api.trello.com/1/members/me/boards?key=${TRELLO_API_KEY}&token=${TRELLO_USER_TOKEN}\" -s | grep HTTP",
"trello:checklist": "ts-node tools/trello-checklist.ts",
"trello:boards": "ts-node tools/trello-boards.ts"
"logs": "firebase functions:log"
},
"engines": {
"node": "12"
Expand Down
2 changes: 1 addition & 1 deletion functions/test/app.test.ts → functions/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import expect from 'expect'
import fetch from 'node-fetch'
import { startApp } from './../src/app'
import { startApp } from './app'

const options = {}

Expand Down
13 changes: 10 additions & 3 deletions functions/src/messageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
addTaskToTicktick,
addTodayTaskToTicktick,
} from './use-cases/addTaskToTicktick'
import { addAsTrelloComment, addAsTrelloTask } from './use-cases/addToTrello'
import {
addAsTrelloComment,
getOrAddTrelloTasks,
} from './use-cases/addToTrello'
import { addSpotifyAlbumToShelfRepo } from './use-cases/addSpotifyAlbumToShelfRepo'
import { BotResponse } from './types'

Expand All @@ -14,7 +17,7 @@ const commandHandlers: { [key: string]: CommandHandler } = {
'/todo': addTaskToTicktick,
'/today': addTodayTaskToTicktick,
'/note': addAsTrelloComment,
'/next': addAsTrelloTask,
'/next': getOrAddTrelloTasks,
'/version': async (_, options): Promise<BotResponse> => {
return { text: `ℹ️ Version: ${options.bot.version}` }
},
Expand Down Expand Up @@ -44,7 +47,11 @@ export async function processMessage(
commandHandlers
).join(', ')}`
} else {
text = (await commandHandler(entities, options)).text
const res = await commandHandler(entities, options)
if (res.error) {
console.error(res.error)
}
text = res.text
}
} catch (err) {
text = `😕 Error while processing: ${err.message}`
Expand Down
9 changes: 9 additions & 0 deletions functions/src/services/Trello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export class Trello {
)) as TrelloChecklist
}

async getNextTodoItem(checklistId: string): Promise<TrelloChecklistItem> {
const { checkItems } = await this.getChecklist(checklistId)
return checkItems
.filter((a: TrelloChecklistItem) => a.state === 'incomplete')
.sort(
(a: TrelloChecklistItem, b: TrelloChecklistItem) => a.pos - b.pos
)[0]
}

async addComment(cardId: string, { text }: { text: string }) {
return await this.trelloLib.makeRequest(
'post',
Expand Down
3 changes: 2 additions & 1 deletion functions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ export type MessageHandlerOptions = {
export type CommandHandler = (
message: ParsedMessageEntities,
options: MessageHandlerOptions
) => Promise<{ text: string }>
) => Promise<BotResponse>

export type BotResponse = {
text: string
error?: Error
}

export type TelegramRequest = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
Options,
parseAlbumId,
addSpotifyAlbumToShelfRepo,
} from './../src/use-cases/addSpotifyAlbumToShelfRepo'
import { ParsedMessageEntities } from './../src/Telegram'
} from './addSpotifyAlbumToShelfRepo'
import { ParsedMessageEntities } from '../Telegram'

const FAKE_CREDS: Options = {
spotify: {
Expand Down
Loading

0 comments on commit ece3b67

Please sign in to comment.