├── .all-contributorsrc ├── .editorconfig ├── .env.example ├── .eslintrc ├── .gitignore ├── .husky └── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── public ├── css │ ├── create.css │ ├── footer.css │ ├── poll-results-compact.css │ ├── poll-results.css │ ├── polls.css │ ├── qrcode.css │ ├── style.css │ └── vote.css └── js │ ├── classes │ └── poll.js │ ├── qrcode.js │ └── sketches │ ├── create.js │ ├── poll.js │ └── vote.js ├── server ├── api.js ├── helpers │ ├── broadcaster.js │ ├── createNewPoll.js │ └── database.js ├── index.js ├── validation │ ├── antipollspam.js │ ├── antipollspam_test.js │ └── basicauth.js └── web.js └── views ├── create.pug ├── footer.pug ├── index.pug ├── notfound.pug ├── poll.pug ├── qrcode.pug └── vote.pug /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "crunchypi", 10 | "name": "crunchypi", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/53178205?v=4", 12 | "profile": "https://github.com/crunchypi", 13 | "contributions": [ 14 | "code", 15 | "ideas" 16 | ] 17 | }, 18 | { 19 | "login": "jriegraf", 20 | "name": "Julian", 21 | "avatar_url": "https://avatars1.githubusercontent.com/u/16071323?v=4", 22 | "profile": "https://github.com/jriegraf", 23 | "contributions": [ 24 | "code", 25 | "ideas" 26 | ] 27 | }, 28 | { 29 | "login": "manthanabc", 30 | "name": "Manthan", 31 | "avatar_url": "https://avatars2.githubusercontent.com/u/48511543?v=4", 32 | "profile": "https://github.com/manthanabc", 33 | "contributions": [ 34 | "code", 35 | "design" 36 | ] 37 | }, 38 | { 39 | "login": "simon-tiger", 40 | "name": "Simon Tiger", 41 | "avatar_url": "https://avatars1.githubusercontent.com/u/21979673?v=4", 42 | "profile": "https://simontiger.com", 43 | "contributions": [ 44 | "code", 45 | "ideas" 46 | ] 47 | }, 48 | { 49 | "login": "pieterdeschepper", 50 | "name": "Pieter De Schepper", 51 | "avatar_url": "https://avatars0.githubusercontent.com/u/4106097?v=4", 52 | "profile": "https://github.com/pieterdeschepper", 53 | "contributions": [ 54 | "design", 55 | "code" 56 | ] 57 | }, 58 | { 59 | "login": "D-T-666", 60 | "name": "Dimitri Tabatadze", 61 | "avatar_url": "https://avatars1.githubusercontent.com/u/35934791?v=4", 62 | "profile": "https://github.com/D-T-666", 63 | "contributions": [ 64 | "code", 65 | "ideas" 66 | ] 67 | }, 68 | { 69 | "login": "ShawKai91", 70 | "name": "Shaw Kai", 71 | "avatar_url": "https://avatars3.githubusercontent.com/u/66273574?v=4", 72 | "profile": "https://github.com/ShawKai91", 73 | "contributions": [ 74 | "code", 75 | "ideas" 76 | ] 77 | }, 78 | { 79 | "login": "BeeryShklar", 80 | "name": "Beery Shklar", 81 | "avatar_url": "https://avatars3.githubusercontent.com/u/52495055?v=4", 82 | "profile": "https://github.com/BeeryShklar", 83 | "contributions": [ 84 | "code" 85 | ] 86 | }, 87 | { 88 | "login": "dipamsen", 89 | "name": "Fun Planet", 90 | "avatar_url": "https://avatars2.githubusercontent.com/u/59444569?v=4", 91 | "profile": "https://github.com/dipamsen", 92 | "contributions": [ 93 | "ideas", 94 | "code" 95 | ] 96 | }, 97 | { 98 | "login": "Samuel-Martineau", 99 | "name": "Samuel Martineau", 100 | "avatar_url": "https://avatars3.githubusercontent.com/u/44237969?v=4", 101 | "profile": "https://smartineau.me", 102 | "contributions": [ 103 | "ideas" 104 | ] 105 | }, 106 | { 107 | "login": "shiffman", 108 | "name": "Daniel Shiffman", 109 | "avatar_url": "https://avatars0.githubusercontent.com/u/191758?v=4", 110 | "profile": "http://www.shiffman.net", 111 | "contributions": [ 112 | "code" 113 | ] 114 | }, 115 | { 116 | "login": "johntalton", 117 | "name": "John", 118 | "avatar_url": "https://avatars1.githubusercontent.com/u/13648537?v=4", 119 | "profile": "https://github.com/johntalton", 120 | "contributions": [ 121 | "ideas" 122 | ] 123 | }, 124 | { 125 | "login": "adriaan1313", 126 | "name": "Bunnygamers", 127 | "avatar_url": "https://avatars0.githubusercontent.com/u/19620346?v=4", 128 | "profile": "https://github.com/adriaan1313", 129 | "contributions": [ 130 | "ideas" 131 | ] 132 | }, 133 | { 134 | "login": "TheCBKM", 135 | "name": "Rajaram Joshi", 136 | "avatar_url": "https://avatars1.githubusercontent.com/u/38382861?v=4", 137 | "profile": "http://cbkm.in", 138 | "contributions": [ 139 | "doc" 140 | ] 141 | }, 142 | { 143 | "login": "younesaassila", 144 | "name": "Younes Aassila", 145 | "avatar_url": "https://avatars1.githubusercontent.com/u/47226184?v=4", 146 | "profile": "http://aassila.com", 147 | "contributions": [ 148 | "doc" 149 | ] 150 | }, 151 | { 152 | "login": "GiggioG", 153 | "name": "GiggioG", 154 | "avatar_url": "https://avatars3.githubusercontent.com/u/47040505?v=4", 155 | "profile": "https://giggiog.github.com", 156 | "contributions": [ 157 | "code", 158 | "design" 159 | ] 160 | }, 161 | { 162 | "login": "elunico", 163 | "name": "Tom", 164 | "avatar_url": "https://avatars3.githubusercontent.com/u/10181211?v=4", 165 | "profile": "http://eluni.co", 166 | "contributions": [ 167 | "code", 168 | "design" 169 | ] 170 | } 171 | ], 172 | "contributorsPerLine": 7, 173 | "projectName": "Live-Poll", 174 | "projectOwner": "CodingTrain", 175 | "repoType": "github", 176 | "repoHost": "https://github.com", 177 | "skipCi": true 178 | } 179 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | [*.js] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LOGIN_USERNAME=codingtrain 2 | LOGIN_PASSWORD=rainbow 3 | PORT=3000 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "prettier" 10 | ], 11 | "extends": [ 12 | "eslint:recommended" 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": 12 16 | }, 17 | "rules": { 18 | "prettier/prettier": [ 19 | "error", 20 | { 21 | "endOfLine": "auto" 22 | } 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | database.db 4 | .env 5 | .idea 6 | package-lock.json -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "printWidth": 80, 6 | "proseWrap": "preserve", 7 | "quoteProps": "consistent", 8 | "semi": true, 9 | "singleQuote": false, 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Coding Train 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 | # Live-Poll 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 | ## Set Up 10 | 11 | 1. Clone repository 12 | 1. Run `npm install` 13 | 1. Copy and rename `.env.example` to `.env` 14 | 1. Run `npm run postinstall` (optional) 15 | 1. Run `npm run dev` (development mode) or `npm start` (production mode) 16 | 1. Open the page in your browser (https://localhost:3000). It will list all the active polls. 17 | 1. Type in the username and password found in `.env` 18 | 19 | ## Available Routes 20 | 21 | - http://localhost:3000/create to create a new poll 22 | - http://localhost:3000/newest to show the results of the newest poll 23 | - http://localhost:3000/vote/{poll_id} to vote in a poll 24 | - http://localhost:3000/poll/{poll_id} to show the results of a poll 25 | - http://localhost:3000/qrcode to show a qrcode with the url to newest poll 26 | 27 | ## Additional URL-Parameters 28 | 29 | - monotone=boolean 30 | - applies a reduced color scheme for voting bars 31 | - ignored if _overlay=true_ is used 32 | - simple=boolean 33 | - applies a basic font type 34 | - overlay=boolean 35 | - overwrites _monotone_ parameter 36 | - applies a compact view especially for live streams / OBS 37 | 38 | Example usage 39 | 40 | ``` 41 | /poll/{poll_id}?monotone=true&simple=true&overlay=true 42 | ``` 43 | 44 | ## Contributors ✨ 45 | 46 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |

crunchypi

💻 🤔

Julian

💻 🤔

Manthan

💻 🎨

Simon Tiger

💻 🤔

Pieter De Schepper

🎨 💻

Dimitri Tabatadze

💻 🤔

Shaw Kai

💻 🤔

Beery Shklar

💻

Fun Planet

🤔 💻

Samuel Martineau

🤔

Daniel Shiffman

💻

John

🤔

Bunnygamers

🤔

Rajaram Joshi

📖

Younes Aassila

📖

GiggioG

💻 🎨

Tom

💻 🎨
76 | 77 | 78 | 79 | 80 | 81 | 82 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live-poll", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "start": "node server/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon server/index.js", 10 | "lint": "eslint server/**/*.js public/**/*.js", 11 | "lint:fix": "npm run lint --fix", 12 | "lint:staged": "lint-staged", 13 | "postinstall": "husky install" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/CodingTrain/Live-Poll.git" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/CodingTrain/Live-Poll/issues" 24 | }, 25 | "homepage": "https://github.com/CodingTrain/Live-Poll#readme", 26 | "dependencies": { 27 | "dotenv": "^8.2.0", 28 | "express": "^4.17.1", 29 | "nedb": "^1.8.0", 30 | "nedb-promises": "^4.0.4", 31 | "pug": "^3.0.0", 32 | "socket.io": "^3.0.3" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^7.14.0", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "husky": "^5.0.4", 38 | "lint-staged": "^10.5.2", 39 | "nodemon": "^2.0.4", 40 | "prettier": "^2.2.0" 41 | }, 42 | "lint-staged": { 43 | "*.js": [ 44 | "eslint . --fix" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/css/create.css: -------------------------------------------------------------------------------- 1 | /* put create poll page styles here */ 2 | #main-create { 3 | margin: 0 20px; 4 | } 5 | 6 | #main-create textarea, 7 | input { 8 | font-family: inherit; 9 | } 10 | 11 | #main-create .options input, 12 | textarea { 13 | font-size: 16px; 14 | position: relative; 15 | margin-bottom: 0.2em; 16 | width: 300px; 17 | padding: 0.5em 1em; 18 | border: 1px solid lightblue; 19 | border-radius: 5px; 20 | left: 50%; 21 | transform: translate(-50%); 22 | } 23 | 24 | #main-create .options { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | #main-create textarea { 30 | font-size: 20px; 31 | height: 80px; 32 | } 33 | 34 | #main-create .buttonsArray { 35 | margin-top: 2em; 36 | display: flex; 37 | flex-direction: column; 38 | } 39 | 40 | #main-create button { 41 | font-size: 20px; 42 | position: relative; 43 | width: 300px; 44 | height: 40px; 45 | margin-top: 0.5em; 46 | background-color: var(--submit-button); 47 | cursor: pointer; 48 | border-radius: 8px; 49 | left: 50%; 50 | transform: translate(-50%) scale(1); 51 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); 52 | transition: box-shadow 200ms ease, transform 200ms ease; 53 | } 54 | 55 | #main-create button:hover { 56 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5); 57 | transform: translate(-50%) scale(1.02); 58 | } 59 | 60 | #main-create button:active { 61 | transform: translate(-50%) scale(1); 62 | } 63 | 64 | #main-create .addOption { 65 | background-color: rgb(81, 219, 81); 66 | } 67 | 68 | #main-create .removeLastOption { 69 | background-color: rgb(219, 97, 81); 70 | } 71 | 72 | #main-create svg { 73 | margin-top: 20px; 74 | } 75 | -------------------------------------------------------------------------------- /public/css/footer.css: -------------------------------------------------------------------------------- 1 | /* footer */ 2 | #footer { 3 | position: fixed; 4 | bottom: 30px; 5 | width: 320px; 6 | left: 50%; 7 | transform: translate(-50%); 8 | } 9 | 10 | #options-wrapper { 11 | display: grid; 12 | grid-template-columns: repeat(3, minmax(min-content, 1fr)); 13 | justify-items: center; 14 | height: 45px; 15 | margin-top: 1em; 16 | } 17 | 18 | div#views ul{ 19 | list-style: none; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: space-evenly; 23 | } 24 | 25 | .btn-options input { 26 | position: absolute; 27 | left: -400px; 28 | visibility: hidden; 29 | overflow: hidden; 30 | } 31 | 32 | .btn-options label { 33 | display: inline-block; 34 | position: relative; 35 | padding: 0.2em 0.7em; 36 | height: 100%; 37 | width: 100px; 38 | text-align: center; 39 | background-color: var(--disabled); 40 | color: white; 41 | border-radius: 8px; 42 | cursor: pointer; 43 | box-shadow: inset 0px 0px 5px rgba(0,0,0,0.5); 44 | transition: box-shadow 300ms ease, 45 | background-color 300ms ease; 46 | } 47 | 48 | .btn-options input:checked + label { 49 | background-color: var(--enabled); 50 | } 51 | 52 | .btn-options label:hover, .btn-options input:checked + label:hover { 53 | box-shadow: inset 0px 0px 15px rgba(0,0,0,0.5); 54 | } 55 | 56 | #vote-main .btn-selection input:checked + label:hover { 57 | background-color: var(--enabled); 58 | } 59 | -------------------------------------------------------------------------------- /public/css/poll-results-compact.css: -------------------------------------------------------------------------------- 1 | /* poll results */ 2 | body { 3 | background: transparent !important; 4 | } 5 | 6 | #poll-result-main { 7 | width: calc(100% - 20px); 8 | text-align: left; 9 | max-width: 500px; 10 | } 11 | 12 | #poll-result-main h1 { 13 | color: white; 14 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1); 15 | } 16 | 17 | #poll-result-main h2 { 18 | margin: 0px 0px 2px 0px; 19 | font-size: 18px; 20 | color: white; 21 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1); 22 | } 23 | 24 | #poll-result-main .option { 25 | position: relative; 26 | height: 32px; 27 | background-color: rgba(0, 0, 0, 0.3); 28 | margin-bottom: 4px; 29 | } 30 | 31 | #poll-result-main .bar-title { 32 | position: absolute; 33 | display: inline; 34 | right: 0; 35 | font-size: 20px; 36 | font-family: var(--codingtrain-fontface); 37 | color: white; 38 | margin: 3px 12px 2px 0px; 39 | text-transform: uppercase; 40 | z-index: 2; 41 | } 42 | 43 | .progressBar { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | height: 100%; 48 | background-color: rgb(32, 145, 170); 49 | transition: width 1s ease, background-color 1s ease; 50 | } 51 | 52 | .leading { 53 | background-color: rgb(50, 168, 82); 54 | } 55 | 56 | .progressBar p { 57 | position: absolute; 58 | width: 200px; 59 | top: 0; 60 | font-size: 20px; 61 | margin: 3px 0px 2px 12px; 62 | color: white; 63 | font-family: var(--codingtrain-fontface); 64 | } 65 | 66 | #totalVotes { 67 | margin: 3px 6px 2px 10px; 68 | font-size: 20px; 69 | font-family: var(--codingtrain-fontface); 70 | color: white; 71 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1); 72 | } 73 | 74 | @media only screen and (max-width: 768px) { 75 | #poll-result-main { 76 | text-align: left; 77 | margin: 0 auto; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /public/css/poll-results.css: -------------------------------------------------------------------------------- 1 | /* poll results */ 2 | #poll-result-main { 3 | width: calc(100% - 20px); 4 | text-align: left; 5 | } 6 | 7 | .bar-title { 8 | text-align: left; 9 | font-size: 20px; 10 | } 11 | 12 | .progressBar { 13 | position: relative; 14 | min-width: 21px; 15 | margin: 0 5px 5px 0px; 16 | padding: 2px 15px; 17 | margin-bottom: 10px; 18 | color: var(--progressbar-color); 19 | background: var(--monochrome-gradient); 20 | transition: width 1s; 21 | font-family: var(--codingtrain-fontface); 22 | font-size: larger; 23 | text-shadow: -1px 1px #000; 24 | border-radius: 100px; 25 | border: 1px solid black; 26 | } 27 | 28 | .progressBar p { 29 | width: 220px; 30 | } 31 | 32 | .gradient { 33 | background: var(--gradient); 34 | } 35 | 36 | #totalVotes { 37 | margin: 5px; 38 | font-size: 22px; 39 | } 40 | 41 | @media only screen and (max-width: 768px) { 42 | #poll-result-main { 43 | text-align: left; 44 | margin: 0 auto; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/css/polls.css: -------------------------------------------------------------------------------- 1 | /* polls */ 2 | #pool-main { 3 | width: 100%; 4 | } 5 | 6 | #pool-main p { 7 | padding: 1em 0; 8 | } 9 | 10 | #pool-main ul { 11 | margin-bottom: 20px; 12 | } 13 | 14 | #pool-main #poll-container li { 15 | display: grid; 16 | grid-template-columns: [s] 2fr [m] 1fr [e]; 17 | grid-template-rows: [t] 1fr [b] 1fr [e]; 18 | 19 | margin: 0.5em auto; 20 | padding: 0.5em 1em; 21 | width: calc(100vw - 40px); 22 | background-color: var(--enabled); 23 | border-radius: 8px; 24 | } 25 | 26 | #pool-main .title { 27 | text-align: left; 28 | grid-column: s/m; 29 | grid-row: t/b; 30 | text-decoration: none; 31 | text-transform: uppercase; 32 | color: white; 33 | font-size: 18px; 34 | } 35 | 36 | #pool-main .date { 37 | grid-column: s/m; 38 | grid-row: b/e; 39 | text-align: left; 40 | text-transform: lowercase; 41 | color: rgba(0, 0, 0, 0.5); 42 | font-size: 14px; 43 | } 44 | 45 | #pool-main .vote-btn { 46 | grid-column: m/e; 47 | grid-row: 1/-1; 48 | justify-self: start; 49 | align-self: center; 50 | padding: 0.5em 1.2em; 51 | height: 35px; 52 | text-decoration: none; 53 | text-align: center; 54 | color: black; 55 | background-color: wheat; 56 | border-radius: 8px; 57 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 58 | } 59 | 60 | #pool-main .delete-btn { 61 | grid-row: 1/-1; 62 | grid-column: m/e; 63 | align-self: center; 64 | padding: 0.5em 1.2em; 65 | height: 35px; 66 | justify-self: end; 67 | background-color: var(--disabled); 68 | color: white; 69 | border-radius: 8px; 70 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 71 | cursor: pointer; 72 | } 73 | 74 | #pool-main #create { 75 | position: relative; 76 | text-decoration: none; 77 | color: black; 78 | text-transform: uppercase; 79 | font-size: 20px; 80 | padding: 0.3em 0.7em; 81 | width: 300px; 82 | height: 40px; 83 | background-color: var(--submit-button); 84 | cursor: pointer; 85 | border-radius: 8px; 86 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5); 87 | } 88 | -------------------------------------------------------------------------------- /public/css/qrcode.css: -------------------------------------------------------------------------------- 1 | #placeHolder { 2 | width: 400px; 3 | height: 400px; 4 | } 5 | 6 | body { 7 | background-color: rgba(255, 255, 255, 0); 8 | margin: 20px 20px; 9 | } 10 | 11 | .content { 12 | display: flex; 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | .qr-question { 18 | position: relative; 19 | font-size: 20px; 20 | margin: 0; 21 | } 22 | 23 | .left { 24 | width: 75%; 25 | } 26 | 27 | .right { 28 | width: 20%; 29 | } 30 | 31 | .left>iframe { 32 | width: 100%; 33 | height: 100% 34 | } 35 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "cubanoregular"; 3 | src: url("https://thecodingtrain.com/assets/fonts/cubano-regular-webfont.woff2"); 4 | } 5 | :root { 6 | --codingtrain-fontface: "cubanoregular"; 7 | --gradient: linear-gradient(90deg, red, yellow, green, blue, violet); 8 | --monochrome-gradient: repeating-linear-gradient( 9 | 60deg, 10 | #a42963, 11 | #a42963 10px, 12 | #f063a4 10px, 13 | #f063a4 20px 14 | ); 15 | --no-gradient: black; 16 | --progressbar-color: white; 17 | --disabled: rgb(189, 65, 65); 18 | --enabled: rgb(52, 168, 91); 19 | --submit-button: rgb(98, 159, 238); 20 | } 21 | 22 | /* resets browser default styling */ 23 | *, 24 | ::after, 25 | ::before { 26 | border: 0; 27 | margin: 0; 28 | padding: 0; 29 | box-sizing: border-box; 30 | } 31 | 32 | /* main */ 33 | html, 34 | body { 35 | background-color: transparent; 36 | } 37 | 38 | body { 39 | font-family: "Open Sans"; 40 | margin: 20px; 41 | } 42 | 43 | main { 44 | text-align: center; 45 | width: 100%; 46 | } 47 | 48 | h1, 49 | h2, 50 | button, 51 | .question, 52 | #total-votes, 53 | #create { 54 | font-family: var(--codingtrain-fontface); 55 | } 56 | 57 | h1 { 58 | margin-bottom: 0.3em; 59 | } 60 | 61 | h2 { 62 | margin-bottom: 0.5em; 63 | } 64 | 65 | h1::before { 66 | content: "🚂 "; 67 | } 68 | h1::after { 69 | content: " 🌈"; 70 | } 71 | 72 | .noselect { 73 | -webkit-touch-callout: none; /* iOS Safari */ 74 | -webkit-user-select: none; /* Safari */ 75 | -khtml-user-select: none; /* Konqueror HTML */ 76 | -moz-user-select: none; /* Old versions of Firefox */ 77 | -ms-user-select: none; /* Internet Explorer/Edge */ 78 | user-select: none; /* Non-prefixed version, currently 79 | supported by Chrome, Edge, Opera and Firefox */ 80 | } 81 | 82 | @media only screen and (max-width: 768px) { 83 | body { 84 | margin: 20px 0px 0px 0px; 85 | } 86 | 87 | h1, 88 | h2 { 89 | font-size: 20px; 90 | } 91 | 92 | h1 { 93 | text-align: center; 94 | margin-bottom: 40px; 95 | } 96 | } 97 | 98 | @media only screen and (max-width: 330px) { 99 | h1 { 100 | font-size: 16px; 101 | color: red; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /public/css/vote.css: -------------------------------------------------------------------------------- 1 | /* add styles for the voting page here */ 2 | #vote-main { 3 | margin: 0 20px; 4 | } 5 | 6 | #vote-main #vote { 7 | position: absolute; 8 | width: 70%; 9 | left: 50%; 10 | transform: translate(-50%); 11 | } 12 | 13 | #vote-main h2 { 14 | position: relative; 15 | width: 100vw; 16 | left: 50%; 17 | transform: translate(-50%); 18 | } 19 | 20 | #vote-main #vote li { 21 | list-style: none; 22 | } 23 | 24 | #vote-main #vote li input { 25 | visibility: hidden; 26 | position: absolute; 27 | overflow: hidden; 28 | left: -500px; 29 | } 30 | 31 | #vote-main .btn-selection label { 32 | display: inline-block; 33 | position: relative; 34 | padding: 0.6em 1em; 35 | margin: 0.3em 0; 36 | width: 100%; 37 | background-color: whitesmoke; 38 | text-transform: uppercase; 39 | text-align: center; 40 | color: black; 41 | border-radius: 8px; 42 | cursor: pointer; 43 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); 44 | transform: scale(1); 45 | transition: box-shadow 200ms ease, background-color 400ms ease, 46 | transform 200ms ease, color 200ms ease; 47 | } 48 | 49 | #vote-main .btn-selection input:checked + label { 50 | background-color: var(--enabled); 51 | color: white; 52 | } 53 | 54 | #vote-main .btn-selection label:hover { 55 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5); 56 | transform: scale(1.02); 57 | } 58 | 59 | #vote-main .submit-vote { 60 | border: 0; 61 | box-sizing: border-box; 62 | position: relative; 63 | padding: 0.5em 1em; 64 | margin-top: 1em; 65 | text-transform: uppercase; 66 | width: 33%; 67 | font-size: 18px; 68 | font-family: var(--codingtrain-fontface); 69 | border-radius: 8px; 70 | cursor: pointer; 71 | background-color: var(--submit-button); 72 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); 73 | transform: scale(1); 74 | transition: box-shadow 200ms ease, transform 200ms ease; 75 | } 76 | 77 | #vote-main .submit-vote:hover { 78 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5); 79 | transform: scale(1.02); 80 | } 81 | 82 | @media only screen and (max-width: 768px) { 83 | #vote-main #vote { 84 | position: relative; 85 | width: 100%; 86 | left: 0%; 87 | transform: translate(0); 88 | } 89 | 90 | #vote-main .btn-selection label { 91 | padding: 1.2em 1em; 92 | margin: 0.6em 0; 93 | font-size: 16px; 94 | } 95 | 96 | #vote-main .submit-vote { 97 | padding: 1.2em 1em; 98 | margin-top: 2em; 99 | width: 100%; 100 | } 101 | 102 | #footer { 103 | display: none; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /public/js/classes/poll.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | 4 | class Poll { 5 | constructor() { 6 | const pollId = document.querySelector("[data-id]").dataset.id; 7 | 8 | this.pollId = pollId; 9 | 10 | this.socket = io({ pollId: this.pollId }); 11 | 12 | this.socket.on( 13 | "connect", 14 | function () { 15 | this.socket.emit("listenForPoll", this.pollId); 16 | }.bind(this) 17 | ); 18 | 19 | this.socket.on("updatePoll", this.updatePollResults.bind(this)); 20 | } 21 | 22 | async initPoll() { 23 | await this.fetchPollResults(); 24 | this.updatePollResults(); 25 | } 26 | 27 | updatePollResults(poll) { 28 | if (poll) { 29 | this.pollDetails = poll; 30 | } 31 | const pollDetails = this.getPollVotesStats(); 32 | 33 | const totalText = 34 | pollDetails.totalVotes == 1 35 | ? `${pollDetails.totalVotes} vote` 36 | : `${pollDetails.totalVotes} votes`; 37 | select("#totalVotes").html(totalText); 38 | 39 | // find leading element 40 | const leadingIndex = this.indexOfMax(this.pollDetails.votes); 41 | 42 | // checks if it's a tie 43 | const tie = 44 | this.pollDetails.votes.filter( 45 | (value) => value == this.pollDetails.votes[leadingIndex] 46 | ).length != 1; 47 | 48 | for (let i = 0; i < this.pollDetails.votes.length; i++) { 49 | let count = this.pollDetails.votes[i]; 50 | 51 | // calculate percentage values 52 | const percent = 53 | pollDetails.totalVotes == 0 54 | ? 0 55 | : (count / pollDetails.totalVotes) * 100; 56 | 57 | // Get the progress bar element 58 | // Set the width by percentage 59 | const progressBar = select("#progressBar_" + i); 60 | progressBar.style("width", percent + "%"); 61 | 62 | const voteText = count == 1 ? `${count} vote` : `${count} votes`; 63 | 64 | if (count > 0) { 65 | progressBar.html(`

${voteText} (${Math.round(percent)}%)

`); 66 | } else { 67 | progressBar.html(`

${voteText}

`); 68 | } 69 | 70 | if (!tie && i == leadingIndex) { 71 | progressBar.addClass("leading"); 72 | } else { 73 | progressBar.removeClass("leading"); 74 | } 75 | } 76 | } 77 | 78 | indexOfMax(arr) { 79 | if (arr.length === 0) { 80 | return -1; 81 | } 82 | 83 | var max = arr[0]; 84 | var maxIndex = 0; 85 | 86 | for (var i = 1; i < arr.length; i++) { 87 | if (arr[i] > max) { 88 | maxIndex = i; 89 | max = arr[i]; 90 | } 91 | } 92 | return maxIndex; 93 | } 94 | 95 | async fetchPollResults() { 96 | // Fetch the poll 97 | const response = await fetch(`/api/poll/${this.pollId}`); 98 | 99 | // Extract the json 100 | poll = await response.json(); 101 | 102 | // Throw an error if the poll has an error message 103 | if (poll.message) { 104 | throw new Error(poll.message); 105 | } 106 | 107 | // After all, display the results 108 | this.pollDetails = poll; 109 | } 110 | 111 | getPollVotesStats() { 112 | // Get the number of votes that the most voted option has. 113 | let maxVotes = this.pollDetails.votes.reduce((a, b) => (a > b ? a : b)); 114 | 115 | // Get the total number of votes. 116 | let totalVotes = this.pollDetails.votes.reduce((a, b) => a + b); 117 | 118 | return { maxVotes, totalVotes }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /public/js/qrcode.js: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------- 2 | // 3 | // QR Code Generator for JavaScript 4 | // 5 | // Copyright (c) 2009 Kazuhiko Arase 6 | // 7 | // URL: http://www.d-project.com/ 8 | // 9 | // Licensed under the MIT license: 10 | // http://www.opensource.org/licenses/mit-license.php 11 | // 12 | // The word 'QR Code' is registered trademark of 13 | // DENSO WAVE INCORPORATED 14 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 15 | // 16 | //--------------------------------------------------------------------- 17 | 18 | var qrcode = function() { 19 | 20 | //--------------------------------------------------------------------- 21 | // qrcode 22 | //--------------------------------------------------------------------- 23 | 24 | /** 25 | * qrcode 26 | * @param typeNumber 1 to 40 27 | * @param errorCorrectionLevel 'L','M','Q','H' 28 | */ 29 | var qrcode = function(typeNumber, errorCorrectionLevel) { 30 | 31 | var PAD0 = 0xEC; 32 | var PAD1 = 0x11; 33 | 34 | var _typeNumber = typeNumber; 35 | var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel]; 36 | var _modules = null; 37 | var _moduleCount = 0; 38 | var _dataCache = null; 39 | var _dataList = []; 40 | 41 | var _this = {}; 42 | 43 | var makeImpl = function(test, maskPattern) { 44 | 45 | _moduleCount = _typeNumber * 4 + 17; 46 | _modules = function(moduleCount) { 47 | var modules = new Array(moduleCount); 48 | for (var row = 0; row < moduleCount; row += 1) { 49 | modules[row] = new Array(moduleCount); 50 | for (var col = 0; col < moduleCount; col += 1) { 51 | modules[row][col] = null; 52 | } 53 | } 54 | return modules; 55 | }(_moduleCount); 56 | 57 | setupPositionProbePattern(0, 0); 58 | setupPositionProbePattern(_moduleCount - 7, 0); 59 | setupPositionProbePattern(0, _moduleCount - 7); 60 | setupPositionAdjustPattern(); 61 | setupTimingPattern(); 62 | setupTypeInfo(test, maskPattern); 63 | 64 | if (_typeNumber >= 7) { 65 | setupTypeNumber(test); 66 | } 67 | 68 | if (_dataCache == null) { 69 | _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList); 70 | } 71 | 72 | mapData(_dataCache, maskPattern); 73 | }; 74 | 75 | var setupPositionProbePattern = function(row, col) { 76 | 77 | for (var r = -1; r <= 7; r += 1) { 78 | 79 | if (row + r <= -1 || _moduleCount <= row + r) continue; 80 | 81 | for (var c = -1; c <= 7; c += 1) { 82 | 83 | if (col + c <= -1 || _moduleCount <= col + c) continue; 84 | 85 | if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) 86 | || (0 <= c && c <= 6 && (r == 0 || r == 6) ) 87 | || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { 88 | _modules[row + r][col + c] = true; 89 | } else { 90 | _modules[row + r][col + c] = false; 91 | } 92 | } 93 | } 94 | }; 95 | 96 | var getBestMaskPattern = function() { 97 | 98 | var minLostPoint = 0; 99 | var pattern = 0; 100 | 101 | for (var i = 0; i < 8; i += 1) { 102 | 103 | makeImpl(true, i); 104 | 105 | var lostPoint = QRUtil.getLostPoint(_this); 106 | 107 | if (i == 0 || minLostPoint > lostPoint) { 108 | minLostPoint = lostPoint; 109 | pattern = i; 110 | } 111 | } 112 | 113 | return pattern; 114 | }; 115 | 116 | var setupTimingPattern = function() { 117 | 118 | for (var r = 8; r < _moduleCount - 8; r += 1) { 119 | if (_modules[r][6] != null) { 120 | continue; 121 | } 122 | _modules[r][6] = (r % 2 == 0); 123 | } 124 | 125 | for (var c = 8; c < _moduleCount - 8; c += 1) { 126 | if (_modules[6][c] != null) { 127 | continue; 128 | } 129 | _modules[6][c] = (c % 2 == 0); 130 | } 131 | }; 132 | 133 | var setupPositionAdjustPattern = function() { 134 | 135 | var pos = QRUtil.getPatternPosition(_typeNumber); 136 | 137 | for (var i = 0; i < pos.length; i += 1) { 138 | 139 | for (var j = 0; j < pos.length; j += 1) { 140 | 141 | var row = pos[i]; 142 | var col = pos[j]; 143 | 144 | if (_modules[row][col] != null) { 145 | continue; 146 | } 147 | 148 | for (var r = -2; r <= 2; r += 1) { 149 | 150 | for (var c = -2; c <= 2; c += 1) { 151 | 152 | if (r == -2 || r == 2 || c == -2 || c == 2 153 | || (r == 0 && c == 0) ) { 154 | _modules[row + r][col + c] = true; 155 | } else { 156 | _modules[row + r][col + c] = false; 157 | } 158 | } 159 | } 160 | } 161 | } 162 | }; 163 | 164 | var setupTypeNumber = function(test) { 165 | 166 | var bits = QRUtil.getBCHTypeNumber(_typeNumber); 167 | 168 | for (var i = 0; i < 18; i += 1) { 169 | var mod = (!test && ( (bits >> i) & 1) == 1); 170 | _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod; 171 | } 172 | 173 | for (var i = 0; i < 18; i += 1) { 174 | var mod = (!test && ( (bits >> i) & 1) == 1); 175 | _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod; 176 | } 177 | }; 178 | 179 | var setupTypeInfo = function(test, maskPattern) { 180 | 181 | var data = (_errorCorrectionLevel << 3) | maskPattern; 182 | var bits = QRUtil.getBCHTypeInfo(data); 183 | 184 | // vertical 185 | for (var i = 0; i < 15; i += 1) { 186 | 187 | var mod = (!test && ( (bits >> i) & 1) == 1); 188 | 189 | if (i < 6) { 190 | _modules[i][8] = mod; 191 | } else if (i < 8) { 192 | _modules[i + 1][8] = mod; 193 | } else { 194 | _modules[_moduleCount - 15 + i][8] = mod; 195 | } 196 | } 197 | 198 | // horizontal 199 | for (var i = 0; i < 15; i += 1) { 200 | 201 | var mod = (!test && ( (bits >> i) & 1) == 1); 202 | 203 | if (i < 8) { 204 | _modules[8][_moduleCount - i - 1] = mod; 205 | } else if (i < 9) { 206 | _modules[8][15 - i - 1 + 1] = mod; 207 | } else { 208 | _modules[8][15 - i - 1] = mod; 209 | } 210 | } 211 | 212 | // fixed module 213 | _modules[_moduleCount - 8][8] = (!test); 214 | }; 215 | 216 | var mapData = function(data, maskPattern) { 217 | 218 | var inc = -1; 219 | var row = _moduleCount - 1; 220 | var bitIndex = 7; 221 | var byteIndex = 0; 222 | var maskFunc = QRUtil.getMaskFunction(maskPattern); 223 | 224 | for (var col = _moduleCount - 1; col > 0; col -= 2) { 225 | 226 | if (col == 6) col -= 1; 227 | 228 | while (true) { 229 | 230 | for (var c = 0; c < 2; c += 1) { 231 | 232 | if (_modules[row][col - c] == null) { 233 | 234 | var dark = false; 235 | 236 | if (byteIndex < data.length) { 237 | dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); 238 | } 239 | 240 | var mask = maskFunc(row, col - c); 241 | 242 | if (mask) { 243 | dark = !dark; 244 | } 245 | 246 | _modules[row][col - c] = dark; 247 | bitIndex -= 1; 248 | 249 | if (bitIndex == -1) { 250 | byteIndex += 1; 251 | bitIndex = 7; 252 | } 253 | } 254 | } 255 | 256 | row += inc; 257 | 258 | if (row < 0 || _moduleCount <= row) { 259 | row -= inc; 260 | inc = -inc; 261 | break; 262 | } 263 | } 264 | } 265 | }; 266 | 267 | var createBytes = function(buffer, rsBlocks) { 268 | 269 | var offset = 0; 270 | 271 | var maxDcCount = 0; 272 | var maxEcCount = 0; 273 | 274 | var dcdata = new Array(rsBlocks.length); 275 | var ecdata = new Array(rsBlocks.length); 276 | 277 | for (var r = 0; r < rsBlocks.length; r += 1) { 278 | 279 | var dcCount = rsBlocks[r].dataCount; 280 | var ecCount = rsBlocks[r].totalCount - dcCount; 281 | 282 | maxDcCount = Math.max(maxDcCount, dcCount); 283 | maxEcCount = Math.max(maxEcCount, ecCount); 284 | 285 | dcdata[r] = new Array(dcCount); 286 | 287 | for (var i = 0; i < dcdata[r].length; i += 1) { 288 | dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset]; 289 | } 290 | offset += dcCount; 291 | 292 | var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); 293 | var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1); 294 | 295 | var modPoly = rawPoly.mod(rsPoly); 296 | ecdata[r] = new Array(rsPoly.getLength() - 1); 297 | for (var i = 0; i < ecdata[r].length; i += 1) { 298 | var modIndex = i + modPoly.getLength() - ecdata[r].length; 299 | ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0; 300 | } 301 | } 302 | 303 | var totalCodeCount = 0; 304 | for (var i = 0; i < rsBlocks.length; i += 1) { 305 | totalCodeCount += rsBlocks[i].totalCount; 306 | } 307 | 308 | var data = new Array(totalCodeCount); 309 | var index = 0; 310 | 311 | for (var i = 0; i < maxDcCount; i += 1) { 312 | for (var r = 0; r < rsBlocks.length; r += 1) { 313 | if (i < dcdata[r].length) { 314 | data[index] = dcdata[r][i]; 315 | index += 1; 316 | } 317 | } 318 | } 319 | 320 | for (var i = 0; i < maxEcCount; i += 1) { 321 | for (var r = 0; r < rsBlocks.length; r += 1) { 322 | if (i < ecdata[r].length) { 323 | data[index] = ecdata[r][i]; 324 | index += 1; 325 | } 326 | } 327 | } 328 | 329 | return data; 330 | }; 331 | 332 | var createData = function(typeNumber, errorCorrectionLevel, dataList) { 333 | 334 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel); 335 | 336 | var buffer = qrBitBuffer(); 337 | 338 | for (var i = 0; i < dataList.length; i += 1) { 339 | var data = dataList[i]; 340 | buffer.put(data.getMode(), 4); 341 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); 342 | data.write(buffer); 343 | } 344 | 345 | // calc num max data. 346 | var totalDataCount = 0; 347 | for (var i = 0; i < rsBlocks.length; i += 1) { 348 | totalDataCount += rsBlocks[i].dataCount; 349 | } 350 | 351 | if (buffer.getLengthInBits() > totalDataCount * 8) { 352 | throw 'code length overflow. (' 353 | + buffer.getLengthInBits() 354 | + '>' 355 | + totalDataCount * 8 356 | + ')'; 357 | } 358 | 359 | // end code 360 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { 361 | buffer.put(0, 4); 362 | } 363 | 364 | // padding 365 | while (buffer.getLengthInBits() % 8 != 0) { 366 | buffer.putBit(false); 367 | } 368 | 369 | // padding 370 | while (true) { 371 | 372 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 373 | break; 374 | } 375 | buffer.put(PAD0, 8); 376 | 377 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 378 | break; 379 | } 380 | buffer.put(PAD1, 8); 381 | } 382 | 383 | return createBytes(buffer, rsBlocks); 384 | }; 385 | 386 | _this.addData = function(data, mode) { 387 | 388 | mode = mode || 'Byte'; 389 | 390 | var newData = null; 391 | 392 | switch(mode) { 393 | case 'Numeric' : 394 | newData = qrNumber(data); 395 | break; 396 | case 'Alphanumeric' : 397 | newData = qrAlphaNum(data); 398 | break; 399 | case 'Byte' : 400 | newData = qr8BitByte(data); 401 | break; 402 | case 'Kanji' : 403 | newData = qrKanji(data); 404 | break; 405 | default : 406 | throw 'mode:' + mode; 407 | } 408 | 409 | _dataList.push(newData); 410 | _dataCache = null; 411 | }; 412 | 413 | _this.isDark = function(row, col) { 414 | if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) { 415 | throw row + ',' + col; 416 | } 417 | return _modules[row][col]; 418 | }; 419 | 420 | _this.getModuleCount = function() { 421 | return _moduleCount; 422 | }; 423 | 424 | _this.make = function() { 425 | if (_typeNumber < 1) { 426 | var typeNumber = 1; 427 | 428 | for (; typeNumber < 40; typeNumber++) { 429 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel); 430 | var buffer = qrBitBuffer(); 431 | 432 | for (var i = 0; i < _dataList.length; i++) { 433 | var data = _dataList[i]; 434 | buffer.put(data.getMode(), 4); 435 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) ); 436 | data.write(buffer); 437 | } 438 | 439 | var totalDataCount = 0; 440 | for (var i = 0; i < rsBlocks.length; i++) { 441 | totalDataCount += rsBlocks[i].dataCount; 442 | } 443 | 444 | if (buffer.getLengthInBits() <= totalDataCount * 8) { 445 | break; 446 | } 447 | } 448 | 449 | _typeNumber = typeNumber; 450 | } 451 | 452 | makeImpl(false, getBestMaskPattern() ); 453 | }; 454 | 455 | _this.createTableTag = function(cellSize, margin) { 456 | 457 | cellSize = cellSize || 2; 458 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin; 459 | 460 | var qrHtml = ''; 461 | 462 | qrHtml += ''; 467 | qrHtml += ''; 468 | 469 | for (var r = 0; r < _this.getModuleCount(); r += 1) { 470 | 471 | qrHtml += ''; 472 | 473 | for (var c = 0; c < _this.getModuleCount(); c += 1) { 474 | qrHtml += ''; 487 | } 488 | 489 | qrHtml += ''; 490 | qrHtml += '
'; 484 | } 485 | 486 | qrHtml += '
'; 491 | 492 | return qrHtml; 493 | }; 494 | 495 | _this.createSvgTag = function(cellSize, margin, alt, title) { 496 | 497 | var opts = {}; 498 | if (typeof arguments[0] == 'object') { 499 | // Called by options. 500 | opts = arguments[0]; 501 | // overwrite cellSize and margin. 502 | cellSize = opts.cellSize; 503 | margin = opts.margin; 504 | alt = opts.alt; 505 | title = opts.title; 506 | } 507 | 508 | cellSize = cellSize || 2; 509 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin; 510 | 511 | // Compose alt property surrogate 512 | alt = (typeof alt === 'string') ? {text: alt} : alt || {}; 513 | alt.text = alt.text || null; 514 | alt.id = (alt.text) ? alt.id || 'qrcode-description' : null; 515 | 516 | // Compose title property surrogate 517 | title = (typeof title === 'string') ? {text: title} : title || {}; 518 | title.text = title.text || null; 519 | title.id = (title.text) ? title.id || 'qrcode-title' : null; 520 | 521 | var size = _this.getModuleCount() * cellSize + margin * 2; 522 | var c, mc, r, mr, qrSvg='', rect; 523 | 524 | rect = 'l' + cellSize + ',0 0,' + cellSize + 525 | ' -' + cellSize + ',0 0,-' + cellSize + 'z '; 526 | 527 | qrSvg += '' + 535 | escapeXml(title.text) + '' : ''; 536 | qrSvg += (alt.text) ? '' + 537 | escapeXml(alt.text) + '' : ''; 538 | qrSvg += ''; 539 | qrSvg += ''; 552 | qrSvg += ''; 553 | 554 | return qrSvg; 555 | }; 556 | 557 | _this.createDataURL = function(cellSize, margin) { 558 | 559 | cellSize = cellSize || 2; 560 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin; 561 | 562 | var size = _this.getModuleCount() * cellSize + margin * 2; 563 | var min = margin; 564 | var max = size - margin; 565 | 566 | return createDataURL(size, size, function(x, y) { 567 | if (min <= x && x < max && min <= y && y < max) { 568 | var c = Math.floor( (x - min) / cellSize); 569 | var r = Math.floor( (y - min) / cellSize); 570 | return _this.isDark(r, c)? 0 : 1; 571 | } else { 572 | return 1; 573 | } 574 | } ); 575 | }; 576 | 577 | _this.createImgTag = function(cellSize, margin, alt) { 578 | 579 | cellSize = cellSize || 2; 580 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin; 581 | 582 | var size = _this.getModuleCount() * cellSize + margin * 2; 583 | 584 | var img = ''; 585 | img += '': escaped += '>'; break; 612 | case '&': escaped += '&'; break; 613 | case '"': escaped += '"'; break; 614 | default : escaped += c; break; 615 | } 616 | } 617 | return escaped; 618 | }; 619 | 620 | var _createHalfASCII = function(margin) { 621 | var cellSize = 1; 622 | margin = (typeof margin == 'undefined')? cellSize * 2 : margin; 623 | 624 | var size = _this.getModuleCount() * cellSize + margin * 2; 625 | var min = margin; 626 | var max = size - margin; 627 | 628 | var y, x, r1, r2, p; 629 | 630 | var blocks = { 631 | '██': '█', 632 | '█ ': '▀', 633 | ' █': '▄', 634 | ' ': ' ' 635 | }; 636 | 637 | var blocksLastLineNoMargin = { 638 | '██': '▀', 639 | '█ ': '▀', 640 | ' █': ' ', 641 | ' ': ' ' 642 | }; 643 | 644 | var ascii = ''; 645 | for (y = 0; y < size; y += 2) { 646 | r1 = Math.floor((y - min) / cellSize); 647 | r2 = Math.floor((y + 1 - min) / cellSize); 648 | for (x = 0; x < size; x += 1) { 649 | p = '█'; 650 | 651 | if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) { 652 | p = ' '; 653 | } 654 | 655 | if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) { 656 | p += ' '; 657 | } 658 | else { 659 | p += '█'; 660 | } 661 | 662 | // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. 663 | ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p]; 664 | } 665 | 666 | ascii += '\n'; 667 | } 668 | 669 | if (size % 2 && margin > 0) { 670 | return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀'); 671 | } 672 | 673 | return ascii.substring(0, ascii.length-1); 674 | }; 675 | 676 | _this.createASCII = function(cellSize, margin) { 677 | cellSize = cellSize || 1; 678 | 679 | if (cellSize < 2) { 680 | return _createHalfASCII(margin); 681 | } 682 | 683 | cellSize -= 1; 684 | margin = (typeof margin == 'undefined')? cellSize * 2 : margin; 685 | 686 | var size = _this.getModuleCount() * cellSize + margin * 2; 687 | var min = margin; 688 | var max = size - margin; 689 | 690 | var y, x, r, p; 691 | 692 | var white = Array(cellSize+1).join('██'); 693 | var black = Array(cellSize+1).join(' '); 694 | 695 | var ascii = ''; 696 | var line = ''; 697 | for (y = 0; y < size; y += 1) { 698 | r = Math.floor( (y - min) / cellSize); 699 | line = ''; 700 | for (x = 0; x < size; x += 1) { 701 | p = 1; 702 | 703 | if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) { 704 | p = 0; 705 | } 706 | 707 | // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square. 708 | line += p ? white : black; 709 | } 710 | 711 | for (r = 0; r < cellSize; r += 1) { 712 | ascii += line + '\n'; 713 | } 714 | } 715 | 716 | return ascii.substring(0, ascii.length-1); 717 | }; 718 | 719 | _this.renderTo2dContext = function(context, cellSize) { 720 | cellSize = cellSize || 2; 721 | var length = _this.getModuleCount(); 722 | for (var row = 0; row < length; row++) { 723 | for (var col = 0; col < length; col++) { 724 | context.fillStyle = _this.isDark(row, col) ? 'black' : 'white'; 725 | context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize); 726 | } 727 | } 728 | } 729 | 730 | return _this; 731 | }; 732 | 733 | //--------------------------------------------------------------------- 734 | // qrcode.stringToBytes 735 | //--------------------------------------------------------------------- 736 | 737 | qrcode.stringToBytesFuncs = { 738 | 'default' : function(s) { 739 | var bytes = []; 740 | for (var i = 0; i < s.length; i += 1) { 741 | var c = s.charCodeAt(i); 742 | bytes.push(c & 0xff); 743 | } 744 | return bytes; 745 | } 746 | }; 747 | 748 | qrcode.stringToBytes = qrcode.stringToBytesFuncs['default']; 749 | 750 | //--------------------------------------------------------------------- 751 | // qrcode.createStringToBytes 752 | //--------------------------------------------------------------------- 753 | 754 | /** 755 | * @param unicodeData base64 string of byte array. 756 | * [16bit Unicode],[16bit Bytes], ... 757 | * @param numChars 758 | */ 759 | qrcode.createStringToBytes = function(unicodeData, numChars) { 760 | 761 | // create conversion map. 762 | 763 | var unicodeMap = function() { 764 | 765 | var bin = base64DecodeInputStream(unicodeData); 766 | var read = function() { 767 | var b = bin.read(); 768 | if (b == -1) throw 'eof'; 769 | return b; 770 | }; 771 | 772 | var count = 0; 773 | var unicodeMap = {}; 774 | while (true) { 775 | var b0 = bin.read(); 776 | if (b0 == -1) break; 777 | var b1 = read(); 778 | var b2 = read(); 779 | var b3 = read(); 780 | var k = String.fromCharCode( (b0 << 8) | b1); 781 | var v = (b2 << 8) | b3; 782 | unicodeMap[k] = v; 783 | count += 1; 784 | } 785 | if (count != numChars) { 786 | throw count + ' != ' + numChars; 787 | } 788 | 789 | return unicodeMap; 790 | }(); 791 | 792 | var unknownChar = '?'.charCodeAt(0); 793 | 794 | return function(s) { 795 | var bytes = []; 796 | for (var i = 0; i < s.length; i += 1) { 797 | var c = s.charCodeAt(i); 798 | if (c < 128) { 799 | bytes.push(c); 800 | } else { 801 | var b = unicodeMap[s.charAt(i)]; 802 | if (typeof b == 'number') { 803 | if ( (b & 0xff) == b) { 804 | // 1byte 805 | bytes.push(b); 806 | } else { 807 | // 2bytes 808 | bytes.push(b >>> 8); 809 | bytes.push(b & 0xff); 810 | } 811 | } else { 812 | bytes.push(unknownChar); 813 | } 814 | } 815 | } 816 | return bytes; 817 | }; 818 | }; 819 | 820 | //--------------------------------------------------------------------- 821 | // QRMode 822 | //--------------------------------------------------------------------- 823 | 824 | var QRMode = { 825 | MODE_NUMBER : 1 << 0, 826 | MODE_ALPHA_NUM : 1 << 1, 827 | MODE_8BIT_BYTE : 1 << 2, 828 | MODE_KANJI : 1 << 3 829 | }; 830 | 831 | //--------------------------------------------------------------------- 832 | // QRErrorCorrectionLevel 833 | //--------------------------------------------------------------------- 834 | 835 | var QRErrorCorrectionLevel = { 836 | L : 1, 837 | M : 0, 838 | Q : 3, 839 | H : 2 840 | }; 841 | 842 | //--------------------------------------------------------------------- 843 | // QRMaskPattern 844 | //--------------------------------------------------------------------- 845 | 846 | var QRMaskPattern = { 847 | PATTERN000 : 0, 848 | PATTERN001 : 1, 849 | PATTERN010 : 2, 850 | PATTERN011 : 3, 851 | PATTERN100 : 4, 852 | PATTERN101 : 5, 853 | PATTERN110 : 6, 854 | PATTERN111 : 7 855 | }; 856 | 857 | //--------------------------------------------------------------------- 858 | // QRUtil 859 | //--------------------------------------------------------------------- 860 | 861 | var QRUtil = function() { 862 | 863 | var PATTERN_POSITION_TABLE = [ 864 | [], 865 | [6, 18], 866 | [6, 22], 867 | [6, 26], 868 | [6, 30], 869 | [6, 34], 870 | [6, 22, 38], 871 | [6, 24, 42], 872 | [6, 26, 46], 873 | [6, 28, 50], 874 | [6, 30, 54], 875 | [6, 32, 58], 876 | [6, 34, 62], 877 | [6, 26, 46, 66], 878 | [6, 26, 48, 70], 879 | [6, 26, 50, 74], 880 | [6, 30, 54, 78], 881 | [6, 30, 56, 82], 882 | [6, 30, 58, 86], 883 | [6, 34, 62, 90], 884 | [6, 28, 50, 72, 94], 885 | [6, 26, 50, 74, 98], 886 | [6, 30, 54, 78, 102], 887 | [6, 28, 54, 80, 106], 888 | [6, 32, 58, 84, 110], 889 | [6, 30, 58, 86, 114], 890 | [6, 34, 62, 90, 118], 891 | [6, 26, 50, 74, 98, 122], 892 | [6, 30, 54, 78, 102, 126], 893 | [6, 26, 52, 78, 104, 130], 894 | [6, 30, 56, 82, 108, 134], 895 | [6, 34, 60, 86, 112, 138], 896 | [6, 30, 58, 86, 114, 142], 897 | [6, 34, 62, 90, 118, 146], 898 | [6, 30, 54, 78, 102, 126, 150], 899 | [6, 24, 50, 76, 102, 128, 154], 900 | [6, 28, 54, 80, 106, 132, 158], 901 | [6, 32, 58, 84, 110, 136, 162], 902 | [6, 26, 54, 82, 110, 138, 166], 903 | [6, 30, 58, 86, 114, 142, 170] 904 | ]; 905 | var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); 906 | var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); 907 | var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); 908 | 909 | var _this = {}; 910 | 911 | var getBCHDigit = function(data) { 912 | var digit = 0; 913 | while (data != 0) { 914 | digit += 1; 915 | data >>>= 1; 916 | } 917 | return digit; 918 | }; 919 | 920 | _this.getBCHTypeInfo = function(data) { 921 | var d = data << 10; 922 | while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { 923 | d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) ); 924 | } 925 | return ( (data << 10) | d) ^ G15_MASK; 926 | }; 927 | 928 | _this.getBCHTypeNumber = function(data) { 929 | var d = data << 12; 930 | while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { 931 | d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) ); 932 | } 933 | return (data << 12) | d; 934 | }; 935 | 936 | _this.getPatternPosition = function(typeNumber) { 937 | return PATTERN_POSITION_TABLE[typeNumber - 1]; 938 | }; 939 | 940 | _this.getMaskFunction = function(maskPattern) { 941 | 942 | switch (maskPattern) { 943 | 944 | case QRMaskPattern.PATTERN000 : 945 | return function(i, j) { return (i + j) % 2 == 0; }; 946 | case QRMaskPattern.PATTERN001 : 947 | return function(i, j) { return i % 2 == 0; }; 948 | case QRMaskPattern.PATTERN010 : 949 | return function(i, j) { return j % 3 == 0; }; 950 | case QRMaskPattern.PATTERN011 : 951 | return function(i, j) { return (i + j) % 3 == 0; }; 952 | case QRMaskPattern.PATTERN100 : 953 | return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; }; 954 | case QRMaskPattern.PATTERN101 : 955 | return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; }; 956 | case QRMaskPattern.PATTERN110 : 957 | return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; }; 958 | case QRMaskPattern.PATTERN111 : 959 | return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; }; 960 | 961 | default : 962 | throw 'bad maskPattern:' + maskPattern; 963 | } 964 | }; 965 | 966 | _this.getErrorCorrectPolynomial = function(errorCorrectLength) { 967 | var a = qrPolynomial([1], 0); 968 | for (var i = 0; i < errorCorrectLength; i += 1) { 969 | a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) ); 970 | } 971 | return a; 972 | }; 973 | 974 | _this.getLengthInBits = function(mode, type) { 975 | 976 | if (1 <= type && type < 10) { 977 | 978 | // 1 - 9 979 | 980 | switch(mode) { 981 | case QRMode.MODE_NUMBER : return 10; 982 | case QRMode.MODE_ALPHA_NUM : return 9; 983 | case QRMode.MODE_8BIT_BYTE : return 8; 984 | case QRMode.MODE_KANJI : return 8; 985 | default : 986 | throw 'mode:' + mode; 987 | } 988 | 989 | } else if (type < 27) { 990 | 991 | // 10 - 26 992 | 993 | switch(mode) { 994 | case QRMode.MODE_NUMBER : return 12; 995 | case QRMode.MODE_ALPHA_NUM : return 11; 996 | case QRMode.MODE_8BIT_BYTE : return 16; 997 | case QRMode.MODE_KANJI : return 10; 998 | default : 999 | throw 'mode:' + mode; 1000 | } 1001 | 1002 | } else if (type < 41) { 1003 | 1004 | // 27 - 40 1005 | 1006 | switch(mode) { 1007 | case QRMode.MODE_NUMBER : return 14; 1008 | case QRMode.MODE_ALPHA_NUM : return 13; 1009 | case QRMode.MODE_8BIT_BYTE : return 16; 1010 | case QRMode.MODE_KANJI : return 12; 1011 | default : 1012 | throw 'mode:' + mode; 1013 | } 1014 | 1015 | } else { 1016 | throw 'type:' + type; 1017 | } 1018 | }; 1019 | 1020 | _this.getLostPoint = function(qrcode) { 1021 | 1022 | var moduleCount = qrcode.getModuleCount(); 1023 | 1024 | var lostPoint = 0; 1025 | 1026 | // LEVEL1 1027 | 1028 | for (var row = 0; row < moduleCount; row += 1) { 1029 | for (var col = 0; col < moduleCount; col += 1) { 1030 | 1031 | var sameCount = 0; 1032 | var dark = qrcode.isDark(row, col); 1033 | 1034 | for (var r = -1; r <= 1; r += 1) { 1035 | 1036 | if (row + r < 0 || moduleCount <= row + r) { 1037 | continue; 1038 | } 1039 | 1040 | for (var c = -1; c <= 1; c += 1) { 1041 | 1042 | if (col + c < 0 || moduleCount <= col + c) { 1043 | continue; 1044 | } 1045 | 1046 | if (r == 0 && c == 0) { 1047 | continue; 1048 | } 1049 | 1050 | if (dark == qrcode.isDark(row + r, col + c) ) { 1051 | sameCount += 1; 1052 | } 1053 | } 1054 | } 1055 | 1056 | if (sameCount > 5) { 1057 | lostPoint += (3 + sameCount - 5); 1058 | } 1059 | } 1060 | }; 1061 | 1062 | // LEVEL2 1063 | 1064 | for (var row = 0; row < moduleCount - 1; row += 1) { 1065 | for (var col = 0; col < moduleCount - 1; col += 1) { 1066 | var count = 0; 1067 | if (qrcode.isDark(row, col) ) count += 1; 1068 | if (qrcode.isDark(row + 1, col) ) count += 1; 1069 | if (qrcode.isDark(row, col + 1) ) count += 1; 1070 | if (qrcode.isDark(row + 1, col + 1) ) count += 1; 1071 | if (count == 0 || count == 4) { 1072 | lostPoint += 3; 1073 | } 1074 | } 1075 | } 1076 | 1077 | // LEVEL3 1078 | 1079 | for (var row = 0; row < moduleCount; row += 1) { 1080 | for (var col = 0; col < moduleCount - 6; col += 1) { 1081 | if (qrcode.isDark(row, col) 1082 | && !qrcode.isDark(row, col + 1) 1083 | && qrcode.isDark(row, col + 2) 1084 | && qrcode.isDark(row, col + 3) 1085 | && qrcode.isDark(row, col + 4) 1086 | && !qrcode.isDark(row, col + 5) 1087 | && qrcode.isDark(row, col + 6) ) { 1088 | lostPoint += 40; 1089 | } 1090 | } 1091 | } 1092 | 1093 | for (var col = 0; col < moduleCount; col += 1) { 1094 | for (var row = 0; row < moduleCount - 6; row += 1) { 1095 | if (qrcode.isDark(row, col) 1096 | && !qrcode.isDark(row + 1, col) 1097 | && qrcode.isDark(row + 2, col) 1098 | && qrcode.isDark(row + 3, col) 1099 | && qrcode.isDark(row + 4, col) 1100 | && !qrcode.isDark(row + 5, col) 1101 | && qrcode.isDark(row + 6, col) ) { 1102 | lostPoint += 40; 1103 | } 1104 | } 1105 | } 1106 | 1107 | // LEVEL4 1108 | 1109 | var darkCount = 0; 1110 | 1111 | for (var col = 0; col < moduleCount; col += 1) { 1112 | for (var row = 0; row < moduleCount; row += 1) { 1113 | if (qrcode.isDark(row, col) ) { 1114 | darkCount += 1; 1115 | } 1116 | } 1117 | } 1118 | 1119 | var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; 1120 | lostPoint += ratio * 10; 1121 | 1122 | return lostPoint; 1123 | }; 1124 | 1125 | return _this; 1126 | }(); 1127 | 1128 | //--------------------------------------------------------------------- 1129 | // QRMath 1130 | //--------------------------------------------------------------------- 1131 | 1132 | var QRMath = function() { 1133 | 1134 | var EXP_TABLE = new Array(256); 1135 | var LOG_TABLE = new Array(256); 1136 | 1137 | // initialize tables 1138 | for (var i = 0; i < 8; i += 1) { 1139 | EXP_TABLE[i] = 1 << i; 1140 | } 1141 | for (var i = 8; i < 256; i += 1) { 1142 | EXP_TABLE[i] = EXP_TABLE[i - 4] 1143 | ^ EXP_TABLE[i - 5] 1144 | ^ EXP_TABLE[i - 6] 1145 | ^ EXP_TABLE[i - 8]; 1146 | } 1147 | for (var i = 0; i < 255; i += 1) { 1148 | LOG_TABLE[EXP_TABLE[i] ] = i; 1149 | } 1150 | 1151 | var _this = {}; 1152 | 1153 | _this.glog = function(n) { 1154 | 1155 | if (n < 1) { 1156 | throw 'glog(' + n + ')'; 1157 | } 1158 | 1159 | return LOG_TABLE[n]; 1160 | }; 1161 | 1162 | _this.gexp = function(n) { 1163 | 1164 | while (n < 0) { 1165 | n += 255; 1166 | } 1167 | 1168 | while (n >= 256) { 1169 | n -= 255; 1170 | } 1171 | 1172 | return EXP_TABLE[n]; 1173 | }; 1174 | 1175 | return _this; 1176 | }(); 1177 | 1178 | //--------------------------------------------------------------------- 1179 | // qrPolynomial 1180 | //--------------------------------------------------------------------- 1181 | 1182 | function qrPolynomial(num, shift) { 1183 | 1184 | if (typeof num.length == 'undefined') { 1185 | throw num.length + '/' + shift; 1186 | } 1187 | 1188 | var _num = function() { 1189 | var offset = 0; 1190 | while (offset < num.length && num[offset] == 0) { 1191 | offset += 1; 1192 | } 1193 | var _num = new Array(num.length - offset + shift); 1194 | for (var i = 0; i < num.length - offset; i += 1) { 1195 | _num[i] = num[i + offset]; 1196 | } 1197 | return _num; 1198 | }(); 1199 | 1200 | var _this = {}; 1201 | 1202 | _this.getAt = function(index) { 1203 | return _num[index]; 1204 | }; 1205 | 1206 | _this.getLength = function() { 1207 | return _num.length; 1208 | }; 1209 | 1210 | _this.multiply = function(e) { 1211 | 1212 | var num = new Array(_this.getLength() + e.getLength() - 1); 1213 | 1214 | for (var i = 0; i < _this.getLength(); i += 1) { 1215 | for (var j = 0; j < e.getLength(); j += 1) { 1216 | num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) ); 1217 | } 1218 | } 1219 | 1220 | return qrPolynomial(num, 0); 1221 | }; 1222 | 1223 | _this.mod = function(e) { 1224 | 1225 | if (_this.getLength() - e.getLength() < 0) { 1226 | return _this; 1227 | } 1228 | 1229 | var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) ); 1230 | 1231 | var num = new Array(_this.getLength() ); 1232 | for (var i = 0; i < _this.getLength(); i += 1) { 1233 | num[i] = _this.getAt(i); 1234 | } 1235 | 1236 | for (var i = 0; i < e.getLength(); i += 1) { 1237 | num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio); 1238 | } 1239 | 1240 | // recursive call 1241 | return qrPolynomial(num, 0).mod(e); 1242 | }; 1243 | 1244 | return _this; 1245 | }; 1246 | 1247 | //--------------------------------------------------------------------- 1248 | // QRRSBlock 1249 | //--------------------------------------------------------------------- 1250 | 1251 | var QRRSBlock = function() { 1252 | 1253 | var RS_BLOCK_TABLE = [ 1254 | 1255 | // L 1256 | // M 1257 | // Q 1258 | // H 1259 | 1260 | // 1 1261 | [1, 26, 19], 1262 | [1, 26, 16], 1263 | [1, 26, 13], 1264 | [1, 26, 9], 1265 | 1266 | // 2 1267 | [1, 44, 34], 1268 | [1, 44, 28], 1269 | [1, 44, 22], 1270 | [1, 44, 16], 1271 | 1272 | // 3 1273 | [1, 70, 55], 1274 | [1, 70, 44], 1275 | [2, 35, 17], 1276 | [2, 35, 13], 1277 | 1278 | // 4 1279 | [1, 100, 80], 1280 | [2, 50, 32], 1281 | [2, 50, 24], 1282 | [4, 25, 9], 1283 | 1284 | // 5 1285 | [1, 134, 108], 1286 | [2, 67, 43], 1287 | [2, 33, 15, 2, 34, 16], 1288 | [2, 33, 11, 2, 34, 12], 1289 | 1290 | // 6 1291 | [2, 86, 68], 1292 | [4, 43, 27], 1293 | [4, 43, 19], 1294 | [4, 43, 15], 1295 | 1296 | // 7 1297 | [2, 98, 78], 1298 | [4, 49, 31], 1299 | [2, 32, 14, 4, 33, 15], 1300 | [4, 39, 13, 1, 40, 14], 1301 | 1302 | // 8 1303 | [2, 121, 97], 1304 | [2, 60, 38, 2, 61, 39], 1305 | [4, 40, 18, 2, 41, 19], 1306 | [4, 40, 14, 2, 41, 15], 1307 | 1308 | // 9 1309 | [2, 146, 116], 1310 | [3, 58, 36, 2, 59, 37], 1311 | [4, 36, 16, 4, 37, 17], 1312 | [4, 36, 12, 4, 37, 13], 1313 | 1314 | // 10 1315 | [2, 86, 68, 2, 87, 69], 1316 | [4, 69, 43, 1, 70, 44], 1317 | [6, 43, 19, 2, 44, 20], 1318 | [6, 43, 15, 2, 44, 16], 1319 | 1320 | // 11 1321 | [4, 101, 81], 1322 | [1, 80, 50, 4, 81, 51], 1323 | [4, 50, 22, 4, 51, 23], 1324 | [3, 36, 12, 8, 37, 13], 1325 | 1326 | // 12 1327 | [2, 116, 92, 2, 117, 93], 1328 | [6, 58, 36, 2, 59, 37], 1329 | [4, 46, 20, 6, 47, 21], 1330 | [7, 42, 14, 4, 43, 15], 1331 | 1332 | // 13 1333 | [4, 133, 107], 1334 | [8, 59, 37, 1, 60, 38], 1335 | [8, 44, 20, 4, 45, 21], 1336 | [12, 33, 11, 4, 34, 12], 1337 | 1338 | // 14 1339 | [3, 145, 115, 1, 146, 116], 1340 | [4, 64, 40, 5, 65, 41], 1341 | [11, 36, 16, 5, 37, 17], 1342 | [11, 36, 12, 5, 37, 13], 1343 | 1344 | // 15 1345 | [5, 109, 87, 1, 110, 88], 1346 | [5, 65, 41, 5, 66, 42], 1347 | [5, 54, 24, 7, 55, 25], 1348 | [11, 36, 12, 7, 37, 13], 1349 | 1350 | // 16 1351 | [5, 122, 98, 1, 123, 99], 1352 | [7, 73, 45, 3, 74, 46], 1353 | [15, 43, 19, 2, 44, 20], 1354 | [3, 45, 15, 13, 46, 16], 1355 | 1356 | // 17 1357 | [1, 135, 107, 5, 136, 108], 1358 | [10, 74, 46, 1, 75, 47], 1359 | [1, 50, 22, 15, 51, 23], 1360 | [2, 42, 14, 17, 43, 15], 1361 | 1362 | // 18 1363 | [5, 150, 120, 1, 151, 121], 1364 | [9, 69, 43, 4, 70, 44], 1365 | [17, 50, 22, 1, 51, 23], 1366 | [2, 42, 14, 19, 43, 15], 1367 | 1368 | // 19 1369 | [3, 141, 113, 4, 142, 114], 1370 | [3, 70, 44, 11, 71, 45], 1371 | [17, 47, 21, 4, 48, 22], 1372 | [9, 39, 13, 16, 40, 14], 1373 | 1374 | // 20 1375 | [3, 135, 107, 5, 136, 108], 1376 | [3, 67, 41, 13, 68, 42], 1377 | [15, 54, 24, 5, 55, 25], 1378 | [15, 43, 15, 10, 44, 16], 1379 | 1380 | // 21 1381 | [4, 144, 116, 4, 145, 117], 1382 | [17, 68, 42], 1383 | [17, 50, 22, 6, 51, 23], 1384 | [19, 46, 16, 6, 47, 17], 1385 | 1386 | // 22 1387 | [2, 139, 111, 7, 140, 112], 1388 | [17, 74, 46], 1389 | [7, 54, 24, 16, 55, 25], 1390 | [34, 37, 13], 1391 | 1392 | // 23 1393 | [4, 151, 121, 5, 152, 122], 1394 | [4, 75, 47, 14, 76, 48], 1395 | [11, 54, 24, 14, 55, 25], 1396 | [16, 45, 15, 14, 46, 16], 1397 | 1398 | // 24 1399 | [6, 147, 117, 4, 148, 118], 1400 | [6, 73, 45, 14, 74, 46], 1401 | [11, 54, 24, 16, 55, 25], 1402 | [30, 46, 16, 2, 47, 17], 1403 | 1404 | // 25 1405 | [8, 132, 106, 4, 133, 107], 1406 | [8, 75, 47, 13, 76, 48], 1407 | [7, 54, 24, 22, 55, 25], 1408 | [22, 45, 15, 13, 46, 16], 1409 | 1410 | // 26 1411 | [10, 142, 114, 2, 143, 115], 1412 | [19, 74, 46, 4, 75, 47], 1413 | [28, 50, 22, 6, 51, 23], 1414 | [33, 46, 16, 4, 47, 17], 1415 | 1416 | // 27 1417 | [8, 152, 122, 4, 153, 123], 1418 | [22, 73, 45, 3, 74, 46], 1419 | [8, 53, 23, 26, 54, 24], 1420 | [12, 45, 15, 28, 46, 16], 1421 | 1422 | // 28 1423 | [3, 147, 117, 10, 148, 118], 1424 | [3, 73, 45, 23, 74, 46], 1425 | [4, 54, 24, 31, 55, 25], 1426 | [11, 45, 15, 31, 46, 16], 1427 | 1428 | // 29 1429 | [7, 146, 116, 7, 147, 117], 1430 | [21, 73, 45, 7, 74, 46], 1431 | [1, 53, 23, 37, 54, 24], 1432 | [19, 45, 15, 26, 46, 16], 1433 | 1434 | // 30 1435 | [5, 145, 115, 10, 146, 116], 1436 | [19, 75, 47, 10, 76, 48], 1437 | [15, 54, 24, 25, 55, 25], 1438 | [23, 45, 15, 25, 46, 16], 1439 | 1440 | // 31 1441 | [13, 145, 115, 3, 146, 116], 1442 | [2, 74, 46, 29, 75, 47], 1443 | [42, 54, 24, 1, 55, 25], 1444 | [23, 45, 15, 28, 46, 16], 1445 | 1446 | // 32 1447 | [17, 145, 115], 1448 | [10, 74, 46, 23, 75, 47], 1449 | [10, 54, 24, 35, 55, 25], 1450 | [19, 45, 15, 35, 46, 16], 1451 | 1452 | // 33 1453 | [17, 145, 115, 1, 146, 116], 1454 | [14, 74, 46, 21, 75, 47], 1455 | [29, 54, 24, 19, 55, 25], 1456 | [11, 45, 15, 46, 46, 16], 1457 | 1458 | // 34 1459 | [13, 145, 115, 6, 146, 116], 1460 | [14, 74, 46, 23, 75, 47], 1461 | [44, 54, 24, 7, 55, 25], 1462 | [59, 46, 16, 1, 47, 17], 1463 | 1464 | // 35 1465 | [12, 151, 121, 7, 152, 122], 1466 | [12, 75, 47, 26, 76, 48], 1467 | [39, 54, 24, 14, 55, 25], 1468 | [22, 45, 15, 41, 46, 16], 1469 | 1470 | // 36 1471 | [6, 151, 121, 14, 152, 122], 1472 | [6, 75, 47, 34, 76, 48], 1473 | [46, 54, 24, 10, 55, 25], 1474 | [2, 45, 15, 64, 46, 16], 1475 | 1476 | // 37 1477 | [17, 152, 122, 4, 153, 123], 1478 | [29, 74, 46, 14, 75, 47], 1479 | [49, 54, 24, 10, 55, 25], 1480 | [24, 45, 15, 46, 46, 16], 1481 | 1482 | // 38 1483 | [4, 152, 122, 18, 153, 123], 1484 | [13, 74, 46, 32, 75, 47], 1485 | [48, 54, 24, 14, 55, 25], 1486 | [42, 45, 15, 32, 46, 16], 1487 | 1488 | // 39 1489 | [20, 147, 117, 4, 148, 118], 1490 | [40, 75, 47, 7, 76, 48], 1491 | [43, 54, 24, 22, 55, 25], 1492 | [10, 45, 15, 67, 46, 16], 1493 | 1494 | // 40 1495 | [19, 148, 118, 6, 149, 119], 1496 | [18, 75, 47, 31, 76, 48], 1497 | [34, 54, 24, 34, 55, 25], 1498 | [20, 45, 15, 61, 46, 16] 1499 | ]; 1500 | 1501 | var qrRSBlock = function(totalCount, dataCount) { 1502 | var _this = {}; 1503 | _this.totalCount = totalCount; 1504 | _this.dataCount = dataCount; 1505 | return _this; 1506 | }; 1507 | 1508 | var _this = {}; 1509 | 1510 | var getRsBlockTable = function(typeNumber, errorCorrectionLevel) { 1511 | 1512 | switch(errorCorrectionLevel) { 1513 | case QRErrorCorrectionLevel.L : 1514 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; 1515 | case QRErrorCorrectionLevel.M : 1516 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; 1517 | case QRErrorCorrectionLevel.Q : 1518 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; 1519 | case QRErrorCorrectionLevel.H : 1520 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; 1521 | default : 1522 | return undefined; 1523 | } 1524 | }; 1525 | 1526 | _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) { 1527 | 1528 | var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel); 1529 | 1530 | if (typeof rsBlock == 'undefined') { 1531 | throw 'bad rs block @ typeNumber:' + typeNumber + 1532 | '/errorCorrectionLevel:' + errorCorrectionLevel; 1533 | } 1534 | 1535 | var length = rsBlock.length / 3; 1536 | 1537 | var list = []; 1538 | 1539 | for (var i = 0; i < length; i += 1) { 1540 | 1541 | var count = rsBlock[i * 3 + 0]; 1542 | var totalCount = rsBlock[i * 3 + 1]; 1543 | var dataCount = rsBlock[i * 3 + 2]; 1544 | 1545 | for (var j = 0; j < count; j += 1) { 1546 | list.push(qrRSBlock(totalCount, dataCount) ); 1547 | } 1548 | } 1549 | 1550 | return list; 1551 | }; 1552 | 1553 | return _this; 1554 | }(); 1555 | 1556 | //--------------------------------------------------------------------- 1557 | // qrBitBuffer 1558 | //--------------------------------------------------------------------- 1559 | 1560 | var qrBitBuffer = function() { 1561 | 1562 | var _buffer = []; 1563 | var _length = 0; 1564 | 1565 | var _this = {}; 1566 | 1567 | _this.getBuffer = function() { 1568 | return _buffer; 1569 | }; 1570 | 1571 | _this.getAt = function(index) { 1572 | var bufIndex = Math.floor(index / 8); 1573 | return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; 1574 | }; 1575 | 1576 | _this.put = function(num, length) { 1577 | for (var i = 0; i < length; i += 1) { 1578 | _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); 1579 | } 1580 | }; 1581 | 1582 | _this.getLengthInBits = function() { 1583 | return _length; 1584 | }; 1585 | 1586 | _this.putBit = function(bit) { 1587 | 1588 | var bufIndex = Math.floor(_length / 8); 1589 | if (_buffer.length <= bufIndex) { 1590 | _buffer.push(0); 1591 | } 1592 | 1593 | if (bit) { 1594 | _buffer[bufIndex] |= (0x80 >>> (_length % 8) ); 1595 | } 1596 | 1597 | _length += 1; 1598 | }; 1599 | 1600 | return _this; 1601 | }; 1602 | 1603 | //--------------------------------------------------------------------- 1604 | // qrNumber 1605 | //--------------------------------------------------------------------- 1606 | 1607 | var qrNumber = function(data) { 1608 | 1609 | var _mode = QRMode.MODE_NUMBER; 1610 | var _data = data; 1611 | 1612 | var _this = {}; 1613 | 1614 | _this.getMode = function() { 1615 | return _mode; 1616 | }; 1617 | 1618 | _this.getLength = function(buffer) { 1619 | return _data.length; 1620 | }; 1621 | 1622 | _this.write = function(buffer) { 1623 | 1624 | var data = _data; 1625 | 1626 | var i = 0; 1627 | 1628 | while (i + 2 < data.length) { 1629 | buffer.put(strToNum(data.substring(i, i + 3) ), 10); 1630 | i += 3; 1631 | } 1632 | 1633 | if (i < data.length) { 1634 | if (data.length - i == 1) { 1635 | buffer.put(strToNum(data.substring(i, i + 1) ), 4); 1636 | } else if (data.length - i == 2) { 1637 | buffer.put(strToNum(data.substring(i, i + 2) ), 7); 1638 | } 1639 | } 1640 | }; 1641 | 1642 | var strToNum = function(s) { 1643 | var num = 0; 1644 | for (var i = 0; i < s.length; i += 1) { 1645 | num = num * 10 + chatToNum(s.charAt(i) ); 1646 | } 1647 | return num; 1648 | }; 1649 | 1650 | var chatToNum = function(c) { 1651 | if ('0' <= c && c <= '9') { 1652 | return c.charCodeAt(0) - '0'.charCodeAt(0); 1653 | } 1654 | throw 'illegal char :' + c; 1655 | }; 1656 | 1657 | return _this; 1658 | }; 1659 | 1660 | //--------------------------------------------------------------------- 1661 | // qrAlphaNum 1662 | //--------------------------------------------------------------------- 1663 | 1664 | var qrAlphaNum = function(data) { 1665 | 1666 | var _mode = QRMode.MODE_ALPHA_NUM; 1667 | var _data = data; 1668 | 1669 | var _this = {}; 1670 | 1671 | _this.getMode = function() { 1672 | return _mode; 1673 | }; 1674 | 1675 | _this.getLength = function(buffer) { 1676 | return _data.length; 1677 | }; 1678 | 1679 | _this.write = function(buffer) { 1680 | 1681 | var s = _data; 1682 | 1683 | var i = 0; 1684 | 1685 | while (i + 1 < s.length) { 1686 | buffer.put( 1687 | getCode(s.charAt(i) ) * 45 + 1688 | getCode(s.charAt(i + 1) ), 11); 1689 | i += 2; 1690 | } 1691 | 1692 | if (i < s.length) { 1693 | buffer.put(getCode(s.charAt(i) ), 6); 1694 | } 1695 | }; 1696 | 1697 | var getCode = function(c) { 1698 | 1699 | if ('0' <= c && c <= '9') { 1700 | return c.charCodeAt(0) - '0'.charCodeAt(0); 1701 | } else if ('A' <= c && c <= 'Z') { 1702 | return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10; 1703 | } else { 1704 | switch (c) { 1705 | case ' ' : return 36; 1706 | case '$' : return 37; 1707 | case '%' : return 38; 1708 | case '*' : return 39; 1709 | case '+' : return 40; 1710 | case '-' : return 41; 1711 | case '.' : return 42; 1712 | case '/' : return 43; 1713 | case ':' : return 44; 1714 | default : 1715 | throw 'illegal char :' + c; 1716 | } 1717 | } 1718 | }; 1719 | 1720 | return _this; 1721 | }; 1722 | 1723 | //--------------------------------------------------------------------- 1724 | // qr8BitByte 1725 | //--------------------------------------------------------------------- 1726 | 1727 | var qr8BitByte = function(data) { 1728 | 1729 | var _mode = QRMode.MODE_8BIT_BYTE; 1730 | var _data = data; 1731 | var _bytes = qrcode.stringToBytes(data); 1732 | 1733 | var _this = {}; 1734 | 1735 | _this.getMode = function() { 1736 | return _mode; 1737 | }; 1738 | 1739 | _this.getLength = function(buffer) { 1740 | return _bytes.length; 1741 | }; 1742 | 1743 | _this.write = function(buffer) { 1744 | for (var i = 0; i < _bytes.length; i += 1) { 1745 | buffer.put(_bytes[i], 8); 1746 | } 1747 | }; 1748 | 1749 | return _this; 1750 | }; 1751 | 1752 | //--------------------------------------------------------------------- 1753 | // qrKanji 1754 | //--------------------------------------------------------------------- 1755 | 1756 | var qrKanji = function(data) { 1757 | 1758 | var _mode = QRMode.MODE_KANJI; 1759 | var _data = data; 1760 | 1761 | var stringToBytes = qrcode.stringToBytesFuncs['SJIS']; 1762 | if (!stringToBytes) { 1763 | throw 'sjis not supported.'; 1764 | } 1765 | !function(c, code) { 1766 | // self test for sjis support. 1767 | var test = stringToBytes(c); 1768 | if (test.length != 2 || ( (test[0] << 8) | test[1]) != code) { 1769 | throw 'sjis not supported.'; 1770 | } 1771 | }('\u53cb', 0x9746); 1772 | 1773 | var _bytes = stringToBytes(data); 1774 | 1775 | var _this = {}; 1776 | 1777 | _this.getMode = function() { 1778 | return _mode; 1779 | }; 1780 | 1781 | _this.getLength = function(buffer) { 1782 | return ~~(_bytes.length / 2); 1783 | }; 1784 | 1785 | _this.write = function(buffer) { 1786 | 1787 | var data = _bytes; 1788 | 1789 | var i = 0; 1790 | 1791 | while (i + 1 < data.length) { 1792 | 1793 | var c = ( (0xff & data[i]) << 8) | (0xff & data[i + 1]); 1794 | 1795 | if (0x8140 <= c && c <= 0x9FFC) { 1796 | c -= 0x8140; 1797 | } else if (0xE040 <= c && c <= 0xEBBF) { 1798 | c -= 0xC140; 1799 | } else { 1800 | throw 'illegal char at ' + (i + 1) + '/' + c; 1801 | } 1802 | 1803 | c = ( (c >>> 8) & 0xff) * 0xC0 + (c & 0xff); 1804 | 1805 | buffer.put(c, 13); 1806 | 1807 | i += 2; 1808 | } 1809 | 1810 | if (i < data.length) { 1811 | throw 'illegal char at ' + (i + 1); 1812 | } 1813 | }; 1814 | 1815 | return _this; 1816 | }; 1817 | 1818 | //===================================================================== 1819 | // GIF Support etc. 1820 | // 1821 | 1822 | //--------------------------------------------------------------------- 1823 | // byteArrayOutputStream 1824 | //--------------------------------------------------------------------- 1825 | 1826 | var byteArrayOutputStream = function() { 1827 | 1828 | var _bytes = []; 1829 | 1830 | var _this = {}; 1831 | 1832 | _this.writeByte = function(b) { 1833 | _bytes.push(b & 0xff); 1834 | }; 1835 | 1836 | _this.writeShort = function(i) { 1837 | _this.writeByte(i); 1838 | _this.writeByte(i >>> 8); 1839 | }; 1840 | 1841 | _this.writeBytes = function(b, off, len) { 1842 | off = off || 0; 1843 | len = len || b.length; 1844 | for (var i = 0; i < len; i += 1) { 1845 | _this.writeByte(b[i + off]); 1846 | } 1847 | }; 1848 | 1849 | _this.writeString = function(s) { 1850 | for (var i = 0; i < s.length; i += 1) { 1851 | _this.writeByte(s.charCodeAt(i) ); 1852 | } 1853 | }; 1854 | 1855 | _this.toByteArray = function() { 1856 | return _bytes; 1857 | }; 1858 | 1859 | _this.toString = function() { 1860 | var s = ''; 1861 | s += '['; 1862 | for (var i = 0; i < _bytes.length; i += 1) { 1863 | if (i > 0) { 1864 | s += ','; 1865 | } 1866 | s += _bytes[i]; 1867 | } 1868 | s += ']'; 1869 | return s; 1870 | }; 1871 | 1872 | return _this; 1873 | }; 1874 | 1875 | //--------------------------------------------------------------------- 1876 | // base64EncodeOutputStream 1877 | //--------------------------------------------------------------------- 1878 | 1879 | var base64EncodeOutputStream = function() { 1880 | 1881 | var _buffer = 0; 1882 | var _buflen = 0; 1883 | var _length = 0; 1884 | var _base64 = ''; 1885 | 1886 | var _this = {}; 1887 | 1888 | var writeEncoded = function(b) { 1889 | _base64 += String.fromCharCode(encode(b & 0x3f) ); 1890 | }; 1891 | 1892 | var encode = function(n) { 1893 | if (n < 0) { 1894 | // error. 1895 | } else if (n < 26) { 1896 | return 0x41 + n; 1897 | } else if (n < 52) { 1898 | return 0x61 + (n - 26); 1899 | } else if (n < 62) { 1900 | return 0x30 + (n - 52); 1901 | } else if (n == 62) { 1902 | return 0x2b; 1903 | } else if (n == 63) { 1904 | return 0x2f; 1905 | } 1906 | throw 'n:' + n; 1907 | }; 1908 | 1909 | _this.writeByte = function(n) { 1910 | 1911 | _buffer = (_buffer << 8) | (n & 0xff); 1912 | _buflen += 8; 1913 | _length += 1; 1914 | 1915 | while (_buflen >= 6) { 1916 | writeEncoded(_buffer >>> (_buflen - 6) ); 1917 | _buflen -= 6; 1918 | } 1919 | }; 1920 | 1921 | _this.flush = function() { 1922 | 1923 | if (_buflen > 0) { 1924 | writeEncoded(_buffer << (6 - _buflen) ); 1925 | _buffer = 0; 1926 | _buflen = 0; 1927 | } 1928 | 1929 | if (_length % 3 != 0) { 1930 | // padding 1931 | var padlen = 3 - _length % 3; 1932 | for (var i = 0; i < padlen; i += 1) { 1933 | _base64 += '='; 1934 | } 1935 | } 1936 | }; 1937 | 1938 | _this.toString = function() { 1939 | return _base64; 1940 | }; 1941 | 1942 | return _this; 1943 | }; 1944 | 1945 | //--------------------------------------------------------------------- 1946 | // base64DecodeInputStream 1947 | //--------------------------------------------------------------------- 1948 | 1949 | var base64DecodeInputStream = function(str) { 1950 | 1951 | var _str = str; 1952 | var _pos = 0; 1953 | var _buffer = 0; 1954 | var _buflen = 0; 1955 | 1956 | var _this = {}; 1957 | 1958 | _this.read = function() { 1959 | 1960 | while (_buflen < 8) { 1961 | 1962 | if (_pos >= _str.length) { 1963 | if (_buflen == 0) { 1964 | return -1; 1965 | } 1966 | throw 'unexpected end of file./' + _buflen; 1967 | } 1968 | 1969 | var c = _str.charAt(_pos); 1970 | _pos += 1; 1971 | 1972 | if (c == '=') { 1973 | _buflen = 0; 1974 | return -1; 1975 | } else if (c.match(/^\s$/) ) { 1976 | // ignore if whitespace. 1977 | continue; 1978 | } 1979 | 1980 | _buffer = (_buffer << 6) | decode(c.charCodeAt(0) ); 1981 | _buflen += 6; 1982 | } 1983 | 1984 | var n = (_buffer >>> (_buflen - 8) ) & 0xff; 1985 | _buflen -= 8; 1986 | return n; 1987 | }; 1988 | 1989 | var decode = function(c) { 1990 | if (0x41 <= c && c <= 0x5a) { 1991 | return c - 0x41; 1992 | } else if (0x61 <= c && c <= 0x7a) { 1993 | return c - 0x61 + 26; 1994 | } else if (0x30 <= c && c <= 0x39) { 1995 | return c - 0x30 + 52; 1996 | } else if (c == 0x2b) { 1997 | return 62; 1998 | } else if (c == 0x2f) { 1999 | return 63; 2000 | } else { 2001 | throw 'c:' + c; 2002 | } 2003 | }; 2004 | 2005 | return _this; 2006 | }; 2007 | 2008 | //--------------------------------------------------------------------- 2009 | // gifImage (B/W) 2010 | //--------------------------------------------------------------------- 2011 | 2012 | var gifImage = function(width, height) { 2013 | 2014 | var _width = width; 2015 | var _height = height; 2016 | var _data = new Array(width * height); 2017 | 2018 | var _this = {}; 2019 | 2020 | _this.setPixel = function(x, y, pixel) { 2021 | _data[y * _width + x] = pixel; 2022 | }; 2023 | 2024 | _this.write = function(out) { 2025 | 2026 | //--------------------------------- 2027 | // GIF Signature 2028 | 2029 | out.writeString('GIF87a'); 2030 | 2031 | //--------------------------------- 2032 | // Screen Descriptor 2033 | 2034 | out.writeShort(_width); 2035 | out.writeShort(_height); 2036 | 2037 | out.writeByte(0x80); // 2bit 2038 | out.writeByte(0); 2039 | out.writeByte(0); 2040 | 2041 | //--------------------------------- 2042 | // Global Color Map 2043 | 2044 | // black 2045 | out.writeByte(0x00); 2046 | out.writeByte(0x00); 2047 | out.writeByte(0x00); 2048 | 2049 | // white 2050 | out.writeByte(0xff); 2051 | out.writeByte(0xff); 2052 | out.writeByte(0xff); 2053 | 2054 | //--------------------------------- 2055 | // Image Descriptor 2056 | 2057 | out.writeString(','); 2058 | out.writeShort(0); 2059 | out.writeShort(0); 2060 | out.writeShort(_width); 2061 | out.writeShort(_height); 2062 | out.writeByte(0); 2063 | 2064 | //--------------------------------- 2065 | // Local Color Map 2066 | 2067 | //--------------------------------- 2068 | // Raster Data 2069 | 2070 | var lzwMinCodeSize = 2; 2071 | var raster = getLZWRaster(lzwMinCodeSize); 2072 | 2073 | out.writeByte(lzwMinCodeSize); 2074 | 2075 | var offset = 0; 2076 | 2077 | while (raster.length - offset > 255) { 2078 | out.writeByte(255); 2079 | out.writeBytes(raster, offset, 255); 2080 | offset += 255; 2081 | } 2082 | 2083 | out.writeByte(raster.length - offset); 2084 | out.writeBytes(raster, offset, raster.length - offset); 2085 | out.writeByte(0x00); 2086 | 2087 | //--------------------------------- 2088 | // GIF Terminator 2089 | out.writeString(';'); 2090 | }; 2091 | 2092 | var bitOutputStream = function(out) { 2093 | 2094 | var _out = out; 2095 | var _bitLength = 0; 2096 | var _bitBuffer = 0; 2097 | 2098 | var _this = {}; 2099 | 2100 | _this.write = function(data, length) { 2101 | 2102 | if ( (data >>> length) != 0) { 2103 | throw 'length over'; 2104 | } 2105 | 2106 | while (_bitLength + length >= 8) { 2107 | _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) ); 2108 | length -= (8 - _bitLength); 2109 | data >>>= (8 - _bitLength); 2110 | _bitBuffer = 0; 2111 | _bitLength = 0; 2112 | } 2113 | 2114 | _bitBuffer = (data << _bitLength) | _bitBuffer; 2115 | _bitLength = _bitLength + length; 2116 | }; 2117 | 2118 | _this.flush = function() { 2119 | if (_bitLength > 0) { 2120 | _out.writeByte(_bitBuffer); 2121 | } 2122 | }; 2123 | 2124 | return _this; 2125 | }; 2126 | 2127 | var getLZWRaster = function(lzwMinCodeSize) { 2128 | 2129 | var clearCode = 1 << lzwMinCodeSize; 2130 | var endCode = (1 << lzwMinCodeSize) + 1; 2131 | var bitLength = lzwMinCodeSize + 1; 2132 | 2133 | // Setup LZWTable 2134 | var table = lzwTable(); 2135 | 2136 | for (var i = 0; i < clearCode; i += 1) { 2137 | table.add(String.fromCharCode(i) ); 2138 | } 2139 | table.add(String.fromCharCode(clearCode) ); 2140 | table.add(String.fromCharCode(endCode) ); 2141 | 2142 | var byteOut = byteArrayOutputStream(); 2143 | var bitOut = bitOutputStream(byteOut); 2144 | 2145 | // clear code 2146 | bitOut.write(clearCode, bitLength); 2147 | 2148 | var dataIndex = 0; 2149 | 2150 | var s = String.fromCharCode(_data[dataIndex]); 2151 | dataIndex += 1; 2152 | 2153 | while (dataIndex < _data.length) { 2154 | 2155 | var c = String.fromCharCode(_data[dataIndex]); 2156 | dataIndex += 1; 2157 | 2158 | if (table.contains(s + c) ) { 2159 | 2160 | s = s + c; 2161 | 2162 | } else { 2163 | 2164 | bitOut.write(table.indexOf(s), bitLength); 2165 | 2166 | if (table.size() < 0xfff) { 2167 | 2168 | if (table.size() == (1 << bitLength) ) { 2169 | bitLength += 1; 2170 | } 2171 | 2172 | table.add(s + c); 2173 | } 2174 | 2175 | s = c; 2176 | } 2177 | } 2178 | 2179 | bitOut.write(table.indexOf(s), bitLength); 2180 | 2181 | // end code 2182 | bitOut.write(endCode, bitLength); 2183 | 2184 | bitOut.flush(); 2185 | 2186 | return byteOut.toByteArray(); 2187 | }; 2188 | 2189 | var lzwTable = function() { 2190 | 2191 | var _map = {}; 2192 | var _size = 0; 2193 | 2194 | var _this = {}; 2195 | 2196 | _this.add = function(key) { 2197 | if (_this.contains(key) ) { 2198 | throw 'dup key:' + key; 2199 | } 2200 | _map[key] = _size; 2201 | _size += 1; 2202 | }; 2203 | 2204 | _this.size = function() { 2205 | return _size; 2206 | }; 2207 | 2208 | _this.indexOf = function(key) { 2209 | return _map[key]; 2210 | }; 2211 | 2212 | _this.contains = function(key) { 2213 | return typeof _map[key] != 'undefined'; 2214 | }; 2215 | 2216 | return _this; 2217 | }; 2218 | 2219 | return _this; 2220 | }; 2221 | 2222 | var createDataURL = function(width, height, getPixel) { 2223 | var gif = gifImage(width, height); 2224 | for (var y = 0; y < height; y += 1) { 2225 | for (var x = 0; x < width; x += 1) { 2226 | gif.setPixel(x, y, getPixel(x, y) ); 2227 | } 2228 | } 2229 | 2230 | var b = byteArrayOutputStream(); 2231 | gif.write(b); 2232 | 2233 | var base64 = base64EncodeOutputStream(); 2234 | var bytes = b.toByteArray(); 2235 | for (var i = 0; i < bytes.length; i += 1) { 2236 | base64.writeByte(bytes[i]); 2237 | } 2238 | base64.flush(); 2239 | 2240 | return 'data:image/gif;base64,' + base64; 2241 | }; 2242 | 2243 | //--------------------------------------------------------------------- 2244 | // returns qrcode function. 2245 | 2246 | return qrcode; 2247 | }(); 2248 | 2249 | // multibyte support 2250 | !function() { 2251 | 2252 | qrcode.stringToBytesFuncs['UTF-8'] = function(s) { 2253 | // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array 2254 | function toUTF8Array(str) { 2255 | var utf8 = []; 2256 | for (var i=0; i < str.length; i++) { 2257 | var charcode = str.charCodeAt(i); 2258 | if (charcode < 0x80) utf8.push(charcode); 2259 | else if (charcode < 0x800) { 2260 | utf8.push(0xc0 | (charcode >> 6), 2261 | 0x80 | (charcode & 0x3f)); 2262 | } 2263 | else if (charcode < 0xd800 || charcode >= 0xe000) { 2264 | utf8.push(0xe0 | (charcode >> 12), 2265 | 0x80 | ((charcode>>6) & 0x3f), 2266 | 0x80 | (charcode & 0x3f)); 2267 | } 2268 | // surrogate pair 2269 | else { 2270 | i++; 2271 | // UTF-16 encodes 0x10000-0x10FFFF by 2272 | // subtracting 0x10000 and splitting the 2273 | // 20 bits of 0x0-0xFFFFF into two halves 2274 | charcode = 0x10000 + (((charcode & 0x3ff)<<10) 2275 | | (str.charCodeAt(i) & 0x3ff)); 2276 | utf8.push(0xf0 | (charcode >>18), 2277 | 0x80 | ((charcode>>12) & 0x3f), 2278 | 0x80 | ((charcode>>6) & 0x3f), 2279 | 0x80 | (charcode & 0x3f)); 2280 | } 2281 | } 2282 | return utf8; 2283 | } 2284 | return toUTF8Array(s); 2285 | }; 2286 | 2287 | }(); 2288 | 2289 | (function (factory) { 2290 | if (typeof define === 'function' && define.amd) { 2291 | define([], factory); 2292 | } else if (typeof exports === 'object') { 2293 | module.exports = factory(); 2294 | } 2295 | }(function () { 2296 | return qrcode; 2297 | })); 2298 | -------------------------------------------------------------------------------- /public/js/sketches/create.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-unused-vars */ 3 | // Feel free to give a better name to this file 4 | // This Javascript is powering /createPoll 5 | 6 | function setup() { 7 | noCanvas(); 8 | const textarea = createElement("textarea"); 9 | textarea.attribute("placeholder", "Enter question here"); 10 | 11 | const defaultOptions = 5; 12 | const inputsDiv = createElement("div").addClass("options"); 13 | inputsDiv.id("inputsDiv"); 14 | for (let i = 0; i < defaultOptions; i++) { 15 | const newInput = createInput().attribute( 16 | "placeholder", 17 | "Option " + (i + 1) 18 | ); 19 | 20 | inputsDiv.child(newInput); 21 | } 22 | 23 | const buttonsDiv = createElement("div").addClass("buttonsArray"); 24 | 25 | const addOption = createButton("Add Option").addClass("addOption"); 26 | const removeLastOption = 27 | createButton("Remove last Option").addClass("removeLastOption"); 28 | const submit = createButton("Create!"); 29 | 30 | buttonsDiv.child(addOption); 31 | buttonsDiv.child(removeLastOption); 32 | buttonsDiv.child(submit); 33 | 34 | addOption.mousePressed(async () => { 35 | const currentOptionsLength = 36 | document.getElementById("inputsDiv").children.length; 37 | 38 | const newInput = createInput().attribute( 39 | "placeholder", 40 | "Option " + (currentOptionsLength + 1) 41 | ); 42 | 43 | inputsDiv.child(newInput); 44 | }); 45 | 46 | removeLastOption.mousePressed(async () => { 47 | const currentOptionsLength = 48 | document.getElementById("inputsDiv").children.length; 49 | 50 | if (currentOptionsLength > 2) { 51 | var list = document.getElementById("inputsDiv"); 52 | list.removeChild(list.childNodes[currentOptionsLength - 1]); 53 | } 54 | }); 55 | 56 | submit.mousePressed(async () => { 57 | const question = textarea.value(); 58 | let options = []; 59 | 60 | const optionsDiv = document.getElementsByClassName("options")[0]; 61 | 62 | for (let option of optionsDiv.children) { 63 | options.push(option.value); 64 | } 65 | 66 | if (!question) return alert("You need to enter a question."); 67 | let actualOptions = []; 68 | for (let option of options) { 69 | if (option != "") { 70 | actualOptions.push(option); 71 | } 72 | } 73 | if (actualOptions.length < 2) 74 | return alert("You need to mention at least two valid options."); 75 | 76 | const response = await fetch("/api/new", { 77 | method: "POST", 78 | headers: { 79 | "Content-Type": "application/json", 80 | }, 81 | body: JSON.stringify({ 82 | question, 83 | actualOptions, 84 | }), 85 | }); 86 | const { id } = await response.json(); 87 | 88 | textarea.hide(); 89 | inputsDiv.hide(); 90 | addOption.hide(); 91 | removeLastOption.hide(); 92 | submit.hide(); 93 | 94 | const voteLink = location.origin + "/vote/" + id; 95 | const pollLink = location.origin + "/poll/" + id; 96 | createDiv(` 97 | Poll Created Successfully.
98 | Poll ID: ${id}
99 | Poll Voting Link: ${voteLink}
100 | Poll Results Link: ${pollLink}
101 | `); 102 | 103 | // add qrcode 104 | const typeNumber = 4; 105 | const errorCorrectionLevel = "L"; 106 | const qr = qrcode(typeNumber, errorCorrectionLevel); 107 | qr.addData(voteLink); 108 | qr.make(); 109 | 110 | createDiv(qr.createSvgTag(5, 5)); 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /public/js/sketches/poll.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | async function setup() { 5 | noCanvas(); 6 | poll = new Poll(); 7 | poll.initPoll(); 8 | 9 | 10 | // from https://github.com/CodingTrain/LateNight/issues/1 11 | const colorPairs = [ 12 | ['#9253a1', '#70327e'], 13 | ['#f063a4', '#ec015a'], 14 | ['#2dc5f4', '#0b6a88'], 15 | ['#fcee21', '#f89e4f'], 16 | ['#f16164', '#ec015a'], 17 | ['#70327e', '#9253a1'], 18 | ['#a42963', '#ec015a'], 19 | ['#0b6a88', '#2dc5f4'], 20 | ['#f89e4f', '#fcee21'], 21 | ['#ec015a', '#a42963'], 22 | ]; 23 | 24 | const [first, second] = random(colorPairs); 25 | const degrees = random(35, 75) * (random() < 0.5 ? -1 : 1); 26 | 27 | const gradient = `repeating-linear-gradient(${nf(degrees, 0, 1)}deg, ${first}, ${first} 10px, ${second} 10px, ${second} 20px`; 28 | 29 | const root = document.documentElement; 30 | root.style.setProperty('--monochrome-gradient', gradient); 31 | 32 | if (green(color(first)) > 127 && green(color(second)) > 127) { 33 | root.style.setProperty('--progressbar-color', 'black'); 34 | } 35 | } -------------------------------------------------------------------------------- /public/js/sketches/vote.js: -------------------------------------------------------------------------------- 1 | // marked for deletion? -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | const database = require("./helpers/database"); 2 | const {requiresAuthentication} = require("./validation/basicauth"); 3 | const createNewPoll = require("./helpers/createNewPoll"); 4 | const express = require("express"); 5 | const router = express.Router(); 6 | 7 | router.post("/new", requiresAuthentication, async (request, response) => { 8 | // request body should be in this form 9 | // { 10 | // question: string, 11 | // options: string[] 12 | // } 13 | 14 | let { question, options } = request.body; 15 | 16 | // Truthy filter (falsy values will be removed from the array) 17 | options = options.filter((x) => x); 18 | 19 | // Create a poll object, insert it in the database and get the ID back 20 | let pollID = await createNewPoll(question, options); 21 | 22 | // Send a response carryinh the poll id 23 | response.send({ 24 | status: "success", 25 | message: "Poll created successfully!", 26 | id: pollID, 27 | }); 28 | }); 29 | 30 | // GET newest poll data 31 | // Have to put this route before the generic one because of how express uses routes 32 | 33 | // GET Poll Data for specific POLL ID 34 | router.get("/poll/:pollId", async (request, response) => { 35 | const _id = request.params.pollId; 36 | 37 | const poll = await database.findOne({ _id }); 38 | 39 | response.send( 40 | poll || { 41 | status: "error", 42 | message: "Poll not found", 43 | } 44 | ); 45 | }); 46 | 47 | 48 | //End point to delete a poll if authenticated 49 | router.delete('/poll/:pollId', requiresAuthentication, async function(req, res) { 50 | // get poll from url 51 | const _id = req.params.pollId; 52 | const poll = await database.findOne({ _id }); 53 | 54 | if (!poll) { 55 | res.status(404); 56 | res.json({ 57 | status: 'error', 58 | message: 'Poll not found' 59 | }) 60 | return; 61 | } 62 | 63 | let count = await database.remove({ _id }); 64 | 65 | if (count == 0) { 66 | res.status(500); 67 | res.json({ 68 | status: 'error', 69 | message: 'Poll not found' 70 | }) 71 | } else { 72 | res.json({ 73 | status: 'success', 74 | message: 'Poll deleted successfully' 75 | }) 76 | } 77 | 78 | 79 | }); 80 | 81 | 82 | module.exports = router; 83 | -------------------------------------------------------------------------------- /server/helpers/broadcaster.js: -------------------------------------------------------------------------------- 1 | class Broadcaster { 2 | constructor() { 3 | this.sockets = [] 4 | } 5 | 6 | registerSocket(pollId, socket) { 7 | if(!this.sockets[pollId]) { 8 | this.sockets[pollId] = {}; 9 | } 10 | 11 | this.sockets[pollId][socket.id] = socket; 12 | } 13 | 14 | unregisterSocket(socketId) { 15 | if (this.sockets) { 16 | for (let pollId in this.sockets) { 17 | if (this.sockets[pollId][socketId]) { 18 | delete this.sockets[pollId][socketId]; 19 | } 20 | } 21 | } 22 | } 23 | 24 | updatePoll(poll) { 25 | const sockets = this.sockets[poll._id]; 26 | if (sockets) { 27 | for(let socketId in sockets) { 28 | sockets[socketId].emit('updatePoll', poll); 29 | } 30 | } 31 | } 32 | } 33 | 34 | module.exports = new Broadcaster(); 35 | -------------------------------------------------------------------------------- /server/helpers/createNewPoll.js: -------------------------------------------------------------------------------- 1 | // TODO: Move into a separate file which interacts with the database 2 | 3 | const database = require("./database"); 4 | 5 | async function createNewPoll(question, options) { 6 | // Poll structure (in database): 7 | // { 8 | // question: string, 9 | // options: string[], 10 | // votes: number[], 11 | // timestamp: created time (in ms) (since UNIX epoch) 12 | // _id: autogenerated by NeDB 13 | // } 14 | 15 | // If no values passed then we use default values 16 | let { _id } = await database.insert({ 17 | question: question || "What should we do now?", 18 | options: options || [ 19 | "Live Poll 📄", 20 | "Community Contributions 🎡", 21 | "Bots 🤖", 22 | ], 23 | votes: new Array(options ? options.length : 3).fill(0), 24 | timestamp: Date.now(), 25 | }); 26 | return _id; 27 | } 28 | 29 | module.exports = createNewPoll; 30 | -------------------------------------------------------------------------------- /server/helpers/database.js: -------------------------------------------------------------------------------- 1 | const Datastore = require("nedb-promises"); 2 | module.exports = Datastore.create("database.db"); 3 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | require("dotenv").config(); 3 | const express = require("express"); 4 | const app = express(); 5 | const http = require('http').createServer(app) 6 | const io = require("socket.io")(http); 7 | broadcaster = require("./helpers/broadcaster"); 8 | 9 | app.set("views", "./views"); 10 | app.set("view engine", "pug"); 11 | app.set("broadcaster", broadcaster); 12 | 13 | app.use(express.static("public")); 14 | app.use(express.json()); // For parsing application/json 15 | app.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies 16 | 17 | // Routes 18 | const webRoutes = require("./web"); 19 | app.use("/", webRoutes); 20 | 21 | const apiRoutes = require("./api"); 22 | app.use("/api", apiRoutes); 23 | 24 | // const createNewPoll = require("./helpers/createNewPoll"); 25 | // createNewPoll("Your question here", ["Option A", "Option B", "Option C"]); 26 | 27 | io.on('connection', (socket) => { 28 | socket.on('listenForPoll', (pollId) => { 29 | broadcaster.registerSocket(pollId, socket); 30 | }); 31 | socket.on('disconnect', () => { 32 | broadcaster.unregisterSocket(socket.id) 33 | }) 34 | }); 35 | 36 | const port = process.env.PORT || 3000; 37 | http.listen(port, () => 38 | console.log(`Server running at http://localhost:${port}`) 39 | ); 40 | -------------------------------------------------------------------------------- /server/validation/antipollspam.js: -------------------------------------------------------------------------------- 1 | // # This package contains a simple time-based anti-spam 2 | // # implementation. 3 | // # 4 | // # Details: 5 | // # The approach is to register ids (for example ip 6 | // # hashes) to a hashtable (JS object) as keys with 7 | // # timestamps as values. Vals are used for flushing. 8 | // # 9 | // # This is done while calling the main interface/func 10 | // # -- if a record already exists, then 11 | // # this func will return false, else true. On each call, 12 | // # old entries will be removed if their timestamp is 13 | // # too stale (dictated by integer), 14 | // # though scans are done on a interval specified with 15 | // # integer. 16 | // # 17 | // # Main interface: 18 | // # check() -- this is the default export. 19 | // # 20 | // # 21 | 22 | // # Holds access state. Keys are IDs while 23 | // # values are timestampts. 24 | let accessState = {}; 25 | 26 | // # How many seconds before a record becomes 27 | // # stale and should be deleted. 28 | const deltaSecondsFlushID = 60 * 60 * 24; //Only 1 vote per day 29 | 30 | // # How many seconds before 31 | // # is scanned for stale records. Used to 32 | // # reduce unnecessary scans. 33 | const deltaSecondsFlushScan = 60; 34 | // # timestamp for last flush. 35 | let timestampLastFlush = nowUnixSeconds(); 36 | 37 | // # Unixtime: milliseconds -> seconds. 38 | function nowUnixSeconds() { 39 | return Date.now() / 1000; 40 | } 41 | 42 | // # Flush records in (pkg lvl var) if they are 43 | // # older than (pkg lvl var). This 44 | // # procedure is aborted if not enough time has passed, 45 | // # which is specified by (pkg lvl var). 46 | function tryFlushStale() { 47 | // # Abort flush procedure if not enough time has passed. 48 | if (timestampLastFlush + deltaSecondsFlushScan > nowUnixSeconds()) return; 49 | 50 | // # Look through all items. 51 | Object.keys(accessState).forEach((key) => { 52 | const timestamp = accessState[key]; 53 | // # Delete if stale. 54 | if (timestamp + deltaSecondsFlushID <= nowUnixSeconds()) { 55 | delete accessState[key]; 56 | } 57 | }); 58 | // # Update timestamp for last flush. 59 | timestampLastFlush = nowUnixSeconds(); 60 | } 61 | 62 | // # Check if an id has used up its access. 63 | // # Each call to this func will also remove 64 | // # all stale records (see 65 | // # in this pkg). 66 | // # 67 | // # Recommended usage for this live-poll app: 68 | // # id=ip -- scope: global. 69 | // # id=poll/ip -- scope: poll. 70 | // # id=poll/option/ip -- scope: option. 71 | function check(id) { 72 | // # Remove old. 73 | tryFlushStale(); 74 | 75 | return accessState[id] == undefined; 76 | } 77 | 78 | // # Check if an id has used up its access. 79 | // # If it is new, then it will be registered 80 | // # and true will be returned, else false. 81 | // # 82 | // # Each call to this func will also remove 83 | // # all stale records (see 84 | // # in this pkg). 85 | // # 86 | // # Recommended usage for this live-poll app: 87 | // # id=ip -- scope: global. 88 | // # id=poll/ip -- scope: poll. 89 | // # id=poll/option/ip -- scope: option. 90 | function checkAndRegister(id) { 91 | // # Remove old. 92 | tryFlushStale(); 93 | 94 | // # id is not registered; register & exit. 95 | if (accessState[id] == undefined) { 96 | accessState[id] = nowUnixSeconds(); 97 | return true; 98 | } 99 | 100 | // # Registered exists. 101 | return false; 102 | } 103 | 104 | // # Export of numbers is done for unit testing. 105 | module.exports = { 106 | check, 107 | checkAndRegister, 108 | deltaSecondsFlushID, 109 | deltaSecondsFlushScan, 110 | }; 111 | -------------------------------------------------------------------------------- /server/validation/antipollspam_test.js: -------------------------------------------------------------------------------- 1 | // # This pkg functions as a unit-test for antipollspam.js. 2 | 3 | const { 4 | check, 5 | deltaSecondsFlushID, 6 | deltaSecondsFlushScan, 7 | } = require("./antipollspam.js"); 8 | 9 | console.log(` 10 | Note: Make sure to reduce and 11 | in './antipollspam.js' 12 | to a low integer before running this test. 13 | Not doing so will make this test time-expensive. 14 | 15 | Current : ${deltaSecondsFlushID} 16 | Current : ${deltaSecondsFlushScan} 17 | `); 18 | 19 | // # Generic sleep func. 20 | function sleep(ms) { 21 | return new Promise((resolve) => setTimeout(resolve, ms)); 22 | } 23 | 24 | // # Verify that an id check passes on first introduction 25 | // # and fails on a second check. 26 | async function testInstantDoubleRegister() { 27 | const id = "test_instantDoubleRegister"; 28 | // # Guard false negative. 29 | if (check(id) == false) return "test_instantDoubleRegister: fail 1"; 30 | 31 | // # Guard false positive. 32 | if (check(id) == true) return "test_instantDoubleRegister: fail 2"; 33 | 34 | // # ok. 35 | return "test_instantDoubleRegister: ok"; 36 | } 37 | 38 | // # Verify that flushing works (won't want a 39 | // # memory leak). 40 | async function testFlushing() { 41 | const id = "test_flushing"; 42 | // # Guard false negative. 43 | if (check(id) == false) return "test_flushing: fail 1"; 44 | 45 | // # Use largest waiting delta; 46 | // # Should guarantee record flush. 47 | let seconds = Math.max(deltaSecondsFlushID, deltaSecondsFlushScan); 48 | // # Wait until flush. 49 | await sleep(1000 * seconds + 1); 50 | 51 | // # Guard false negative. -- should act as new 52 | // # introduction since the old record is gone. 53 | if (check(id) == false) return "test_flushing: fail 2"; 54 | 55 | // ok. 56 | return "test_flushing: ok"; 57 | } 58 | 59 | // # Run all tests. 60 | function test() { 61 | // # Collection. 62 | const funcs = [testFlushing, testInstantDoubleRegister]; 63 | // # Run & print. 64 | funcs.forEach((f) => { 65 | f().then((r) => { 66 | console.log(r); 67 | }); 68 | }); 69 | } 70 | 71 | test(); 72 | -------------------------------------------------------------------------------- /server/validation/basicauth.js: -------------------------------------------------------------------------------- 1 | // Middleware function that blocks the route unless basicauth headers are present 2 | function requiresAuthentication(req, res, next) { 3 | // parse login and password from headers 4 | const b64auth = (req.headers.authorization || "").split(" ")[1] || ""; 5 | const [login, password] = Buffer.from(b64auth, "base64") 6 | .toString() 7 | .split(":"); 8 | 9 | // Verify login and password are set and correct and allow the request to continue 10 | if ( 11 | login && 12 | password && 13 | login === process.env.LOGIN_USERNAME && 14 | password === process.env.LOGIN_PASSWORD 15 | ) { 16 | next(); 17 | } else { 18 | // Access denied! end the request-response cycle 19 | res.set("WWW-Authenticate", 'Basic realm="401"'); // change this 20 | res.status(401).send("Authentication required."); // custom message 21 | } 22 | } 23 | 24 | module.exports = {requiresAuthentication}; 25 | -------------------------------------------------------------------------------- /server/web.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const database = require("./helpers/database"); 4 | const floodChecker = require("./validation/antipollspam"); 5 | const { requiresAuthentication } = require("./validation/basicauth"); 6 | 7 | //Index page to have an overview of active polls (and be able to manage them perhaps) - might need some password protection 8 | router.get("/", requiresAuthentication, async (req, res) => { 9 | res.render("index", { 10 | polls: await database.find({}).sort({ timestamp: -1 }), 11 | styling: req.query, 12 | }); 13 | }); 14 | 15 | //Page to create a new poll 16 | router.get("/create", requiresAuthentication, function (req, res) { 17 | res.render("create", { styling: req.query }); 18 | }); 19 | 20 | //Route for getting the newest poll 21 | router.get("/newest", async (req, res) => { 22 | // Get all polls, sort descending by timestamp, get the first poll 23 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0]; 24 | 25 | if (!poll) { 26 | res.status(404); 27 | res.render("notfound", { styling: req.query }); 28 | } else { 29 | res.render("poll", { poll: poll, styling: req.query }); 30 | } 31 | }); 32 | 33 | //Route for getting the newest poll 34 | router.get("/vote-now", async (req, res) => { 35 | // Get all polls, sort descending by timestamp, get the first poll 36 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0]; 37 | 38 | if (!poll) { 39 | res.status(404); 40 | res.render("notfound", { styling: req.query }); 41 | } else { 42 | res.redirect("/vote/" + poll._id); 43 | } 44 | }); 45 | 46 | //Page to see the results of a poll 47 | router.get("/poll/:pollId", async function (req, res) { 48 | const _id = req.params.pollId; 49 | const poll = await database.findOne({ _id }); 50 | 51 | if (!poll) { 52 | res.status(404); 53 | res.render("notfound", { styling: req.query }); 54 | } else { 55 | res.render("poll", { poll: poll, styling: req.query }); 56 | } 57 | }); 58 | 59 | //Page to add a vote to a poll 60 | router.get("/vote/:pollId", async function (req, res) { 61 | const _id = req.params.pollId; 62 | 63 | let ip = req.ip; 64 | if (req.headers["x-forwarded-for"]) { 65 | ip = req.headers["x-forwarded-for"]; 66 | } 67 | 68 | const floodCheckId = _id + "_" + ip; 69 | const hasVoted = !floodChecker.check(floodCheckId); 70 | 71 | // uncomment this to disable this check 72 | // const hasVoted = false 73 | 74 | //Forward user to poll results page if already voted 75 | if (hasVoted) { 76 | res.redirect("/poll/" + _id); 77 | return; 78 | } 79 | 80 | const poll = await database.findOne({ _id }); 81 | 82 | if (!poll) { 83 | res.status(404); 84 | res.render("notfound"); 85 | } else { 86 | res.render("vote", { poll: poll, styling: req.query }); 87 | } 88 | }); 89 | 90 | //Post request to do a vote 91 | router.post("/vote/:pollId", async function (req, res) { 92 | const _id = req.params.pollId; 93 | 94 | let ip = req.ip; 95 | if (req.headers["x-forwarded-for"]) { 96 | ip = req.headers["x-forwarded-for"]; 97 | } 98 | 99 | const floodCheckId = _id + "_" + ip; 100 | const isValidVote = floodChecker.checkAndRegister(floodCheckId); 101 | 102 | // uncomment this to disable this check 103 | // const isValidVote = true 104 | 105 | //Forward user to poll results page if already voted 106 | if (!isValidVote) { 107 | res.redirect("/poll/" + _id); 108 | return; 109 | } 110 | 111 | const poll = await database.findOne({ _id }); 112 | 113 | if (!poll) { 114 | res.status(404); 115 | res.render("notfound"); 116 | return; 117 | } 118 | 119 | const choice = req.body.vote; 120 | // If the choice is out of range from the possible options, 121 | // send and error response 122 | if (choice < 0 || choice >= poll.options.length) { 123 | res.status(404); 124 | res.render("notfound"); 125 | return; 126 | } 127 | 128 | // Update the votes 129 | poll.votes[choice]++; 130 | 131 | // Push the update to the database 132 | database.update({ _id }, poll); 133 | 134 | //Push update to all connected clients 135 | req.app.get("broadcaster").updatePoll(poll); 136 | 137 | // Forward user to poll results page 138 | res.redirect("/poll/" + _id); 139 | }); 140 | 141 | //Page to view the latest qrcode 142 | router.get("/qrcode/", async (req, res) => { 143 | // Get all polls, sort descending by timestamp, get the first poll 144 | 145 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0]; 146 | 147 | const hostAddress = req.get("host"); 148 | const pollURL = `http://${hostAddress}/vote/${poll._id}`; 149 | 150 | if (!poll) { 151 | res.status(404); 152 | res.render("notfound"); 153 | } else { 154 | res.render("qrcode", { pollURL, question: poll.question }); 155 | } 156 | }); 157 | 158 | module.exports = router; 159 | -------------------------------------------------------------------------------- /views/create.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover") 6 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 7 | title Live Coding Train Poll 8 | link(rel="stylesheet" href="css/style.css") 9 | link(rel="stylesheet" href="css/create.css") 10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js") 11 | script(src="/js/qrcode.js") 12 | 13 | body#main-create 14 | main 15 | h1 The Coding Train Live Poll 16 | h2 Create a poll 17 | 18 | script(src="/js/sketches/create.js") 19 | 20 | //- template(data-styling=styling) 21 | //- include footer.pug 22 | -------------------------------------------------------------------------------- /views/footer.pug: -------------------------------------------------------------------------------- 1 | head 2 | link(rel="stylesheet" href="/css/footer.css") 3 | 4 | div(id="footer") 5 | h3 View options   6 | a(style="font-size: 0.7em" onclick="this.parentNode.parentNode.remove()") 7 | i [close] 8 | 9 | div#options-wrapper 10 | div.btn-options(class="noselect") 11 | input(type="checkbox" id="font-toggle") 12 | label(for="font-toggle") simplier font 13 | div.btn-options(class="noselect") 14 | input(type="checkbox" id="gradient-toggle") 15 | label(for="gradient-toggle") monotone gradient 16 | div.btn-options(class="noselect") 17 | input(type="checkbox" checked id="overlay-toggle") 18 | label(for="overlay-toggle") overlay mode 19 | 20 | script. 21 | 22 | // checkboxes 23 | const fontToggle = document.querySelector('#font-toggle'); 24 | const gradientToggle = document.querySelector('#gradient-toggle'); 25 | const overlayToggle = document.querySelector('#overlay-toggle'); 26 | 27 | // first check url-parameters 28 | // extracts url-styling parameters from poll.pug 29 | const styling = document.querySelector('[data-styling]').dataset.styling; 30 | const urlStyling = (JSON.parse(styling)); 31 | 32 | // check if any url-parameter was submitted (if not) 33 | if(Object.keys(urlStyling).length === 0) { 34 | 35 | // initial state from localStorage 36 | fontToggle.checked = localStorage.getItem('font-simple') == 'true'; 37 | gradientToggle.checked = localStorage.getItem('no-gradient') == 'true'; 38 | overlayToggle.checked = localStorage.getItem('overlay-on') != 'false'; 39 | 40 | } else { 41 | 42 | // removes local storage if url-parameters are used 43 | localStorage.removeItem('font-simple'); 44 | localStorage.removeItem('no-gradient'); 45 | localStorage.removeItem('overlay-on'); 46 | 47 | // set states from url-paramaters 48 | fontToggle.checked = urlStyling.simple == 'true'; 49 | gradientToggle.checked = urlStyling.monotone == 'true'; 50 | overlayToggle.checked = urlStyling.overlay == 'true'; 51 | } 52 | 53 | changeFonts(fontToggle.checked); 54 | changeGradient(gradientToggle.checked); 55 | changeOverlay(overlayToggle.checked); 56 | 57 | // input handlers 58 | fontToggle.oninput = function() { 59 | localStorage.setItem('font-simple', fontToggle.checked); 60 | changeFonts(fontToggle.checked); 61 | }; 62 | 63 | gradientToggle.oninput = function() { 64 | localStorage.setItem('no-gradient', gradientToggle.checked); 65 | changeGradient(gradientToggle.checked); 66 | }; 67 | 68 | overlayToggle.oninput = function() { 69 | localStorage.setItem('overlay-on', overlayToggle.checked); 70 | changeOverlay(overlayToggle.checked); 71 | }; 72 | 73 | // toggle font by switching the variable in css 74 | function changeFonts(simple) { 75 | if (simple) 76 | document.documentElement.style.setProperty('--codingtrain-fontface', 'Open Sans'); 77 | else 78 | document.documentElement.style.setProperty('--codingtrain-fontface', 'cubanoregular'); 79 | } 80 | 81 | 82 | // remove gradient by removing or adding the correct class to the progress bar 83 | function changeGradient(off) { 84 | if (off) { 85 | let elements = document.getElementsByClassName('progressBar'); 86 | for (let element of elements) { 87 | element.classList.remove('gradient') 88 | } 89 | } else { 90 | let elements = document.getElementsByClassName('progressBar'); 91 | for (let element of elements) { 92 | element.classList.add('gradient') 93 | } 94 | } 95 | } 96 | 97 | // toggle the overlay 98 | // default value is a transparency = 50 99 | function changeOverlay(overlay) { 100 | const style = document.querySelector('#poll-result-style'); 101 | if(!style) return; 102 | 103 | if(overlay) { 104 | document.querySelector('#poll-result-style').href = "/css/poll-results-compact.css"; 105 | } else { 106 | document.querySelector('#poll-result-style').href = "/css/poll-results.css"; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover") 6 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 7 | title Live Coding Train Poll 8 | link(rel="stylesheet" href="css/style.css") 9 | link(rel="stylesheet" href="css/polls.css") 10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js") 11 | 12 | body 13 | main#pool-main 14 | h1 The Coding Train Live Poll 15 | h2 Currently Active Polls 16 | 17 | if polls.length 18 | ul(id="poll-container") 19 | each poll in polls 20 | li(id=`poll-${poll._id}`) 21 | a(class="title" href=`/poll/${poll._id}`)=`${poll.question}` 22 | br 23 | span(class="date")=`Created on: ${new Date(poll.timestamp).toDateString()}` 24 | br 25 | a(class="vote-btn" href=`/vote/${poll._id}`)="Go to vote" 26 | input(id=`delete_${poll._id}` class="delete-btn" data-poll-id=`${poll._id}` type="button" value="Delete poll") 27 | 28 | a(id='create' href="/create") Create a Poll   29 | else 30 | p No Polls Found! 31 | a(id="create" href="/create") Create One Here 32 | 33 | template(data-styling=styling) 34 | 35 | include footer.pug 36 | 37 | script. 38 | let delBTN = document.querySelectorAll('.delete-btn'); 39 | 40 | for (let button of delBTN) { 41 | button.onclick = function() { 42 | 43 | let confirmed = confirm('Are you sure you want to delete this poll?'); 44 | if (!confirmed) { 45 | return ; 46 | } 47 | 48 | let id = this.getAttribute('data-poll-id'); 49 | fetch('/api/poll/' + id, { 50 | method: 'DELETE' 51 | }).then(resp => resp.json()).then(data => { 52 | if (data.status == 'success') { 53 | alert('Deleted!'); 54 | let pollContainer = document.querySelector('#poll-container'); 55 | let pollNode = document.querySelector(`#poll-${id}`); 56 | pollContainer.removeChild(pollNode); 57 | } else { 58 | alert('Failed to delete poll') 59 | } 60 | }).catch(err => { 61 | alert('Failed to delete poll because of error'); 62 | alert(err); 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /views/notfound.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover") 6 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 7 | title Live Coding Train Poll 8 | link(rel="stylesheet" href="/css/style.css") 9 | link(rel="stylesheet" id="poll-result-style" href="/css/poll-results.css") 10 | 11 | body 12 | main#poll-result-main 13 | h1 The Coding Train Live Poll 14 | h2 There is no ongoing vote 15 | 16 | template(data-styling=styling) 17 | 18 | include footer.pug -------------------------------------------------------------------------------- /views/poll.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover") 6 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 7 | title Live Coding Train Poll 8 | link(rel="stylesheet" href="/css/style.css") 9 | link(rel="stylesheet" id="poll-result-style" href="/css/poll-results.css") 10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js") 11 | script(src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.3/socket.io.min.js") 12 | 13 | body 14 | main#poll-result-main 15 | h1 The Coding Train Live Poll 16 | h2=poll.question 17 | 18 | div#results 19 | each val, index in poll.options 20 | div.option 21 | div.bar-title=val 22 | div(id="progressBar_" + index class="progressBar") 23 | p#totalVotes 24 | 25 | template(data-styling=styling, data-id=poll._id) 26 | 27 | include footer.pug 28 | 29 | script(src="/js/classes/poll.js") 30 | script(src="/js/sketches/poll.js") 31 | -------------------------------------------------------------------------------- /views/qrcode.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Live Coding Train Poll 5 | link(rel="stylesheet" href="css/style.css") 6 | link(rel="stylesheet" href="css/qrcode.css") 7 | script(src="/js/qrcode.js") 8 | 9 | body 10 | h1 The Coding Train Live Poll 11 | h2 QR-Code to newest Poll 12 | section.content 13 | section.left 14 | iframe#results() 15 | section.right 16 | h3.qr-question=question 17 | div#placeHolder 18 | template(data-pollurl=pollURL) 19 | 20 | 21 | script. 22 | var typeNumber = 4; 23 | var errorCorrectionLevel = 'L'; 24 | var qr = qrcode(typeNumber, errorCorrectionLevel); 25 | var pollURL = document.querySelector('[data-pollurl]').dataset.pollurl; 26 | results.src = pollURL.replace('/vote/', '/poll/'); 27 | qr.addData(pollURL); 28 | qr.make(); 29 | document.querySelector('#placeHolder').innerHTML = qr.createSvgTag(5, 5); 30 | -------------------------------------------------------------------------------- /views/vote.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="UTF-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover") 6 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 7 | title Live Coding Train Poll 8 | link(rel="stylesheet" href="/css/style.css") 9 | link(rel="stylesheet" href="/css/vote.css") 10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js") 11 | 12 | body#vote-main 13 | main 14 | h1 The Coding Train Live Poll 15 | h2=poll.question 16 | 17 | form(action="/vote/" + poll._id method="post") 18 | ul#vote 19 | each val, index in poll.options 20 | li.btn-selection 21 | input(type="radio" name="vote" id="option_" + index value=index) 22 | label(for="option_" + index)=val 23 | input(class="submit-vote" type="submit" value="Vote") 24 | 25 | template(data-styling=styling, data-id=poll._id) 26 | 27 | script(src="/js/classes/poll.js") 28 | script(src="/js/sketches/vote.js") 29 | 30 | include footer.pug 31 | --------------------------------------------------------------------------------