├── .gitignore ├── src ├── util │ ├── spinner.js │ ├── localStorage.js │ ├── prompt.js │ ├── profile.js │ └── crawler.js └── index.js ├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── package.json ├── .eslintrc.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vscode -------------------------------------------------------------------------------- /src/util/spinner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CLI = require('clui'); 4 | const Spinner = CLI.Spinner; 5 | 6 | 7 | /** 8 | * This is a Countdown Object generated using `clui` package 9 | */ 10 | 11 | const spinner = new Spinner('Fetching Awesome content... ', ['👻','🤓','👩🏻‍💻','👨🏻‍💻','🦄','💻','👾','👽']); 12 | 13 | module.exports = spinner; -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 devtocli 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtocli", 3 | "version": "1.5.0", 4 | "description": "A simple package to let you use Dev.to in Terminal", 5 | "main": "src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/sarthology/dev.to-cli.git" 9 | }, 10 | "author": "Sarthology", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/sarthology/dev.to-cli/issues" 14 | }, 15 | "homepage": "https://github.com/sarthology/dev.to-cli#readme", 16 | "dependencies": { 17 | "algoliasearch": "^3.32.0", 18 | "chalk": "^2.4.2", 19 | "cheerio": "^1.0.0-rc.2", 20 | "clui": "^0.3.6", 21 | "commander": "^2.19.0", 22 | "didyoumean": "^1.2.1", 23 | "esc-exit": "^2.0.1", 24 | "inquirer": "^6.2.1", 25 | "inquirer-autocomplete-prompt": "^1.0.1", 26 | "node-banner": "^1.3.2", 27 | "open": "^6.4.0", 28 | "proxy-agent": "^3.1.0", 29 | "x-ray": "^2.3.4" 30 | }, 31 | "devDependencies": { 32 | "eslint": "^5.12.1", 33 | "eslint-plugin-prettier": "^3.0.1", 34 | "prettier": "^1.16.1" 35 | }, 36 | "preferGlobal": true, 37 | "bin": { 38 | "devto": "src/index.js" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": ["plugin:prettier/recommended"], 7 | "parserOptions": { 8 | "ecmaVersion": 2016 9 | }, 10 | "rules": { 11 | "prettier/prettier": "error", 12 | "strict": "error", 13 | "no-console": "off", 14 | "no-eval": "error", 15 | "no-implied-eval": "error", 16 | "no-implicit-globals": "error", 17 | "no-control-regex":"off", 18 | "no-empty-function": "error", 19 | "no-unused-vars": "warn", 20 | "no-undefined": "error", 21 | "no-undef-init": "error", 22 | "no-param-reassign": "warn", 23 | "no-unneeded-ternary": "error", 24 | "no-nested-ternary": "error", 25 | "no-throw-literal": "error", 26 | "no-tabs": "error", 27 | "indent": [ 28 | "error", 29 | 4 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "always" 38 | ], 39 | "block-scoped-var": "warn", 40 | "prefer-const": "warn", 41 | "prefer-arrow-callback": "error", 42 | "prefer-template": "error", 43 | "default-case": "warn", 44 | "require-await": "error", 45 | "yoda": "warn", 46 | "no-new-func": "error", 47 | "no-new-wrappers": "error", 48 | "handle-callback-err": "error" 49 | }, 50 | "plugins": ["prettier"] 51 | } 52 | -------------------------------------------------------------------------------- /src/util/localStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const STORAGE_PATH = path.join(process.cwd(), '.bookmarks.json'); 7 | 8 | /** 9 | * This is a function to initialize the bookmark JSON file 10 | * @param {null} null 11 | * @returns {null} null 12 | */ 13 | const initBookmark = () => { 14 | let emptyBookmark = { 15 | bookmarks: [] 16 | }; 17 | 18 | try{ 19 | fs.writeFileSync(STORAGE_PATH, JSON.stringify(emptyBookmark)); 20 | }catch(err) { 21 | throw new Error('unexpected error occurred :('); 22 | } 23 | } 24 | 25 | /** 26 | * This is a function to read the saved bookmark if available 27 | * or else initialize it 28 | * @param {null} null 29 | * @returns {Object} JSON object with bookmark array list 30 | */ 31 | 32 | const readBookmark = () => { 33 | try{ 34 | return JSON.parse(fs.readFileSync(STORAGE_PATH)); 35 | }catch(err) { 36 | if(err.code === 'ENOENT') { 37 | initBookmark(); 38 | return JSON.parse(fs.readFileSync(STORAGE_PATH)); 39 | } else { 40 | throw new Error('unexpected error occurred :('); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * This is a function to add the selected post as the new 47 | * bookmark to the local bookmark storage 48 | * @param {Object} selectedPost 49 | * @returns {null} null 50 | */ 51 | 52 | const addBookmark = (selectedPost) => { 53 | let bookmark = readBookmark(); 54 | 55 | bookmark.bookmarks.push({ 56 | title: selectedPost.title, 57 | link: selectedPost.link 58 | }); 59 | 60 | fs.writeFileSync(STORAGE_PATH, JSON.stringify(bookmark)); 61 | } 62 | 63 | /** 64 | * This is a function to delete the selected bookmark 65 | * @param {Object} selectedPostTitle 66 | * @returns {null} null 67 | */ 68 | const deleteBookmark = (selectedPostTitle) => { 69 | let bookmark = readBookmark().bookmarks; 70 | bookmark.splice(bookmark.indexOf(bookmark.find(data => data.title === selectedPostTitle)), 1) 71 | fs.writeFileSync(STORAGE_PATH, JSON.stringify({bookmarks:bookmark})); 72 | } 73 | 74 | module.exports = { 75 | readBookmark, 76 | addBookmark, 77 | deleteBookmark 78 | }; 79 | -------------------------------------------------------------------------------- /src/util/prompt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Native import 4 | const inquirer = require('inquirer'); 5 | 6 | inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 7 | 8 | /** 9 | * This is a function to show autocomplete prompt for tag search 10 | * @param {Object} tags - Tags fetched from dev.to 11 | * @returns {Promise} The promise with the tag choosen 12 | */ 13 | 14 | const searchTags = (tags) => { 15 | return inquirer.prompt([{ 16 | type: 'autocomplete', 17 | name: 'tag', 18 | message: '🕵🏻‍♂️ Search popular tags:', 19 | pageSize: 4, 20 | source: function (answers, input) { 21 | return new Promise((resolve) => { 22 | resolve(tags.filter(data => data.indexOf(input) > -1)) 23 | }); 24 | } 25 | }]) 26 | } 27 | 28 | 29 | /** 30 | * This is a function to show prompt for all fetched Posts 31 | * @param {Object} titles - titles of fetched Posts 32 | * @returns {Promise} The promise with the post choosen 33 | */ 34 | 35 | const showPosts = (titles) => { 36 | if(titles.length === 0){ 37 | console.error("😱 No posts found. Please try again."); 38 | process.exit(1); 39 | } 40 | 41 | return inquirer.prompt([{ 42 | type: 'rawlist', 43 | name: 'title', 44 | message: '📚 Here are your posts:', 45 | choices: titles, 46 | paginated: true 47 | }]) 48 | } 49 | 50 | /** 51 | * This is a function to show prompt for timeline suggestion in searching top posts 52 | * @param {null} null 53 | * @returns {Promise} The promise with the timeline choosen 54 | */ 55 | 56 | const selectTimline = () => { 57 | return inquirer.prompt([{ 58 | type: 'list', 59 | name: 'timeline', 60 | message: '📆 Please choose the timeline:', 61 | choices: ["week","month","year","infinity"], 62 | paginated: true 63 | }]) 64 | } 65 | 66 | /** 67 | * This is a function to show prompt for the post operation 68 | * @param {null} null 69 | * @returns {Promise} The promise with the operation choosen 70 | */ 71 | 72 | const postOperation = (choices) => { 73 | return inquirer.prompt([{ 74 | type: 'list', 75 | name: 'postOperation', 76 | message: 'What do we do with this post : ', 77 | choices: choices 78 | }]); 79 | } 80 | 81 | module.exports = { 82 | showPosts, 83 | searchTags, 84 | selectTimline, 85 | postOperation 86 | }; -------------------------------------------------------------------------------- /src/util/profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | const log = console.log; 6 | const title = chalk.yellow; 7 | const body = chalk.green; 8 | 9 | /** 10 | * This is a function to display name and description of author. 11 | * @param {Object} profile - profile of the author 12 | * @returns {null} null 13 | */ 14 | 15 | const showNameAndDesc = (profile) => { 16 | log(title('name')) 17 | log(body(profile.name)); 18 | log(title('\ndescription')) 19 | log(body(`${profile.desc}\n`)); 20 | } 21 | 22 | /** 23 | * This is a function to display author profile details such as email,work,location,etc. 24 | * @param {Object} profile - profile of the author 25 | * @returns {null} null 26 | */ 27 | 28 | const showAuthorInfo = (profile) => { 29 | profile.field.forEach((field, index) => { 30 | log(title(field)); 31 | log(body(`${profile.value[index]}\n`)); 32 | }); 33 | } 34 | 35 | /** 36 | * This is a function to display the links of the author mentioned in profile. 37 | * @param {Object} profile - profile of the author 38 | * @returns {null} null 39 | */ 40 | 41 | const showLinks = (profile) => { 42 | log(title('links')); 43 | profile.links.forEach(link => { 44 | log(body(link)); 45 | }); 46 | } 47 | 48 | /** 49 | * This is a function to display the details in sidebar such as skills/languages, projects and hacks, etc. 50 | * @param {Object} profile - profile of the author 51 | * @returns {null} null 52 | */ 53 | 54 | const showSidebarDetails = (profile) => { 55 | log(''); 56 | profile.sidebarHeader.forEach((header, index) => { 57 | log(title(header)); 58 | log(body(`${profile.sidebarBody[index]}\n`)); 59 | }); 60 | } 61 | 62 | /** 63 | * This is a function to display stats such as posts published, comments written, tags followed. 64 | * @param {Object} profile - profile of the author 65 | * @returns {null} null 66 | */ 67 | 68 | const showStats = (profile) => { 69 | log(title('stats')); 70 | profile.stats.forEach(stat => { 71 | log(body(stat)); 72 | }); 73 | } 74 | 75 | /** 76 | * This is a driver function call the above the functions that display profile details of author. 77 | * @param {Object} profile - profile of the author 78 | * @returns {null} null 79 | */ 80 | 81 | const showProfile = (profile) => { 82 | showNameAndDesc(profile); 83 | showAuthorInfo(profile); 84 | showLinks(profile); 85 | showSidebarDetails(profile); 86 | showStats(profile); 87 | } 88 | 89 | module.exports = showProfile; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ## Dev.to CLI 4 | 5 | Wanted to get [Dev.to](https://www.dev.to/) on your terminal? Ahoy! now you can. 6 | 7 | ## Usage 8 | 9 | #### 1. Check the top posts 10 | 11 | ![](https://media.giphy.com/media/jpEnZqtCO41ptgnm3o/giphy.gif) 12 | 13 | You can get top posts from the Dev.to feed in your terminal using: 14 | 15 | ``` 16 | devto 17 | ``` 18 | 19 | #### 2. Check top posts by tag 20 | 21 | ![](https://media.giphy.com/media/ienMTGDAB0OLNKFEMB/giphy.gif) 22 | 23 | Easy post search using the tag, you can use 24 | 25 | 26 | ``` 27 | devto tag javascript 28 | ``` 29 | 30 | 31 | if you don't know the keyword, you can use the search tag feature using 32 | 33 | ``` 34 | devto tag 35 | ``` 36 | 37 | #### 3. Check recent posts by any author 38 | 39 | ![](https://media.giphy.com/media/lMsDWWJzuOvmDqfVLW/giphy.gif) 40 | 41 | You can check 5 most recent posts of any author using 42 | 43 | ``` 44 | devto author sarthology 45 | ``` 46 | 47 | #### 4. Check top posts 48 | 49 | ![](https://media.giphy.com/media/gI4m67QOXn8BYip38z/giphy.gif) 50 | 51 | You can check top posts on Dev.to using 52 | 53 | ``` 54 | devto top 55 | ``` 56 | 57 | If you want top posts for a specific timeframe, just type in 58 | 59 | ``` 60 | devto top week 61 | ``` 62 | 63 | There are several options like `week` `month` `year` `infinity`. If you want to see the options just type 64 | 65 | #### 5. Search top posts by a keyword 66 | 67 | ![](https://media.giphy.com/media/S6lI6KD4ZGzD06BkA3/giphy.gif) 68 | 69 | Use the powerful search feature to find posts using keywords 70 | 71 | ``` 72 | devto search sarthology 73 | ``` 74 | 75 | With this, you can even search posts using a tag, author name, title... basically anything. 76 | 77 | #### 6. Check latest posts on Dev.to 78 | 79 | ![](https://media.giphy.com/media/WrPDZnOf3jKDy8Sysr/giphy.gif) 80 | 81 | You can also see the latest posts on Dev.to using 82 | 83 | ``` 84 | devto latest 85 | ``` 86 | 87 | ## Prerequisites 88 | 89 | Before running this locally you must have these installed 90 | 91 | - [Node](https://nodejs.org/) 92 | - [npm](https://www.npmjs.com/) 93 | ## Installing 94 | 95 | It's built in node so the process to start this is really easy 96 | 97 | 1. `npm install devtocli -g` 98 | 2. `devto` 99 | 100 | That's it, you will see it running in your terminal. 101 | 102 | ## Contributing 103 | 104 | Feel free to contribute to this project and treat it like your own. 😊 105 | 106 | ## License 107 | 108 | [MIT License](https://github.com/teamxenox/devtocli/blob/master/LICENSE) 109 | 110 | ## Authors 111 | 112 | [Sarthak Sharma](https://twitter.com/sarthology) 113 | 114 | [James George](https://twitter.com/james_madhacks) 115 | 116 | ## Acknowledgments 117 | 118 | Thanks [Dev.to](https://www.dev.to/)👩🏻‍💻👨🏻‍💻, for being a massively inspiring platform. -------------------------------------------------------------------------------- /src/util/crawler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Native import 4 | const Xray = require('x-ray'); 5 | const algoliasearch = require('algoliasearch'); 6 | const client = algoliasearch('YE5Y9R600C', 'OTU1YjU5MWNlZTk1MjQ0YmExOTRjZmY4NDM2ZTM2YWZiYTM2ODA2NThhMzNjMDkzYTEzYjFmNDY0MDcwNjRkOHJlc3RyaWN0SW5kaWNlcz1zZWFyY2hhYmxlc19wcm9kdWN0aW9uJTJDVGFnX3Byb2R1Y3Rpb24lMkNvcmRlcmVkX2FydGljbGVzX3Byb2R1Y3Rpb24lMkNvcmRlcmVkX2FydGljbGVzX2J5X3B1Ymxpc2hlZF9hdF9wcm9kdWN0aW9uJTJDb3JkZXJlZF9hcnRpY2xlc19ieV9wb3NpdGl2ZV9yZWFjdGlvbnNfY291bnRfcHJvZHVjdGlvbiUyQ29yZGVyZWRfY29tbWVudHNfcHJvZHVjdGlvbg=='); 7 | const index = client.initIndex('searchables_production'); 8 | const ProxyAgent = require('proxy-agent'); 9 | const http = require('http'); 10 | const https = require('https'); 11 | 12 | //Global Variable 13 | const xray = Xray({ 14 | filters: { 15 | trim: function (value) { 16 | return typeof value === 'string' ? value.trim() : value 17 | }, 18 | removeNewLine: function (value) { 19 | return value.replace(/\s\s+/g, ' '); 20 | } 21 | } 22 | }); 23 | 24 | 25 | const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy; 26 | if (proxyUrl) { 27 | http.globalAgent = new ProxyAgent(proxyUrl); 28 | https.globalAgent = new ProxyAgent(proxyUrl); 29 | } 30 | 31 | /** 32 | * This is a function to fetch home feed of `dev.to` using the `x-ray` module. 33 | * @param {null} null 34 | * @returns {Promise} The promise with data scrapped from webpage. 35 | */ 36 | 37 | const fetchHome = () => { 38 | return xray('https://dev.to', '#substories .single-article', [{ 39 | title: '.index-article-link .content h3 | trim', 40 | author: 'h4 a | trim', 41 | link: '.index-article-link@href', 42 | tag: ['.tags .tag | trim'] 43 | }]).then(data => { 44 | return formatTitle(data); 45 | }); 46 | } 47 | 48 | /** 49 | * This is a function to fetch home feed of `dev.to` using the `x-ray` module. 50 | * @param {string} timeline - the time by which posts will be fetched 51 | * @returns {Promise} The promise with data scrapped from webpage. 52 | */ 53 | 54 | const fetchTop = (timeline) => { 55 | return xray('https://dev.to/top/'+timeline, '#substories .single-article', [{ 56 | title: '.index-article-link .content h3 | trim', 57 | author: 'h4 a | trim', 58 | link: '.index-article-link@href', 59 | tag: ['.tags .tag | trim'] 60 | }]).then(data => { 61 | return formatTitle(data); 62 | }); 63 | } 64 | 65 | /** 66 | * This is a function to fetch latest feed of `dev.to` using the `x-ray` module. 67 | * @param {null} null 68 | * @returns {Promise} The promise with data scrapped from webpage. 69 | */ 70 | 71 | const fetchLatest = () => { 72 | return xray('https://dev.to/latest/', '#substories .single-article', [{ 73 | title: '.index-article-link .content h3 | trim', 74 | author: 'h4 a | trim', 75 | link: '.index-article-link@href', 76 | tag: ['.tags .tag | trim'] 77 | }]).then(data => { 78 | return formatTitle(data); 79 | }); 80 | } 81 | 82 | 83 | 84 | /** 85 | * This is a function to fetch feed by tags of `dev.to` using the `x-ray` module. 86 | * @param {string} tag - Tag by which articles will be fetched 87 | * @returns {Promise} The promise with data scrapped from webpage. 88 | */ 89 | 90 | const fetchByTags = (tag) => { 91 | return xray('https://dev.to/t/' + tag, '#substories .single-article', [{ 92 | title: '.index-article-link .content h3 | trim', 93 | author: 'h4 a | trim', 94 | link: '.index-article-link@href', 95 | tag: ['.tags .tag | trim'] 96 | }]).then(data => { 97 | return formatTitle(data); 98 | }); 99 | } 100 | 101 | /** 102 | * This is a function to fetch posts by a author from `dev.to` using the `x-ray` module. 103 | * @param {string} username - username by which posts will be fetched 104 | * @returns {Promise} The promise with data scrapped from webpage. 105 | */ 106 | 107 | const fetchByAuthor = (username) => { 108 | return xray('https://dev.to/' + username, '#substories .single-article', [{ 109 | title: '.index-article-link .content h3 | trim', 110 | author: 'h4 a | trim', 111 | link: '.index-article-link@href', 112 | tag: ['.tags .tag | trim'] 113 | }]).then(data => { 114 | return formatTitle(data); 115 | }); 116 | } 117 | 118 | /** 119 | * This is a function to fetch profile details of a author from `dev.to` using the `x-ray` module. 120 | * @param {string} username - username by which the profile details of author will be fetched 121 | * @returns {Promise} The promise with profile details of author. 122 | */ 123 | 124 | const fetchAuthorProfile = (username) => { 125 | return xray('https://dev.to/' + username, 'body', { 126 | name: '.profile-details h1 span | trim', 127 | desc: '.profile-details p | trim', 128 | field: ['.key | trim'], 129 | value: ['.value | trim | removeNewLine'], 130 | links: ['.profile-details .social a@href'], 131 | sidebarHeader: ['.user-sidebar .widget header h4 | trim'], 132 | sidebarBody: ['.widget-body | trim'], 133 | stats: ['.sidebar-data div | trim'] 134 | }) 135 | .then(data => { 136 | return data; 137 | }); 138 | } 139 | 140 | /** 141 | * This is a function to fetch Article from link. 142 | * @param {string} null 143 | * @returns {Promise} The promise with data scrapped from webpage. 144 | */ 145 | 146 | const fetchArticle = (url) => { 147 | return xray(url, '#article-body | trim').then(data => console.log(data)); 148 | } 149 | 150 | /** 151 | * This is a function to fetch all poplar tags from dev.to. 152 | * @param {null} url - url of article to fetched 153 | * @returns {Promise} The promise with data scrapped from webpage. 154 | */ 155 | 156 | const fetchTags = () => { 157 | return xray('https://dev.to/tags', ['.articles-list .tag-list-container h2']) 158 | .then(data => data.map(data => data.split("#")[1])); 159 | } 160 | 161 | /** 162 | * This is a function to Search posts on `dev.to` by keyword. 163 | * @param {string} keyword - Keyword of article to fetched 164 | * @returns {Promise} The promise with data scrapped from webpage. 165 | */ 166 | 167 | const searchPost = (keyword) => { 168 | const searchObj = { 169 | hitsPerPage: 10, 170 | page: "0", 171 | queryType: "prefixNone", 172 | filters: "class_name:Article", 173 | attributesToRetrieve: [ 174 | 'title', 175 | 'path' 176 | ], 177 | exactOnSingleWordQuery: "none", 178 | } 179 | 180 | return index.search(keyword, searchObj); 181 | 182 | } 183 | 184 | /** 185 | * This is a function to Take the tag from the post title. 186 | * @param {Array} posts - Posts to be edited 187 | * @returns {Array} edited post 188 | */ 189 | const formatTitle = (posts) => { 190 | return posts.map(post => { 191 | post.tag.forEach(tag => { 192 | if (post.title.indexOf(tag) > -1) post.title = post.title.split(tag)[1]; 193 | }) 194 | return post; 195 | }); 196 | } 197 | 198 | 199 | module.exports = { 200 | fetchHome, 201 | fetchArticle, 202 | fetchByTags, 203 | fetchTags, 204 | searchPost, 205 | fetchByAuthor, 206 | fetchAuthorProfile, 207 | fetchTop, 208 | fetchLatest 209 | }; 210 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const open = require('open'); 5 | const escExit = require('esc-exit'); 6 | const chalk = require('chalk'); 7 | const didYouMean = require('didyoumean'); 8 | const showBanner = require('node-banner'); 9 | 10 | const { description, version } = require('../package'); 11 | const crawler = require('./util/crawler'); 12 | const countdown = require('./util/spinner'); 13 | const prompt = require('./util/prompt'); 14 | const showProfile = require('./util/profile'); 15 | const localStorage = require('./util/localStorage'); 16 | 17 | const BOOKMARK_TAG = ' ![BOOKMARK]'; 18 | 19 | let articles; 20 | 21 | escExit(); 22 | 23 | const openLink = (answers) => { 24 | open(articles.find(data => data.title === answers.title).link); 25 | } 26 | 27 | /** 28 | * A function to append the ![BOOKMARK] tag to the bookmarked title 29 | * @params {null} null 30 | * @return {null} null 31 | */ 32 | const tagBookmark = () => { 33 | let titles = articles.map(article => { 34 | let finalTitle; 35 | 36 | let bookmarks = localStorage.readBookmark().bookmarks; 37 | if(bookmarks.length > 0) { 38 | for(let i = 0; i < bookmarks.length; i++) { 39 | if(article.link === bookmarks[i].link) { 40 | finalTitle = article.title + BOOKMARK_TAG; 41 | break; 42 | }else { 43 | finalTitle = article.title; 44 | } 45 | } 46 | return finalTitle; 47 | }else 48 | return article.title; 49 | }); 50 | 51 | return titles; 52 | } 53 | 54 | /** 55 | * This is a function to show the prompt for the articles passed 56 | * @param {Array} articles 57 | * @returns {null} null 58 | */ 59 | 60 | const postPrompt = () => { 61 | prompt.showPosts(tagBookmark()).then(answers => { 62 | if(!answers.title.includes(BOOKMARK_TAG)){ 63 | 64 | prompt.postOperation(['Open Link', 'Add to Bookmark']).then(data => { 65 | if(data.postOperation === 'Add to Bookmark') { 66 | // push the answer to bookmark storage 67 | localStorage.addBookmark(articles.find(data => data.title === answers.title)); 68 | return postPrompt(); 69 | } 70 | openLink(answers); 71 | }).catch(err => { 72 | console.log('unexpected error occurred :( - ', err); 73 | }); 74 | }else{ 75 | // remove the ![BOOKMARK] tag from title 76 | answers.title = answers.title.slice(0, -(BOOKMARK_TAG.length)); 77 | openLink(answers); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * This is a function to show the bookmark with post prompt 84 | * i.e open link or delete bookmark 85 | * @param {null} null 86 | * @returns {null} open link or load updated bookmark 87 | */ 88 | const postBookmarkPrompt = () => { 89 | bookmark = localStorage.readBookmark().bookmarks; 90 | prompt.showPosts(bookmark.map(data => data.title)).then(answer => { 91 | prompt.postOperation(['Open Link', 'Remove from Bookmark']).then(choice => { 92 | if(choice.postOperation === 'Remove from Bookmark') { 93 | localStorage.deleteBookmark(answer.title); 94 | return postBookmarkPrompt(); 95 | } 96 | open(bookmark.find(data => data.title === answer.title).link); 97 | process.exit(); 98 | }).catch(err => { 99 | console.log(err); 100 | }); 101 | 102 | }).catch(err => { 103 | console.log(err); 104 | }); 105 | } 106 | 107 | /** 108 | * This is a function to fetch top posts of a tags. 109 | * @param {string} tag - tag by which posts will be fetched 110 | * @returns {null} null 111 | */ 112 | 113 | const showPostsByTags = (tag) => { 114 | countdown.start(); 115 | crawler.fetchByTags(tag).then(data => { 116 | countdown.stop(); 117 | articles = data.filter(data => data.title != undefined); 118 | postPrompt(); 119 | }); 120 | } 121 | 122 | /** 123 | * This is a function to fetch top posts by time. 124 | * @param {string} timeline - timeline by which posts will be fetched 125 | * @returns {null} null 126 | */ 127 | 128 | const showPostsByTimeline = (timeline) => { 129 | countdown.start(); 130 | crawler.fetchTop(timeline).then(data => { 131 | countdown.stop(); 132 | articles = data.filter(data => data.title != undefined); 133 | postPrompt(); 134 | }) 135 | } 136 | 137 | /** 138 | * This is a function to display the profile details of the author. 139 | * @param {string} username - username of the author 140 | * @returns {null} null 141 | */ 142 | 143 | const showAuthorProfile = (username) => { 144 | countdown.start(); 145 | crawler.fetchAuthorProfile(username).then((profileInfo) => { 146 | countdown.stop(); 147 | if (!profileInfo.name) { 148 | console.error("😱 User not found. Please try again."); 149 | process.exit(1); 150 | } 151 | showProfile(profileInfo); 152 | }); 153 | } 154 | 155 | const suggestCommands = cmd => { 156 | const availableCommands = program.commands.map(c => c._name); 157 | 158 | const suggestion = didYouMean(cmd, availableCommands); 159 | if (suggestion) { 160 | console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`)); 161 | } 162 | } 163 | 164 | program 165 | .version(version) 166 | .usage(' [options]') 167 | .description(description) 168 | 169 | program 170 | .command("top [timeline]") 171 | .action(async (timeline) => { 172 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 173 | if (timeline) 174 | showPostsByTimeline(timeline); 175 | 176 | else 177 | prompt.selectTimline().then(answers => showPostsByTimeline(answers.timeline)); 178 | 179 | }) 180 | 181 | program 182 | .command("tag [tag]") 183 | .alias("t") 184 | .action(async (tag) => { 185 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 186 | if (tag) showPostsByTags(tag); 187 | 188 | else { 189 | crawler.fetchTags().then(data => { 190 | prompt.searchTags(data).then((answers) => { 191 | showPostsByTags(answers.tag); 192 | }); 193 | }); 194 | } 195 | }) 196 | 197 | program 198 | .command("latest") 199 | .alias("l") 200 | .action(async () => { 201 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 202 | countdown.start(); 203 | crawler.fetchLatest().then(data => { 204 | countdown.stop(); 205 | articles = data.filter(data => data.title != undefined); 206 | postPrompt(); 207 | }) 208 | }) 209 | 210 | program 211 | .command("bookmark") 212 | .alias("bm") 213 | .action(async () => { 214 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 215 | postBookmarkPrompt(); 216 | }) 217 | 218 | program 219 | .command("search ") 220 | .alias("s") 221 | .action(async (keyword) => { 222 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 223 | countdown.start(); 224 | crawler.searchPost(keyword).then(data => { 225 | countdown.stop(); 226 | articles = data.hits.map(post => { 227 | return { 228 | title: post.title, 229 | link: "https://dev.to" + post.path 230 | } 231 | }); 232 | postPrompt(); 233 | }); 234 | }) 235 | 236 | program 237 | .command("author ") 238 | .option("-p, --profile", "Show author profile") 239 | .alias("a") 240 | .action(async (username, cmd) => { 241 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 242 | if (cmd.profile) { 243 | showAuthorProfile(username); 244 | } else { 245 | countdown.start(); 246 | crawler.fetchByAuthor(username).then(data => { 247 | countdown.stop(); 248 | articles = data.filter(data => data.title != undefined); 249 | postPrompt(); 250 | }); 251 | } 252 | }) 253 | 254 | // error on unknown commands 255 | program.arguments('').action(async cmd => { 256 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 257 | program.outputHelp(); 258 | console.log(` ` + chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)); 259 | console.log(); 260 | suggestCommands(cmd); 261 | process.exit(1); 262 | }); 263 | 264 | program.parse(process.argv); 265 | 266 | if (program.args.length === 0) { 267 | (async () => { 268 | await showBanner('Devto', 'Browse and Search Dev.to Posts from Command Line', 'white'); 269 | countdown.start(); 270 | crawler.fetchHome().then(data => { 271 | countdown.stop(); 272 | articles = data.filter(data => data.title != undefined); 273 | postPrompt(); 274 | }) 275 | })(); 276 | } --------------------------------------------------------------------------------