├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── app.json ├── config.sample.json ├── env.template ├── example_scripts ├── README.md ├── args.js ├── misc │ └── verify-credentials.js ├── subscription_management │ ├── add-subscription-app-owner.js │ ├── add-subscription-other-user.js │ ├── get-subscription.js │ ├── get-subscriptions-count.js │ ├── get-subscriptions-list.js │ ├── remove-subscription-app-owner.js │ └── remove-subscription-other-user.js └── webhook_management │ ├── create-webhook-config.js │ ├── delete-webhook-config.js │ ├── get-webhook-config.js │ ├── validate-webhook-config-oauth2.js │ └── validate-webhook-config.js ├── helpers ├── auth.js ├── cache-route.js ├── security.js └── socket.js ├── package.json ├── public ├── javascript │ ├── activity.js │ └── main.js ├── stylesheets │ └── main.css └── webhook-48.png ├── routes ├── activity.js ├── sub-callbacks.js ├── subscriptions.js └── webhook.js ├── screenshot.png └── views ├── activity.ejs ├── index.ejs ├── partials ├── footer.ejs └── header.ejs ├── status.ejs ├── subscriptions.ejs └── webhook.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # OSX file 61 | .DS_Store 62 | 63 | # Project files 64 | .env 65 | config.json 66 | package-lock.json -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct v2.0 2 | 3 | This code of conduct outlines our expectations for participants within the [@TwitterOSS](https://twitter.com/twitteross) community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 4 | 5 | Our open source community strives to: 6 | 7 | * **Be friendly and patient.** 8 | * **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 9 | * **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 10 | * **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. 11 | * **Be careful in the words that you choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 12 | * Violent threats or language directed against another person. 13 | * Discriminatory jokes and language. 14 | * Posting sexually explicit or violent material. 15 | * Posting (or threatening to post) other people's personally identifying information ("doxing"). 16 | * Personal insults, especially those using racist or sexist terms. 17 | * Unwelcome sexual attention. 18 | * Advocating for, or encouraging, any of the above behavior. 19 | * Repeated harassment of others. In general, if someone asks you to stop, then stop. 20 | * **When we disagree, try to understand why**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 21 | 22 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. 23 | 24 | ### Diversity Statement 25 | 26 | We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. 27 | 28 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected 29 | characteristics above, including participants with disabilities. 30 | 31 | ### Reporting Issues 32 | 33 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via [opensource+codeofconduct@twitter.com](mailto:opensource+codeofconduct@twitter.com). All reports will be handled with discretion. In your report please include: 34 | 35 | - Your contact information. 36 | - Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please 37 | include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 38 | - Any additional information that may be helpful. 39 | 40 | After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. 41 | 42 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. 43 | 44 | ## Thanks 45 | 46 | This code of conduct is based on the [Open Code of Conduct](https://github.com/todogroup/opencodeofconduct) from the [TODOGroup](http://todogroup.org). 47 | 48 | We are thankful for their work and all the communities who have paved the way with code of conducts. 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to get patches from you! 4 | 5 | ## Getting Started 6 | 7 | We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/) 8 | 9 | 1. Fork the project 10 | 1. Check out the `master` branch 11 | 1. Create a feature branch 12 | 1. Write code and tests for your change 13 | 1. From your branch, make a pull request against `twitterdev/account-activity-dashboard/master` 14 | 1. Work with repo maintainers to get your change reviewed 15 | 1. Wait for your change to be pulled into `twitterdev/account-activity-dashboard/master` 16 | 1. Delete your feature branch 17 | 18 | # License 19 | 20 | By contributing your code, you agree to license your contribution under the 21 | terms of the APLv2: https://github.com/twitterdev/account-activity-dashboard/blob/master/LICENSE 22 | 23 | # Code of Conduct 24 | 25 | Read our [Code of Conduct](CODE_OF_CONDUCT.md) for the project. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # account-activity-dashboard 2 | 3 | Sample web app and helper scripts to get started with Twitter's premium Account Activity API (All Activities). Written in Node.js. Full documentation for this API can be found on the [Account Activity API reference](https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/overview). 4 | 5 | For the enterprise tier of the Account Activity API, please check out the [Enterprise Account Activity Dashboard sample app](https://github.com/twitterdev/account-activity-dashboard-enterprise). 6 | 7 | ## Dependencies 8 | 9 | * A Twitter app created on [developer.twitter.com](https://developer.twitter.com/en/apps), enabled for access to the Account Activity API 10 | * [Node.js](https://nodejs.org) 11 | * [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) or other webhost (optional) 12 | * [ngrok](https://ngrok.com/) or other tunneling service (optional) 13 | 14 | ## Create and configure a Twitter app 15 | 16 | 1. Create a Twitter app on [Twitter Developer](https://developer.twitter.com/en/apps) 17 | 18 | 2. On the **Permissions** tab ➡️ **Edit** ➡️ **Access permission** section ➡️ enable **Read, Write and direct messages**. 19 | 20 | 3. On the **Keys and Tokens** tab ➡️ **Access token & access token secret** section ➡️ click **Create** button. 21 | 22 | 4. On the **Keys and Tokens** tab, take note of the **consumer API key**, **consumer API secret**, **access token** and **access token secret**. 23 | 24 | ## Setup & run the Node.js web app 25 | 26 | 1. Clone this repository: 27 | 28 | ```bash 29 | git clone https://github.com/twitterdev/account-activity-dashboard.git 30 | ``` 31 | 32 | 2. Install Node.js dependencies: 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | 3. Pass your Twitter keys, tokens and webhook environment name as environment variables. Twitter keys and access tokens are found on your app page on your [App Dashboard](https://developer.twitter.com/apps). The basic auth properties can be anything you want, and are used for simple password protection to access the configuration UI. As an alternative, instead of setting up env variables, you can copy the `env.template` file into a file named `.env` and and add these details there. 39 | 40 | ```bash 41 | TWITTER_CONSUMER_KEY= # your consumer key 42 | TWITTER_CONSUMER_SECRET= # your consimer secret 43 | TWITTER_ACCESS_TOKEN= # your access token 44 | TWITTER_ACCESS_TOKEN_SECRET= # your access token secret 45 | TWITTER_WEBHOOK_ENV= # the name of your environment as specified in your App environment on Twitter Developer 46 | BASIC_AUTH_USER= # your basic auth user 47 | BASIC_AUTH_PASSWORD= # your basic auth password 48 | ``` 49 | 50 | 51 | 52 | 4. Run locally: 53 | 54 | ```bash 55 | npm start 56 | ``` 57 | 58 | 5. Deploy app or setup a tunnel to localhost. To deploy to Heroku see "Deploy to Heroku" instructions below. To setup a tunnel use something like [ngrok](https://ngrok.com/). 59 | 60 | Take note of your webhook URL. For example: 61 | 62 | ```text 63 | https://your.app.domain/webhook/twitter 64 | ``` 65 | 66 | 6. Take note of the deployed URL, revisit your developer.twitter.com Apps **Settings** page, and add the following URL values as whitelisted Callback URLs: 67 | 68 | ```text 69 | http(s)://your.app.domain/callbacks/addsub 70 | http(s)://your.app.domain/callbacks/removesub 71 | ``` 72 | 73 | ## Configure webhook to receive events 74 | 75 | To configure your webhook you can use this apps' web UI, or use the example scripts from the command line. 76 | 77 | ### Using the web UI 78 | 79 | Load the web app in your browser and follow the instructions below. 80 | 81 | 1. Setup webhook config. Navigate to the "manage webhook" view. Enter your webhook URL noted earlier and click "Create/Update." 82 | 83 | 2. Add a user subscription. Navigate to the "manage subscriptions" view. Click "add" and proceed with Twitter sign-in. Once complete your webhook will start to receive account activity events for the user. 84 | 85 | ### Using the command line example scripts 86 | 87 | These scripts should be executed from root of the project folder. Your environment, url or webhook ID should be passed in as command line arguments. 88 | 89 | 1. Create webhook config. 90 | 91 | ```bash 92 | node example_scripts/webhook_management/create-webhook-config.js -e -u 93 | ``` 94 | 95 | 2. Add a user subscription for the user that owns the app. 96 | 97 | ```bash 98 | node example_scripts/subscription_management/add-subscription-app-owner.js -e 99 | ``` 100 | 101 | 3. To add a user subscription for another user using PIN-based Twitter sign-in. 102 | 103 | ```bash 104 | node example_scripts/subscription_management/add-subscription-other-user.js -e 105 | ``` 106 | 107 | **Note:** More example scripts can be found in the [example_scripts](example_scripts) directory to: 108 | 109 | * Create, delete, retrieve and validate webhook configs. 110 | * Add, remove, retrieve, count and list user subscriptions. 111 | 112 | ## Deploy to Heroku (optional) 113 | 114 | 1. Init Heroku app. 115 | 116 | ```bash 117 | heroku create 118 | ``` 119 | 120 | 2. Run locally. 121 | 122 | ```text 123 | heroku local 124 | ``` 125 | 126 | 3. Configure environment variables for each See Heroku documentation on [Configuration and Config Vars](https://devcenter.heroku.com/articles/config-vars). 127 | 128 | 4. Deploy to Heroku. 129 | 130 | ```bash 131 | git push heroku master 132 | ``` 133 | 134 | **Note:** The free tier of Heroku will put your app to sleep after 30 minutes. On cold start, you app will have very high latency which may result in a CRC failure that deactivates your webhook. To trigger a challenge response request and re-validate, run the following script. 135 | 136 | ```bash 137 | node example_scripts/webhook_management/validate-webhook-config.js -e -i 138 | ``` 139 | 140 | ## Production considerations 141 | 142 | This app is for demonstration purposes only, and should not be used in production without further modifcations. Dependencies on databases, and other types of services are intentionally not within the scope of this sample app. Some considerations below: 143 | 144 | * With this basic application, user information is stored in server side sessions. This may not provide the best user experience or be the best solution for your use case, especially if you are adding more functionality. 145 | * The application can handle light usage, but you may experience API rate limit issues under heavier load. Consider storing data locally in a secure database, or caching requests. 146 | * To support multiple users (admins, team members, customers, etc), consider implementing a form of Access Control List for better security. 147 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const session = require('express-session') 4 | const passport = require('passport') 5 | const TwitterStrategy = require('passport-twitter') 6 | const uuid = require('uuid/v4') 7 | const security = require('./helpers/security') 8 | const auth = require('./helpers/auth') 9 | const cacheRoute = require('./helpers/cache-route') 10 | const socket = require('./helpers/socket') 11 | 12 | const app = express() 13 | 14 | app.set('port', (process.env.PORT || 5000)) 15 | app.set('views', __dirname + '/views') 16 | app.set('view engine', 'ejs') 17 | 18 | app.use(express.static(__dirname + '/public')) 19 | app.use(bodyParser.json()) 20 | app.use(bodyParser.urlencoded({ extended: true })) 21 | app.use(passport.initialize()); 22 | app.use(session({ 23 | secret: 'keyboard cat', 24 | resave: false, 25 | saveUninitialized: true 26 | })) 27 | 28 | // start server 29 | const server = app.listen(app.get('port'), function() { 30 | console.log('Node app is running on port', app.get('port')) 31 | }) 32 | 33 | // initialize socket.io 34 | socket.init(server) 35 | 36 | // form parser middleware 37 | var parseForm = bodyParser.urlencoded({ extended: false }) 38 | 39 | 40 | /** 41 | * Receives challenge response check (CRC) 42 | **/ 43 | app.get('/webhook/twitter', function(request, response) { 44 | 45 | var crc_token = request.query.crc_token 46 | 47 | if (crc_token) { 48 | var hash = security.get_challenge_response(crc_token, auth.twitter_oauth.consumer_secret) 49 | 50 | response.status(200); 51 | response.send({ 52 | response_token: 'sha256=' + hash 53 | }) 54 | } else { 55 | response.status(400); 56 | response.send('Error: crc_token missing from request.') 57 | } 58 | }) 59 | 60 | 61 | /** 62 | * Receives Account Acitivity events 63 | **/ 64 | app.post('/webhook/twitter', function(request, response) { 65 | 66 | console.log(request.body) 67 | 68 | socket.io.emit(socket.activity_event, { 69 | internal_id: uuid(), 70 | event: request.body 71 | }) 72 | 73 | response.send('200 OK') 74 | }) 75 | 76 | 77 | /** 78 | * Serves the home page 79 | **/ 80 | app.get('/', function(request, response) { 81 | response.render('index') 82 | }) 83 | 84 | 85 | /** 86 | * Subscription management 87 | **/ 88 | 89 | auth.basic = auth.basic || ((req, res, next) => next()) 90 | 91 | app.get('/subscriptions', auth.basic, cacheRoute(1000), require('./routes/subscriptions')) 92 | 93 | 94 | /** 95 | * Starts Twitter sign-in process for adding a user subscription 96 | **/ 97 | app.get('/subscriptions/add', passport.authenticate('twitter', { 98 | callbackURL: '/callbacks/addsub' 99 | })); 100 | 101 | /** 102 | * Starts Twitter sign-in process for removing a user subscription 103 | **/ 104 | app.get('/subscriptions/remove', passport.authenticate('twitter', { 105 | callbackURL: '/callbacks/removesub' 106 | })); 107 | 108 | 109 | /** 110 | * Webhook management routes 111 | **/ 112 | var webhook_view = require('./routes/webhook') 113 | app.get('/webhook', auth.basic, auth.csrf, webhook_view.get_config) 114 | app.post('/webhook/update', parseForm, auth.csrf, webhook_view.update_config) 115 | app.post('/webhook/validate', parseForm, auth.csrf, webhook_view.validate_config) 116 | app.post('/webhook/delete', parseForm, auth.csrf, webhook_view.delete_config) 117 | 118 | 119 | /** 120 | * Activity view 121 | **/ 122 | app.get('/activity', auth.basic, require('./routes/activity')) 123 | 124 | 125 | /** 126 | * Handles Twitter sign-in OAuth1.0a callbacks 127 | **/ 128 | app.get('/callbacks/:action', passport.authenticate('twitter', { failureRedirect: '/' }), 129 | require('./routes/sub-callbacks')) 130 | 131 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account-activity-dashboard", 3 | "description": "Sample web app and helper scripts to get started with Twitter's Account Activity API. Written in Node.js.", 4 | "repository": "https://github.com/twitterdev/account-activity-dashboard", 5 | "keywords": ["node", "express", "api", "webhook", "twitter"], 6 | "success_url": "/" 7 | } 8 | -------------------------------------------------------------------------------- /config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "TWITTER_CONSUMER_KEY": "your-consumer-key", 3 | "TWITTER_CONSUMER_SECRET": "your-consumer-secret", 4 | "TWITTER_ACCESS_TOKEN": "your-access-token", 5 | "TWITTER_ACCESS_TOKEN_SECRET": "your-access-token-secret", 6 | "TWITTER_WEBHOOK_ENV": "env-beta", 7 | "BASIC_AUTH_USER": "admin", 8 | "BASIC_AUTH_PASSWORD": "bluebird" 9 | } -------------------------------------------------------------------------------- /env.template: -------------------------------------------------------------------------------- 1 | TWITTER_CONSUMER_KEY= 2 | TWITTER_CONSUMER_SECRET= 3 | TWITTER_ACCESS_TOKEN= 4 | TWITTER_ACCESS_TOKEN_SECRET= 5 | TWITTER_WEBHOOK_ENV= -------------------------------------------------------------------------------- /example_scripts/README.md: -------------------------------------------------------------------------------- 1 | # Command line example scripts 2 | These scripts should be executed from root of the project folder. Your environment, url or webhook ID should be passed in as command line arguments. Your app keys and tokens should be defined in a `config.json` file at the root of this project folder. 3 | 4 | These scripts only work with the premium Account Activity API. 5 | 6 | --- 7 | 8 | ## Webhook Management 9 | 10 | **Create** webhook config. 11 | 12 | node example_scripts/webhook_management/create-webhook-config.js -e -u 13 | 14 | **Get** webook config details. 15 | 16 | node example_scripts/webhook_management/get-webhook-config.js -e 17 | 18 | **Delete** webook config. 19 | 20 | node example_scripts/webhook_management/delete-webhook-config.js -e 21 | 22 | **Validate** webook config (OAuth1). 23 | 24 | node example_scripts/webhook_management/validate-webhook-config.js -e -i 25 | 26 | **Validate** webook config (OAuth2). 27 | 28 | node example_scripts/webhook_management/validate-webhook-config-oauth2.js -e -i 29 | 30 | --- 31 | 32 | ## Subscription Management 33 | 34 | **Add** user subscription for the user that owns the app. 35 | 36 | node example_scripts/subscription_management/add-subscription-app-owner.js -e 37 | 38 | 39 | **Add** a user subscription for another user using PIN-based Twitter sign-in. 40 | 41 | node example_scripts/subscription_management/add-subscription-other-user.js -e 42 | 43 | **Get** a user subscription (check if it exists). 44 | 45 | node example_scripts/subscription_management/get-subscription.js -e 46 | 47 | **Remove** a user subscription for the user that owns the app. 48 | 49 | node example_scripts/subscription_management/remove-subscription-app-owner.js -e 50 | 51 | **Remove** a user subscription for another user using PIN-based Twitter sign-in. 52 | 53 | node example_scripts/subscription_management/remove-subscription-other-user.js -e 54 | 55 | **Get** subscriptions count. 56 | 57 | node example_scripts/subscription_management/get-subscriptions-count.js 58 | 59 | **Get** subscriptions list. 60 | 61 | node example_scripts/subscription_management/get-subscriptions-list.js -e 62 | -------------------------------------------------------------------------------- /example_scripts/args.js: -------------------------------------------------------------------------------- 1 | const commandLineArgs = require('command-line-args') 2 | 3 | 4 | /** 5 | * Sets up command line arguments for 6 | * all example scripts 7 | */ 8 | const optionDefinitions = [ 9 | { name: 'url', alias: 'u', type: String }, 10 | { name: 'environment', alias: 'e', type: String }, 11 | { name: 'webhookid', alias: 'i', type: String } 12 | ] 13 | 14 | 15 | module.exports = commandLineArgs(optionDefinitions) -------------------------------------------------------------------------------- /example_scripts/misc/verify-credentials.js: -------------------------------------------------------------------------------- 1 | var request = require('request-promise') 2 | var auth = require('../../helpers/auth.js') 3 | 4 | 5 | // request options 6 | request_options = { 7 | url: 'https://api.twitter.com/1.1/account/verify_credentials.json', 8 | oauth: auth.twitter_oauth 9 | } 10 | 11 | // get current user info 12 | request.get(request_options, function (error, response, body) { 13 | 14 | if (error) { 15 | console.log('Error retrieving user data.') 16 | console.log(error) 17 | return; 18 | } 19 | 20 | console.log(body) 21 | }) -------------------------------------------------------------------------------- /example_scripts/subscription_management/add-subscription-app-owner.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const queryString = require('query-string'); 3 | const prompt = require('prompt-promise'); 4 | const auth = require('../../helpers/auth.js') 5 | const args = require('../args.js') 6 | 7 | 8 | var request_options = { 9 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions.json', 10 | oauth: auth.twitter_oauth, 11 | resolveWithFullResponse: true 12 | } 13 | 14 | request.post(request_options).then(function (response) { 15 | console.log('HTTP response code:', response.statusCode) 16 | 17 | if (response.statusCode == 204) { 18 | console.log('Subscription added.') 19 | } 20 | }).catch(function (response) { 21 | console.log('Subscription was not able to be added.') 22 | console.log('- Verify environment name.') 23 | console.log('- Verify "Read, Write and Access direct messages" is enabled on apps.twitter.com.') 24 | console.log('Full error message below:') 25 | console.log(response.error) 26 | }) -------------------------------------------------------------------------------- /example_scripts/subscription_management/add-subscription-other-user.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const queryString = require('query-string'); 3 | const prompt = require('prompt-promise'); 4 | const auth = require('../../helpers/auth.js') 5 | const args = require('../args.js') 6 | 7 | 8 | // request options to start PIN-based Twitter sign-in process 9 | var request_token_request_options = { 10 | url: 'https://api.twitter.com/oauth/request_token?oauth_callback=oob', 11 | oauth: auth.twitter_oauth 12 | } 13 | 14 | var request_token_response; 15 | 16 | // generates URL for login and prompts for PIN 17 | request.get(request_token_request_options).then(function (body) { 18 | request_token_response = queryString.parse(body) 19 | 20 | console.log('Open this URL in a browser and sign-in with the Twitter account you wish to subscribe to:') 21 | console.log('https://api.twitter.com/oauth/authorize?oauth_token=' + request_token_response['oauth_token'] + '&force_login=true') 22 | 23 | return prompt('Enter the generated PIN:') 24 | }) 25 | 26 | // validates PIN and generates access tokens 27 | .then(function (prompt_reponse) { 28 | prompt.end() 29 | 30 | var access_token_request_options = { 31 | url: 'https://api.twitter.com/oauth/access_token?oauth_verifier=' + prompt_reponse, 32 | oauth: { 33 | consumer_key: auth.twitter_oauth['consumer_key'], 34 | consumer_secret: auth.twitter_oauth['consumer_secret'], 35 | token: request_token_response['oauth_token'], 36 | token_secret: request_token_response['oauth_token_secret'] 37 | } 38 | } 39 | 40 | return request.get(access_token_request_options) 41 | }) 42 | 43 | // adds subscription for user 44 | .then(function (body) { 45 | var access_tokens = queryString.parse(body) 46 | 47 | var subscription_request_options = { 48 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions.json', 49 | oauth: { 50 | consumer_key: auth.twitter_oauth['consumer_key'], 51 | consumer_secret: auth.twitter_oauth['consumer_secret'], 52 | token: access_tokens['oauth_token'], 53 | token_secret: access_tokens['oauth_token_secret'] 54 | }, 55 | resolveWithFullResponse: true 56 | } 57 | 58 | return request.post(subscription_request_options) 59 | }) 60 | 61 | // add subscription success 62 | .then(function (response) { 63 | console.log('HTTP response code:', response.statusCode) 64 | 65 | if (response.statusCode == 204) { 66 | console.log('Subscription added.') 67 | } 68 | }) 69 | 70 | // add subscrition error 71 | .catch(function (response) { 72 | console.log('Subscription was not able to be added.') 73 | console.log('- Verify environment name.') 74 | console.log('- Verify correct PIN was used.') 75 | console.log('- Verify "Read, Write and Access direct messages" is enabled on apps.twitter.com.') 76 | console.log('Full error message below:') 77 | console.log(response) 78 | }) 79 | -------------------------------------------------------------------------------- /example_scripts/subscription_management/get-subscription.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions.json', 9 | oauth: auth.twitter_oauth, 10 | resolveWithFullResponse: true 11 | } 12 | 13 | 14 | // GET request to retrieve webhook config 15 | request.get(request_options).then(function (response) { 16 | console.log('HTTP response code:', response.statusCode) 17 | 18 | if (response.statusCode == 204) { 19 | console.log('Subscription exists for user.') 20 | } 21 | }).catch(function (response) { 22 | console.log('HTTP response code:', response.statusCode) 23 | console.log('Incorrect environment name or subscription for user does not exist.') 24 | }) -------------------------------------------------------------------------------- /example_scripts/subscription_management/get-subscriptions-count.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | 4 | 5 | auth.get_twitter_bearer_token().then(function (bearer_token) { 6 | 7 | // request options 8 | var request_options = { 9 | url: 'https://api.twitter.com/1.1/account_activity/all/count.json', 10 | auth: { 11 | 'bearer': bearer_token 12 | } 13 | } 14 | 15 | request.get(request_options).then(function (body) { 16 | console.log(body) 17 | }) 18 | }) 19 | 20 | 21 | -------------------------------------------------------------------------------- /example_scripts/subscription_management/get-subscriptions-list.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | auth.get_twitter_bearer_token().then(function (bearer_token) { 7 | 8 | // request options 9 | var request_options = { 10 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions/list.json', 11 | auth: { 12 | 'bearer': bearer_token 13 | } 14 | } 15 | console.log(request_options) 16 | request.get(request_options).then(function (body) { 17 | console.log(body) 18 | }) 19 | }) -------------------------------------------------------------------------------- /example_scripts/subscription_management/remove-subscription-app-owner.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions.json', 9 | oauth: auth.twitter_oauth, 10 | resolveWithFullResponse: true 11 | } 12 | 13 | // POST request to create webhook config 14 | request.delete(request_options).then(function (response) { 15 | console.log('HTTP response code:', response.statusCode) 16 | 17 | if (response.statusCode == 204) { 18 | console.log('Subscription removed.') 19 | } 20 | }).catch(function (response) { 21 | console.log('HTTP response code:', response.statusCode) 22 | console.log('Incorrect environment name or user has not authorized your app.') 23 | }) -------------------------------------------------------------------------------- /example_scripts/subscription_management/remove-subscription-other-user.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const queryString = require('query-string'); 3 | const prompt = require('prompt-promise'); 4 | const auth = require('../../helpers/auth.js') 5 | const args = require('../args.js') 6 | 7 | 8 | // request options to start PIN-based Twitter sign-in process 9 | var request_token_request_options = { 10 | url: 'https://api.twitter.com/oauth/request_token?oauth_callback=oob', 11 | oauth: auth.twitter_oauth 12 | } 13 | 14 | var request_token_response; 15 | 16 | // generates URL for login and prompts for PIN 17 | request.get(request_token_request_options).then(function (body) { 18 | request_token_response = queryString.parse(body) 19 | 20 | console.log('Open this URL in a browser and sign-in with the Twitter account you wish to remove subscription from:') 21 | console.log('https://api.twitter.com/oauth/authorize?oauth_token=' + request_token_response['oauth_token'] + '&force_login=true') 22 | 23 | return prompt('Enter the generated PIN:') 24 | }) 25 | 26 | // validates PIN and generates access tokens 27 | .then(function (prompt_reponse) { 28 | prompt.end() 29 | 30 | var access_token_request_options = { 31 | url: 'https://api.twitter.com/oauth/access_token?oauth_verifier=' + prompt_reponse, 32 | oauth: { 33 | consumer_key: auth.twitter_oauth['consumer_key'], 34 | consumer_secret: auth.twitter_oauth['consumer_secret'], 35 | token: request_token_response['oauth_token'], 36 | token_secret: request_token_response['oauth_token_secret'] 37 | } 38 | } 39 | 40 | return request.get(access_token_request_options) 41 | }) 42 | 43 | // removes subscription for user 44 | .then(function (body) { 45 | var access_tokens = queryString.parse(body) 46 | 47 | var subscription_request_options = { 48 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/subscriptions.json', 49 | oauth: { 50 | consumer_key: auth.twitter_oauth['consumer_key'], 51 | consumer_secret: auth.twitter_oauth['consumer_secret'], 52 | token: access_tokens['oauth_token'], 53 | token_secret: access_tokens['oauth_token_secret'] 54 | }, 55 | resolveWithFullResponse: true 56 | } 57 | 58 | return request.delete(subscription_request_options) 59 | }) 60 | 61 | // add subscription success 62 | .then(function (response) { 63 | console.log('HTTP response code:', response.statusCode) 64 | 65 | if (response.statusCode == 204) { 66 | console.log('Subscription removed.') 67 | } 68 | }) 69 | 70 | // add subscrition error 71 | .catch(function (response) { 72 | console.log('Subscription was not able to be removed.') 73 | console.log('- Verify environment name.') 74 | console.log('- Verify correct PIN was used.') 75 | console.log('- Verify "Read, Write and Access direct messages" is enabled on apps.twitter.com.') 76 | console.log('Full error message below:') 77 | console.log(response) 78 | }) 79 | -------------------------------------------------------------------------------- /example_scripts/webhook_management/create-webhook-config.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks.json', 9 | oauth: auth.twitter_oauth, 10 | headers: { 11 | 'Content-type': 'application/x-www-form-urlencoded' 12 | }, 13 | form: { 14 | url: args.url 15 | } 16 | } 17 | 18 | 19 | // POST request to create webhook config 20 | request.post(request_options).then(function (body) { 21 | console.log(body) 22 | }).catch(function (body) { 23 | console.log(body) 24 | }) -------------------------------------------------------------------------------- /example_scripts/webhook_management/delete-webhook-config.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks.json', 9 | oauth: auth.twitter_oauth 10 | } 11 | 12 | 13 | // GET request to retreive webhook config 14 | request.get(request_options).then( function (body) { 15 | // parse webhook ID 16 | var webhook_id = JSON.parse(body)[0].id 17 | 18 | console.log('Deleting webhook config:', webhook_id) 19 | 20 | // update request options for delete endpoint 21 | request_options = { 22 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks/' + webhook_id + '.json', 23 | oauth: auth.twitter_oauth, 24 | resolveWithFullResponse: true 25 | } 26 | 27 | // DELETE request to delete webhook config 28 | return request.delete(request_options) 29 | 30 | }).then(function (response) { 31 | console.log('HTTP response code:', response.statusCode) 32 | 33 | if (response.statusCode == 204) { 34 | console.log('Webhook config deleted.') 35 | } 36 | }).catch(function (response) { 37 | console.log('HTTP response code:', response.statusCode) 38 | console.log('Error deleting webhook config.') 39 | }) 40 | -------------------------------------------------------------------------------- /example_scripts/webhook_management/get-webhook-config.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks.json', 9 | oauth: auth.twitter_oauth 10 | } 11 | 12 | 13 | // GET request to retreive webhook config 14 | request.get(request_options, function (error, response, body) { 15 | console.log(body) 16 | }) -------------------------------------------------------------------------------- /example_scripts/webhook_management/validate-webhook-config-oauth2.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | auth.get_twitter_bearer_token().then(function (bearer_token) { 7 | 8 | // request options 9 | var request_options = { 10 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks/' + args.webhookid + '.json', 11 | resolveWithFullResponse: true, 12 | auth: { 13 | 'bearer': bearer_token 14 | } 15 | } 16 | 17 | // PUT request to retrieve webhook config 18 | request.put(request_options).then(function (response) { 19 | console.log('HTTP response code:', response.statusCode) 20 | console.log('CRC request successful and webhook status set to valid.') 21 | }).catch(function (response) { 22 | console.log('HTTP response code:', response.statusCode) 23 | console.log(response.error) 24 | }) 25 | }) -------------------------------------------------------------------------------- /example_scripts/webhook_management/validate-webhook-config.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../../helpers/auth.js') 3 | const args = require('../args.js') 4 | 5 | 6 | // request options 7 | var request_options = { 8 | url: 'https://api.twitter.com/1.1/account_activity/all/' + args.environment + '/webhooks/' + args.webhookid + '.json', 9 | oauth: auth.twitter_oauth, 10 | resolveWithFullResponse: true 11 | } 12 | 13 | 14 | // PUT request to validate webhook config 15 | request.put(request_options).then(function (response) { 16 | console.log('HTTP response code:', response.statusCode) 17 | console.log('CRC request successful and webhook status set to valid.') 18 | }).catch(function (response) { 19 | console.log('HTTP response code:', response.statusCode) 20 | console.log(response.error) 21 | }) -------------------------------------------------------------------------------- /helpers/auth.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const queryString = require('query-string') 3 | const passport = require('passport') 4 | const TwitterStrategy = require('passport-twitter') 5 | const httpAuth = require('http-auth') 6 | 7 | require('dotenv').config() 8 | 9 | var auth = {} 10 | 11 | 12 | const RequiredEnv = [ 13 | 'TWITTER_CONSUMER_KEY', 14 | 'TWITTER_CONSUMER_SECRET', 15 | 'TWITTER_ACCESS_TOKEN', 16 | 'TWITTER_ACCESS_TOKEN_SECRET', 17 | 'TWITTER_WEBHOOK_ENV', 18 | ] 19 | 20 | if (!RequiredEnv.every(key => typeof process.env[key] !== 'undefined')) { 21 | console.error(`One of more of the required environment variables (${RequiredEnv.join(', ')}) are not defined. Please check your environment and try again.`) 22 | process.exit(-1) 23 | } 24 | 25 | // twitter info 26 | auth.twitter_oauth = { 27 | consumer_key: process.env.TWITTER_CONSUMER_KEY, 28 | consumer_secret: process.env.TWITTER_CONSUMER_SECRET, 29 | token: process.env.TWITTER_ACCESS_TOKEN, 30 | token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET 31 | } 32 | auth.twitter_webhook_environment = process.env.TWITTER_WEBHOOK_ENV 33 | 34 | 35 | // basic auth middleware for express 36 | 37 | if (typeof process.env.BASIC_AUTH_USER !== 'undefined' && 38 | typeof process.env.BASIC_AUTH_PASSWORD !== 'undefined') { 39 | auth.basic = httpAuth.connect(httpAuth.basic({ 40 | realm: 'admin-dashboard' 41 | }, function(username, password, callback) { 42 | callback(username === process.env.BASIC_AUTH_USER && password === process.env.BASIC_AUTH_PASSWORD) 43 | })) 44 | } else { 45 | console.warn([ 46 | 'Your admin dashboard is accessible by everybody.', 47 | 'To restrict access, setup BASIC_AUTH_USER and BASIC_AUTH_PASSWORD', 48 | 'as environment variables.', 49 | ].join(' ')) 50 | } 51 | 52 | 53 | // csrf protection middleware for express 54 | auth.csrf = require('csurf')() 55 | 56 | 57 | // Configure the Twitter strategy for use by Passport. 58 | passport.use(new TwitterStrategy({ 59 | consumerKey: auth.twitter_oauth.consumer_key, 60 | consumerSecret: auth.twitter_oauth.consumer_secret, 61 | // we want force login, so we set the URL with the force_login=true 62 | userAuthorizationURL: 'https://api.twitter.com/oauth/authenticate?force_login=true' 63 | }, 64 | // stores profile and tokens in the sesion user object 65 | // this may not be the best solution for your application 66 | function(token, tokenSecret, profile, cb) { 67 | return cb(null, { 68 | profile: profile, 69 | access_token: token, 70 | access_token_secret: tokenSecret 71 | }) 72 | } 73 | )) 74 | 75 | // Configure Passport authenticated session persistence. 76 | passport.serializeUser(function(user, cb) { 77 | cb(null, user); 78 | }) 79 | 80 | passport.deserializeUser(function(obj, cb) { 81 | cb(null, obj); 82 | }) 83 | 84 | 85 | /** 86 | * Retrieves a Twitter Sign-in auth URL for OAuth1.0a 87 | */ 88 | auth.get_twitter_auth_url = function (host, callback_action) { 89 | 90 | // construct request to retrieve authorization token 91 | var request_options = { 92 | url: 'https://api.twitter.com/oauth/request_token', 93 | method: 'POST', 94 | oauth: { 95 | callback: 'https://' + host + '/callbacks/twitter/' + callback_action, 96 | consumer_key: auth.twitter_oauth.consumer_key, 97 | consumer_secret: auth.twitter_oauth.consumer_secret 98 | } 99 | } 100 | 101 | return new Promise (function (resolve, reject) { 102 | request(request_options, function(error, response) { 103 | if (error) { 104 | reject(error) 105 | } 106 | else { 107 | // construct sign-in URL from returned authorization token 108 | var response_params = queryString.parse(response.body) 109 | console.log(response_params) 110 | var twitter_auth_url = 'https://api.twitter.com/oauth/authenticate?force_login=true&oauth_token=' + response_params.oauth_token 111 | 112 | resolve({ 113 | response_params: response_params, 114 | twitter_auth_url: twitter_auth_url 115 | }) 116 | } 117 | }) 118 | }) 119 | } 120 | 121 | 122 | /** 123 | * Retrieves a bearer token for OAuth2 124 | */ 125 | auth.get_twitter_bearer_token = function () { 126 | 127 | // just return the bearer token if we already have one 128 | if (auth.twitter_bearer_token) { 129 | return new Promise (function (resolve, reject) { 130 | resolve(auth.twitter_bearer_token) 131 | }) 132 | } 133 | 134 | // construct request for bearer token 135 | var request_options = { 136 | url: 'https://api.twitter.com/oauth2/token', 137 | method: 'POST', 138 | auth: { 139 | user: auth.twitter_oauth.consumer_key, 140 | pass: auth.twitter_oauth.consumer_secret 141 | }, 142 | form: { 143 | 'grant_type': 'client_credentials' 144 | } 145 | } 146 | 147 | return new Promise (function (resolve, reject) { 148 | request(request_options, function(error, response) { 149 | if (error) { 150 | reject(error) 151 | } 152 | else { 153 | var json_body = JSON.parse(response.body) 154 | console.log("Bearer Token:", json_body.access_token) 155 | auth.twitter_bearer_token = json_body.access_token 156 | resolve(auth.twitter_bearer_token) 157 | } 158 | }) 159 | }) 160 | } 161 | 162 | 163 | module.exports = auth -------------------------------------------------------------------------------- /helpers/cache-route.js: -------------------------------------------------------------------------------- 1 | const mcache = require('memory-cache') 2 | 3 | 4 | /** 5 | * Express.js middleware to cache route 6 | * responses in memory. 7 | */ 8 | module.exports = (duration) => { 9 | return (request, response, next) => { 10 | var key = '__express__' + request.originalUrl || request.url 11 | var cached_response = mcache.get(key) 12 | if (cached_response) { 13 | response.send(cached_response) 14 | return 15 | } else { 16 | response.sendResponse = response.send 17 | response.send = (body) => { 18 | mcache.put(key, body, duration); 19 | response.sendResponse(body) 20 | } 21 | next() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /helpers/security.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | 4 | /** 5 | * Creates a HMAC SHA-256 hash created from the app TOKEN and 6 | * your app Consumer Secret. 7 | * @param token the token provided by the incoming GET request 8 | * @return string 9 | */ 10 | module.exports.get_challenge_response = function(crc_token, consumer_secret) { 11 | 12 | hmac = crypto.createHmac('sha256', consumer_secret).update(crc_token).digest('base64') 13 | 14 | return hmac 15 | } -------------------------------------------------------------------------------- /helpers/socket.js: -------------------------------------------------------------------------------- 1 | const socketIO = require('socket.io') 2 | const http = require('http') 3 | const uuid = require('uuid/v4') 4 | 5 | 6 | var socket = { } 7 | 8 | socket.activity_event = 'activity_event_' + uuid() 9 | 10 | 11 | /** 12 | * Initilaizes socket.io 13 | */ 14 | socket.init = function (server) { 15 | socket.io = socketIO(server) 16 | 17 | // connect and disconnect handlers 18 | socket.io.on('connection', (s) => { 19 | console.log('Client connected') 20 | s.on('disconnect', () => { 21 | console.log('Client disconnected') 22 | }) 23 | }) 24 | } 25 | 26 | 27 | module.exports = socket -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-webhook-boilerplate-node", 3 | "version": "1.0.0", 4 | "description": "Sample web app and helper scripts to get started with Twitter's Account Activity API", 5 | "engines": { 6 | "node": "5.9.1" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "start": "node app.js" 11 | }, 12 | "dependencies": { 13 | "body-parser": "^1.16.1", 14 | "command-line-args": "^5.0.1", 15 | "csurf": "^1.9.0", 16 | "dotenv": "^8.0.0", 17 | "ejs": "^2.5.7", 18 | "express": "4.13.3", 19 | "express-session": "^1.15.6", 20 | "http-auth": "^3.2.3", 21 | "memory-cache": "^0.2.0", 22 | "passport": "^0.4.0", 23 | "passport-twitter": "^1.0.4", 24 | "prompt-promise": "^1.0.3", 25 | "query-string": "^5.0.1", 26 | "request": "^2.83.0", 27 | "request-promise": "^4.2.2", 28 | "socket.io": "2.0.4", 29 | "uuid": "^3.2.1" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/twitterdev/account-activity-dashboard" 34 | }, 35 | "keywords": [ 36 | "node", 37 | "heroku", 38 | "express", 39 | "webhook", 40 | "twitter" 41 | ], 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /public/javascript/activity.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var activity = {}; 4 | 5 | 6 | /** 7 | * Connect to socket 8 | */ 9 | activity.connect = function (socket_host, activity_event) { 10 | io.connect(socket_host).on(activity_event, function (data) { 11 | // get template 12 | var tmpl_source = document.getElementById('json_template').innerHTML; 13 | var template = Handlebars.compile(tmpl_source); 14 | var cards = $('#event-container .card'); 15 | // control max events 16 | if(cards.length >= 100) { 17 | cards.last().remove(); 18 | } 19 | // render 20 | $('#event-container').prepend(template({ 21 | internal_id: data.internal_id, 22 | json_str: JSON.stringify(data.event, null, 2) 23 | })); 24 | $('#waiting-msg').hide(); 25 | }); 26 | }; 27 | 28 | 29 | /** 30 | * Copies json string to clipboard 31 | */ 32 | activity.copy_json = function (internal_id) { 33 | var textarea = $('#json-str-'+internal_id); 34 | textarea.prop('disabled', false); 35 | textarea.select(); 36 | document.execCommand('copy'); 37 | document.getSelection().removeAllRanges(); 38 | textarea.prop('disabled', true); 39 | textarea.blur(); 40 | }; 41 | 42 | 43 | /** 44 | * Expands card to full height of json string 45 | */ 46 | activity.expand = function (internal_id) { 47 | var textarea = $('#json-str-'+internal_id); 48 | if (textarea.height() > 200) { 49 | textarea.height(200); 50 | $('#max-btn-'+internal_id).show(); 51 | $('#min-btn-'+internal_id).hide(); 52 | } else { 53 | textarea.height(textarea[0].scrollHeight); 54 | $('#max-btn-'+internal_id).hide(); 55 | $('#min-btn-'+internal_id).show(); 56 | } 57 | }; 58 | 59 | 60 | /** 61 | * Marke card by highlighting header yello 62 | */ 63 | activity.mark = function (internal_id) { 64 | var header = $('#json-header-'+internal_id); 65 | if (header.hasClass('bg-warning')) { 66 | header.removeClass('bg-warning'); 67 | } else { 68 | header.addClass('bg-warning'); 69 | } 70 | }; 71 | 72 | 73 | /** 74 | * Dismisses about message 75 | */ 76 | activity.dismiss_about_msg = function () { 77 | $('#about-msg').hide(); 78 | }; 79 | 80 | 81 | window.activity = activity; 82 | 83 | })() -------------------------------------------------------------------------------- /public/javascript/main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | })(); -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0 0 0 0; 7 | background: linear-gradient(#FFF, #DDD); 8 | background-attachment: fixed; 9 | height: 100%; 10 | overflow-y: scroll; 11 | } 12 | 13 | nav, .card { 14 | margin: 0 0 30px 0; 15 | } 16 | 17 | .jumbotron { 18 | background-color: #EFEFEF; 19 | } 20 | 21 | nav, .jumbotron, .card { 22 | box-shadow: 0px 8px 10px #CCC; 23 | } 24 | 25 | .json-event textarea { 26 | height: 200px; 27 | font-family: "Courier New", Courier, monospace; 28 | background-color: #f8f9fa; 29 | } 30 | 31 | .json-event textarea:disabled { 32 | background-color: #f8f9fa; 33 | } 34 | 35 | .fa-window-minimize { 36 | display: none; 37 | } 38 | 39 | .icon-btn { 40 | cursor: pointer; 41 | } 42 | 43 | .icon-btn:hover { 44 | color: #333; 45 | } 46 | 47 | #home-menu li { 48 | list-style: none; 49 | } -------------------------------------------------------------------------------- /public/webhook-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/account-activity-dashboard/565c0a5c31476078fdf27b4804f9c36c2ad91a1d/public/webhook-48.png -------------------------------------------------------------------------------- /routes/activity.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../helpers/auth') 3 | const socket = require('../helpers/socket') 4 | 5 | 6 | var activity = function (req, resp) { 7 | var json_response = { 8 | socket_host: req.headers.host.indexOf('localhost') == 0 ? 'http://' + req.headers.host : 'https://' + req.headers.host, 9 | activity_event: socket.activity_event 10 | } 11 | resp.render('activity', json_response) 12 | } 13 | 14 | 15 | module.exports = activity -------------------------------------------------------------------------------- /routes/sub-callbacks.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const passport = require('passport') 3 | const auth = require('../helpers/auth.js') 4 | 5 | 6 | var sub_request_options = { 7 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/subscriptions.json', 8 | oauth: auth.twitter_oauth, 9 | resolveWithFullResponse: true 10 | } 11 | 12 | var actions = {} 13 | 14 | actions.addsub = function (user) { 15 | sub_request_options.oauth.token = user.access_token 16 | sub_request_options.oauth.token_secret = user.access_token_secret 17 | 18 | return request.post(sub_request_options) 19 | } 20 | 21 | actions.removesub = function (user) { 22 | sub_request_options.oauth.token = user.access_token 23 | sub_request_options.oauth.token_secret = user.access_token_secret 24 | 25 | return request.delete(sub_request_options) 26 | } 27 | 28 | 29 | module.exports = function (req, resp) { 30 | if (actions[req.params.action]) { 31 | actions[req.params.action](req.user).then(function (response) { 32 | var json_response = { 33 | title: 'Success', 34 | message: 'Subscriptions successfully modified.', 35 | button: { 36 | title: 'Ok', 37 | url: '/subscriptions' 38 | } 39 | } 40 | resp.render('status', json_response) 41 | }).catch(function (response) { 42 | console.log(response) 43 | var json_response = { 44 | title: 'Error', 45 | message: 'Subscriptions unable to be modified.', 46 | button: { 47 | title: 'Ok', 48 | url: '/subscriptions' 49 | } 50 | } 51 | if (response.error) { 52 | json_response.message = JSON.parse(response.error).errors[0].message 53 | } 54 | resp.status(500) 55 | resp.render('status', json_response) 56 | }) 57 | } else { 58 | var json_response = { 59 | title: 'Error', 60 | message: 'Action "' + req.params.action + '"" not defined.', 61 | button: { 62 | title: 'Ok', 63 | url: '/subscriptions' 64 | } 65 | } 66 | resp.status(404); 67 | resp.render('status', json_response) 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /routes/subscriptions.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../helpers/auth.js') 3 | 4 | 5 | module.exports = function (req, response) { 6 | var saved_bearer_token 7 | var json_response 8 | 9 | // get list of subs 10 | auth.get_twitter_bearer_token().then(function (bearer_token) { 11 | saved_bearer_token = bearer_token 12 | var request_options = { 13 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/subscriptions/list.json', 14 | auth: { 15 | 'bearer': saved_bearer_token 16 | } 17 | } 18 | 19 | return request.get(request_options) 20 | 21 | }) 22 | 23 | // hydrate user objects from IDs 24 | .then(function (body) { 25 | var json_body = json_response = JSON.parse(body) 26 | 27 | // if no subs, render as is and skip user hydration 28 | if (!json_body.subscriptions.length) { 29 | response.render('subscriptions', json_body) 30 | return Promise.resolve() 31 | } 32 | 33 | // construct comma delimited list of user IDs for user hydration 34 | var user_id 35 | json_body.subscriptions.forEach(function(sub) { 36 | if (user_id) { 37 | user_id = user_id + ',' + sub.user_id 38 | } else { 39 | user_id = sub.user_id 40 | } 41 | }); 42 | 43 | var request_options = { 44 | url: 'https://api.twitter.com/1.1/users/lookup.json?user_id=' + user_id, 45 | auth: { 46 | 'bearer': saved_bearer_token 47 | } 48 | } 49 | 50 | return request.get(request_options) 51 | }) 52 | 53 | // replace the subscriptions list with list of user objects 54 | // and render list 55 | .then(function (body) { 56 | // only render if we didn't skip user hydration 57 | if (body) { 58 | json_response.subscriptions = JSON.parse(body) 59 | response.render('subscriptions', json_response) 60 | } 61 | }) 62 | 63 | .catch(function (body) { 64 | console.log(body) 65 | 66 | var json_response = { 67 | title: 'Error', 68 | message: 'Subscriptions could not be retrieved.', 69 | button: { 70 | title: 'Ok', 71 | url: '/' 72 | } 73 | } 74 | 75 | resp.status(500); 76 | resp.render('status', json_response) 77 | }) 78 | 79 | } -------------------------------------------------------------------------------- /routes/webhook.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | const auth = require('../helpers/auth.js') 3 | 4 | 5 | var webhook = {} 6 | 7 | 8 | /** 9 | * Retrieves existing webhook config and renders 10 | */ 11 | webhook.get_config = function (req, resp) { 12 | // construct request to retrieve webhook config 13 | var request_options = { 14 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/webhooks.json', 15 | oauth: auth.twitter_oauth 16 | } 17 | 18 | request.get(request_options) 19 | 20 | // success 21 | .then(function (body) { 22 | var json_response = { 23 | configs: JSON.parse(body), 24 | csrf_token: req.csrfToken(), 25 | update_webhook_url: 'https://' + req.headers.host + '/webhook/twitter' 26 | } 27 | 28 | if (json_response.configs.length) { 29 | json_response.update_webhook_url = json_response.configs[0].url 30 | } 31 | 32 | console.log(json_response) 33 | resp.render('webhook', json_response) 34 | }) 35 | 36 | // failure 37 | .catch(function (body) { 38 | if (body) { 39 | console.log(body) 40 | } 41 | var json_response = { 42 | title: 'Error', 43 | message: 'Webhook config unable to be retrieved', 44 | button: { 45 | title: 'Ok', 46 | url: '/webhook' 47 | } 48 | } 49 | 50 | resp.status(500); 51 | resp.render('status', json_response) 52 | }) 53 | } 54 | 55 | 56 | /** 57 | * Triggers challenge response check 58 | */ 59 | webhook.validate_config = function (req, resp) { 60 | // get bearer token 61 | auth.get_twitter_bearer_token() 62 | 63 | // validate webhook config 64 | .then(function (bearer_token) { 65 | 66 | // request options 67 | var request_options = { 68 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/webhooks/' + req.body.webhook_id + '.json', 69 | resolveWithFullResponse: true, 70 | auth: { 71 | 'bearer': bearer_token 72 | } 73 | } 74 | 75 | // PUT request to retreive webhook config 76 | request.put(request_options) 77 | 78 | // success 79 | .then(function (response) { 80 | var json_response = { 81 | title: 'Success', 82 | message: 'Challenge request successful and webhook status set to valid.', 83 | button: { 84 | title: 'Ok', 85 | url: '/webhook' 86 | } 87 | } 88 | 89 | resp.render('status', json_response) 90 | }) 91 | 92 | // failure 93 | .catch(function (response) { 94 | var json_response = { 95 | title: 'Error', 96 | message: response.error, 97 | button: { 98 | title: 'Ok', 99 | url: '/webhook' 100 | } 101 | } 102 | 103 | resp.render('status', json_response) 104 | }) 105 | }) 106 | } 107 | 108 | 109 | /** 110 | * Deletes exiting webhook config 111 | * then creates new webhook config 112 | */ 113 | webhook.update_config = function (req, resp) { 114 | // delete webhook config 115 | delete_webhook(req.body.webhook_id) 116 | 117 | // create new webhook config 118 | .then(function () { 119 | var request_options = { 120 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/webhooks.json', 121 | oauth: auth.twitter_oauth, 122 | headers: { 123 | 'Content-type': 'application/x-www-form-urlencoded' 124 | }, 125 | form: { 126 | url: req.body.url 127 | } 128 | } 129 | 130 | return request.post(request_options) 131 | }) 132 | 133 | // render success response 134 | .then(function (body) { 135 | var json_response = { 136 | title: 'Success', 137 | message: 'Webhook successfully updated.', 138 | button: { 139 | title: 'Ok', 140 | url: '/webhook' 141 | } 142 | } 143 | 144 | resp.render('status', json_response) 145 | }) 146 | 147 | // render error response 148 | .catch(function (body) { 149 | var json_response = { 150 | title: 'Error', 151 | message: 'Webhook not updated.', 152 | button: { 153 | title: 'Ok', 154 | url: '/webhook' 155 | } 156 | } 157 | console.log(body) 158 | // Look for detailed error 159 | if (body.error) { 160 | json_response.message = JSON.parse(body.error).errors[0].message 161 | } 162 | 163 | resp.render('status', json_response) 164 | }) 165 | } 166 | 167 | 168 | /** 169 | * Deletes existing webhook config 170 | */ 171 | webhook.delete_config = function (req, resp) { 172 | 173 | // delete webhook config 174 | delete_webhook(req.body.webhook_id) 175 | 176 | // render success response 177 | .then(function (body) { 178 | var json_response = { 179 | title: 'Success', 180 | message: 'Webhook successfully deleted.', 181 | button: { 182 | title: 'Ok', 183 | url: '/webhook' 184 | } 185 | } 186 | 187 | resp.render('status', json_response) 188 | }) 189 | 190 | // render error response 191 | .catch(function () { 192 | var json_response = { 193 | title: 'Error', 194 | message: 'Webhook was not deleted.', 195 | button: { 196 | title: 'Ok', 197 | url: '/webhook' 198 | } 199 | } 200 | 201 | resp.render('status', json_response) 202 | }) 203 | } 204 | 205 | 206 | /** 207 | * Helper function that deletes the webhook config. 208 | * Returns a promise. 209 | */ 210 | function delete_webhook (webhook_id) { 211 | return new Promise (function (resolve, reject) { 212 | // if no webhook id provided, assume there is none to delete 213 | if (!webhook_id) { 214 | resolve() 215 | return; 216 | } 217 | 218 | // construct request to delete webhook config 219 | var request_options = { 220 | url: 'https://api.twitter.com/1.1/account_activity/all/' + auth.twitter_webhook_environment + '/webhooks/' + webhook_id + '.json', 221 | oauth: auth.twitter_oauth, 222 | resolveWithFullResponse: true 223 | } 224 | 225 | request.delete(request_options).then(function () { 226 | resolve() 227 | }).catch(function () { 228 | reject() 229 | }) 230 | }) 231 | } 232 | 233 | 234 | module.exports = webhook 235 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/account-activity-dashboard/565c0a5c31476078fdf27b4804f9c36c2ad91a1d/screenshot.png -------------------------------------------------------------------------------- /views/activity.ejs: -------------------------------------------------------------------------------- 1 | <% include partials/header.ejs %> 2 | 3 |
4 |
5 | 6 | 7 |
8 |

Live Activity

9 |
10 |

Account Activity events sent to your webhook will be displayed here when received in real-time. Events will be received for all user subscriptions.

11 | 12 |
13 | 16 |
17 | 18 | 19 |
20 |
21 | Waiting for activity . . . 22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | <% include partials/footer.ejs %> -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | <% include partials/header.ejs %> 2 | 3 |
4 |

Account Activity Dashboard

5 |

A sample application to demonstrate webhook and subscription management. 6 | This app uses the premium Account Activity API.
7 | Full API reference can be found on developer.twitter.com 8 | here.

9 |
10 |
    11 |
  • 12 |
    Manage Webhook
    13 |

    Create, update, delete and validate webhook configurations.

    14 |
  • 15 |
  • 16 |
    Manage Subscriptions
    17 |

    Add and remove Twitter user subscriptions.

    18 |
  • 19 |
  • 20 |
    Live Activity
    21 |

    View live incoming Account Activity events for all users subscribed to.

    22 |
  • 23 |
24 |
25 | 26 |
27 |
28 |
29 |
Production Considerations
30 |
31 |

This app is for demonstration purposes only and should not be used in 32 | production without further modifcations. Dependencies on databases, and other 33 | types of services are purposley not within the scope of this app. 34 | Some considerations below:

35 |
    36 |
  • User information is stored in server side sessions. This may not provide 37 | the best user experience or be the best solution for your usecase, especially 38 | if you are adding more functionality.
  • 39 |
  • The application can handle light usage, but you may experience API rate 40 | limit issues under heavier usage. Consider storing data locally in a secure database 41 | or caching requests.
  • 42 |
  • Specifc pages are only protected by basic auth. To support multiple users 43 | (admins, team members, customers, etc) consider implementing a form of Access Control List.
  • 44 |
45 |
46 |
47 |
48 |
49 | <% include partials/footer.ejs %> 50 | 51 | -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Account Activity API Sample App 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 44 | 45 |
46 | 47 | -------------------------------------------------------------------------------- /views/status.ejs: -------------------------------------------------------------------------------- 1 | <% include partials/header.ejs %> 2 | 3 |
4 |
5 |
6 |

<%= title %>

7 |
8 |

<%= message %>

9 | <%= button.title %> 10 |
11 |
12 |
13 |
14 | 15 | <% include partials/footer.ejs %> -------------------------------------------------------------------------------- /views/subscriptions.ejs: -------------------------------------------------------------------------------- 1 | <% include partials/header.ejs %> 2 | 3 |
4 |
5 |
6 |

Manage Subscriptions

7 |
8 |

Environment: <%= environment %>
9 | Application ID: <%= application_id %>

10 |
11 |

To add or remove a user subscription, you will be asked to sign-in with the 12 | Twitter account you wish to add or remove the subscription for.

13 | 14 |
15 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |

All Subscriptions

27 | <% if(subscriptions.length) { %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <% subscriptions.forEach(function(sub) { %> 38 | 39 | 40 | 41 | 42 | 43 | <% }); %> 44 | 45 |
IDUsernameName
<%= sub.id_str %>@<%= sub.screen_name %><%= sub.name %>
46 | <% } else { %> 47 |
48 |

No subscriptions exist. Add one above.

49 |
50 | <% } %> 51 |
52 |
53 |
54 | 55 | <% include partials/footer.ejs %> 56 | -------------------------------------------------------------------------------- /views/webhook.ejs: -------------------------------------------------------------------------------- 1 | <% include partials/header.ejs %> 2 | 3 |
4 |
5 |
6 |

Manage Webhook

7 |
8 | 9 | 10 | <% if (configs.length) { %> 11 |

ID: <%= configs[0].id %>
12 | URL: <%= configs[0].url %>
13 | Valid: <%= configs[0].valid %>
14 | Date Created: <%= configs[0].created_timestamp %>

15 | <% } else { %> 16 | No webhook config registered. Register one below. 17 | <% } %> 18 |
19 | 20 | 21 |
Create or Update Webhook
22 |
23 | 24 | 25 | <% if (configs.length) { %> 26 |
27 | 28 |
29 | 30 |

Your current webhook config will be deleted before creating a new one. If the new 31 | webhook config is not valid, you may experience some down time until you can resolve it.

32 | 33 | 34 | <% } else { %> 35 |
36 | 37 |
38 | <% } %> 39 | 40 | 41 |
42 |
43 | 46 |
47 |
48 |
49 | 50 | <% if (configs.length) { %> 51 | 52 | 53 |
54 |
55 |
56 |

Validate Webhook

57 |
58 |

Trigger a challenge request from the Twitter webhook server to your 59 | registered webhook URL. Helpful if you need to manually trigger validation to re-enable your webhook.

60 |
61 | 62 | 63 |
64 |
65 | 68 |
69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 |

Delete Webhook

77 |
78 |

By deleting your webhook, you will no longer receive Account Activity events 79 | for all users your webhook is subscribed to. Proceed with caution.

80 |
81 | 84 |
85 |
86 |
87 | 88 | 112 | 113 | <% } %> 114 | 115 | <% include partials/footer.ejs %> 116 | --------------------------------------------------------------------------------