├── .gitignore ├── LICENSE ├── README.md ├── TasksForCourseApply ├── 1-Beers-and-Fries.md ├── 2-Canvas-Triangles.md └── 3-Node-Task.md ├── exams └── exam1 │ ├── 1-Smart-Phonebook │ └── README.md │ ├── 2-Image-Filters-library │ └── README.md │ └── 3-Site-Mapper │ └── README.md ├── week0 ├── 1-ini-parsing │ ├── .gitignore │ ├── README.md │ ├── config.ini │ ├── config.json │ ├── package.json │ ├── sloppy_config.ini │ └── test.js ├── 2-Chirper │ ├── README.md │ ├── test-client.js │ └── test-server.js ├── README.md └── slides.markdown ├── week1 ├── 0-express-example │ ├── README.md │ ├── app.js │ └── package.json ├── 1-hackernews-scraper │ └── README.md ├── callbacks.md └── solutions │ ├── async-workflow.js │ ├── async.js │ ├── hell.js │ ├── promises.js │ └── scraper.js ├── week2 ├── 1-JSON-To-Mongo │ └── README.md ├── 2-Skip-Skip-Next │ ├── README.md │ ├── app │ │ ├── bower.json │ │ ├── index.html │ │ └── scripts │ │ │ └── app.js │ └── example-express │ │ ├── package.json │ │ └── server.js ├── 3-Geolocation-and-stuff │ ├── README.md │ ├── find-it │ │ ├── bower.json │ │ ├── index.html │ │ └── scripts │ │ │ └── app.js │ ├── geo.md │ ├── geo.zip │ └── save-it │ │ ├── bower.json │ │ ├── index.html │ │ └── scripts │ │ └── app.js ├── dump.zip └── materials.md ├── week3 ├── 1-Who-Follows-You-Back │ └── README.md └── README.md ├── week4 ├── 0-Exam │ └── README.md └── 1-Putting-It-All-Together │ └── README.md ├── week5 ├── 1-Passport-Basics │ ├── README.md │ └── static-app │ │ ├── app.js │ │ └── public │ │ └── index.html ├── 2-Linking-Accounts │ └── README.md └── Recap-and-Summary │ ├── package.json │ └── scrape-urls.js ├── week6 ├── 1-Stream-RegEx │ └── README.md ├── 2-HTTP-Proxy │ └── README.md ├── 3-A-Big-File │ └── README.md ├── 4-Video-Consumer │ ├── README.md │ └── streamer.js └── README.md ├── week7 └── 1-Chat-Server-And-Clients │ └── README.md └── week8 ├── 1-Nodeventure-Final └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 HackBulgaria 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NodeJS-1 2 | ========= 3 | 4 | The repository for the Web Programming with NodeJS course at http://hackbulgaria.com 5 | 6 | ## Course Program, split over weeks 7 | 8 | ## week0: 9 | 10 | * We are going to start using `node` and `npm` and `nvm` by doing simple console-based NodeJS applications. 11 | * We are going to show Node Inspector for Node debugging 12 | 13 | ## week1: 14 | 15 | * What is Node all about? Single threaded app & Event Loop & Thread pools 16 | * First steps in HTTP with `express` 17 | 18 | ## week2: 19 | 20 | * A+ Promises in Node (Q.js / async) 21 | * Problems with express and external APIs to use 22 | 23 | ## week3: 24 | 25 | * Introduction to Mongo and mongoose / mongoose-q 26 | * Basic CRUD applications 27 | 28 | ## week4: 29 | 30 | * Headless browsers & scrapping - PhantomJS / CasperJS / Nightmare 31 | * Testing web applications 32 | 33 | ## week5: 34 | 35 | * Passport for web apps - having users in our app 36 | * Mongo aggregation & making some simple statistics & dashboards 37 | 38 | ## week6: 39 | 40 | * Streams & Stream API 41 | * Sockets (TCP/IP) + JSON protocol implementation 42 | * WebSockets 43 | 44 | ## week7: 45 | 46 | * Introduction to Node Cluster API - scaling things 47 | * Introduction to Redis - simple communication between clusters 48 | 49 | ## week8: 50 | 51 | * Adding AI to our web application 52 | 53 | ## Slides 54 | The slides from the courses can be found on [the GitHub page of the course](http://hackbulgaria.github.io/NodeJS-1/) 55 | -------------------------------------------------------------------------------- /TasksForCourseApply/1-Beers-and-Fries.md: -------------------------------------------------------------------------------- 1 | # Beer and Fries - The Ultimate Combination! 2 | 3 | Ivan is the owner of a popular Ale House in Sofia. 4 | 5 | He has developed a new formula, that gives each beer or fries a score, that is an integer number. 6 | 7 | Also, he has found out that if you pair a beer with fries, the total score of them is the multiple of each individual score. 8 | 9 | For example, if we pair a beer with a score of `10` and fries with a score of `5`, the total score for the pair is `5 * 10 = 50` 10 | 11 | Ivan has an equal number of beer types and french fries types. He wants to find out how to combine them in pairs in order to get the maximum score of all! (The sum of all pair scores) 12 | 13 | Implement a function, called `beerAndFries(items)` that takes an array of objects: 14 | 15 | ``` 16 | [{ 17 | type : "beer", 18 | score : 10 19 | }, 20 | { 21 | type : "fries", 22 | score : 10 23 | }, 24 | ... 25 | ] 26 | ``` 27 | 28 | Each object in the array is going to have only two properties - `type`, which will be either `beer` of `fries`, and a `score` - an integer number. 29 | 30 | The function should output the maximum score that can be achieved by pairing a beer with fries. 31 | 32 | __The number of beers in the array will be equal to the number of fries!__ 33 | 34 | 35 | ## Example cases 36 | 37 | Here is a full unit-test example (using `nodeunit`) with test data: 38 | 39 | ```javascript 40 | "use strict"; 41 | 42 | var beerAndFries = require("./beerAndFries.js").beerAndFries; 43 | 44 | 45 | exports.testBeerAndFriesScenario1 = function(test) { 46 | var testData = [{ 47 | type: "beer", 48 | score: 10 49 | }, { 50 | type: "beer", 51 | score: 11 52 | }, { 53 | type: "fries", 54 | score: 1 55 | }, { 56 | type: "fries", 57 | score: 5 58 | }]; 59 | 60 | test.equal(65, beerAndFries(testData)); 61 | test.done(); 62 | }; 63 | 64 | 65 | exports.testBeerAndFriesScenario2 = function(test) { 66 | var testData = [{ 67 | type: "beer", 68 | score: 1 69 | }, { 70 | type: "beer", 71 | score: 11 72 | }, { 73 | type: "fries", 74 | score: 0 75 | }, { 76 | type: "fries", 77 | score: 50 78 | }]; 79 | 80 | test.equal(550, beerAndFries(testData)); 81 | test.done(); 82 | }; 83 | 84 | 85 | exports.testBeerAndFriesScenario3 = function(test) { 86 | var testData = [{ 87 | type: "beer", 88 | score: 5 89 | }, { 90 | type: "fries", 91 | score: 5 92 | }]; 93 | 94 | test.equal(25, beerAndFries(testData)); 95 | test.done(); 96 | }; 97 | 98 | exports.testBeerAndFriesScenario4 = function(test) { 99 | var testData = []; 100 | 101 | test.equal(0, beerAndFries(testData)); 102 | test.done(); 103 | }; 104 | 105 | exports.testBeerAndFriesScenario5 = function(test) { 106 | var testData = [{ 107 | type: "beer", 108 | score: 1000 109 | }, { 110 | type: "beer", 111 | score: 1010 112 | }, { 113 | type: "beer", 114 | score: 1020 115 | }, { 116 | type: "beer", 117 | score: 1030 118 | }, { 119 | type: "beer", 120 | score: 1040 121 | }, { 122 | type: "fries", 123 | score: 834 124 | }, { 125 | type: "fries", 126 | score: 500 127 | }, { 128 | type: "fries", 129 | score: -1 130 | }, { 131 | type: "fries", 132 | score: 0 133 | }, { 134 | type: "fries", 135 | score: 60 136 | }]; 137 | 138 | test.equal(1442560, beerAndFries(testData)); 139 | test.done(); 140 | }; 141 | ``` 142 | 143 | ## Details regarding implementation 144 | 145 | Upload your solution to your GitHub account in a repository of your choice. We prefer that over gists. 146 | 147 | __If you are using 3rd party libraries, don't forget to include `package.json` as well.__ -------------------------------------------------------------------------------- /TasksForCourseApply/2-Canvas-Triangles.md: -------------------------------------------------------------------------------- 1 | # Drawing Triangles On Canvas 2 | 3 | Implement a web application, that uses HTML5 Canvas where users can draw triangles in different colors. 4 | 5 | A quick reminder for what a triangle is - http://en.wikipedia.org/wiki/Triangle 6 | 7 | The app should have functionality for saving all drawn triangles to a `localStorage` so they can be loaded after this. 8 | 9 | ## Requirements 10 | 11 | ### Drawing Triangles with fill color 12 | 13 | Every tree points that the user clicks on the canvas, should create a new triangle. As simple as that. 14 | 15 | Every triangle should have a fill color, that should be selected by a color input. Once a color is selected, every next triangle is filled by that color. 16 | 17 | [__Use an HTML 5 color input!__](http://www.w3schools.com/html/tryit.asp?filename=tryhtml5_input_type_color) 18 | 19 | The position of the color input is up to you! 20 | 21 | ### Clearing the canvas 22 | 23 | There should be a button called "Clear Canvas" which clears the entire canvas for new triangles! 24 | 25 | ## Bonus Round - If you want to 26 | 27 | ### Saving the canvas to local storage 28 | 29 | __There should be a button called "Save to local storage".__ 30 | 31 | Once the button is clicked, the app should prompt the user to give name for the current save. 32 | 33 | This means we can have multiple saves for our canvas! 34 | 35 | ### Loading the canvas from local storage 36 | 37 | There should be a drop-down menu with all saves from the local storage and a button __Load!__ 38 | 39 | Once a save is selected and the button is clicked, the canvas should be populated with the triangles from the save. 40 | 41 | ## Bonus Bonus Round - If you are up to a challenge 42 | 43 | For every triangle drawn on the canvas, calculate its area (in pixels) and place the number as a label, inside the triangle. 44 | 45 | The label should be centered around the center of the triangle! 46 | 47 | Make sure that the color of the text is not matching the color of the triangle. -------------------------------------------------------------------------------- /TasksForCourseApply/3-Node-Task.md: -------------------------------------------------------------------------------- 1 | # The node task (optional) 2 | 3 | [https://github.com/asotirov/that-node-task](https://github.com/asotirov/that-node-task) 4 | 5 | This link leads to the code of a node.js application. The application runs an express server, that is used to make simple CRUD requests to a database. Users of the application can create, read, update and delete objects in the database. 6 | 7 | Some methods are left unimplemented and your task is to complete them, so the provided tests pass. 8 | 9 | Full details are available in the **README.md** of the repository. 10 | 11 | Good luck! 12 | 13 | ## Optional task 14 | 15 | You can apply without solving this problem. But if you manage to solve it, this will help you in the interview process :) 16 | -------------------------------------------------------------------------------- /exams/exam1/1-Smart-Phonebook/README.md: -------------------------------------------------------------------------------- 1 | # Smart Phonebook 2 | 3 | We are going to create a simple web application that keeps phone numbers and names together. 4 | 5 | We are going to need a REST API for our server, in order to perform all the CRUD operations required. 6 | 7 | __There are two main objects:__ 8 | 9 | * **contacts**, which consist of a phone number and a name 10 | * **groups**, which are collection of contacts. 11 | 12 | ## CRUD for Contacts 13 | 14 | First and foremost, we will need a way to manage our contacts. 15 | 16 | A contacts consist of two things: 17 | 18 | ```json 19 | { 20 | "phoneNumber": "....", 21 | "personIdentifier": "..." 22 | } 23 | ``` 24 | 25 | Every contact should be identified by some unique identifier (For example, the id from Mongo) 26 | 27 | ### Endpoints 28 | 29 | We are interested in the following CRUD operations: 30 | 31 | * Creating a new contact 32 | * Reading all contacts 33 | * Reading a given contact 34 | * Deleting a given contact 35 | 36 | Don't bother handling the updating of a contact. 37 | 38 | ### Testing the endpoints 39 | 40 | Be sure to test your endpoints one way or another. There is a rich toolsuite for testing in the Node ecosystem, so choose wisely! 41 | 42 | ## Groups and Smart Groups 43 | 44 | Our app will have the feature to group contacts together, under a name. 45 | 46 | But to have a little twist, there won't be any **Create / Update / Delete** operations for creating groups. 47 | 48 | Groups will be created only from our application - if certain criteria for all contacts are met, the app will create a new group for us. 49 | 50 | ### Criteria for groups 51 | 52 | #### Common Words in Names 53 | 54 | If there is a common word between two `personIdentifiers` in two contacts, those contacts are grouped together. If there is no existing group with that common word, such group is created. Otherwise - it is updated with the new contacts. 55 | 56 | For example, lets have two concat names: 57 | 58 | * `"Ivan Mladost"` 59 | * `"Maria MLADOST"` 60 | 61 | We see that if we ignore the case (and so should your app), there is a common word `"mladost"` in it, which will result in a new group: 62 | 63 | ```json 64 | { 65 | "groupName": "Mladost", 66 | "contacts": [ 67 | .... 68 | ] 69 | } 70 | ``` 71 | 72 | If there are two common words, that are different from each other, two groups should be created! 73 | 74 | For example, if we have: 75 | 76 | * `"Rado Georgiev Mladost"` 77 | * `"Rado Ivanov Mladost"` 78 | 79 | Two groups with names `"Rado"` and `"Mladost"` should be created. 80 | 81 | #### Really close words, should also form Fuzzy groups! 82 | 83 | Sometimes, when we write names for our contacts, we misspell things. 84 | 85 | But our app should handle that case too! 86 | 87 | If there are two words in the names of two contacts, that has **edit distance** less than or equal to two, they should form a **fuzzy group**, which has two names - both the two words. 88 | 89 | For example, lets have: 90 | 91 | * `"Rado Mladost"` 92 | * `"Rado Mldost"` 93 | 94 | The edit distance (also called Levenshtein distance) between `"Mladost"` and `"Mldost"` is 1, so this should form a group, which looks like this: 95 | 96 | ```json 97 | { 98 | "groupName": ["Mladost", "Mldost"], 99 | "type": "fuzzy", 100 | "contacts": [ 101 | ... 102 | ] 103 | } 104 | ``` 105 | 106 | [You can use the following library, to compute the Levenshtein distance.](https://github.com/gf3/Levenshtein) 107 | 108 | ### Endpoints 109 | 110 | We should be able to list all groups and contacts in there. 111 | 112 | ## Guides 113 | 114 | ### Libraries 115 | 116 | * Use Express for HTTP routing 117 | * Use Mongo for storage 118 | * Promises / Mongoose is up to you 119 | 120 | ### Group forming 121 | 122 | * Always ignore case, but when you create a new group, the name should start with an upper case! 123 | * To take different words, just split by whitespace - this should be enough for the task (No need for tokenizing) 124 | -------------------------------------------------------------------------------- /exams/exam1/2-Image-Filters-library/README.md: -------------------------------------------------------------------------------- 1 | # Image filter library 2 | 3 | We want to create a node module for applying simple image processing with convolution, defined with a [kernel](https://en.wikipedia.org/wiki/Kernel_%28image_processing%29). We want our module's API to be implemented with a promise interface. 4 | 5 | # Interface and data format 6 | 7 | Our module should expose two objects, with corresponding methods: 8 | 9 | * `monochrome` - an object holding the methods for manipulating monochrome images 10 | * `rgb` - an object holding the methods for manipulating rgb images 11 | 12 | ## Data formats 13 | 14 | ### Monochrome 15 | All `imageData` objects passed to methods of `monochrome` are expected to be `Array`s of `Array`s of integer numbers. Here's a black square image with a white X mark on it. 16 | 17 | ```javascript 18 | var xMarksTheSpot = [ 19 | [255, 0, 0, 0, 255], 20 | [ 0, 255, 0, 255, 0], 21 | [ 0, 0, 255, 0, 0] 22 | [ 0, 255, 0, 255, 0], 23 | [255, 0, 0, 0, 255], 24 | ] 25 | ``` 26 | 27 | ### RGB 28 | 29 | All `imageData` objects passed to methods of `rgb` are expected to have `red`, `green` and `blue` properties, each of them holding one monochrome `imageData` as the one shown above(an `Array` of `Array`s of integers). 30 | 31 | Applying a kernel to an RGB image is essentially applying it to each of its three colour components. 32 | 33 | ### Methods 34 | Both the `monochrome` and `rgb` objects should have the following methods: 35 | 36 | * `edgeDetection(imageData)` - accepts an image to apply edge detection to, returns a promise object that will be resolved once the image processing has finished. 37 | * `boxBlur(imageData)` - accepts an image to apply box blur to, returns a promise object that will be resolved onec the image processing has finished. 38 | * `applyKernel(imageData, kernel)` - accepts an image and a kernel to apply to that image, returns a promise object, that will be resolved once the kernel has been applied to the image. 39 | 40 | 41 | ### Promise resolution values 42 | * The promises returned by `monochrome`'s methods should eventually be resolved with a monochrome `imageData` object, that is an `Array` of `Array`s of integers. 43 | * The promises returne by `rgb`'s methods should eventually be resolved with an RGB `imageData` object, that is an object with a `red`, `green` and `blue` property, each holding a monochrome `imageData` object. 44 | 45 | ## Notes 46 | Expect `imageData` and `kernel` to be `Array`s of `Array`s containing integers. They're basically just matrices, that need to be manipulated in some manner. 47 | 48 | This format implies that `imageData` will always be a monochrome image, not a coloured one. 49 | 50 | Assuming our module is called `convolution` 51 | 52 | When convolving a kernel and image treat any needed pixels outside the image as having a value of 0. 53 | 54 | The input data could potentially be very big. Think of a good way to divide it into separate small units of execution, so as to not 55 | 56 | # Example usage 57 | ```javascript 58 | var convolution = require('convolution'), 59 | xMarksTheSpot = [ 60 | [1, 0, 1], 61 | [0, 1, 0] 62 | [1, 0, 1], 63 | ], 64 | verticalBlur = [ 65 | [0, 0.5, 0], 66 | [0, 0, 0], 67 | [0, 1, 0] 68 | ]; 69 | 70 | convolution.monochrome.applyKernel(xMarksTheSpot, verticalBlur) 71 | .then(function (blurredX) { 72 | // [ 0, 1, 0], 73 | // [1.5, 0, 1.5], 74 | // [ 0, 0.5, 0] 75 | }); 76 | ``` 77 | -------------------------------------------------------------------------------- /exams/exam1/3-Site-Mapper/README.md: -------------------------------------------------------------------------------- 1 | # Site mapper 2 | 3 | The task is to expose an HTTP API for requesting site maps. 4 | 5 | ## Endpoints 6 | 7 | It needs to have the following endpoints: 8 | 9 | * `POST` `/map` - accepts JSON requests that should have a key `"url"`, which value is the address of the site we want to crawl(scheme, host name and optionally a port). Return an id assigned to the site and starts building it. 10 | * `GET` `/sitemap` - accepts JSON requests with one key `"id"`, to get the site map for that site. The possible return values are: 11 | * if the crawling has not yet finished: `{"status": "currently crawling"}` 12 | * if it's finished: 13 | ```javascript 14 | { 15 | "status": "done", 16 | "sitemap": [ 17 | { 18 | "url": "http://reddit.com", // this url 19 | "links": […], // holds links to these urls 20 | }, 21 | … 22 | ] 23 | } 24 | ``` 25 | 26 | ## Notes 27 | 28 | ### Don't repeat work 29 | If we send a request to `/map` for generating a site map for a URL we've already requested we should always return the same id, thus not crawling a site twice. 30 | 31 | ### Don't go too deep 32 | Site maps could get quite big, so limit them to 500 links in total. This means the response to `/sitemap` should have a `"sitemap"` property with length of no more than 500. 33 | 34 | ### Don't crawl outside the domain 35 | If the starting url given is `http://reddit.com/r/nodejs` than you should only make requests to links from it pointing to other things on `reddit.com`. `http://reddit.com/r/javascript` is OK. `http://dev.reddit.com` is OK. Anything on `http://news.ycombinator.com` is not OK 36 | 37 | ### robots.txt 38 | Respect a site's [robots.txt](http://www.robotstxt.org/) when crawling it to build a site map. 39 | 40 | 41 | ### Libraries 42 | You can use whatever libraries you find useful for this task, but here are our recommendations: 43 | 44 | * [robots.js](https://github.com/ekalinin/robots.js) - gives you an interface for parsing robots.txt files and checking their rules 45 | * [node-htmlparser](https://github.com/tautologistics/node-htmlparser) - lets you parse html into an object representing the DOM 46 | * [node-soupselect](https://github.com/harryf/node-soupselect) - lets you use css selectors on the object produced by node-htmlparser 47 | 48 | --- 49 | * [Q](https://github.com/kriskowal/q) - if things are about to turn into a callback hell, refactor a bit and use a deferred, it always helps to make code more readable. 50 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/README.md: -------------------------------------------------------------------------------- 1 | # Week 0 2 | 3 | ## Parsing ini files 4 | 5 | The task is to read in a [INI file](https://en.wikipedia.org/wiki/INI_file) and parse its contents. 6 | 7 | So given `config.ini` with the following contents: 8 | 9 | ```ini 10 | [panda] 11 | name=Stamat 12 | lazyness=95 13 | cuteness=123 14 | 15 | [unicorn] 16 | name=Pencho 17 | age=0.3 bilion 18 | horns=1 19 | probability=0.1e-50000 20 | ``` 21 | 22 | Running `node our_script.js config.ini` should create `config.json` with the following contents: 23 | 24 | ```json 25 | { 26 | "panda": { 27 | "name": "Stamat", 28 | "lazyness": "95", 29 | "cuteness": "123" 30 | }, 31 | "unicorn": { 32 | "name": "Pencho", 33 | "age": "0.3 bilion", 34 | "horns": "1", 35 | "probability": "0.1e-50000" 36 | } 37 | } 38 | ``` 39 | 40 | ### Gotchas 41 | * There can be multiple blank lines all over the file for readability purposes, they should just be ignored when parsing it. 42 | * Spaces around equal signs and trailing spaces should be ok, although it's a good idea to warn that other parsers might not be as benevolent as ours. 43 | * Lines starting with a semicolon(`;`) are comments, and should be ignored when parsing. 44 | 45 | 46 | 47 | ### Local files are boring 48 | If we give our script an HTTP(S) URL instead of a filename it should fetch what's on that URL assuming it's a valid ini file. The name of the produced file should be the last part of the path of the URL. If the URL ends with `.ini` it should be replaced by `.json`, otherwise `.json` should just be appended to it. 49 | 50 | So calling `node our_script.js https://raw.githubusercontent.com/HackBulgaria/NodeJS-1/master/week0/1-ini-parsing/config.ini` should create `config.json` 51 | 52 | ## The other way around 53 | Once we know how to convert `.ini` to `.json` it would be nice to have it work both ways. Extend the script to also accept a json file and write the corresponding ini file. Again running `node our_script.js config.json` should create `config.ini`. The script should detect if the argument is a json or an ini file based on the file extension. If we can't deduce the file type from the extension(or there is no extension) assume it's an ini file. 54 | 55 | ### Explicit file type 56 | 57 | ```bash 58 | npm install argparse --save 59 | ``` 60 | 61 | Using the [argparse](https://github.com/nodeca/argparse) module add the capability to explicitly state the type of the file we are giving. Add an argument `--type` which can be either `ini` or `json` and tells the type of the input file given. 62 | 63 | So we could call `node our_script.js typeless_config --type=ini`. That invocation should mean that `typeless_config` is actually an ini file. 64 | 65 | ### Testing your solution 66 | 67 | This directory has a `package.json` file defining the tools needed to test your solution. Assuming your code is in `solution.js` if you run `node test` will run a small test suite on it, testing with the two local files. 68 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/config.ini: -------------------------------------------------------------------------------- 1 | [panda] 2 | name=Stamat 3 | lazyness=95 4 | cuteness=123 5 | 6 | [unicorn] 7 | name=Pencho 8 | age=0.3 bilion 9 | horns=1 10 | probability=0.1e-50000 11 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "panda": { 3 | "name": "Stamat", 4 | "lazyness": "95", 5 | "cuteness": "123" 6 | }, 7 | "unicorn": { 8 | "name": "Pencho", 9 | "age": "0.3 bilion", 10 | "horns": "1", 11 | "probability": "0.1e-50000" 12 | } 13 | } -------------------------------------------------------------------------------- /week0/1-ini-parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1-ini-parsing", 3 | "version": "0.0.0", 4 | "description": "first task for nodejs course @ hack bulgaria", 5 | "main": "solution.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "chai": "^1.9.2" 13 | }, 14 | "dependencies": { 15 | "argparse": "^0.1.15" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/sloppy_config.ini: -------------------------------------------------------------------------------- 1 | [panda] 2 | name=Stamat 3 | lazyness=95 4 | cuteness=123 5 | 6 | [unicorn] 7 | name=Pencho 8 | age=0.3 bilion 9 | horns=1 10 | probability=0.1e-50000 11 | 12 | [narwal] 13 | ; Narwhals, Narwhals 14 | ; Swimming in the ocean 15 | ; Causing a commotion 16 | ; Coz they are so awesome 17 | mamal = true 18 | 19 | 20 | horns = 0 21 | 22 | 23 | huge_teeth = 1 24 | 25 | ; [OMG! The section is a lie] 26 | -------------------------------------------------------------------------------- /week0/1-ini-parsing/test.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'), 2 | expect = require('chai').expect; 3 | 4 | 5 | function testWithFile(fileName, expected) { 6 | var process = child_process.spawn('node', ['./solution.js', fileName]); 7 | 8 | process.on('error', function (error) { 9 | console.error('Error'); 10 | console.error(error); 11 | }); 12 | 13 | process.on('close', function (code) { 14 | var result; 15 | if (code) { 16 | console.error('Return code ' + code); 17 | } else { 18 | result = require('./' + fileName.replace(/.ini$/, '.json')); 19 | expect(result).to.deep.eq(expected); 20 | } 21 | }); 22 | } 23 | 24 | testWithFile('config.ini', { 25 | "panda": { 26 | "name": "Stamat", 27 | "lazyness": "95", 28 | "cuteness": "123" 29 | }, 30 | "unicorn": { 31 | "name": "Pencho", 32 | "age": "0.3 bilion", 33 | "horns": "1", 34 | "probability": "0.1e-50000" 35 | } 36 | }); 37 | 38 | testWithFile('sloppy_config.ini', { 39 | "panda": { 40 | "name": "Stamat", 41 | "lazyness": "95", 42 | "cuteness": "123" 43 | }, 44 | "unicorn": { 45 | "name": "Pencho", 46 | "age": "0.3 bilion", 47 | "horns": "1", 48 | "probability": "0.1e-50000" 49 | }, 50 | "narwal": { 51 | "mamal": "true", 52 | "horns": "0", 53 | "huge_teeth": "1" 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /week0/2-Chirper/README.md: -------------------------------------------------------------------------------- 1 | # Twitter clone in a single process 2 | 3 | Using the `http` module write a Chirpr(über innovative naming right there!) - a twitter-like service. 4 | 5 | ## The Chirp API 6 | 7 | We want to be able to make the following calls: 8 | 9 | * `GET /all_chirps` - returns all the chirps for all the users we have. Newest chirps should be first. 10 | * `POST /chirp` - expects `user`, `key` and `chirpText` arguments. Creates a new chirp on behalf of `user` and returns a `chirpId`, which should be unique for every chirp! 11 | * `GET /all_users` - returns all the registered users. 12 | * `POST /register` - expects `user` as argument. Creates a new user and returns a `key` for that user. If the user already exists just returns a 409 response code. 13 | * `GET /my_chirps` - expects `user` and `key` as arguments. Returns all chirps of `user` 14 | * `DELETE /chirp` - expects `key` and `chirpId` as arguments. Deletes the chirp with the given id if the key matches the key of the chirp owner. Otherwise returns a 403 response code. 15 | * `GET /chirps` - expects either `chirpId` or `userId` as an argument. If given both ignores `chirpId`. Returns a list of chirps. 16 | 17 | Some specification: 18 | 19 | * `user` should be a username - a string 20 | * `key` should be a unique string for each user 21 | * **The arguments to the API calls should be in JSON format!** 22 | * **All returns should be in JSON format!** 23 | 24 | ### Formats 25 | 26 | `/chirps`, `/all_chirps` and `/my_chirps` should return a json list with objects for the chirps: 27 | 28 | ```json 29 | [ 30 | { 31 | "userId": 12, 32 | "chirpId": 48, 33 | "chirpText": "Търсим нов служител. Нужно е да е наполовина човеко-прасе, наполовина - мечка. Желание за работа с #WordPress е многу от съществено значка.", 34 | "chirpTime": "10-10-2014 12:54" 35 | } 36 | ] 37 | ``` 38 | 39 | `/all_users` should return a list of objects for all the registered users with their id, name and the number of chirps they have. 40 | 41 | ```json 42 | [ 43 | { 44 | "user": "Stancho", 45 | "userId": 31, 46 | "chirps": 43, 47 | } 48 | ] 49 | ``` 50 | 51 | ### Testing things out 52 | 53 | Now, you should implement a Client for our Chirp API. Of course, using NodeJS. 54 | This should be entirely different code and project! 55 | 56 | The client should be console-like and accept different arguments in order to use the API. 57 | 58 | For example, if we run the client like that: 59 | 60 | ``` 61 | $ node chirp_client.js --register --user=RadoRado 62 | 63 | ``` 64 | 65 | [Use argparse to parse your arguments.](https://github.com/nodeca/argparse) 66 | 67 | ### Specification of all API calls 68 | 69 | The most important thing for our API client should be one `config.json` file, located relatively in the same directory, which will store the API url: 70 | 71 | ```json 72 | { 73 | "api_url": "http://localhost:8080" 74 | } 75 | ``` 76 | 77 | If you have an API key (a registered user), you should add it to the `config.json` file too: 78 | 79 | ```json 80 | { 81 | "api_url": "http://localhost:8080", 82 | "user": "RadoRado", 83 | "key": "bananispijami" 84 | } 85 | ``` 86 | 87 | All other API calls should rely on that `config.json`. The only exception is the API call for registering. 88 | 89 | #### Registering user 90 | 91 | This should look like this: 92 | 93 | ``` 94 | $ node chirp_client.js --register --user=RadoRado 95 | 96 | ``` 97 | 98 | Once you have the response, update the `config.json` file with the user and the returned API key. If there is an existing user in `config.json`, overwrite it. 99 | 100 | #### Get all chirps 101 | 102 | This should look like this: 103 | 104 | ``` 105 | $ node chirp_client.js --getall 106 | 107 | ``` 108 | 109 | This call does not require `user` and `key` in `config.json` 110 | 111 | 112 | #### Get my chirps 113 | 114 | This should look like this: 115 | 116 | ``` 117 | $ node chirp_client.js --getself 118 | 119 | ``` 120 | 121 | This call requires `user` and `key` in `config.json` 122 | 123 | #### Create new chirp 124 | 125 | This should look like this: 126 | 127 | ``` 128 | $ node chirp_client.js --create --message="Relationship status: пътувам с автобус" 129 | 130 | ``` 131 | 132 | This call requires `user` and `key` in `config.json` 133 | 134 | 135 | #### Delete a chirp 136 | 137 | This should look like this: 138 | 139 | ``` 140 | $ node chirp_client.js --delete --chirpid=12 141 | 142 | ``` 143 | 144 | This call requires `user` and `key` in `config.json` 145 | 146 | ## NB 147 | 148 | We're going extreme NODB! Keep all your data in-memory. 149 | 150 | You can use this to make JSON curl requests: 151 | 152 | ``` 153 | curl -H "Content-Type: application/json" --data @body.json http://localhost:8080/ 154 | ``` 155 | 156 | Where `body.json` is local file. 157 | -------------------------------------------------------------------------------- /week0/2-Chirper/test-client.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | 3 | http.get("http://localhost:8080", function(res) { 4 | res.on("data", function(data) { 5 | console.log(data.toString()); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /week0/2-Chirper/test-server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | pandaCounter = 0; 3 | 4 | http.createServer(function (req, res) { 5 | var payload = ""; 6 | 7 | console.log(req.url); 8 | console.log(req.method); 9 | 10 | req.on('data', function(chunk) { 11 | console.log("Received body data:"); 12 | console.log(chunk.toString()); 13 | payload += chunk.toString(); 14 | }); 15 | 16 | req.on('end', function() { 17 | pandaCounter ++; 18 | res.writeHead(200, "OK", {'Content-Type': 'text/html'}); 19 | res.end("PANDATIGAN " + pandaCounter); 20 | }); 21 | 22 | }).listen(8080); 23 | -------------------------------------------------------------------------------- /week0/README.md: -------------------------------------------------------------------------------- 1 | # Materials to read before Week 0: 2 | 3 | Since we are not going to teach JavaScript in the Node course, be sure that you know the language well. 4 | 5 | Here is a list of good materials for refresh your memory about JavaScript. If you find anything that is not familiar - read it & understand it! 6 | 7 | * Review on JavaScript after the Frontend JavaScript course - https://www.youtube.com/watch?v=R6jES4mDNzM 8 | * Review on JavaScript as slides can be found here - http://hackbulgaria.github.io/NodeJS-1/ 9 | * A very good book on the entire topic is http://eloquentjavascript.net/ 10 | * If you are not familiar with functional programming and higher order functions, read this chapter - http://eloquentjavascript.net/05_higher_order.html 11 | * A very good explanation on the `this` object in JavaScript - http://stackoverflow.com/questions/133973/how-does-this-keyword-work-within-a-javascript-object-literal 12 | -------------------------------------------------------------------------------- /week0/slides.markdown: -------------------------------------------------------------------------------- 1 | class: center, inverse 2 | ![hackbg](src/img/HackBG-logo.png) 3 | ![node](src/img/node.png) 4 | 5 | --- 6 | 7 | 8 | # First thing's first 9 | ### nvm 10 | 11 | https://github.com/creationix/nvm 12 | 13 | ``` 14 | curl https://raw.githubusercontent.com/creationix/nvm/v0.17.1/install.sh | bash 15 | ``` 16 | 17 | --- 18 | # First thing's fisrt 19 | ### windows 20 | http://nodejs.org/download/ 21 | 22 | --- 23 | # REPL 24 | Pretty much what you're used to with Chrome/Firefox dev tools 25 | 26 | ```javascript 27 | node 28 | > var a = 42; 29 | undefined 30 | > var b = 73; 31 | undefined 32 | > a + b; 33 | 115 34 | > "" + a; 35 | '42' 36 | > ['coffee', 'tea', 'sugar', 'cinnamon'].length 37 | 4 38 | > 39 | ``` 40 | 41 | --- 42 | # Applications/packages 43 | Every applications can be treated as a node package. It's not mandatory to do this in order to run an application, but is considered a good practice and offers quite a bit of conveniences. 44 | 45 | --- 46 | 47 | ## npm 48 | #### Node Package Manager 49 | 50 | ```bash 51 | npm init 52 | … 53 | ``` 54 | 55 | Creates a `package.json` file, which describes our package: 56 | 57 | ```json 58 | { 59 | "name": "example_package", 60 | "version": "0.0.0", 61 | "description": "an example package for the Hack Bulgaria nodejs course", 62 | "main": "index.js", 63 | "scripts": { 64 | "test": "test.js" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git://git@github.com:HackBulgaria/NodeJS-1/example_package" 69 | }, 70 | "author": "Hack Bulgaria", 71 | "license": "MIT" 72 | } 73 | ``` 74 | 75 | --- 76 | # Dependencies 77 | 78 | Every language has this issue to some extent. Different applications often depend on different libraries/modules/packages. Some times those dependencies result in applications conflicting with one another. 79 | 80 | * RVM gem sets/Bundler GEMFILEs 81 | * Python's virtualenv 82 | * etc … 83 | 84 | #### Node has `./node_modules` 85 | 86 | --- 87 | # node_modules 88 | Each application/package holds it's own dependencies locally in a `./node_modules` directory within it's own root directory. Those can be specified in `package.json` and are usually installed using `npm`. 89 | 90 | ```bash 91 | npm install express --save 92 | npm install mocha chai --save-dev 93 | npm remove mocha --save-dev 94 | ``` 95 | 96 | --- 97 | So our `package.json` now looks like this: 98 | 99 | ```json 100 | { 101 | "name": "example_package", 102 | "version": "0.0.0", 103 | "description": "an example package for the Hack Bulgaria nodejs course", 104 | "main": "index.js", 105 | "scripts": { 106 | "test": "test.js" 107 | }, 108 | "repository": { 109 | "type": "git", 110 | "url": "git://git@github.com:HackBulgaria/NodeJS-1/example_package" 111 | }, 112 | "author": "Hack Bulgaria", 113 | "license": "MIT", 114 | "devDependencies": { 115 | "chai": "^1.9.2", 116 | "mocha": "^1.21.4" 117 | }, 118 | "dependencies": { 119 | "express": "^4.9.5" 120 | } 121 | } 122 | ``` 123 | 124 | --- 125 | # Global packages 126 | Some packages need to be installed globally on your system in order for them to work. Things like coffee script, grunt etc. These offer a global executable file which needs to be in yout `$PATH` in order to be easily runnable. For such cases there is the `-g` flag for `npm install` which tells npm to install to some global location, rather than the local `./node_modules`. 127 | 128 | **NB!!** This is only useful for packages that *MUST* be installed globally in order to be useful. Installing in `./node_modules` is preferred. 129 | 130 | If using `nvm` or `n` you can simply invode `npm install -g `. If you are using `node` installed from your system's package manager you will most probably need privileged access to the system in order to install packages globally. 131 | 132 | --- 133 | 134 | # fs 135 | 136 | ```javascript 137 | var fs = require('fs'); 138 | fs.readFile(fileName, function(error, data) { 139 | if (error) { 140 | console.error('Error reading file: ' + error); 141 | } else { 142 | console.log('File contents: ' + data.toString()) 143 | } 144 | }); 145 | ``` 146 | 147 | --- 148 | 149 | # http(https) 150 | ```javascript 151 | var http = require('http'); 152 | http.get('http://some.awesome.place.com/interesting_thin.gz', function(res) { 153 | res.on('data', function(data) { 154 | console.log(data.toString()) 155 | }); 156 | }); 157 | ``` 158 | 159 | # API documentation 160 | 161 | http://nodejs.org/api 162 | -------------------------------------------------------------------------------- /week1/0-express-example/README.md: -------------------------------------------------------------------------------- 1 | # Express 2 | 3 | Express is a nodejs web framework. It's a nice way to write web APIs or full blown web applications without having to bother with all the hassle of working directly with the `http` module's interface. 4 | 5 | [The ExpressJS site](http://expressjs.com/) 6 | [The API reference](http://expressjs.com/api.html) 7 | [The source on GitHub](https://github.com/expressjs) 8 | 9 | Here we'll show a simple example of a web API built with express. 10 | -------------------------------------------------------------------------------- /week1/0-express-example/app.js: -------------------------------------------------------------------------------- 1 | var express = require ('express'), 2 | bodyParser = require('body-parser'), 3 | rand = require('generate-key'), 4 | app = express(), 5 | data = { 6 | users: [], 7 | chirps: [], 8 | }; 9 | 10 | app.use(bodyParser.json()); 11 | app.use(bodyParser.urlencoded({extended: true})); 12 | 13 | function authUser (username, key) { 14 | var user = data.users.filter(function (user) { 15 | return user.name === username; 16 | }); 17 | 18 | if (user[0]) { 19 | return key === user[0].key ? user[0].id : false; 20 | } 21 | } 22 | 23 | function userExists (username) { 24 | var user = data.users.filter(function (user) { 25 | return user.name === username; 26 | })[0]; 27 | 28 | return user; 29 | } 30 | 31 | app.post('/chirp', function (req, res) { 32 | var chirp = req.body, 33 | userId = authUser(chirp.user, chirp.key); 34 | 35 | if (userId) { 36 | data.chirps.push({ 37 | 'userId': userId, 38 | 'chirpTime': Date.now(), 39 | 'chirpText': chirp.chirpText, 40 | }); 41 | } 42 | 43 | res.json({ 44 | 'chirpId': data.length - 1, 45 | }); 46 | }); 47 | 48 | app.post('/register', function (req, res) { 49 | var user, 50 | userId; 51 | 52 | console.log(req.body); 53 | if (userExists(req.body.username)) { 54 | res.status(403); 55 | res.end(); 56 | return; 57 | } 58 | 59 | user = { 60 | username: req.body.user, 61 | key: rand.generateKey(100), 62 | }; 63 | 64 | data.users.push(user); 65 | 66 | userId = data.users.length - 1; 67 | console.log('Registered user: ' + user.username); 68 | res.json({ 69 | userId: userId, 70 | key: user.key, 71 | }); 72 | }); 73 | 74 | app.get('/all_chirps', function (req, res) { 75 | res.json(data.chirps); 76 | }); 77 | 78 | 79 | app.listen(8000); 80 | -------------------------------------------------------------------------------- /week1/0-express-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chirp", 3 | "version": "1.0.0", 4 | "description": "Express is a nodejs web framework. It's a nice way to write web APIs or full blown web applications without having to bother with all the hassle of working directly with the `http` module's interface.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.9.0", 13 | "express": "^4.9.7", 14 | "generate-key": "0.0.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /week1/1-hackernews-scraper/README.md: -------------------------------------------------------------------------------- 1 | # HackerNews Subscriber App 2 | 3 | We want to build a larger system that does something more interesting and complex this time. 4 | 5 | And we like [Hacker News](https://news.ycombinator.com/). It's a simple concept, which is in a big part what makes it popular. 6 | 7 | It's a nice example of doing things [The UNIX Way](https://en.wikipedia.org/wiki/Unix_philosophy#Mike_Gancarz:_The_UNIX_Philosophyx). A simple application that does one thing: lets people post links and comments. Sadly people operate pretty badly when allowed to use just one such tools embodying a UNIX-style manner of work. We need at least a few to be happy. 8 | 9 | First we really wish we wouldn't miss really interesting stories from HN, just because we couldn't watch it constantly and decide which we like. 10 | 11 | We'd prefer to be able to just **scrape for new threads about pandas** (you never saw that one coming, did you?). And here's an idea of how to solve our issue, of course trying to maintain a UNIX-style architecture. 12 | 13 | **We are going to build 4 separate parts of software to deal with this task.** 14 | 15 | Each of them is pretty capable of working on its own and should we ever decide one of them is ineffective and should be changed or heavily modified, doing so should be practically invisible to the other components. 16 | 17 | We are going to have: 18 | 19 | * A subscriber to manage emails & keywords 20 | * A notifier to send emails about new threads 21 | * A scraper, that communicates with the HackerNews API 22 | * A scraper, that wants to read the entire HackreNews over time 23 | 24 | ## Subscriber 25 | 26 | We want to have a simple service listening for HTTP requests which allows us to subscribe for a "bag" of words that we find interesting. We should be able to post json data to the subscriber in the following format: 27 | 28 | ```json 29 | { 30 | "email": "ziltoid@4th.dim", 31 | "keywords": [ 32 | "coffee", 33 | "destruction", 34 | "omniscience" 35 | ], 36 | "type": ["story"] 37 | } 38 | ``` 39 | 40 | This means that from now on we want to get notified at `ziltoid@4th.dim` for all new HN articles with `story` type, containing any of the words `coffee`, `destruction` or `omniscience`. 41 | 42 | **The server should return a unique subscriber id**, which we can use later to unsubscribe from the service: 43 | 44 | ```json 45 | { 46 | "email": "ziltoid@4th.dim", 47 | "subscriberId": "bananispijami123123" 48 | } 49 | ``` 50 | 51 | **Submitting a mail several times with different keywords is totally fine.** This is a nice way to cluster articles into different mails. 52 | 53 | We can do the following requests: 54 | 55 | ```json 56 | { 57 | "email": "ziltoid@4th.dim", 58 | "keywords": [ 59 | "meshuggah", 60 | "planet smasher" 61 | ], 62 | "type": ["comment"] 63 | } 64 | ```` 65 | 66 | ```json 67 | { 68 | "email": "ziltoid@4th.dim", 69 | "keywords": [ 70 | "panda destroyer" 71 | ], 72 | "type": ["story", "comment"] 73 | } 74 | ``` 75 | 76 | We will have 3 different subscriber IDs for `ziltoid` and this is fine. 77 | 78 | All data about emails and keywords should be persisted in `subscribrers.json` file(of course you can call the file anything else). 79 | 80 | You can use [node-persist](https://github.com/simonlast/node-persist) library for that. 81 | 82 | ### Different types for subscribing 83 | 84 | We can subscribe with your keywords only for stories (main articles), comments for those stories or both - stories and comments. 85 | 86 | In the JSON that is sent, there is a `type` key, that accepts a list of strings, containing `"story"` and `"comment"`. 87 | 88 | You can have: 89 | 90 | * Only a story 91 | 92 | ```json 93 | { 94 | "type": ["story"] 95 | } 96 | ``` 97 | 98 | * Only a comment 99 | 100 | ```json 101 | { 102 | "type": ["comment"] 103 | } 104 | ``` 105 | 106 | * Or both (order doesn't matter - story, comment or comment, story) 107 | 108 | ```json 109 | { 110 | "type": ["story", "comment"] 111 | } 112 | ``` 113 | 114 | Ignore everything else. 115 | 116 | ### Email confirmation for subscriber 117 | 118 | It is not a good idea to let everyone subscribe with every email without any verification. This will easily become a spam bot! 119 | 120 | So you are going to implement an email-verification mechanism: 121 | 122 | * A new user subscribes with a given email - `user@awesome.com` 123 | * An email is sent to `user@awesome.com` with a **confirmation link** - you should think about how to implement that 124 | * The user clicks on the confirmation link and he is confirmed. 125 | 126 | **Keep in mind that you should not send emails to non-confirmed users!** 127 | 128 | We want to avoid spamming innocent souls. 129 | 130 | ### API Endpoints for the subscriber 131 | 132 | We should have the following API endpoints: 133 | 134 | * `POST /subscribe` - as explained above, this creates a new subscriber 135 | * `POST /unsubscribe` - takes a JSON payload with a single `"subscriberId"` and unsubscribes it if possible 136 | * `GET /listSubscribers` - returns a list of all subscribers with their emails, ids and keywords. This is great for testing purposes 137 | * There should be an endpoint for email confirmation! 138 | 139 | ## Notifier 140 | 141 | It *periodically reads* data from a json file containing information about new articles, then reads the data submitted through the subscriber and decides what mails need to be sent. 142 | 143 | The notifier marks each article as processed once it sends mails about it to all the addresses in the `subscribers.json` file. 144 | 145 | Two important things: 146 | 147 | * **The notifier makes the matching phase between the titles of the articles and the keywords for each subscriber!** 148 | * **Keep in mind that you should not send emails to non-confirmed users!** 149 | 150 | ### Sending emails about comments 151 | 152 | If the notifier has found a subscriber, that is interested in a comment and there is new comment from HackerNews, use the following mechanism: 153 | 154 | * Find the story, that is parent of that comment. **Keep in mind that there are parents for comments that are also comments, so you have to walk up the tree until you find a `type: "story"` key.** 155 | * With the comment in the email, add a link to the story, so the user can track from where it came. 156 | 157 | An example comment with parent comment is that - https://hacker-news.firebaseio.com/v0/item/2922097.json?print=pretty 158 | 159 | You have to go up the parents, to get to the story! 160 | 161 | #### Design decisions 162 | 163 | Give it a good tought: 164 | 165 | * You can make API calls from the Notifier - this will introduce new dependencies 166 | * You can scrape the entire tree for the comment and add it to the database - this will make the scraping harder 167 | * You can add API endoints to the Scraper so it can get parent stories for a comment - again, that way, Notifier will know about the scraper 168 | * You can decouple the Notifier and the Scraper by adding layer between them. This will make the entire architecture more complex. 169 | 170 | There is no right way, so it is up to you to decide! 171 | 172 | ### Note on *periodically reads* 173 | 174 | The notifier has an HTTP server up, listening for a POST request to `/newArticles`. A request to this URL makes the notifier read the data from `articles.json` and `subscribers.json` and send the respective mails. 175 | 176 | ### Matching phase & Sending notifications 177 | 178 | It is enough only 1 of the keywords from a bag of keywords for a given subscriber to match in a story's title, for the notifier to send an email. 179 | 180 | For one subscriber, the notifier should check for all new articles if there are any matches and send the articles that match in one email. 181 | 182 | **Again, lets repeat:** 183 | 184 | * There can be multiple subscriptions for 1 email - the notifier should send different emails for different subscriptions 185 | * For one subscriber, if there are more than 1 articles that match some of his keywords, the notifier should send **1 email** with articles listed in there. 186 | 187 | ## Scraper 188 | 189 | The most complex part of our setup is going to be the scraper which will poll the [HackerNews API](https://github.com/HackerNews/API) every two minutes and write all new articles to `articles.json`. 190 | 191 | We'll poll for the latest [maxitem](https://hacker-news.firebaseio.com/v0/maxitem) from the API and decide which items we want to fetch. To make the decision easier we want to keep the last `maxitem` in our `articles.json` file. 192 | 193 | We're interested in items with `item['type'] === 'story'` or `item['type'] === 'comment'` - both in main articles and comments 194 | 195 | **Once the scrapre polls & saves the articles, it sends a POST request to the notifier, to wake him up.** 196 | 197 | ### Polling mechanism 198 | 199 | For example, lets say that we poll the `maxitem` and we get `8452389`. We fetch the following article or comment - https://hacker-news.firebaseio.com/v0/item/8452389.json?print=pretty 200 | 201 | This returns a story in JSON format: 202 | 203 | ``` 204 | { 205 | "by" : "oliveremberton", 206 | "id" : 8452389, 207 | "score" : 1, 208 | "text" : "", 209 | "time" : 1413273932, 210 | "title" : "How to debug your brain", 211 | "type" : "story", 212 | "url" : "http://oliveremberton.com/2014/how-to-debug-your-brain/" 213 | } 214 | ``` 215 | 216 | After two minutes, the `maxitem` is `8452393`. Our last `maxitem` was `8452389`, so we have 4 new API calls to make, in order to get: 217 | 218 | * `8452390` 219 | * `8452391` 220 | * `8452392` 221 | * `8452393` - this is where we stop. 222 | 223 | And we repeat that every two minutes. 224 | 225 | ### Gotchas 226 | 227 | Keep in mind that HTTP get requests are async and you should fetch new article only after the previous is done. You will have to make an async While loop, so give it a good think! 228 | 229 | ## Libraries 230 | 231 | * [express](http://expressjs.com/) 232 | * [node-persist](https://github.com/simonlast/node-persist) 233 | * [Nodemailer](https://github.com/andris9/Nodemailer) 234 | 235 | ## The Scraper that wants to read the entire HackerNews 236 | 237 | We are going to have a 4th application, that wants to read the entire HackerNews, generating and index of keywords that are occuring the most. 238 | 239 | We are going to make a histogram of the entire HackerNews and update a file called `histogram.json`, where we keep the following things: 240 | 241 | ```json 242 | { 243 | "keyword": # of occurences so far 244 | } 245 | ``` 246 | 247 | ### What is the idea? 248 | 249 | * Start from the very beginning of time with `id = 1` (https://hacker-news.firebaseio.com/v0/item/1.json?print=pretty) 250 | * Poll over time (lets say10-20 seconds) for the next id 251 | * **Keep track which was the last id in case we stop this scraper and run it later again!** 252 | 253 | ### What to read? 254 | 255 | **Read everything you can** - comments, stories, titles, bodies - look for every source of text you can get from the API. 256 | 257 | In order to extract the keywords, you can use the following library - https://github.com/NaturalNode/natural 258 | 259 | There is a tokenizer that can help. 260 | 261 | ### API Endpoint to show the results so far 262 | 263 | Using express, create a simple `GET` endpoint `/keywords` that returns in JSON format the result of scraping HackerNews so far. 264 | 265 | That way we can observe the fruits of our labor! 266 | -------------------------------------------------------------------------------- /week1/callbacks.md: -------------------------------------------------------------------------------- 1 | class: center, middle 2 | # Callback hell 3 | 4 | --- 5 | # Callback hell 6 | 7 | Everything in node and in javascript as a whole is mostly done through the callback mechanism. We issue an action to be taken and provide a function to be called once the action has ended that receives the result(or possibly the error) of the action execution. 8 | 9 | Like so: 10 | 11 | ```javascript 12 | fs.readFile('/etc/passwd', function (err, data) { 13 | if (error) { 14 | … 15 | } else { 16 | … 17 | } 18 | }); 19 | ``` 20 | 21 | --- 22 | # Callback hell 23 | 24 | That's a pretty trivial example and most real world apps do far more complex stuff than this. A big useful interesting app written in node could include logic that almost literally looks like this 25 | 26 | ```javascript 27 | panda(pandaArgs, function(err, result) { 28 | if (err) { 29 | … 30 | } else { 31 | unicorn(unicornArgs, function(err, result) { 32 | if (err) { 33 | … 34 | } else { 35 | narwal(narwalArgs, function(err, result) { 36 | … 37 | } 38 | } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | --- 45 | class: center, middle 46 | ![callbacks](img/callbacks.jpg) 47 | 48 | --- 49 | # async 50 | 51 | [async](https://github.com/caolan/async) is a javascript library that exposes a nice API for solving the callback issue by making everything asynchronous and allowing us to cope with it in a somewhat centralised manner. 52 | 53 | It has its own realisations of `map`, `each`, `filter`, `result`, etc. 54 | 55 | --- 56 | # async 57 | 58 | It also has a bunch of [control flow functions](https://github.com/caolan/async#control-flow) that solve the callback hell problem in some of its most popular forms and patterns. 59 | 60 | 61 | --- 62 | # Promises 63 | 64 | Promises are a concept that solves the issue with asynchronous calls depending strongly on one another. 65 | 66 | The basic concept is that whenever a function has to do some asynchronous task instead of accepting a callback that will be called when the action is executed it returns a promise object, that acts as a relay between the asynchronous action and the rest of the application. 67 | 68 | Promises are part of the ES6 standard. 69 | 70 | The basic concept for promises is described in the [Promises/A+](https://promisesaplus.com/) standard. 71 | 72 | --- 73 | # Promises 74 | 75 | The simple idea of a promise interface: 76 | 77 | ```javascript 78 | var promise = fetchDataFromNetwork('www.someplace.tld'); 79 | promise.then(function success(data) { 80 | … 81 | }, function fail (error) { 82 | … 83 | }); 84 | ``` 85 | 86 | --- 87 | # Q 88 | 89 | [Q](https://github.com/kriskowal/q#the-beginning) realises a Promises/A+ compatible API. It also has a nice way of promisifying/denodeifying standard callback base async functions. 90 | 91 | --- 92 | # defered 93 | 94 | ```javascript 95 | function () { 96 | var deferred = Q.defer(); 97 | fs.readFile("foo.txt", "utf-8", function (error, text) { 98 | if (error) { 99 | deferred.reject(new Error(error)); 100 | } else { 101 | deferred.resolve(text); 102 | } 103 | }); 104 | return deferred.promise; 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /week1/solutions/async-workflow.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | 3 | // callback hell: 4 | 5 | // 1. Nested async functions 6 | // 2. Async workflow = [a1, a2, ..., an], async 7 | 8 | function async() { 9 | var defer = Q.defer(); 10 | setTimeout(function() { 11 | defer.resolve(); 12 | }, 1000); 13 | 14 | return defer.promise; 15 | } 16 | 17 | async() 18 | .then(function() { 19 | console.log("First"); 20 | return async(); 21 | }) 22 | .then(function() { 23 | console.log("Second"); 24 | }); 25 | 26 | function eventualSquare(n) { 27 | var defer = Q.defer(); 28 | setTimeout(function() { 29 | defer.resolve(n * n); 30 | }, 100); 31 | 32 | return defer.promise; 33 | } 34 | 35 | var numbers = [1, 2, 3, 4, 5]; 36 | var promisedNumbers = numbers.map(function(n) { 37 | return eventualSquare(n); 38 | }); 39 | 40 | Q 41 | .all(promisedNumbers) 42 | .then(function(result) { 43 | console.log(result); 44 | }); 45 | -------------------------------------------------------------------------------- /week1/solutions/async.js: -------------------------------------------------------------------------------- 1 | var async = require("async"); 2 | 3 | function eventualSquare(n, cb) { 4 | setTimeout(function() { 5 | cb(n * n); 6 | }, 100); 7 | } 8 | 9 | var numbers = [1, 2, 3, 4, 5]; 10 | 11 | var asyncPrepared = numbers.map(function(n){ 12 | return function(callback) { 13 | eventualSquare(n, function(squareResult) { 14 | callback(null, squareResult); 15 | }); 16 | }; 17 | }); 18 | 19 | async.series(asyncPrepared, function(err, results) { 20 | console.log(results); 21 | }); 22 | 23 | // Искаме да приложим eventualSquare в/у numbers: 24 | // 1. Да изпълняваме последователно: eventualSquare(1), eventualSquare(2), eventualSquare(3), ..., 25 | // Да знаем, кога сме готови с целият списък. 26 | 27 | // 2. Искаме да изпълним паралелно - не ни интересува последователност 28 | // Искаме да знаем, кога сме готови с целият списък 29 | -------------------------------------------------------------------------------- /week1/solutions/hell.js: -------------------------------------------------------------------------------- 1 | var request= require("request"), 2 | printf = require("printf"), 3 | currentMaxItem = 8624816; 4 | 5 | function getMaxItem(cb) { 6 | request("https://hacker-news.firebaseio.com/v0/maxitem.json", function(err, res, body) { 7 | cb(body); 8 | }); 9 | } 10 | 11 | function getItem(id, cb) { 12 | var url = printf("https://hacker-news.firebaseio.com/v0/item/%d.json", id); 13 | request(url, function(err, res, body) { 14 | cb(body); 15 | }); 16 | } 17 | 18 | function range(a, b) { 19 | var result = []; 20 | 21 | while(a <= b) { 22 | result.push(a); 23 | a = a + 1; 24 | } 25 | 26 | return result; 27 | } 28 | 29 | function loop() { 30 | getMaxItem(function(maxItem) { 31 | maxItem = parseInt(maxItem, 10); 32 | 33 | if(currentMaxItem === -1) { 34 | // Пускаме за 1ви път 35 | getItem(maxItem, function(item) { 36 | console.log(item); 37 | currentMaxItem = maxItem; 38 | loop(); 39 | }); 40 | } else { 41 | // Имаме някакъв друг maxItem 42 | console.log("We have maxItem already"); 43 | var itemsToGet = range(currentMaxItem, maxItem), 44 | lastItem = itemsToGet[itemsToGet.length - 1]; 45 | console.log(itemsToGet); 46 | // [id1, id2, id3], getItem 47 | itemsToGet.forEach(function(itemId) { 48 | getItem(itemId, function(item) { 49 | console.log(item); 50 | if(itemId === lastItem) { 51 | console.log("Last item"); 52 | currentMaxItem = maxItem; 53 | loop(); 54 | } 55 | }); 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | loop(); 62 | 63 | // ако за първи път пускаме програмата - трябва да вземем сегашния maxItem 64 | // и след това, на определен интервал, питаме кой е новият maxItem? 65 | // Взимаме списъка с id-тата между currentMaxItem и maxItem 66 | // За всички id-та, взимаме getItem от съответното id 67 | // Пак повтаряме, на определен интервал от време 68 | -------------------------------------------------------------------------------- /week1/solutions/promises.js: -------------------------------------------------------------------------------- 1 | var request= require("request"), 2 | Q = require("q"), 3 | printf = require("printf"), 4 | currentMaxItem = -1, 5 | itemsQueue = []; 6 | 7 | function getMaxItem() { 8 | var defer = Q.defer(); 9 | 10 | request("https://hacker-news.firebaseio.com/v0/maxitem.json", function(err, res, body) { 11 | if(err) { 12 | return defer.reject(err); 13 | } 14 | 15 | defer.resolve(body); 16 | }); 17 | 18 | return defer.promise; 19 | } 20 | 21 | function getItem(id) { 22 | var defer = Q.defer(); 23 | 24 | var url = printf("https://hacker-news.firebaseio.com/v0/item/%d.json", id); 25 | request(url, function(err, res, body) { 26 | if(err) { 27 | return defer.reject(err); 28 | } 29 | 30 | defer.resolve(body); 31 | }); 32 | 33 | return defer.promise; 34 | } 35 | 36 | function range(a, b) { 37 | var result = []; 38 | 39 | while(a <= b) { 40 | result.push(a); 41 | a = a + 1; 42 | } 43 | 44 | return result; 45 | } 46 | 47 | function doWork(done) { 48 | if(itemsQueue.length === 0) { 49 | return done(); 50 | } 51 | 52 | var itemId = itemsQueue.pop(); 53 | 54 | getItem(itemId) 55 | .then(function(item) { 56 | console.log(item); 57 | doWork(done); 58 | }); 59 | } 60 | 61 | function getItems() { 62 | var defer = Q.defer(); 63 | 64 | doWork(function() { 65 | defer.resolve(); 66 | }); 67 | 68 | return defer.promise; 69 | } 70 | 71 | function loop() { 72 | getMaxItem() 73 | .then(function(maxItem) { 74 | maxItem = parseInt(maxItem, 10); 75 | 76 | if(currentMaxItem === maxItem) { 77 | return loop(); 78 | } 79 | 80 | if(currentMaxItem === -1) { 81 | currentMaxItem = maxItem; 82 | } 83 | 84 | itemsQueue = range(currentMaxItem, maxItem); 85 | 86 | console.log("Getting items for:"); 87 | console.log(itemsQueue); 88 | 89 | currentMaxItem = maxItem; 90 | 91 | getItems() 92 | .then(function() { 93 | loop(); 94 | }); 95 | }); 96 | } 97 | 98 | loop(); 99 | -------------------------------------------------------------------------------- /week1/solutions/scraper.js: -------------------------------------------------------------------------------- 1 | /* global require, console */ 2 | (function() { 3 | 'use strict'; 4 | 5 | var lastMaxItem = 8486991; 6 | var request = require('request'); 7 | var printf = require('printf'); 8 | 9 | function getMaxItem(cb) { 10 | request('https://hacker-news.firebaseio.com/v0/maxitem.json', function(error, response, body) { 11 | cb(parseInt(body, 10)); 12 | }); 13 | } 14 | 15 | function getItem(id, cb) { 16 | var itemUrl = printf('https://hacker-news.firebaseio.com/v0/item/%d.json', id); 17 | request(itemUrl, function(error, response, body) { 18 | cb(JSON.parse(body)); 19 | }); 20 | } 21 | 22 | function getRangeFrom(prevItemId, maxItemId) { 23 | var result = []; 24 | 25 | while(prevItemId < maxItemId - 1) { 26 | result.push(prevItemId); 27 | prevItemId += 1; 28 | } 29 | 30 | return result; 31 | } 32 | 33 | function asyncWhile(items, oper, done) { 34 | if(items.length === 0) { 35 | return done(); 36 | } 37 | 38 | var itemToGet = items.shift(); 39 | console.log('Getting item with id: ' + itemToGet); 40 | oper(itemToGet, function(data) { 41 | console.log(data); 42 | asyncWhile(items, oper, done); 43 | }); 44 | } 45 | 46 | function poll(doneCb) { 47 | getMaxItem(function(currentMaxItem) { 48 | console.log('max item is ' + currentMaxItem); 49 | var rangeToGet = getRangeFrom(lastMaxItem, currentMaxItem); 50 | asyncWhile(rangeToGet, getItem, doneCb); 51 | }); 52 | } 53 | 54 | function everyX(seconds) { 55 | setTimeout(function() { 56 | poll(function() { 57 | console.log("Done"); 58 | everyX(seconds); 59 | }); 60 | }, seconds) 61 | } 62 | 63 | everyX(3000); 64 | } () ); 65 | -------------------------------------------------------------------------------- /week2/1-JSON-To-Mongo/README.md: -------------------------------------------------------------------------------- 1 | # Importing JSON to Mongo 2 | 3 | We are going to warm-up with a very simple task - a node script that reads JSON files are arguments and import them in mongo collections. 4 | 5 | For the task, use the [Native Node Adapter](https://github.com/mongodb/node-mongodb-native), to connect with Mongo. 6 | 7 | Lets have the following directory structure: 8 | 9 | ``` 10 | . 11 | ├── config.json 12 | ├── json-to-mongo.js 13 | └── people.json 14 | ``` 15 | 16 | Where `json-to-mongo.js` is our import script. 17 | 18 | If we run: 19 | 20 | ``` 21 | $ node json-to-mongo.js people.json 22 | ``` 23 | 24 | The following things should happen: 25 | 26 | * In `config.json`, there is a `mongoConnectionUrl` key, holding the connection string for Mongo - `"mongodb://localhost:27017/database-name"` 27 | * The script should import in `database-name`, in the collection `people` the data from `people.json` 28 | * The collection, in which the data is going to is determined by the name of the JSON file we are importing 29 | * `people.json` shold contain an array of JSON objects. 30 | 31 | Implement the `json-to-mongo.js` script, following the specification above. 32 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/README.md: -------------------------------------------------------------------------------- 1 | # Displaying top keywords from our HackerNews Scraper 2 | 3 | Last week, we finished the Scraper that wanted to read the entire HackerNews! Now, we are going to migrate the persist layer to Django and after this, play with the different queries and commands. 4 | 5 | At the end, we must bring the web application, located in `app` to life with read data! 6 | 7 | 8 | ## Moving data to Mongo 9 | 10 | The data from the Scraper is kept in a JSON format: 11 | 12 | * You should migrate that data to Mongo one way or another. 13 | * You should change the persistence layer to Mongo too. This means new keywords are saved directly there. 14 | 15 | You can use the `json-to-mongo.js` app that we wrote in the previous task. 16 | 17 | ## Web Application 18 | 19 | We have constructed for you a simple web application, that consists of a table with two buttons. 20 | 21 | The table displays 3 columns - rank (position), keyword and count for that keyword. 22 | 23 | We want to display only ten items in that table, starting from top 10 keywords (descending order)! 24 | 25 | * If the `next` button is clicked, display the `(last_item_in_table) + 10` items - the next 10 26 | * If the `prev` button is clicked, display the `(first_item_in_table) - 10` items - the previous 10 27 | * Make sure to handle corner cases - when there are no next or prev data to display 28 | 29 | #### Starting the application 30 | 31 | To start the application, you have to do: 32 | 33 | ``` 34 | $ bower install 35 | ``` 36 | 37 | After this open index.html in your favourite web browser and enjoy! 38 | 39 | If you do not have bower, try with: 40 | 41 | ``` 42 | $ npm install -g bower 43 | ``` 44 | 45 | ## Making thing happen 46 | 47 | Once the data is migrated, you can finish the code to the Scraper so it works with the web application. 48 | 49 | Things to use and try out: 50 | 51 | * http://docs.mongodb.org/manual/reference/method/cursor.sort/ 52 | * http://docs.mongodb.org/manual/reference/method/cursor.limit/ 53 | * http://docs.mongodb.org/manual/reference/method/cursor.skip/ 54 | 55 | Use the Native Driver to achieve everything! 56 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/app/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Keywords Dashboard", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/HackBulgaria/NodeJS-1", 5 | "authors": [ 6 | "Radoslav RadoRado Georgiev " 7 | ], 8 | "description": "Simple table for displaying keywords", 9 | "main": "index.html", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "requirejs": "~2.1.15", 20 | "handlebars": "~2.0.0", 21 | "jquery": "~2.1.1", 22 | "bootstrap": "~3.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/app/scripts/app.js: -------------------------------------------------------------------------------- 1 | /* global require, console, alert */ 2 | 3 | require.config({ 4 | paths: { 5 | "jquery": "../bower_components/jquery/dist/jquery", 6 | "handlebars": "../bower_components/handlebars/handlebars", 7 | "bootstrap": "../bower_components/bootstrap/dist/js/bootstrap" 8 | }, 9 | shim: { 10 | "handlebars": { 11 | exports: "Handlebars" 12 | }, 13 | "bootstrap": { 14 | "deps": ["jquery"] 15 | } 16 | } 17 | }); 18 | 19 | 20 | require(["jquery", "handlebars", "bootstrap"], function($, Handlebars) { 21 | "use strict"; 22 | 23 | function reloadUI(data) { 24 | var 25 | templateString = $("#table-template").html(), 26 | compiledTemplate = Handlebars.compile(templateString), 27 | html = compiledTemplate({ 28 | data: data 29 | }); 30 | 31 | $("body").html(html); 32 | } 33 | 34 | function fetchKeywords(data, cb) { 35 | $.ajax({ 36 | type: "GET", 37 | url: "http://localhost:8000/keywords", 38 | data: data 39 | }) 40 | .done(function(keywords) { 41 | cb(keywords); 42 | }) 43 | .fail(function(error) { 44 | console.log(error); 45 | }); 46 | } 47 | 48 | ["#next","#prev"].forEach(function(buttonId) { 49 | $(document).on("click", buttonId, function() { 50 | fetchKeywords({ 51 | fromPosition: parseInt($("table tr").last().find("td").first().html(), 10), 52 | direction: $(this).attr("id") 53 | }, reloadUI); 54 | }); 55 | }); 56 | 57 | reloadUI(); 58 | }); 59 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/example-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-express", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.9.8" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /week2/2-Skip-Skip-Next/example-express/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var app = express(); 3 | 4 | var data = [{ 5 | "rank": 1, 6 | "keyword": "JavaScript", 7 | "count": 10000 8 | }, { 9 | "rank": 2, 10 | "keyword": "Python", 11 | "count": 8000 12 | }]; 13 | 14 | 15 | app.all("*", function(req, res, next) { 16 | res.header("Access-Control-Allow-Origin", "*"); 17 | res.header("Access-Control-Allow-Headers", ["X-Requested-With", "Content-Type", "Access-Control-Allow-Methods"]); 18 | res.header("Access-Control-Allow-Methods", ["GET"]); 19 | next(); 20 | }); 21 | 22 | app.get("/keywords", function(req, res) { 23 | res.json(data); 24 | }); 25 | 26 | app.listen(8000); 27 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/README.md: -------------------------------------------------------------------------------- 1 | ### Geospatial stuff in mongo 2 | 3 | Geospatial indexes in mongo allow us to make "geo requests" to our data such as: 4 | 5 | - get the nearest X points within a certain distance 6 | - get the points from our data that are in a certain box/polygon/circle 7 | 8 | In order to do that we create a special [2d sphere index](http://docs.mongodb.org/manual/core/2dsphere/) on a collection. Once we have done that we can do various [geospatial](http://docs.mongodb.org/manual/reference/operator/query-geospatial/) to that collection. 9 | 10 | We will use this functionality to create the service for our simple geo-enabled web app. 11 | 12 | ###The Save-It-Find-It web applications 13 | 14 | We have 2 web apps which work complementary. We called them SaveIt and FindIt. One will be used to save the location, and the other when a user wants to find locations that are within some range from his position (range is in km). 15 | 16 | #####Save-It app 17 | 18 | The first web app saves a location (restaurant, landmark or w/e). 19 | 20 | A location is defined by a name, location and tags. 21 | 22 | - The name is an arbitrary string 23 | - The location can be set by clicking on the map. 24 | - The tags are a list of words (space separated) that will be used for filtering. 25 | 26 | A sample json that will be **sent** by the web app would be: 27 | > { 28 | > 'name': 'MyOwnRestaurant', 29 | > 'position': { 'lng': 3.412, 'lat': 5.132 }, 'tags': ['blackjack', 'hookers'] 30 | > } 31 | 32 | 33 | Your job on the client is to make the appropriate **$.ajax** call to the `POST locations` endpoint that will save the location in mongo. 34 | 35 | > Do not forget to run **bower install** in */week2/3-Geolocation-and-stuff/save-it*. Then just open **index.html** 36 | 37 | #####Find-It app 38 | 39 | The find-it app is used when a user wants to find all locations with the specified tags within a certain range (in km). 40 | 41 | You will need to implement the **$.ajax** call to the `GET locations` endpoint, that gets all locations within a certain range. The locations should be further filtered by the tags that are provided. 42 | 43 | > Do not forget to run **bower install** in */week2/3-Geolocation-and-stuff/find-it*. Then just open **index.html** 44 | 45 | #### And for our NodeJs side 46 | 47 | On the server side we need a server that has 2 endpoints: 48 | 49 | `GET locations` - Used by our Find-It app. By providing a **range**, **position** and **tags** will return all locations with the specified tags that are within the specified range from the position (range is in km). 50 | 51 | `POST locations` - Used by our Save-It app. Sending a location with name, position and tags will save the location to our mongodb. 52 | > Do not forget to convert the position from { lat, lng } object to GeoJson format. Follow the [Link](http://geojson.org/geojson-spec.html#id2) to see the specification of the point structure. 53 | 54 | 55 | ### Hints 56 | [http://geojson.org/geojson-spec.html#id2](http://geojson.org/geojson-spec.html#id2) - GeoJson specification that is used by mongo 57 | 58 | [2dsphere index in mongo](http://docs.mongodb.org/manual/core/2dsphere/) will be used so mongo can work with geo queries for our data. 59 | 60 | [$nearSphere](http://docs.mongodb.org/manual/reference/operator/query/nearSphere/) how to make the 'near' query. 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/find-it/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Find it", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/HackBulgaria/NodeJS-1", 5 | "authors": [ 6 | "Anton Sotirov" 7 | ], 8 | "description": "The FindIt web app from the SaveItFindIt bundle", 9 | "main": "index.html", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "requirejs": "~2.1.15", 20 | "jquery": "*", 21 | "requirejs-plugins": "~1.0.2", 22 | "jquery-color": "~2.1.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/find-it/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | 26 |
27 | Find It 28 |

Where are you?

29 |
30 |
31 |
Click on the map to select where are you
32 |
33 |
34 | 35 |

36 |

Details:

37 |
38 |
39 |

40 | 41 |

42 | 43 |

44 |
45 |
46 | 47 |

Test the map

48 | with [{name: 'TestPoint1', coordinates: [3.363882, 51.044922]}, {name: 'TestPoint2', coordinates: [3.563882, 50.044922]}] 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/find-it/scripts/app.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'jquery': '../bower_components/jquery/dist/jquery', 4 | 'async': '../bower_components/requirejs-plugins/src/async', 5 | 'jquery-color': '../bower_components/jquery-color/jquery.color' 6 | }, 7 | shim: { 8 | 'lodash': { 9 | exports: '_' 10 | }, 11 | 'google': { 12 | 'deps': ['document'], 13 | 'exports' : 'google' 14 | }, 15 | 'jquery-color': { 16 | 'deps': ['jquery'] 17 | } 18 | } 19 | }); 20 | 21 | require(['jquery','async!http://maps.google.com/maps/api/js?sensor=false', 'jquery-color'], function($) { 22 | var currentPosition; 23 | 24 | initializeSmallMap(); 25 | 26 | $("#getLocationsBtn").click(getLocations); 27 | $("#test").click(function() { 28 | //we test our showPoints function with 2 points 29 | var points = [ 30 | { 31 | name: 'TestPoint1', 32 | coordinates: [3.363882, 51.044922] 33 | }, 34 | { 35 | name: 'TestPoint2', 36 | coordinates: [3.563882, 50.044922] 37 | } 38 | ]; 39 | showPoints(points); 40 | }); 41 | 42 | function getLocations() { 43 | var data = getFormData(); 44 | if(!data) { 45 | return; 46 | } 47 | 48 | // *** Insert the get locations $.ajax logic here and call showPoints *** 49 | alert(JSON.stringify(data, null, 4)); 50 | } 51 | 52 | var largeMap, markers = []; 53 | /** 54 | * Shows the specified points on the map 55 | * @param points 56 | */ 57 | function showPoints(points) { 58 | if(!largeMap) { 59 | initializeLargeMap(); 60 | } 61 | var marker, i; 62 | 63 | //clear previous markers 64 | markers.forEach(function(marker) { 65 | marker.setMap(null); 66 | }); 67 | markers = []; 68 | 69 | //add current points to the map 70 | points.forEach(function(point) { 71 | var latLng = new google.maps.LatLng(point.coordinates[1], point.coordinates[0]); 72 | marker = new google.maps.Marker({ 73 | position: latLng, 74 | map: largeMap, 75 | title: point.name 76 | }); 77 | markers.push(marker); 78 | 79 | largeMap.setCenter(latLng); 80 | 81 | google.maps.event.addListener(marker, 'click', (function(marker) { 82 | return function() { 83 | largeMap.panTo(marker.getPosition()); 84 | largeMap.setZoom(4); 85 | } 86 | })(marker, i)); 87 | }); 88 | } 89 | 90 | function initializeLargeMap() { 91 | var mapOptions = { 92 | zoom: 5, 93 | center: new google.maps.LatLng(-25.363882, 131.044922) 94 | }; 95 | largeMap = new google.maps.Map(document.getElementById('large-map-canvas'), 96 | mapOptions); 97 | } 98 | 99 | function initializeSmallMap() { 100 | var mapOptions = { 101 | zoom: 11, 102 | center: new google.maps.LatLng(42.647714978827366, 23.38468909263611) 103 | }; 104 | var map = new google.maps.Map(document.getElementById('small-map-canvas'), 105 | mapOptions); 106 | var marker; 107 | 108 | google.maps.event.addListener(map, 'click', function(e) { 109 | changePosition(e.latLng); 110 | }); 111 | 112 | function changePosition(latLng) { 113 | marker = marker || new google.maps.Marker({ 114 | position: latLng, 115 | map: map 116 | }); 117 | 118 | marker.setPosition(latLng); 119 | currentPosition = latLng; 120 | 121 | $('#latitude').html(latLng.lat()); 122 | $('#longitude').html(latLng.lng()); 123 | } 124 | } 125 | 126 | /** 127 | * Validates the form and does some UI logic. Ensures we have entered currentPosition and tags. 128 | * @returns {Object | null} 129 | */ 130 | function getFormData() { 131 | var isValid = true; 132 | var $tags = $("#tags"); 133 | var tags = $tags.val(); 134 | var $range = $("#range"); 135 | var range = $range.val(); 136 | 137 | var $noRangeMsg = $("#noRangeMsg"); 138 | 139 | if(!range) { 140 | $range.keypress(function() { 141 | $noRangeMsg.hide(); 142 | }); 143 | flashElement($noRangeMsg); 144 | $noRangeMsg.show(); 145 | isValid = false; 146 | } else { 147 | $noRangeMsg.hide(); 148 | } 149 | 150 | if(!currentPosition) { 151 | flashElement($("#positionWrp")); 152 | isValid = false; 153 | } 154 | 155 | if(isValid) { 156 | return { 157 | range: range, 158 | position: { 159 | lng: currentPosition.lng(), 160 | lat: currentPosition.lat() 161 | }, 162 | tags: tags ? tags.trim().split(' ') : null 163 | } 164 | } 165 | } 166 | 167 | function flashElement(el){ 168 | el.stop().css("background-color", "#FFAFAF").animate({ backgroundColor: "#FFFFFF"}, 1500); 169 | } 170 | }); 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/geo.md: -------------------------------------------------------------------------------- 1 | ### Data about Bulgarian cities 2 | 3 | `geo.zip` in this folder holds geospacial data about cities in Bulgaria. It defines each city as a list of points. That's the polygon describing the city outline. 4 | 5 | You can unzip it to a directory called `geo` and then load it in your mongodb server by running `mongorestore geo` from the command line. That will add a database called `geo` with one collection in it called `features`. 6 | 7 | ```json 8 | { 9 | "_id" : ObjectId("544b571756b55d1bc11af6ed"), 10 | "type" : "Feature", 11 | "properties" : { 12 | "name" : "Аксаково", 13 | "id" : 83, 14 | "pid" : 4, 15 | "pop" : 20426, 16 | "oblast" : "Варна" 17 | }, 18 | "geometry" : { 19 | "type" : "Polygon", 20 | "coordinates" : [ 21 | [ 22 | [ 23 | 27.985, 24 | 43.404 25 | ], 26 | [ 27 | 28.024, 28 | 43.36 29 | ], 30 | [ 31 | 28.042, 32 | 43.309 33 | ], 34 | … 35 | … 36 | … 37 | [ 38 | 27.966, 39 | 43.407 40 | ], 41 | [ 42 | 27.985, 43 | 43.404 44 | ] 45 | ] 46 | ] 47 | } 48 | } 49 | ``` 50 | 51 | The key `geometry` holds a GeoJSON definition of a polygon. We can run `db.features.ensureIndex({ 'geometry' : '2dsphere' }` to tell mongo that the field `geometry` in each document in the `features` collection should be indexed as a GeoJSON object, in order to be able to make geo queries on our data. 52 | 53 | We can now make a query to check in which city is a point on the map located. 54 | 55 | ```javascript 56 | db.features.find( 57 | { 58 | "geometry": { // we are querying the geometry property of each document in the collection 59 | "$geoIntersects": { 60 | "$geometry": { 61 | "type": "Point", 62 | "coordinates": [23.985729217529297, 42.030679234530766] 63 | } 64 | } 65 | } 66 | }, 67 | { "properties.name": 1} 68 | ) 69 | ``` 70 | 71 | Here [`$geoIntersects`](http://docs.mongodb.org/manual/reference/operator/query/geoIntersects/#op._S_geoIntersects) is the geospacial query operator we want to use to check for intersection objects and [`$geometry`](http://docs.mongodb.org/manual/reference/operator/query/geometry/#op._S_geometry) is another geospacial operator which converts a GeoJSON object to something other geospacial operators can use as an argument. 72 | 73 | If you run this query on the data in `geo.zip` you should get `{ "_id" : ObjectId("544b571756b55d1bc11af7e6"), "properties" : { "name" : "Велинград" } }` as a result. 74 | 75 | Alternatively the following queries for all cities that are within the polygon defined with `coordinates`. 76 | 77 | ```javascript 78 | db.features.find( 79 | { 80 | "geometry": { 81 | "$geoWithin": { 82 | "$geometry": { 83 | "type": "Polygon", 84 | "coordinates": [ 85 | [ 86 | [24.312744140625, 42.35042512243457], 87 | [25.191650390625, 42.391008609205045], 88 | [24.686279296875, 41.75492216766298], 89 | [24.312744140625, 42.35042512243457] 90 | ] 91 | ] 92 | } 93 | } 94 | } 95 | }, 96 | { "properties.name": 1} 97 | ) 98 | ``` 99 | 100 | If we used `$geoIntersects` instead of `$geoWithin` we would get all the cities that intersect with our polygon, but aren't necessarily completely inside it. 101 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/geo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackBulgaria/NodeJS-1/c76dfa9e2287e122fa927880b5f9b307a6b009f6/week2/3-Geolocation-and-stuff/geo.zip -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/save-it/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Save it", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/HackBulgaria/NodeJS-1", 5 | "authors": [ 6 | "Anton Sotirov" 7 | ], 8 | "description": "The SaveIt web app from the SaveItFindIt bundle", 9 | "main": "index.html", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "requirejs": "~2.1.15", 20 | "jquery": "*", 21 | "requirejs-plugins": "~1.0.2", 22 | "jquery-color": "~2.1.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/save-it/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
Click on the map to select a point
20 |
21 |
22 | 23 |

24 |

25 |
26 |

27 | 28 |

29 | 30 |

31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /week2/3-Geolocation-and-stuff/save-it/scripts/app.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'jquery': '../bower_components/jquery/dist/jquery', 4 | 'async': '../bower_components/requirejs-plugins/src/async', 5 | 'jquery-color': '../bower_components/jquery-color/jquery.color' 6 | }, 7 | shim: { 8 | 'lodash': { 9 | exports: '_' 10 | }, 11 | 'google': { 12 | 'deps': ['document'], 13 | 'exports' : 'google' 14 | }, 15 | 'jquery-color': { 16 | 'deps': ['jquery'] 17 | } 18 | } 19 | }); 20 | 21 | require(['jquery','async!http://maps.google.com/maps/api/js?sensor=false', 'jquery-color'], function($) { 22 | var currentPosition; 23 | 24 | initialize(); 25 | 26 | function saveLocation() { 27 | var data = getFormData(); 28 | if(!data) { 29 | return; 30 | } 31 | 32 | // *** Insert the save logic here *** 33 | alert(JSON.stringify(data, null, 4)); 34 | } 35 | 36 | function initialize() { 37 | var mapOptions = { 38 | zoom: 8, 39 | center: new google.maps.LatLng(-25.363882, 131.044922) 40 | }; 41 | var map = new google.maps.Map(document.getElementById('map-canvas'), 42 | mapOptions); 43 | var marker; 44 | 45 | google.maps.event.addListener(map, 'click', function(e) { 46 | changePosition(e.latLng); 47 | }); 48 | 49 | function changePosition(latLng) { 50 | marker = marker || new google.maps.Marker({ 51 | position: map.getCenter(), 52 | map: map 53 | }); 54 | marker.setPosition(latLng); 55 | currentPosition = latLng; 56 | 57 | $('#latitude').html(latLng.lat()); 58 | $('#longitude').html(latLng.lng()); 59 | } 60 | 61 | $("#saveLocationBtn").click(saveLocation); 62 | } 63 | 64 | 65 | /** 66 | * Validates the form and does some UI logic. Ensures we have entered currentPosition and tags. 67 | * @returns {Object | null} 68 | */ 69 | function getFormData() { 70 | var isValid = true; 71 | var $tags = $("#tags"); 72 | var tags = $tags.val(); 73 | var $name = $("#name"); 74 | var name = $name.val(); 75 | 76 | var $noTagsMsg = $("#noTagsMsg"); 77 | var $noNameMsg = $("#noNameMsg"); 78 | 79 | if(!tags) { 80 | $tags.keypress(function() { 81 | $noTagsMsg.hide(); 82 | }); 83 | flashElement($noTagsMsg); 84 | $noTagsMsg.show(); 85 | isValid = false; 86 | } else { 87 | $noTagsMsg.hide(); 88 | } 89 | 90 | if(!name) { 91 | $name.keypress(function() { 92 | $noNameMsg.hide(); 93 | }); 94 | flashElement($noNameMsg); 95 | $noNameMsg.show(); 96 | isValid = false; 97 | } else { 98 | $noNameMsg.hide(); 99 | } 100 | 101 | if(!currentPosition) { 102 | flashElement($("#positionWrp")); 103 | isValid = false; 104 | } 105 | 106 | if(isValid) { 107 | return { 108 | name: name, 109 | position: { 110 | lng: currentPosition.lng(), 111 | lat: currentPosition.lat() 112 | }, 113 | tags: tags.trim().split(' ') 114 | } 115 | } 116 | } 117 | 118 | function flashElement(el){ 119 | el.stop().css("background-color", "#FFAFAF").animate({ backgroundColor: "#FFFFFF"}, 1500); 120 | } 121 | }); 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /week2/dump.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackBulgaria/NodeJS-1/c76dfa9e2287e122fa927880b5f9b307a6b009f6/week2/dump.zip -------------------------------------------------------------------------------- /week2/materials.md: -------------------------------------------------------------------------------- 1 | # Materials for week2 2 | 3 | We are goint to tap into Mongo and Mongoose. 4 | 5 | ## Materials for Mongo 6 | 7 | * A good introduction to Mongo is this webcast - https://www.youtube.com/watch?v=w5qr4sx5Vt0 (1hour of length) 8 | * After this, you can just go to the MongoDB official manual - http://docs.mongodb.org/manual/ - it is very good! 9 | * In order to install Mongo properly, check this - http://www.mongodb.org/downloads 10 | * User Interface for exploring Mongo - http://www.mongovue.com/ 11 | * Mongo commands cheat sheet - http://www.mongodbspain.com/wp-content/uploads/2014/03/MongoDBSpain-CheatSheet-p1.jpg 12 | * BigDataBorat Twitter Account - https://twitter.com/BigDataBorat 13 | ## Importing the test data 14 | 15 | Thre is a `dump.zip` file, located in week2 folder. 16 | 17 | Extract it and find the `data.bson` file. You can import it to mongo with the following command: 18 | 19 | ``` 20 | $ mongorestore --collection data --db weather data.bson 21 | ``` 22 | 23 | This will create a new database, called `weather` with one collection, called `data`. We can use this for testing purposes. 24 | 25 | The collection is from the [Mongo University MOOC](https://university.mongodb.com/) 26 | -------------------------------------------------------------------------------- /week3/1-Who-Follows-You-Back/README.md: -------------------------------------------------------------------------------- 1 | # Who follows you back on GitHub 2 | 3 | ## Graph module 4 | 5 | Write a module that exports a constructor function `DirectedGraph` for instantiating objects. 6 | Let the module be called `graph.js` 7 | 8 | To export something from the module, you have to do: 9 | 10 | ```javascript 11 | function DirectedGraph() { 12 | // ... 13 | } 14 | 15 | module.exports = DirectedGraph; 16 | ``` 17 | 18 | After this, you can require it like that: 19 | 20 | ```javascript 21 | var Graph = require("./graph"); 22 | var graph1 = new Graph(..); 23 | ``` 24 | 25 | ### Graph class 26 | 27 | The Graph should be: 28 | 29 | * Directed 30 | * Unweighted 31 | * Node names should be strings 32 | 33 | Don't bother making it more abstract to handle more cases. 34 | 35 | There should be the following public methods for the `Graph`: 36 | 37 | * A method, called `addEdge(nodeA, nodeB)` - which adds an edge between two nodes. If the nodes does not exist, they should be created. 38 | * A method, called `getNeighborsFor(node)` which returns a list of nodes (strings) for the given `node` 39 | * A method, called `pathBetween(nodeA, nodeB)`, which returns `true` if there is a path between `nodeA` and `nodeB`. Keep in kind that the graph is directed! 40 | * A method, called `toString()` which returns a string representation of the grap. This can be the stringified version of the internal structure of the graph. **Don't draw circles and `-->`** 41 | 42 | ### Test the Graph class. 43 | 44 | Using a library of your choice, make a test-suite for testing the `DirectedGraph` class. 45 | 46 | Make sure all public methods works just fine - create a test graph and assert if methods work OK. 47 | 48 | **You can use the following testing libraries:** 49 | 50 | * Mocha - https://github.com/mochajs/mocha - as a test runner 51 | * Chai - http://chaijs.com/ - as an assertion library 52 | 53 | ## Who Follows you back? 54 | 55 | We are going to implement a NodeJS application, which can give the answer to the fundamental question of the universe - Who follows you back on GitHub? 56 | 57 | We want to have the following high-level functionality: 58 | 59 | * A app / module that calls the GitHub API and builds the social graph for a given users 60 | * Several HTTP endpoints (use expresss!) for querying someone's social graph. 61 | 62 | **Since building the social graph can take a long time, think about how to make it more efficient from user's perspective!** 63 | 64 | ### Implementation details 65 | 66 | * Use the [GitHub API](https://developer.github.com/v3/) to fetch the users that a given users follow. 67 | * Make sure to create yourself a GitHub Application from your settings and obtain `client_id` and `client_secret`. This is because of API Rate Limiting - https://developer.github.com/v3/rate_limit/ 68 | * Make calls with your `client_id` and `client_secret` in order to have `5000` requests per hour! 69 | * Make a module / class that takes a given GitHub username and a **depth of the social graph to build**, which uses the `graph.js` module from the previous task. 70 | 71 | Be sure not to build graphs with depth `>= 4` - it's going to take forever ;) 72 | 73 | **The class the represents the GitHub social network should have the following methods:** 74 | 75 | * `following` - returns a list with the usersnames of everyone the user follows 76 | * `isFollowing` - accepts a username and returns `true`/`false` if the main user follows the one specified by the argument 77 | * `stepsTo` - accepts a username and return the number of hops needed to ge to that user following the `following`(pun not quite intended) relation 78 | 79 | 80 | ### Endpoints 81 | 82 | The app we are building should have the following endpoints: 83 | 84 | * `POST /createGraphFor` - accepts a JSON in the form: 85 | 86 | ```json 87 | { 88 | "username": "kunev", 89 | "depth": 3 90 | } 91 | ``` 92 | 93 | **and returns a unique graph id, which we will use to query the graph.** 94 | 95 | * `GET /graph/{graphId}` - returns the social graph for the given `graphId`. If the graph has not been created yet, return a message that says so. 96 | 97 | * `GET /mutually_follow/{graphId}/{username}` - this methods checks for the social graph with `graphId` and the given `{username}` the following thing: 98 | 99 | If the user's social graph (`graphId`) and the given `username` follows each other, this should return: 100 | 101 | ```json 102 | { 103 | "relation": "mutual" 104 | } 105 | ``` 106 | 107 | If the first user follows the second, but not vice versa: 108 | 109 | ```json 110 | { 111 | "relation": "first" 112 | } 113 | ``` 114 | 115 | In the opposite situation: 116 | 117 | ```json 118 | { 119 | "relation": "second" 120 | } 121 | ``` 122 | -------------------------------------------------------------------------------- /week3/README.md: -------------------------------------------------------------------------------- 1 | class: center, middle 2 | # Testing 3 | 4 | --- 5 | 6 | # Jasmine 7 | 8 | [Jasmine](http://jasmine.github.io/) is one of the most famous javascript testing frameworks. It defines itself as a tool for behaviour driven development. 9 | 10 | --- 11 | 12 | # Jasmine examples 13 | 14 | In the [jasmine github repo](https://github.com/mhevery/jasmine-node/tree/master/spec) 15 | 16 | --- 17 | # Chai 18 | 19 | [Chai](http://chaijs.com/) is an assertion library for node and client side javascript. It supports both `expect().to` and `should` style assertions. It also provides an extended version of the built in `assert` module. 20 | 21 | It has a plugin system. A nice example for a plugin is [Chai as Promised](http://chaijs.com/plugins/chai-as-promised) which exposes a nicer API for testing promises. 22 | 23 | --- 24 | # Mocha 25 | 26 | [Mocha](http://mochajs.org/#getting-started) is a testing framework for node and client side javascript. It can be used with any assertion library(for instance chai). It supports [several interfaces](http://mochajs.org/#interfaces) for running tests. 27 | 28 | --- 29 | # Sinon 30 | 31 | [Sinon](http://sinonjs.org/) supports testing with spies, stubs, mocks etc. It can be [used together with chai](http://chaijs.com/plugins/sinon-chai). 32 | 33 | --- 34 | # Testing HTTP(S) APIs 35 | 36 | Visinmedia's [SuperAgent](https://github.com/visionmedia/superagent) can be used to test HTTP(S) APIs with a [pretty simple setup](https://github.com/visionmedia/superagent/blob/master/test/node/agency.js) 37 | 38 | --- 39 | class: center, middle 40 | # Headless browser 41 | 42 | --- 43 | # PhantomJS/SlimerJS 44 | 45 | [PhantomJS](http://phantomjs.org/) - WebKit 46 | [SlimerJS](http://www.slimerjs.org/) - Gecko 47 | [Chimera](https://github.com/deanmao/node-chimera) 48 | 49 | --- 50 | # Testing in headless browsers 51 | 52 | [CaspeJS](http://casperjs.org/) 53 | 54 | --- 55 | # ZombieJS 56 | 57 | [ZombieJS](http://zombie.labnotes.org/) 58 | -------------------------------------------------------------------------------- /week4/0-Exam/README.md: -------------------------------------------------------------------------------- 1 | # First Exam for the NodeJS Course 2 | 3 | The exam is located here - https://github.com/HackBulgaria/NodeJS-1/tree/master/exams/exam1 4 | -------------------------------------------------------------------------------- /week4/1-Putting-It-All-Together/README.md: -------------------------------------------------------------------------------- 1 | # Express with Mongo / Mongoose & Tests 2 | 3 | So far, we have finished the first part in our NodeJS Journey. 4 | 5 | **We have grasped the following topics:** 6 | 7 | * Working with the file system 8 | * Working with the HTTP module 9 | * Why is everything so async? 10 | * Working with Express for making HTTP APIs 11 | * Consuming 3rd party APIs and making async while loops 12 | * Working with Mongo and the Native Driver for Node 13 | * Testing our applications in different styles. 14 | 15 | Now, we want to recap everything and make a simple & stupid CRUD application, so everything becomes clear. 16 | 17 | ## The idea - A simple code-snippet system 18 | 19 | We are going to make a small web app (frontend is up to you, if you want), which keeps snippets of code. 20 | 21 | Each snippet has the following data: 22 | 23 | * The language of the code 24 | * The filename 25 | * The code itself 26 | * The name of the creator of the snippet (Can be nickname or some unique id. Your choice) 27 | 28 | **You will have two primary goals:** 29 | 30 | 1. Create an RESTful Express API which CRUDs code snippets. 31 | 2. Test RESTful API for working correctly 32 | 33 | ## Libraries to use 34 | 35 | * Express for the REST API 36 | * [Mongoose](http://mongoosejs.com/) for making models & storing them in Mongo 37 | * [Mocha](https://github.com/mochajs/mocha), [Chai](http://chaijs.com/) and [SuperTest](https://www.npmjs.org/package/supertest) for testing 38 | 39 | Everything else you think can do the job for you. 40 | 41 | ## Endpoints for CRUD 42 | 43 | This is up to you, but be sure to include endpoints for: 44 | 45 | * Creating snippets 46 | * Updating existing snippet 47 | * Deleting a snippet 48 | * Listing all snippets 49 | * Listing snippets by creator 50 | * Listing a single snippet by some unique identifier 51 | 52 | ## Making the app smart 53 | 54 | You can make the app a little-bit smarter by infering the language just from the file name. 55 | 56 | For example, if you upload a `app.js` file, the language should be infered as JavaScript. 57 | 58 | You can take a look at [`gist.github.com`](https://gist.github.com/) 59 | -------------------------------------------------------------------------------- /week5/1-Passport-Basics/README.md: -------------------------------------------------------------------------------- 1 | # Apps with Passport.js 2 | 3 | We are going to make some basic authentication for our web applications - username & password. 4 | 5 | We are goint to use [Passport.js](http://passportjs.org/) as our primary weapon! 6 | 7 | Since the library has some things we should know about, we are going to implement a few different login/logout scenarios! 8 | 9 | ## Good old username / password login! 10 | 11 | ### The Express App 12 | 13 | Make a simple **static express app**, with the following structure: 14 | 15 | ``` 16 | . 17 | ├── app.js 18 | └── public 19 | └── index.html 20 | ``` 21 | 22 | Our application should serve static files with the following mapping: 23 | 24 | * At `/` url, use `public/` folder as root. This means - serve everything from the public folder. 25 | 26 | You can read more about serving static files here - http://expressjs.com/4x/api.html#express.static 27 | 28 | ### The Database 29 | 30 | Use a simple Mongo database, that knows about our users. 31 | 32 | Each user consists of two things: 33 | 34 | * username 35 | * password 36 | 37 | You can use ODM, if you like it. 38 | 39 | ### HTML for Login / Logout 40 | 41 | In `index.html`, create a simple login / logout functionallity. 42 | 43 | Use forms / buttons / JavaScript - whatever you like! 44 | 45 | ### Putting it all together 46 | 47 | And when we are ready, use the [Passport's LocalStrategy](http://passportjs.org/guide/username-password/) to implement the login / logout functionallity! 48 | -------------------------------------------------------------------------------- /week5/1-Passport-Basics/static-app/app.js: -------------------------------------------------------------------------------- 1 | var express = require("express"), 2 | app = express(); 3 | 4 | app.use(express.static(__dirname + "/public")) 5 | 6 | app.listen(3000); 7 | -------------------------------------------------------------------------------- /week5/1-Passport-Basics/static-app/public/index.html: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /week5/2-Linking-Accounts/README.md: -------------------------------------------------------------------------------- 1 | # Tee Book 2 | 3 | (Check http://unixhelp.ed.ac.uk/CGI/man-cgi?tee) 4 | 5 | ## Login with GitHub 6 | 7 | Using express and [PassportJS](http://passportjs.org/) create a simple web app that lets you login with your GitHub account. 8 | 9 | You can use this - https://github.com/jaredhanson/passport-github 10 | 11 | ## Once logged in, link Facebook and / or Twitter accounts 12 | 13 | Once you have logged in, there should be a choice for the user to link more social accounts. Because moar! 14 | 15 | ### Connect to Facebook 16 | 17 | Once you are ready, connect your Facebook account to the web app and get the user information! 18 | 19 | You can use this - http://passportjs.org/guide/facebook/ 20 | 21 | ### Connect to Twitter 22 | 23 | Once you are ready, connect your Twitter account to the web app and get the user information! 24 | 25 | You can use this - http://passportjs.org/guide/twitter/ 26 | 27 | ## Problems to solve 28 | 29 | Should you have an independent User model and just link facebook and twitter account models to it or use one of the two as a "master" account and just link the second one? 30 | 31 | ## Making it more beautiful 32 | 33 | There are social login bootstrap buttons here - http://lipis.github.io/bootstrap-social/ - you can use them. 34 | -------------------------------------------------------------------------------- /week5/Recap-and-Summary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Recap-and-Summary", 3 | "version": "1.0.0", 4 | "description": "Simple recap & summary scripts for what we know so far", 5 | "main": "scrape-urls.js", 6 | "dependencies": { 7 | "q": "^1.1.1", 8 | "request": "^2.48.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /week5/Recap-and-Summary/scrape-urls.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | URI_PATTERN = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig; 3 | var Q = require("q"); 4 | 5 | function getUrlsFrom(url) { 6 | var defered = Q.defer(); 7 | 8 | request(url, function (error, response, body) { 9 | if (!error && response.statusCode == 200) { 10 | defered.resolve(body.match(URI_PATTERN)); 11 | } else { 12 | defered.reject(error); 13 | } 14 | }); 15 | 16 | return defered.promise; 17 | } 18 | 19 | 20 | var urlsQueue = ["https://hackbulgaria.com"]; 21 | 22 | function scrape() { 23 | var url = urlsQueue.shift(); 24 | 25 | getUrlsFrom(url) 26 | .then(function(urls) { 27 | console.log(urls); 28 | urlsQueue = urlsQueue.concat(urls); 29 | scrape(); 30 | }) 31 | .fail(function(error) { 32 | console.log("FAILED"); 33 | console.log(error); 34 | }) 35 | .done(); 36 | } 37 | 38 | scrape(); 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /week6/1-Stream-RegEx/README.md: -------------------------------------------------------------------------------- 1 | #Regular expression streams 2 | 3 | ##Filter Transform stream 4 | Write a constructor, inheriting from `Transform` stream, that takes as an argument a regular expression as an argument. The constructed stream object should output only the chunks input to it, that match the regular expression. 5 | 6 | Extend your code when a `RegExp` object is written to the stream, that causes it to start using that as the pattern from now on. 7 | -------------------------------------------------------------------------------- /week6/2-HTTP-Proxy/README.md: -------------------------------------------------------------------------------- 1 | ##Simple proxy server 2 | `req` and `res` objects implement `Readable` and `Writable` stream interfaces respectively. Use that to right a simple HTTP proxy server. Keep in mind that if you use `http` or `express` some parts of the `req` stream will be consumed by the time your handlers get executed. 3 | 4 | Think about a way to preserver cookies for each client separately. 5 | 6 | You can use a `Transform` stream to "fix" links on the page to point to your server, so you can use it for continuous browsing sessions. 7 | -------------------------------------------------------------------------------- /week6/3-A-Big-File/README.md: -------------------------------------------------------------------------------- 1 | # A Big File 2 | 3 | We want to use Node streams in order to read a big file, containing random numbers, separated by `,` and compute their sum. 4 | The file should be of size around `5GB`, so it wont be easy to load it in memory. 5 | 6 | ## Create a big file with numbers in it 7 | 8 | Make a Node program that generates random numbers, separated by `,` and `\n` and saves / streams them to a file. 9 | 10 | Make sure that you can call your program with flags, saying how big the file should become, before closing it. 11 | 12 | For example: 13 | 14 | ``` 15 | $ node make-file.js --size 5GB --output BIG_FAT_FILE_WITH_NUMBERS 16 | ``` 17 | 18 | This will create a `5GB` file called `BIG_FAT_FILE_WITH_NUMBERS` 19 | 20 | 21 | You can support the basic measurements - `MB` and `GB` 22 | 23 | ## Read the big file and find the sum of the numbers 24 | 25 | Once you have a large enough file, try to read it entirely in memory and make sure that Node crashes with the following error: 26 | 27 | ``` 28 | FATAL ERROR: CALL_AND_RETRY_0 Allocation failed - process out of memory 29 | ``` 30 | 31 | Once you have reached that point, you can start using the stream API in order to process the file line by line and find the sum of all numbers. 32 | 33 | ## Libraries 34 | 35 | * [Event Stream](https://github.com/dominictarr/event-stream) should do fine, but you are free to searc for whatever libraries you want to use for this problem. 36 | * [Node-Bigint](https://github.com/substack/node-bigint) for calculating big sums / numbers 37 | -------------------------------------------------------------------------------- /week6/4-Video-Consumer/README.md: -------------------------------------------------------------------------------- 1 | #Video consumer 2 | 3 | ##Streamer 4 | The file `streamer.js` implements a server, that accepts connections on port 3000 and starts sending image data to them live from a camera on the machine the server is running on. 5 | 6 | Each time a connection is made the following data is sent: 7 | 8 | * one `UInt16` for the width of the image 9 | * one `UInt16` for the height of the image 10 | * `3 * width * height` bytes of RGB data for the actual image 11 | * `0` byte signifying the end of the image 12 | 13 | ##Consumer 14 | Your task is to establish a connection to the server in `streamer.js` and save the output from it as a png file. Implement a `Transform` stream that takes the bytes sent from the server as input and produces a `Buffer` with the data to be written to the png file. 15 | 16 | Write each frame you get from the server as a separate png file. 17 | 18 | You can use the [png node module](https://www.npmjs.org/package/png) or [pngjs](https://www.npmjs.org/package/pngjs) for creating the file. 19 | 20 | ##Faking a video source 21 | If you want to use a fake video instance you can look into [v4l2loopback](https://github.com/umlaeute/v4l2loopback), which can create a fake video device for you. Then you just need to feed it some signal, which you could do with gstreamer [as shown here](https://github.com/umlaeute/v4l2loopback/wiki/Gstreamer). 22 | -------------------------------------------------------------------------------- /week6/4-Video-Consumer/streamer.js: -------------------------------------------------------------------------------- 1 | var v4l2camera = require('v4l2camera'), 2 | debug = require('debug')('streamer'), 3 | net = require('net'), 4 | camera = new v4l2camera.Camera('/dev/video0'); 5 | 6 | function writeImage(connection) { 7 | var size = camera.width * camera.height, 8 | rgb = camera.toRGB(), 9 | metaDataBuffer = new Buffer(4); 10 | 11 | metaDataBuffer.writeUInt16BE(camera.width, 0); 12 | metaDataBuffer.writeUInt16BE(camera.height, 2); 13 | 14 | var buffer = Buffer.concat([metaDataBuffer, new Buffer(rgb), new Buffer([0])]); 15 | 16 | debug('writing', buffer.length, 'bytes to client'); 17 | connection.write(buffer); 18 | } 19 | 20 | net.createServer({allowHalfOpen: true}, function (connection) { 21 | connection.on('error', function (error) { 22 | debug('connection error:', error); 23 | clearTimeout(connection.timeout); 24 | }); 25 | 26 | connection.on('close', function () { 27 | debug('client closed'); 28 | clearTimeout(connection.timeout); 29 | }); 30 | 31 | connection.on('data', function (chunk) { 32 | debug('got:' + chunk); 33 | }); 34 | 35 | connection.timeout = setTimeout(function writeLoop() { 36 | writeImage(connection); 37 | connection.timeout = setTimeout(writeLoop, 1000); 38 | }, 100); 39 | }).listen(3000); 40 | 41 | camera.start(); 42 | 43 | camera.capture(function captureLoop() { 44 | camera.capture(captureLoop); 45 | }); 46 | -------------------------------------------------------------------------------- /week6/README.md: -------------------------------------------------------------------------------- 1 | class: center 2 | #Streams 3 | ![stream](src/img/stream.jpg) 4 | --- 5 | #Streams 6 | 7 | A `stream` is an abstraction that represents a sequence of data being "fed" to a program(or being produced by it) over the course of time. 8 | 9 | -- 10 | 11 | It could be extended to a lot of things, but at first you can think about reading/writing from/to files or over the network. 12 | 13 | --- 14 | #Streams 15 | 16 | NodeJS has a built in abstraction for streams. It's implemented in the `stream` module. 17 | 18 | * `Readable` 19 | * `Writable` 20 | * `Duplex` 21 | * `Transform` 22 | 23 | --- 24 | #Streams are event emitters 25 | 26 | * `readable` - from now on data will start flowing 27 | * `data` - a new piece of data has come 28 | * `end` - no more data will be transmitted 29 | * `close` - the underlying resource(file descriptor, network socket) has been closed 30 | * `error` - there was some error 31 | 32 | --- 33 | #Stream modes 34 | 35 | ### non-flowing 36 | The default mode for each stream. It means data is only read out of the resource when explicitly asked for. 37 | 38 | This means data is read into the buffer of the stream and accessible through a call to `read([size])`. 39 | 40 | `read` attempts to read as much bytes as requested(or all in the buffer if called without an argument). Returns `null` if the buffer is empty or doesn't contain enough data. 41 | 42 | --- 43 | #Stream methods 44 | ### flowing 45 | 46 | A stream in flowing mode will read as much data as possible and emit the `data` event whenever there is new data available. 47 | 48 | --- 49 | #Readable stream methods 50 | 51 | * `read([size])` - only useful in non-flowing mode 52 | * `setEncoding([encoding])` - state the stream data's encoding, so you get a string in the `'data'` handlers instead of a buffer 53 | * `pause()`/`resume()` - pause and resume the emitting of `data` events 54 | * `pipe(destination,[options])`/`unpipe([destination])` - pipe and unpipe a `Readable` stream's output to a `Writable` stream 55 | * `unshift(chunk)` - return data to the buffer[*](#transform-footnote) 56 | * `wrap(stream)` - for wrapping old-style streams(pre node 0.10) 57 | 58 | .footnote[ 59 | using too much of these means you should consider using `stream.Transform` 60 | ] 61 | 62 | --- 63 | #Writable stream 64 | 65 | ### methods 66 | * `write(data, [encoding], [callback])` - write data to the stream. The return value indicates if you can keep writing right away 67 | * `end([chunk], [encoding], [callback])` - end writing to the stream. Calling `write` after you've called `end` will raise an error 68 | 69 | -- 70 | 71 | ### events 72 | * `drain` - if `write` returns `false`, the `drain` event will signal when it's ok to continue writing to the stream 73 | * `finish` - emitted when after calling `end` all data has been sent to the underlying system 74 | * `pipe` - emitted when a `Readable` stream is piped to this one 75 | * `unpipe` - what could this be?! 76 | 77 | --- 78 | #Duplex and Transform 79 | 80 | `Duplex` streams implement both the `Readable` and the `Writable` interface 81 | 82 | `Transform` streams are like `Duplex`, but they manipulate the data written to them to produce the data they stream back 83 | 84 | --- 85 | #Implementing `Readable` streams 86 | 87 | * `_read([size])` - called from other internal methods of `Readable` as a result of someone calling `read([size])` 88 | * `push(chunk, [encoding])` - writes data to the stream, so the consumer can access it 89 | 90 | ####**NB** 91 | * `_read` and `push` should **never** be called from client code, only frmo the internal methods of the stream object 92 | * when implementing a `Readable` stream our code should **never** call `read` or set handlers for `data`, `end` etc. events 93 | 94 | --- 95 | #Implementing `Writable` streams 96 | 97 | * `_write(chunk, encoding, callback)` - called when someone wants to write to the stream 98 | 99 | ####**NB** 100 | * `_write` should not be called from client code, only by the internal methods of the stream object 101 | * when implementing a `Writable` stream our code should **never** call `write` and set handlers for `drain`, `finish` etc. events 102 | 103 | --- 104 | #Implementing `Duplex` streams 105 | 106 | There is no multiple inheritance in javascript. So `Duplex` implements both a `Readable` and a `Writable` stream's interface, but does that not by inheriting both. It inherits `Readable` and then ["parasitically" inherits]() `Writable`. 107 | 108 | --- 109 | #Implementign `Transform` streams 110 | 111 | A common task is to want to do an operation on a big chunk of data. This can be achieved with a `Duplex` stream, that defines its `_write` method to save data in some buffer and then the `_read` method to attempt to get the data from that buffer and process it somehow. But like we said that is a *pretty common task*. 112 | 113 | For that reason the `Transform` stream expects us to define only one method when implementing it, the `_transform` method. 114 | 115 | * `_transform(chunk, encoding, callback)` - expects a chunk of data, possibly the encoding of it's a `String` and a callback to call when the processing is finished 116 | * `push(data, [encoding]` - same as `Readable`'s `push` 117 | * `_flush(callback)` - called when all potentially buffered data should be processed and sent for reading 118 | 119 | -- 120 | ###events 121 | * `finish` - emitted when `end()` is called 122 | * `end` - emitted after all data has been output, that is after the callback of `_flush` has been called 123 | 124 | --- 125 | #Quirks 126 | 127 | * `_writableState.buffer` and `_readableState.buffer` 128 | * `stream.read(0)` and `stream.push('')` 129 | 130 | --- 131 | #Materials 132 | Take a look at: 133 | * [node's documentation on streams](http://nodejs.org/api/stream.html) 134 | * [stream-handbook](https://github.com/substack/stream-handbook) 135 | * [nodestreams](http://nodestreams.com/) is a helpful playground project to help you better grasp what streams can do 136 | -------------------------------------------------------------------------------- /week7/1-Chat-Server-And-Clients/README.md: -------------------------------------------------------------------------------- 1 | #Chat server 2 | 3 | Implement a simple chat server supporting some form of simple user identification(authentication would be a nice plus). That means each user should have a unique nickname. Multiple users connected to the server should be able to see each other's messages. 4 | 5 | -- 6 | 7 | #Clients 8 | 9 | ##Console client 10 | 11 | Make a minimal console client allowing you to send a message to a user, a room or everyone on the server and receive messages meant for you or for rooms you've joined. 12 | 13 | ##WebApp client 14 | 15 | Make a simple web application to display messages send to users and rooms on the server. Add the ability to send messages to users/rooms. 16 | 17 | --- 18 | 19 | #Development stages 20 | 21 | A good idea would be to go through the following stages of implementing your solution: 22 | 23 | ##First stage 24 | 25 | One server acts as one room. That means all users connected to the server see the messages from all other users. No channels/rooms, no personal queries. 26 | 27 | ##Second stage 28 | 29 | Add personal queries between users so people could talk one on one. 30 | 31 | ##Third stage 32 | 33 | Add rooms. Each user can choose a room to join. All users in one room get the messages from all other users in the same room. 34 | 35 | --- 36 | 37 | #Architecture 38 | 39 | You need to decide how to organise the different parts to work together. 40 | 41 | ## WebSocket proxy 42 | 43 | You can have a main server handling all the messages, taking care of rooms, queries and users implementing only a simple TCP socket interface with some protocol of your own choice. That way the console clients would be pretty simple to implement and can exist without ever caring about websockets. 44 | 45 | Then to include a web application in the whole picture you'll need a separate application that exposes a websocket server for the web client and relays that to the main server via TCP sockets. 46 | 47 | ### Proxy communication 48 | 49 | * one socket open between the websocket proxy and the main chat server and send all information through it 50 | * or keep a separate socket for each websocket connection you receive 51 | 52 | ## WebSocket only 53 | 54 | The main chat server could only expose a websocket interface. That way your console clients need to also connect via websockets, but your webapp can talk directly to the server and not suffer the delay of being proxied. 55 | 56 | 57 | #Libraries 58 | 59 | * [ws](https://www.npmjs.org/package/ws) - an implementation of WebSockets for node 60 | * [prompt](https://www.npmjs.org/package/prompt) - a nice way to make interactive console applications with node 61 | * [node-term-ui](https://github.com/jocafa/node-term-ui) - a UI toolkit for console apps 62 | * [blessed](https://github.com/chjj/blessed) - a curses like library for node in case you're into sleeker text UI 63 | * [socket.io](http://socket.io) - an abstraction over websockets allowing you to send more complex messages than just text 64 | -------------------------------------------------------------------------------- /week8/1-Nodeventure-Final/README.md: -------------------------------------------------------------------------------- 1 | #Nodeventures 2 | 3 | Your final task will be a bit more ambitious. We want you to build an MMORPG powered by a node back end. 4 | 5 | You're free to choose a theme and story for your world, anything goes. 6 | 7 | What we want you to do is design a nice API both for real time data(hero movement, battles, item exchange, etc.…) and for some one-off requests(fetching avatars, history of events, etc.…). 8 | 9 | Feel free to split your implementation into as many node applications as you feel are needed. 10 | 11 | --- 12 | 13 | ##Required functionality 14 | ###World navigation 15 | You have a world, potentially endless, that your players get to explore and walk around. It's a 2D plane, nothing too fancy. 16 | 17 | You should have some abstraction for buildings(houses, castles, space stations, whatever you like). Heroes and items could either be in the open world, or inside a building. Buildings might not always be accessible, or might require the possession of a certain item to enter them. 18 | 19 | --- 20 | 21 | ###Items 22 | Items can be scattered around the world, including inside buildings. Heroes can acquire items by either finding them in the world, or trading with other heroes. 23 | 24 | Each item has a weight. A hero should only be able to carry around up to a certain amount of items based on their collective weight. 25 | 26 | ####Boost items 27 | Some items boost heroes' stats when they acquire them. Once the hero sells the item or loses it in battle the effect of acquiring it is reverted. 28 | 29 | A good idea is to have items enhancing your max health, or granting you additional armour. Some items might work over time, restoring your health a little over some period of time. 30 | 31 | ####Weapons 32 | Some items are weapons. That means they have some amount of damage they inflict to the enemy they are used on. 33 | 34 | ####Passive items 35 | Items might not boost a hero's stats if they server another purpose(e.g. unlocking buildings). 36 | 37 | --- 38 | 39 | ###Heroes 40 | ####Movement 41 | Heroes should be able to move around the world freely. The only obstacles should be buildings or other heroes along the way. The possible directions are north, west, east, south and the respective combinations - a total of 8 directions. 42 | 43 | ####Inventory 44 | Each hero has some type of inventory(a backpack, magical extremely big pockets, etc.…). The inventory has a maximum amount of weight in can handle, it should be finite and might be expanded by acquiring some item. 45 | 46 | ####Battles 47 | When two heroes are close enough to each other, or inside the same building, they can engage in a battle. The battle is turn based, on each turn the hero who's attacking gets to choose a weapon from their inventory to use. The battle ends after a certain number of rounds or after one of the heroes' health goes down to zero. In case one of the heroes dies the winner gets to choose among the items possessed by the defeated one and take anything, as long as that doesn't conflict with their inventory limit. 48 | 49 | --- 50 | ###Buildings 51 | ####Mechanics 52 | A building is essentially a separate world map linked to the main one via its entrance. A building might take up less space on the real world map than is actually accessible from inside it. It can be bigger on the outside/smaller on the inside. 53 | 54 | 55 | ##Clients 56 | There should be two types of clients: one running on the command line and one built as a web app. 57 | 58 | Both clients need to just give a good way to communicate with the server and expose it's functionalities. All and any bells and whistles are more than welcome, but they're not the main focus. 59 | 60 | ###Command line 61 | The command line client can employ any type of interface. 62 | 63 | A fully textual client, as a game book, but with some elements of real time action as the world changes not only based upon your actions, but also the actions of other heroes in the world. 64 | 65 | A more graphical command line client employing [blessed](https://github.com/chjj/blessed), [drawille](https://github.com/madbence/node-drawille), [drawille-canvas](https://github.com/madbence/node-drawille-canvas) or [ansi-canvas](https://github.com/TooTallNate/ansi-canvas), showing some sort of graphical representation of the world. 66 | 67 | ###Web app client 68 | The best idea would be to use a `` to draw some state of the world around your hero on it. 69 | -------------------------------------------------------------------------------- /week8/README.md: -------------------------------------------------------------------------------- 1 | class: center, middle 2 | #Fork it… 3 | 4 | --- 5 | class:center, middle 6 | #`child_process` 7 | 8 | --- 9 | #`child_process` 10 | A module allowing us to start separate node applications, or just run arbitrary commands. 11 | 12 | --- 13 | #`exec` 14 | 15 | `exec` starts a shell and gives the first argument as a command to that shell 16 | ```javascript 17 | var command = 'kill $(ps fax | grep [/]usr/lib/firefox/firefox | cut -d" " -f 1)'; 18 | child_process.exec(command, function(error, stdout, stderr) { 19 | if (error) { 20 | console.log(error); 21 | return; 22 | } 23 | 24 | console.log(stdout); 25 | console.log(stderr); 26 | }); 27 | ``` 28 | 29 | --- 30 | #`execFile` 31 | `execFile` runs a file directly diving it command line parameters 32 | 33 | ```javascript 34 | var file = 'ls', 35 | args = ['-l' '-a']; 36 | child_process.execFile(file, args, function (error, stdout, stderr) { 37 | if (error) { 38 | console.log(error); 39 | return; 40 | } 41 | 42 | console.log(stdout); 43 | console.log(stderr); 44 | }); 45 | ``` 46 | 47 | --- 48 | #`child_process.spawn` 49 | Starts a process and returns an object representing that process. 50 | 51 | The returned object has as properties `stdin`(a writable stream), `stdout` and `stderr`(readable streams). 52 | 53 | It emits `end` when the process ends, `close` when all streams are closed and `error` in case of some error. 54 | 55 | --- 56 | #`child_process.fork` 57 | A special case of `spawn` that can be used to run node modules, giving you an interface to communicate between the two processes more conveniently. 58 | 59 | The returned object has a `send` method, allowing you to send messages to the child process. In the child process, the `process` object also has a `send` method allowing it to send data back to the parent object. Both sides of the IPC channel emit a `message` event when the other end `send`s a message. 60 | 61 | Messages can me just strings/buffers or instances of some standard "classes". 62 | 63 | ```javascript 64 | if (handle instanceof net.Socket) { 65 | message.type = 'net.Socket'; 66 | } else if (handle instanceof net.Server) { 67 | message.type = 'net.Server'; 68 | } else if (handle instanceof process.binding('tcp_wrap').TCP || 69 | handle instanceof process.binding('pipe_wrap').Pipe) { 70 | message.type = 'net.Native'; 71 | } else if (handle instanceof dgram.Socket) { 72 | message.type = 'dgram.Socket'; 73 | } else if (handle instanceof process.binding('udp_wrap').UDP) { 74 | message.type = 'dgram.Native'; 75 | } else { 76 | throw new TypeError("This handle type can't be sent"); 77 | } 78 | ``` 79 | 80 | --- 81 | class: center, middle 82 | #`cluster` 83 | 84 | --- 85 | class: center, middle 86 | Stability: 1 - Experimental 87 | 88 | --- 89 | #`fork` 90 | Most often you would just call `cluster.fork` to fork your process. 91 | 92 | `cluster.isMaster` tells you if you're running in the master process of the cluster or in a worker. 93 | 94 | --- 95 | #Sharing servers 96 | The main feature of the `cluster` module is the ability to share servers. 97 | 98 | When a worker calls `server.listen` the parameters are serialized and sent as a message to the master process. If the master process doesn't have a server like the one the worker wants to create it will be created, otherwise the same server will be sent back to the worker. 99 | 100 | When several processes bind to the same port your OS can load balance between them. Your code does not need to(and practically can't) do any load balancing, node's internal code does not do any load balancing. It's all the work of your OS. Node processes exchange server objects via node's IPC mechanism. 101 | --------------------------------------------------------------------------------