├── .env.example ├── .gitignore ├── examples └── amiralitaheri64.png ├── readme.md ├── package.json ├── LICENSE └── src ├── imageLoader.js ├── index.js └── circleImage.js /.env.example: -------------------------------------------------------------------------------- 1 | CONSUMER_KEY='xyz' 2 | CONSUMER_SECRET='zyx' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .env 4 | *.png 5 | -------------------------------------------------------------------------------- /examples/amiralitaheri64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiralitaheri/twitter-followers/HEAD/examples/amiralitaheri64.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Twitter followers 2 | It simply creates an image from all of your twitter followers avatars. 3 | 4 | ## How to run 5 | 1. clone the repository 6 | `git clone https://github.com/amiralitaheri/twitter-followers.git` 7 | 2. create the `.env` file 8 | 3. change the username in `src/index.js` 9 | 4. run `npm run-script run` in your terminal 10 | 11 | ## Example results 12 | ![amiralitaheri64](/examples/amiralitaheri64.png "amiralitaheri64") 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-followers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "run": "node ./src/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "canvas": "^2.6.1", 15 | "console-progress-bar": "^1.0.4", 16 | "dotenv": "^8.2.0", 17 | "twitter-lite": "^0.14.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Seyed Amirali Taheri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/imageLoader.js: -------------------------------------------------------------------------------- 1 | const {createCanvas, loadImage} = require('canvas'); 2 | const fs = require('fs'); 3 | 4 | const cache = '.cache'; 5 | 6 | module.exports = async function imageLoader([userId, profileUrl]) { 7 | if (!fs.existsSync(cache)) { 8 | fs.mkdirSync(cache); 9 | } 10 | try { 11 | const img = await loadImage(`${cache}/${userId}.png`); 12 | return img; 13 | } catch (e) { 14 | const defaultAvatarUrl = 15 | "https://abs.twimg.com/sticky/default_profile_images/default_profile_200x200.png"; 16 | const url = profileUrl || defaultAvatarUrl; 17 | const img = await loadImage(url); 18 | try { 19 | const canvas = createCanvas(400, 400); 20 | const ctx = canvas.getContext("2d"); 21 | ctx.drawImage( 22 | img, 23 | 0, 24 | 0, 25 | 400, 26 | 400 27 | ); 28 | const out = fs.createWriteStream(`${cache}/${userId}.png`); 29 | const stream = canvas.createPNGStream(); 30 | stream.pipe(out); 31 | } catch (e) { 32 | //nothing 33 | } 34 | return img; 35 | } 36 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const Twitter = require('twitter-lite'); 3 | const circleImage = require('./circleImage'); 4 | 5 | /** 6 | * Load the environment variables from the .env file 7 | */ 8 | dotenv.config(); 9 | 10 | async function main() { 11 | //configs 12 | const username = 'amiralitaheri64'; 13 | const skipDefaultImages = true 14 | 15 | // Create an instance of the API client using the consumer keys for your app 16 | const client = new Twitter({ 17 | consumer_key: process.env.CONSUMER_KEY, 18 | consumer_secret: process.env.CONSUMER_SECRET 19 | }); 20 | 21 | // client.get("account/verify_credentials") 22 | const response = await client.getBearerToken(); 23 | const twitter = new Twitter({ 24 | bearer_token: response.access_token 25 | }); 26 | 27 | //get followers 28 | let user = await getUser(username, twitter) 29 | let avatars = await getAvatars(username, twitter, skipDefaultImages); 30 | 31 | avatars = [user, ...avatars] 32 | 33 | circleImage(avatars) 34 | 35 | } 36 | 37 | async function getUser(username, twitter) { 38 | let params = { 39 | screen_name: username, 40 | include_entities: false, 41 | }; 42 | let res = (await twitter.get('users/lookup', params))[0]; 43 | return [res.id_str, res.profile_image_url.replace('normal', '400x400'),] 44 | } 45 | 46 | async function getAvatars(username, twitter, skipDefaultImages) { 47 | let params = { 48 | screen_name: username, 49 | count: 200, 50 | include_entities: false, 51 | skip_status: true, 52 | cursor: -1 53 | }; 54 | console.log('Fetching followers'); 55 | let avatars = [] 56 | do { 57 | let res = await twitter.get('followers/list', params); 58 | let temp = res['users'].reduce((list, user) => { 59 | if (!user.profile_image_url.includes('default_profile_images') || !skipDefaultImages) { 60 | list.push([ 61 | user.id_str, 62 | user.profile_image_url.replace('normal', '400x400') 63 | ]) 64 | } 65 | return list 66 | }, []) 67 | 68 | avatars = [...avatars, ...temp] 69 | params['cursor'] = res['next_cursor_str'] 70 | } while (params['cursor'] !== '0') 71 | 72 | return avatars.reverse(); 73 | } 74 | 75 | 76 | // entry point 77 | main(); -------------------------------------------------------------------------------- /src/circleImage.js: -------------------------------------------------------------------------------- 1 | const {createCanvas} = require('canvas'); 2 | const fs = require('fs'); 3 | const ConsoleProgressBar = require('console-progress-bar'); 4 | const imageLoader = require('./imageLoader'); 5 | 6 | const toRad = (x) => x * (Math.PI / 180); 7 | 8 | 9 | module.exports = async function render(followers) { 10 | //configs 11 | const circlesRadius = 100; 12 | const imageRadius = 50; 13 | const padding = 100; 14 | const offset_conf = 0; 15 | 16 | 17 | let layers = [1]; 18 | let count = 1; 19 | 20 | while (count < followers.length) { 21 | let c = Math.floor(layers.length * circlesRadius * 3.14 / imageRadius); 22 | layers.push(c); 23 | count += c; 24 | } 25 | 26 | const width = 2 * layers.length * circlesRadius + padding; 27 | const height = 2 * layers.length * circlesRadius + padding; 28 | 29 | const canvas = createCanvas(width, height); 30 | const ctx = canvas.getContext("2d"); 31 | 32 | // fill the background 33 | ctx.fillStyle = "#FFFFFF"; 34 | ctx.fillRect(0, 0, width, height); 35 | console.log('Fetching images') 36 | const cpb = new ConsoleProgressBar({maxValue: followers.length}) 37 | let counter = 0; 38 | // loop over the layers 39 | for (let count of layers) { 40 | let layerIndex = layers.indexOf(count); 41 | count = Math.min(count, followers.length - counter); 42 | const angleSize = 360 / count; 43 | 44 | 45 | // loop over each circle of the layer 46 | for (let i = 0; i < count; i++) { 47 | // We need an offset or the first circle will always on the same line and it looks weird 48 | // Try removing this to see what happens 49 | const offset = layerIndex * offset_conf; 50 | 51 | // i * angleSize is the angle at which our circle goes 52 | // We need to converting to radiant to work with the cos/sin 53 | const r = toRad(i * angleSize + offset); 54 | 55 | const centerX = Math.cos(r) * layerIndex * circlesRadius + width / 2; 56 | const centerY = Math.sin(r) * layerIndex * circlesRadius + height / 2; 57 | 58 | // if we are trying to render a circle but we ran out of users, just exit the loop. We are done. 59 | if (counter === followers.length) break; 60 | 61 | ctx.save(); 62 | ctx.beginPath(); 63 | ctx.arc(centerX, centerY, imageRadius - 2, 0, 2 * Math.PI); 64 | ctx.clip(); 65 | 66 | const img = await imageLoader(followers[counter++]); 67 | 68 | cpb.addValue(1); 69 | 70 | ctx.drawImage( 71 | img, 72 | centerX - imageRadius, 73 | centerY - imageRadius, 74 | imageRadius * 2, 75 | imageRadius * 2 76 | ); 77 | 78 | ctx.restore(); 79 | } 80 | } 81 | 82 | // write the resulting canvas to file 83 | const out = fs.createWriteStream("./circle.png"); 84 | const stream = canvas.createPNGStream(); 85 | stream.pipe(out); 86 | out.on("finish", () => console.log("Done!")); 87 | }; 88 | --------------------------------------------------------------------------------