Make sure the user you're looking for is a public user. Also, make sure the user exists! Otherwise, know that Twitter can throttle their data access for apps like this if it's being used too much. Try again, and if it still doesn't work, try again tomorrow when the capitalist overlords hand out more data stamps.
10 |
11 | )
12 | }
13 | // Error message can show here
14 | if(props.tweetsLength === 0)
15 | {
16 | return (
17 |
18 |
Welcome to this sorting tool!
19 |
Type in a twitter username (the one with the '@' before it) to get up to ~3,200 of an account's most recent tweets. You can sort their tweets by Date, Likes, or Retweets, and choose either ascending or descending order.
20 |
A loading symbol will show while the tweets are being fetched. If this area is empty (not even this welcome message) it means the tweets are being rendered :)
21 |
If you enjoy this little tool, follow me @averywlittle.
22 |
The code, along with more information, is available here.
{props.tweetsLength} tweets loaded! Showing page {props.page} of {props.maxPage}.
30 |
31 | )
32 | }
33 | }
34 |
35 | export default ListTweets
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const express = require('express')
3 | const cors = require('cors')
4 | const app = express()
5 |
6 | app.use(cors())
7 | // Serves static pages from React build
8 | app.use(express.static('build'))
9 | app.use(express.json())
10 |
11 | const twitter = require('./twitter')
12 | const sorting = require('./sorting')
13 | const comparators = require('./comparators')
14 |
15 | //////////
16 |
17 | app.post('/api/query/', (request, response, next) => {
18 |
19 | console.log('body', request.body)
20 |
21 | let params = {
22 | user_id: request.body.query,
23 | screen_name: request.body.query,
24 | include_entities: false,
25 | count: 200
26 | }
27 |
28 | const mergeParams = {
29 | order: request.body.queryOrder
30 | }
31 |
32 | switch (request.body.queryType) {
33 | case "favorites":
34 | mergeParams.comparator = comparators.options.byFavorites
35 | break;
36 |
37 | case "retweets":
38 | mergeParams.comparator = comparators.options.byReach
39 | break;
40 |
41 | case "date":
42 | mergeParams.comparator = comparators.options.byDate
43 | break;
44 | default:
45 | break;
46 | }
47 |
48 | twitter.getTweets(params)
49 | .then(result => {
50 | console.log(`fetch success from user @${result.user.screen_name}, number of tweets: ${result.allTweets.length}`)
51 | // filter our tweets of just plain retweets, but keep quote tweets
52 | let tweetsWithoutRetweets = result.allTweets.filter(tweet => !tweet.retweeted_status)
53 | // sort tweets
54 | let sortedArray = sorting.sortTweets(tweetsWithoutRetweets, mergeParams)
55 |
56 | response.json({ tweets: sortedArray, user: result.user })
57 | })
58 | .catch(error => {
59 | console.log('ERROR', error)
60 | next(error)
61 | })
62 | })
63 |
64 |
65 | const PORT = process.env.PORT || 3001
66 | app.listen(PORT, () => {
67 | console.log(`Server running on port ${PORT}`)
68 | })
--------------------------------------------------------------------------------
/server/twitter.js:
--------------------------------------------------------------------------------
1 |
2 | const Twitter = require('twitter')
3 |
4 | const client = new Twitter({
5 | consumer_key: process.env.API_KEY,
6 | consumer_secret: process.env.API_KEY_SECRET,
7 | access_token_key: process.env.ACCESS_TOKEN,
8 | access_token_secret: process.env.ACCESS_TOKEN_SECRET,
9 | bearer_token: process.env.BEARER_TOKEN
10 | })
11 |
12 | const getTweets = async (queryParams) => {
13 |
14 | let allTweets = []
15 | let newTweets = await client.get('statuses/user_timeline', queryParams)
16 | .then(tweets => {
17 | if (tweets !== undefined) {
18 | return tweets
19 | }
20 | })
21 | .catch(error => {
22 | return Promise.reject(error)
23 | })
24 |
25 | allTweets = allTweets.concat(newTweets)
26 | console.log(`first tweets length ${allTweets.length}`)
27 |
28 | let oldestTweetID = allTweets[allTweets.length-1].id
29 |
30 | const user = allTweets[0].user
31 |
32 | while (newTweets.length > 0) {
33 | console.log(`getting tweets before ${oldestTweetID}`)
34 |
35 | // The next request is aligned using the previous request's oldest tweet id
36 | newTweets = await client.get('statuses/user_timeline', { ...queryParams, max_id: oldestTweetID, trim_user: true })
37 | .then(tweets => {
38 | console.log('new tweets length', tweets.length)
39 | return tweets
40 | })
41 | .catch(error => {
42 | return Promise.reject(error)
43 | })
44 |
45 | if (newTweets.length <= 1) {
46 | break
47 | } else {
48 | // If duplicate tweet by id, slice the duplicated tweet out
49 | if (allTweets[allTweets.length-1].id === newTweets[0].id) {
50 |
51 | newTweets = newTweets.slice(1)
52 | }
53 |
54 | allTweets = allTweets.concat(newTweets)
55 |
56 | // Adjust lowest ID to new lowest id in newTweets if available
57 | // No initial value because we assume the newTweets
58 | oldestTweetID = newTweets.reduce((oldestID, currentTweet) => currentTweet.id < oldestID ? currentTweet.id : oldestID.id)
59 |
60 | console.log(`${allTweets.length} tweets downloaded so far`)
61 | }
62 | }
63 |
64 | console.log('all tweets length =', allTweets.length)
65 | return { allTweets, user }
66 | }
67 |
68 | exports.getTweets = getTweets
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TweetSort
2 | A tool to sort through a user's tweets.
3 |
4 | # HEADS UP: THIS PROJECT IS DEPRECATED AND THE CODE IS UP FOR EDUCATIONAL PURPOSES ONLY
5 |
6 | The reason I wish this existed is because I love lurking through the top of all time on Reddit. I want to provide that experience on Twitter.
7 | There are so many talented and experienced people on the site sharing their expertise, insight, or humor. Twitter's ephemeral nature prevents us from seeing a user's
8 | valuable input from the past. Search is great if you know the syntax for commands, but is also dependent on you as a user knowing what content you're looking for.
9 |
10 | I've found that users that thread a lot will resurface their own valuable tweets. What about users that don't do that? This tool seeks to enable people to seek out valuable past
11 | tweets from people they want to learn more from.
12 |
13 | This is available at https://www.tweetsort.io/.
14 |
15 | Challenges:
16 | Getting a user's tweets (API only returns ~200 at a time)
17 | Avoiding being rate limited
18 | -[ ] I could easily just make a simple mongo collection to track request data, and check if I'm at my limit for a specified time period.
19 | Protecting API keys (used environment variables for the first time)
20 | Storing data in cache to reduce calls to API
21 | -[ ] I could only return just the tweet id since that's all that's needed to embed a tweet. Would reduce the amount of data sent
22 | -[ ] I could only return the id, str_id, created_on, favorite_count, and retweet_count variables for reduced data, but would also have enough to sort tweets on the front end without making more requests (as long as they are for the same account/username)
23 |
24 | I could pay for more access to the twitter api and could possibly get tons more tweets. But frankly, it's expensive. This is not going to make money, so that's not going to happen.
25 |
26 | Another approach I could take is to build a web scraper utility (probably in python) and use it to get all a user's tweets. While that would accomplish the goal, it would take me some time to build. Also, for an account with a lot of tweets, idk how long a scraper would have to run to get all the data needed. I'm not storing any tweets.
27 |
28 | Overall this was a great project to practice the React fundamentals I'm learning through the full stack open course (https://fullstackopen.com/en/about).
29 | I made a node and express backend, and while I'm not storing any data I did use a merge sort to transform the data dynamically. There are no tests for this, and probably won't be. Although, that is what I'm learning about currently. This was also my first time deploying an app that wasn't a static website. So that was fun. I learned how to use environment variables. Heroku makes it easy.
30 |
31 | Can't wait for the next project. I'm going to tweet about this tomorrow and I hope the twitter api doesn't get salty about the number of requests.
32 |
33 | That's another thing I learned, that I think going forward I should be picky about what I build on top of.
34 |
--------------------------------------------------------------------------------
/react-ui/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/react-ui/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import QueryForm from './QueryForm'
3 | import ListTweets from './ListTweets'
4 | import Loading from './Loading'
5 | import QueryTypeSelector from './QueryTypeSelector'
6 | import QueryOrderSelector from './QueryOrderSelector'
7 | import PageSelector from './PageSelector'
8 | import InfoPanel from './InfoPanel'
9 | import axios from 'axios'
10 |
11 |
12 | const App = () => {
13 |
14 | const [ query, setQuery ] = useState("")
15 | const [ user, setUser ] = useState(null)
16 | const [ tweets, setTweets ] = useState([])
17 | const [ loading, setLoading ] = useState(false)
18 | const [ queryType, setQueryType ] = useState("favorites")
19 | const [ queryOrder, setQueryOrder ] = useState("desc")
20 | const [ page, setPage ] = useState(1)
21 | const [ error, setError ] = useState(null)
22 |
23 | const handleQueryChange = (event) => {
24 | setQuery(event.target.value)
25 |
26 | if (event.target.value.length === 0) {
27 | setTweets([])
28 | setError(null)
29 | }
30 | }
31 |
32 | const handleQueryTypeChange = (event) => {
33 | event.preventDefault()
34 | setQueryType(event.target.value)
35 | }
36 |
37 | const handleQueryOrderChange = (event) => {
38 | event.preventDefault()
39 | setQueryOrder(event.target.value)
40 | }
41 |
42 | // page cannot go lower than 1 or higher than tweets.length/10 + 1
43 | const handlePageUp = (event) => {
44 | event.preventDefault()
45 | if (tweets.length !== 0) {
46 |
47 | if (page < (Math.ceil(tweets.length/10))) {
48 | setPage(page + 1)
49 | }
50 | else console.log('Page maximum')
51 | }
52 | }
53 |
54 | const handlePageDown = (event) => {
55 | event.preventDefault()
56 | if (tweets.length !== 0) {
57 | if (page !== 1) {
58 | setPage(page - 1)
59 | }
60 | else console.log('Page minimum')
61 | }
62 | }
63 |
64 | const queryTweets = () => {
65 | console.log('query', query)
66 | if (query.length > 0) {
67 | setError(null)
68 | setLoading(true)
69 |
70 | const queryObject = { query, queryType, queryOrder }
71 |
72 | // post request because we need to send some data to form the query params
73 | axios.post('/api/query/', queryObject)
74 | .then(response => {
75 | console.log(response.data.user)
76 | console.log('tweets returned', response.data.tweets.length)
77 | setUser(response.data.user)
78 | setQuery(response.data.user.screen_name)
79 | setTweets(response.data.tweets)
80 | setPage(1)
81 | setLoading(false)
82 | })
83 | .catch(error => {
84 | console.log('POST ERROR', error)
85 | setError(error)
86 | setLoading(false)
87 | setTweets([])
88 | })
89 | }
90 | }
91 |
92 | return (
93 |
\n )\n}\n\nexport default App","/home/averywlittle/code/tweetsort/react-ui/src/components/QueryForm.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/ListTweets.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/Loading.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/QueryOrderSelector.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/QueryTypeSelector.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/PageSelector.js",[],"/home/averywlittle/code/tweetsort/react-ui/src/components/InfoPanel.js",[],{"ruleId":"43","replacedBy":"44"},{"ruleId":"45","replacedBy":"46"},{"ruleId":"47","severity":1,"message":"48","line":15,"column":13,"nodeType":"49","messageId":"50","endLine":15,"endColumn":17},"no-native-reassign",["51"],"no-negated-in-lhs",["52"],"no-unused-vars","'user' is assigned a value but never used.","Identifier","unusedVar","no-global-assign","no-unsafe-negation"]
--------------------------------------------------------------------------------