From b7b083f0940309b4c5fb6fb5c99c4b832cd17229 Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Tue, 23 Jan 2018 22:59:26 +0530 Subject: [PATCH] interactive bots: Add Trello Interactive Bot. --- tools/run-mypy | 2 + zulip_bots/zulip_bots/bots/trello/__init__.py | 0 .../bots/trello/assets/list_commands.png | Bin 0 -> 639266 bytes zulip_bots/zulip_bots/bots/trello/doc.md | 41 +++++ .../trello/fixtures/exception_boards.json | 48 +++++ .../bots/trello/fixtures/exception_cards.json | 48 +++++ .../trello/fixtures/exception_checklists.json | 48 +++++ .../bots/trello/fixtures/exception_lists.json | 48 +++++ .../bots/trello/fixtures/get_all_boards.json | 106 +++++++++++ .../bots/trello/fixtures/get_board_descs.json | 90 +++++++++ .../bots/trello/fixtures/get_cards.json | 92 ++++++++++ .../bots/trello/fixtures/get_checklists.json | 88 +++++++++ .../bots/trello/fixtures/get_lists.json | 67 +++++++ .../zulip_bots/bots/trello/requirements.txt | 1 + .../zulip_bots/bots/trello/test_trello.py | 109 +++++++++++ zulip_bots/zulip_bots/bots/trello/trello.conf | 4 + zulip_bots/zulip_bots/bots/trello/trello.py | 172 ++++++++++++++++++ 17 files changed, 964 insertions(+) create mode 100644 zulip_bots/zulip_bots/bots/trello/__init__.py create mode 100644 zulip_bots/zulip_bots/bots/trello/assets/list_commands.png create mode 100644 zulip_bots/zulip_bots/bots/trello/doc.md create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/exception_boards.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/exception_cards.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/exception_checklists.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/exception_lists.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/get_all_boards.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/get_board_descs.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/get_cards.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/get_checklists.json create mode 100644 zulip_bots/zulip_bots/bots/trello/fixtures/get_lists.json create mode 100644 zulip_bots/zulip_bots/bots/trello/requirements.txt create mode 100644 zulip_bots/zulip_bots/bots/trello/test_trello.py create mode 100644 zulip_bots/zulip_bots/bots/trello/trello.conf create mode 100644 zulip_bots/zulip_bots/bots/trello/trello.py diff --git a/tools/run-mypy b/tools/run-mypy index c6cbf5e..78a3e2b 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -89,6 +89,8 @@ force_include = [ "zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py", "zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py", "zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py", + "zulip_bots/zulip_bots/bots/trello/trello.py", + "zulip_bots/zulip_bots/bots/trello/test_trello.py" ] parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") diff --git a/zulip_bots/zulip_bots/bots/trello/__init__.py b/zulip_bots/zulip_bots/bots/trello/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/trello/assets/list_commands.png b/zulip_bots/zulip_bots/bots/trello/assets/list_commands.png new file mode 100644 index 0000000000000000000000000000000000000000..0eaf748ad66a6631da2e3cc68c7a7c8851f52f4c GIT binary patch literal 639266 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 followed +by a command, like so: + +``` +@Trello help +``` + +## Setup + +Before usage, you will need to configure the bot by putting the value of the ``, +``, and `` in the config file. +To do this, follow the given steps: + +1. Go to [this]( https://trello.com/app-key) link after logging in at +[Trello]( https://trello.com/). +2. Generate an `access_token` and note it down. Continue to get your +`api_key`. +3. Go to your profile page in Trello and note down your `username`. +4. Open up `zulip_bots/bots/trello/trello.conf` in an editor and +change the values of the ``, ``, and `` +attributes to the corresponding noted values. + +## Developer Notes + +Be sure to add the additional commands and their descriptions to the `supported_commands` +list in `trello.py` so that they can be displayed with the other available commands using +`@ list-commands`. Also modify the `test_list_commands_command` in +`test_trello.py`. + +## Usage + +`@Trello list-commands` - This command gives a list of all available commands along with +short descriptions. + +Example: +![](assets/list_commands.png) diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/exception_boards.json b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_boards.json new file mode 100644 index 0000000..07d964f --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_boards.json @@ -0,0 +1,48 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/members/TEST/", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "error": "invalid id" + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/exception_cards.json b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_cards.json new file mode 100644 index 0000000..2755401 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_cards.json @@ -0,0 +1,48 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/boards/TEST/cards", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "error": "invalid id" + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/exception_checklists.json b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_checklists.json new file mode 100644 index 0000000..cf78dc6 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_checklists.json @@ -0,0 +1,48 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/cards/TEST/checklists/", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "error": "invalid id" + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/exception_lists.json b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_lists.json new file mode 100644 index 0000000..20f3798 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/exception_lists.json @@ -0,0 +1,48 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/boards/TEST/lists", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "error": "invalid id" + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/get_all_boards.json b/zulip_bots/zulip_bots/bots/trello/fixtures/get_all_boards.json new file mode 100644 index 0000000..939c85b --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/get_all_boards.json @@ -0,0 +1,106 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/members/TEST/", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "id": "TEST", + "avatarHash": null, + "bio": "", + "bioData": null, + "confirmed": true, + "fullName": "TEST", + "idEnterprisesDeactivated": [], + "idPremOrgsAdmin": [], + "initials": "TEST", + "memberType": "normal", + "products": [], + "status": "disconnected", + "url": "https://trello.com/TEST", + "username": "TEST", + "avatarSource": "none", + "email": "TEST", + "gravatarHash": "TEST", + "idBoards": [ + ], + "idEnterprise": null, + "idOrganizations": [], + "idEnterprisesAdmin": [], + "limits": { + "boards": { + "totalPerMember": { + "status": "ok", + "disableAt": 950, + "warnAt": 900 + } + }, + "orgs": { + "totalPerMember": { + "status": "ok", + "disableAt": 95, + "warnAt": 90 + } + } + }, + "loginTypes": [ + "android", + "password" + ], + "oneTimeMessagesDismissed": [ + "PowerUpsLimitFullAd", + "GhostListDismissed-2", + "GhostListDismissed-3" + ], + "messagesDismissed": [], + "prefs": { + "sendSummaries": true, + "minutesBetweenSummaries": 15, + "minutesBeforeDeadlineToNotify": 1440, + "colorBlind": false, + "locale": "en-GB" + }, + "trophies": [], + "uploadedAvatarHash": null, + "premiumFeatures": [], + "idBoardsPinned": null + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/get_board_descs.json b/zulip_bots/zulip_bots/bots/trello/fixtures/get_board_descs.json new file mode 100644 index 0000000..e0123fe --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/get_board_descs.json @@ -0,0 +1,90 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/boards/TEST/", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": { + "id": "TEST", + "name": "TEST", + "desc": "", + "descData": null, + "closed": false, + "idOrganization": null, + "pinned": false, + "url": "TEST", + "shortUrl": "TEST", + "prefs": { + "permissionLevel": "private", + "voting": "disabled", + "comments": "members", + "invitations": "members", + "selfJoin": false, + "cardCovers": true, + "cardAging": "regular", + "calendarFeedEnabled": false, + "background": "green", + "backgroundImage": null, + "backgroundImageScaled": null, + "backgroundTile": false, + "backgroundBrightness": "dark", + "backgroundColor": "#519839", + "backgroundBottomColor": "#519839", + "backgroundTopColor": "#519839", + "canBePublic": true, + "canBeOrg": true, + "canBePrivate": true, + "canInvite": true + }, + "labelNames": { + "green": "", + "yellow": "", + "orange": "", + "red": "", + "purple": "", + "blue": "", + "sky": "", + "lime": "", + "pink": "", + "black": "" + } + }, + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/get_cards.json b/zulip_bots/zulip_bots/bots/trello/fixtures/get_cards.json new file mode 100644 index 0000000..dcda4c1 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/get_cards.json @@ -0,0 +1,92 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/boards/TEST/cards", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": [ + { + "id": "TEST", + "checkItemStates": null, + "closed": false, + "dateLastActivity": "2018-01-03T08:27:54.579Z", + "desc": "", + "descData": null, + "idBoard": "TEST", + "idList": "TEST", + "idMembersVoted": [], + "idShort": 12, + "idAttachmentCover": null, + "idLabels": [], + "manualCoverAttachment": false, + "name": "TEST", + "pos": 65535, + "shortLink": "TEST", + "badges": { + "votes": 0, + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 0, + "checkItemsChecked": 0, + "comments": 0, + "attachments": 0, + "description": false, + "due": null, + "dueComplete": false + }, + "dueComplete": false, + "due": null, + "idChecklists": [], + "idMembers": [], + "labels": [], + "shortUrl": "TEST", + "subscribed": false, + "url": "TEST" + } + ], + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/get_checklists.json b/zulip_bots/zulip_bots/bots/trello/fixtures/get_checklists.json new file mode 100644 index 0000000..0a6082f --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/get_checklists.json @@ -0,0 +1,88 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/cards/TEST/checklists/", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": [ + { + "id": "TEST", + "name": "TEST", + "idBoard": "TEST", + "idCard": "TEST", + "pos": 16384, + "checkItems": [ + { + "state": "complete", + "idChecklist": "TEST", + "id": "TEST", + "name": "TEST_1", + "nameData": null, + "pos": 17350 + }, + { + "state": "complete", + "idChecklist": "TEST", + "id": "TEST", + "name": "TEST_2", + "nameData": null, + "pos": 34343 + }, + { + "state": "incomplete", + "idChecklist": "TEST", + "id": "TEST", + "name": "TEST_3", + "nameData": null, + "pos": 51594 + }, + { + "state": "incomplete", + "idChecklist": "TEST", + "id": "TEST", + "name": "TEST_4", + "nameData": null, + "pos": 67998 + } + ] + } + ], + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/fixtures/get_lists.json b/zulip_bots/zulip_bots/bots/trello/fixtures/get_lists.json new file mode 100644 index 0000000..434e037 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/fixtures/get_lists.json @@ -0,0 +1,67 @@ +{ + "request": { + "api_url": "https://api.trello.com/1/boards/TEST/lists", + "params": { + "key": "TEST", + "token": "TEST" + } + }, + "response": [ + { + "id": "TEST", + "name": "TEST_A", + "cards": [ + { + "id": "TEST", + "name": "TEST_1" + } + ] + }, + { + "id": "TEST", + "name": "TEST_B", + "cards": [ + { + "id": "TEST", + "name": "TEST_2" + } + ] + } + ], + "response-headers": { + "X-Trello-Environment": "Production", + "Content-Length": "595", + "X-Trello-Version": "68", + "X-RATE-LIMIT-MEMBER-MAX": "200", + "Set-Cookie": "dsc=5cf993d4c95bff2ee0273937e979ace5b89a6629f4ec355860d1aac0e4b565cd; Path=/; Expires=Fri, 26 Jan 2018 11:29:12 GMT; Secure", + "X-Server-Time": "1516706952891", + "Surrogate-Control": "no-store", + "Expires": "Thu, 01 Jan 1970 00:00:00", + "X-RATE-LIMIT-MEMBER-REMAINING": "199", + "X-RATE-LIMIT-API-TOKEN-MAX": "100", + "X-Content-Type-Options": "nosniff", + "X-RATE-LIMIT-API-KEY-MAX": "300", + "Content-Encoding": "gzip", + "X-RATE-LIMIT-API-TOKEN-INTERVAL-MS": "10000", + "ETag": "W/\"FTxOEKThSaPt1Pa7iy1iSg==\"", + "Access-Control-Allow-Methods": "GET, PUT, POST, DELETE", + "X-RATE-LIMIT-API-KEY-INTERVAL-MS": "10000", + "Date": "Tue, 23 Jan 2018 11:29:13 GMT", + "X-Frame-Options": "DENY", + "Access-Control-Allow-Headers": "Authorization, Accept, Content-Type", + "Pragma": "no-cache", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Download-Options": "noopen", + "X-RATE-LIMIT-API-KEY-REMAINING": "299", + "Cache-Control": "max-age=0, must-revalidate, no-cache, no-store", + "X-DNS-Prefetch-Control": "off", + "Strict-Transport-Security": "max-age=15552000", + "Content-Type": "application/json; charset=utf-8", + "X-RATE-LIMIT-MEMBER-INTERVAL-MS": "10000", + "X-RATE-LIMIT-API-TOKEN-REMAINING": "99", + "Access-Control-Allow-Origin": "*", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + } +} diff --git a/zulip_bots/zulip_bots/bots/trello/requirements.txt b/zulip_bots/zulip_bots/bots/trello/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/zulip_bots/zulip_bots/bots/trello/test_trello.py b/zulip_bots/zulip_bots/bots/trello/test_trello.py new file mode 100644 index 0000000..aa2c884 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/test_trello.py @@ -0,0 +1,109 @@ +from unittest.mock import patch + +from zulip_bots.bots.trello.trello import TrelloHandler +from zulip_bots.test_lib import BotTestCase +from zulip_bots.test_lib import StubBotHandler + +mock_config = { + 'api_key': 'TEST', + 'access_token': 'TEST', + 'user_name': 'TEST' +} + +class TestTrelloBot(BotTestCase): + bot_name = "trello" # type: str + + def test_bot_responds_to_empty_message(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + self.verify_reply('', 'Empty Query') + + def test_bot_usage(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + self.verify_reply('help', ''' + This interactive bot can be used to interact with Trello. + + Use `list-commands` to get information about the supported commands. + ''') + + def test_bot_quit_with_invalid_config(self) -> None: + with self.mock_config_info(mock_config), self.assertRaises(StubBotHandler.BotQuitException): + TrelloHandler().initialize(StubBotHandler()) + + def test_invalid_command(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + self.verify_reply('abcd', 'Command not supported') + + def test_list_commands_command(self) -> None: + expected_reply = ('**Commands:** \n' + '1. **help**: Get the bot usage information.\n' + '2. **list-commands**: Get information about the commands supported by the bot.\n' + '3. **get-all-boards**: Get all the boards under the configured account.\n' + '4. **get-all-cards **: Get all the cards in the given board.\n' + '5. **get-all-checklists **: Get all the checklists in the given card.\n' + '6. **get-all-lists **: Get all the lists in the given board.\n') + + with self.mock_config_info(mock_config), patch('requests.get'): + self.verify_reply('list-commands', expected_reply) + + def test_get_all_boards_command(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_http_conversation('get_all_boards'): + self.verify_reply('get-all-boards', '**Boards:** \n') + + with self.mock_http_conversation('get_board_descs'): + bot_instance = TrelloHandler() + bot_instance.initialize(StubBotHandler) + + self.assertEqual(bot_instance.get_board_descs(['TEST']), '1.[TEST](TEST) (`TEST`)\n') + + def test_get_all_cards_command(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_http_conversation('get_cards'): + self.verify_reply('get-all-cards TEST', '**Cards:** \n1. [TEST](TEST) (`TEST`)\n') + + def test_get_all_checklists_command(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_http_conversation('get_checklists'): + self.verify_reply('get-all-checklists TEST', '**Checklists:** \n' + '1. `TEST`:\n' + ' * [X] TEST_1\n * [X] TEST_2\n' + ' * [-] TEST_3\n * [-] TEST_4\n') + + def test_get_all_lists_command(self) -> None: + with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_http_conversation('get_lists'): + self.verify_reply('get-all-lists TEST', ('**Lists:** \n' + '1. TEST_A\n' + ' * TEST_1\n' + '2. TEST_B\n' + ' * TEST_2\n')) + + def test_command_exceptions(self) -> None: + """Add appropriate tests here for all additional commands with try/except blocks. + This ensures consistency.""" + + expected_error_response = 'Invalid Response. Please check configuration and parameters.' + + with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_http_conversation('exception_boards'): + self.verify_reply('get-all-boards', expected_error_response) + + with self.mock_http_conversation('exception_cards'): + self.verify_reply('get-all-cards TEST', expected_error_response) + + with self.mock_http_conversation('exception_checklists'): + self.verify_reply('get-all-checklists TEST', expected_error_response) + + with self.mock_http_conversation('exception_lists'): + self.verify_reply('get-all-lists TEST', expected_error_response) + + def test_command_invalid_arguments(self) -> None: + """Add appropriate tests here for all additional commands with more than one arguments. + This ensures consistency.""" + + expected_error_response = 'Invalid Arguments.' + + with self.mock_config_info(mock_config), patch('requests.get'): + self.verify_reply('get-all-cards', expected_error_response) + self.verify_reply('get-all-checklists', expected_error_response) + self.verify_reply('get-all-lists', expected_error_response) diff --git a/zulip_bots/zulip_bots/bots/trello/trello.conf b/zulip_bots/zulip_bots/bots/trello/trello.conf new file mode 100644 index 0000000..0de899a --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/trello.conf @@ -0,0 +1,4 @@ +[trello] +api_key = +access_token = +user_name = diff --git a/zulip_bots/zulip_bots/bots/trello/trello.py b/zulip_bots/zulip_bots/bots/trello/trello.py new file mode 100644 index 0000000..c4db62b --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/trello.py @@ -0,0 +1,172 @@ +from typing import Any, List + +import requests + +supported_commands = [ + ('help', 'Get the bot usage information.'), + ('list-commands', 'Get information about the commands supported by the bot.'), + ('get-all-boards', 'Get all the boards under the configured account.'), + ('get-all-cards ', 'Get all the cards in the given board.'), + ('get-all-checklists ', 'Get all the checklists in the given card.'), + ('get-all-lists ', 'Get all the lists in the given board.') +] + +INVALID_ARGUMENTS_ERROR_MESSAGE = 'Invalid Arguments.' +RESPONSE_ERROR_MESSAGE = 'Invalid Response. Please check configuration and parameters.' + +class TrelloHandler(object): + def initialize(self, bot_handler: Any) -> None: + self.config_info = bot_handler.get_config_info('trello') + self.api_key = self.config_info['api_key'] + self.access_token = self.config_info['access_token'] + self.user_name = self.config_info['user_name'] + + self.auth_params = { + 'key': self.api_key, + 'token': self.access_token + } + + self.check_access_token(bot_handler) + + def check_access_token(self, bot_handler: Any) -> None: + test_query_response = requests.get('https://api.trello.com/1/members/{}/'.format(self.user_name), + params=self.auth_params) + + if test_query_response.text == 'invalid key': + bot_handler.quit('Invalid Credentials. Please see doc.md to find out how to get them.') + + def usage(self) -> str: + return ''' + This interactive bot can be used to interact with Trello. + + Use `list-commands` to get information about the supported commands. + ''' + + def handle_message(self, message: Any, bot_handler: Any) -> None: + content = message['content'].strip() + + if content == '': + bot_handler.send_reply(message, 'Empty Query') + return + elif content.lower() == 'help': + bot_handler.send_reply(message, self.usage()) + return + + if content.lower() == 'list-commands': + bot_reply = self.get_all_supported_commands() + elif content.lower() == 'get-all-boards': + bot_reply = self.get_all_boards() + else: + content = content.split() + content[0] = content[0].lower() + + if content[0] == 'get-all-cards': + bot_reply = self.get_all_cards(content) + elif content[0] == 'get-all-checklists': + bot_reply = self.get_all_checklists(content) + elif content[0] == 'get-all-lists': + bot_reply = self.get_all_lists(content) + else: + bot_reply = 'Command not supported' + + bot_handler.send_reply(message, bot_reply) + + def get_all_supported_commands(self) -> str: + bot_response = '**Commands:** \n' + for index, (command, desc) in enumerate(supported_commands): + bot_response += '{}. **{}**: {}\n'.format(index + 1, command, desc) + + return bot_response + + def get_all_boards(self) -> str: + get_board_ids_url = 'https://api.trello.com/1/members/{}/'.format(self.user_name) + board_ids_response = requests.get(get_board_ids_url, params=self.auth_params) + + try: + boards = board_ids_response.json()['idBoards'] + bot_response = '**Boards:** \n' + self.get_board_descs(boards) + + except (KeyError, ValueError, TypeError): + return RESPONSE_ERROR_MESSAGE + + return bot_response + + def get_board_descs(self, boards: List[str]) -> str: + bot_response = '' + get_board_desc_url = 'https://api.trello.com/1/boards/{}/' + for index, board in enumerate(boards): + board_desc_response = requests.get(get_board_desc_url.format(board), params=self.auth_params) + + board_data = board_desc_response.json() + bot_response += '{}.[{}]({}) (`{}`)\n'.format(index + 1, board_data['name'], board_data['url'], + board_data['id']) + + return bot_response + + def get_all_cards(self, content: List[str]) -> str: + if len(content) != 2: + return INVALID_ARGUMENTS_ERROR_MESSAGE + + board_id = content[1] + get_cards_url = 'https://api.trello.com/1/boards/{}/cards'.format(board_id) + cards_response = requests.get(get_cards_url, params=self.auth_params) + + try: + cards = cards_response.json() + bot_response = '**Cards:** \n' + for index, card in enumerate(cards): + bot_response += '{}. [{}]({}) (`{}`)\n'.format(index + 1, card['name'], card['url'], card['id']) + + except (KeyError, ValueError, TypeError): + return RESPONSE_ERROR_MESSAGE + + return bot_response + + def get_all_checklists(self, content: List[str]) -> str: + if len(content) != 2: + return INVALID_ARGUMENTS_ERROR_MESSAGE + + card_id = content[1] + get_checklists_url = 'https://api.trello.com/1/cards/{}/checklists/'.format(card_id) + checklists_response = requests.get(get_checklists_url, params=self.auth_params) + + try: + checklists = checklists_response.json() + bot_response = '**Checklists:** \n' + for index, checklist in enumerate(checklists): + bot_response += '{}. `{}`:\n'.format(index + 1, checklist['name']) + + if 'checkItems' in checklist: + for item in checklist['checkItems']: + bot_response += ' * [{}] {}\n'.format('X' if item['state'] == 'complete' else '-', item['name']) + + except (KeyError, ValueError, TypeError): + return RESPONSE_ERROR_MESSAGE + + return bot_response + + def get_all_lists(self, content: List[str]) -> str: + if len(content) != 2: + return INVALID_ARGUMENTS_ERROR_MESSAGE + + board_id = content[1] + get_lists_url = 'https://api.trello.com/1/boards/{}/lists'.format(board_id) + lists_response = requests.get(get_lists_url, params=self.auth_params) + + try: + lists = lists_response.json() + bot_response = '**Lists:** \n' + + for index, _list in enumerate(lists): + bot_response += '{}. {}\n'.format(index + 1, _list['name']) + + if 'cards' in _list: + for card in _list['cards']: + bot_response += ' * {}\n'.format(card['name']) + + except (KeyError, ValueError, TypeError): + return RESPONSE_ERROR_MESSAGE + + return bot_response + +handler_class = TrelloHandler