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 0000000..0eaf748 Binary files /dev/null and b/zulip_bots/zulip_bots/bots/trello/assets/list_commands.png differ diff --git a/zulip_bots/zulip_bots/bots/trello/doc.md b/zulip_bots/zulip_bots/bots/trello/doc.md new file mode 100644 index 0000000..1a9d301 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trello/doc.md @@ -0,0 +1,41 @@ +# Trello bot + +The Trello bot is a Zulip bot that enables interaction with Trello using the +[Trello API](https://developers.trello.com). + +To use the Trello bot, you can simply call it with `@` 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