├── .gitignore
├── nodemon.json
├── screencaptures
├── chatroom.png
└── homepage.png
├── app.json
├── webpack.config.js
├── .eslintrc.json
├── public
├── chatroom
│ └── index.html
├── homepage
│ └── index.html
├── js
│ ├── homepage.js
│ └── chatroom.js
└── css
│ └── style.css
├── package.json
├── src
├── homepage.js
├── escape-html.js
└── chatroom.js
├── README.md
└── app.js
/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | .DS_Store
3 | node_modules
4 | .env
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "app.js"
4 | ]
5 | }
--------------------------------------------------------------------------------
/screencaptures/chatroom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liam-ilan/sendverse/HEAD/screencaptures/chatroom.png
--------------------------------------------------------------------------------
/screencaptures/homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liam-ilan/sendverse/HEAD/screencaptures/homepage.png
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Sendverse chatting app",
3 | "description": "A chatting app based on socket.io and Express",
4 | "repository": "https://github.com/liam-ilan/sendverse"
5 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: {
5 | chatroom: './src/chatroom.js',
6 | homepage: './src/homepage.js'
7 | },
8 | output: {
9 | filename: '[name].js',
10 | path: path.resolve(__dirname, 'public/js/')
11 | },
12 | watch: true,
13 | watchOptions: {
14 | ignored: ['../app.js', 'node_modules']
15 | }
16 | };
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": "airbnb-base",
9 | "globals": {
10 | "Atomics": "readonly",
11 | "SharedArrayBuffer": "readonly"
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2018
15 | },
16 | "rules": {
17 | }
18 | }
--------------------------------------------------------------------------------
/public/chatroom/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | (ツ)
6 |
7 |
8 |
9 |
10 |
11 |
12 | Copy Link
13 |
14 |
15 | Say
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/public/homepage/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | (ツ)
6 |
7 |
8 |
9 |
10 |
11 |
12 | Enter
13 |
14 |
18 | online: -
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sendverse",
3 | "version": "1.0.0",
4 | "description": "A public chatting room for everyone.",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "node app.js",
8 | "lint": "npx eslint ./src --fix",
9 | "dev": "npx nodemon app.js & npx webpack --config webpack.config.js"
10 | },
11 | "author": "Liam Ilan",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "eslint": "^5.16.0",
15 | "eslint-config-airbnb-base": "^13.1.0",
16 | "eslint-plugin-import": "^2.17.3",
17 | "nodemon": "^1.19.1",
18 | "webpack": "^4.35.0",
19 | "webpack-cli": "^3.3.5"
20 | },
21 | "dependencies": {
22 | "express": "^4.17.1",
23 | "socket.io": "^2.2.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/homepage.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * sendverse
3 | * Copyright(c) 2019 Liam Ilan
4 | * MIT Licensed
5 | */
6 |
7 | const roomName = document.getElementById('room-name');
8 | const enter = document.getElementById('enter');
9 | const onlineCount = document.getElementById('online-count');
10 |
11 | // request josn function
12 | async function reqJson(url, method = 'GET', data = null) {
13 | // set fetch options
14 | const options = {
15 | method,
16 | headers: {
17 | 'Content-Type': 'application/json; charset=utf-8',
18 | },
19 | };
20 |
21 | // add body if content exists
22 | if (data) {
23 | options.body = JSON.stringify(data);
24 | }
25 |
26 | // await the fetch from the url
27 | const res = await window.fetch(url, options);
28 |
29 | // await until the json promise is resolved
30 | const json = await res.json();
31 | return json;
32 | }
33 |
34 | async function putCount() {
35 | const countJSON = await reqJson('/data/count');
36 | onlineCount.innerHTML = `Online: ${countJSON.online}`;
37 | }
38 |
39 | enter.addEventListener('click', () => {
40 | window.location.href += encodeURIComponent(roomName.value);
41 | });
42 |
43 | document.addEventListener('keypress', (e) => {
44 | if (e.key === 'Enter') {
45 | e.preventDefault();
46 | window.location.href += encodeURIComponent(roomName.value);
47 | }
48 | });
49 |
50 | putCount();
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sendverse
2 | An anonymous chatting server you can deploy for your friends.
3 | See it in action:
4 | https://sendverse.herokuapp.com/
5 |
6 | #### The software
7 | This repo includes the server side and frontend software required to run your own chatting app. The software is written in javascript. It uses Express.js to serve files, and socket.io, for the the realtime communication between the server and client.
8 |
9 | 
10 | 
11 |
12 | ##### Contents
13 | In this repo, you will find the server-side software, *app.js*, and the frontend, deployable software, located in the *public* directory. The frontend src files are located in the *src* directory.
14 |
15 | #### Deploy your own
16 | *To clone this repo:*
17 | ``` bash
18 | git clone https://github.com/liam-ilan/sendverse.git
19 | ```
20 |
21 | ##### Running the app
22 | *To start up a local server on port 3000:*
23 | ``` bash
24 | npm start
25 | ```
26 |
27 | *Or you can:*
28 |
29 |
30 | [](https://heroku.com/deploy)
31 |
32 | #### Development
33 | *Start up a localhost on port 3000 for development with Nodemon and Webpack:*
34 |
35 | ``` bash
36 | npm run dev
37 | ```
38 |
39 | *Lint javascript:*
40 | ``` bash
41 | npm run lint
42 | ```
43 |
44 | #### Author
45 | I'm Liam Ilan, a 13 year old software developer who is never working, but always playing around.
--------------------------------------------------------------------------------
/public/js/homepage.js:
--------------------------------------------------------------------------------
1 | !function(e){var n={};function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)t.d(o,r,function(n){return e[n]}.bind(null,r));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n){
2 | /*!
3 | * sendverse
4 | * Copyright(c) 2019 Liam Ilan
5 | * MIT Licensed
6 | */
7 | const t=document.getElementById("room-name"),o=document.getElementById("enter"),r=document.getElementById("online-count");o.addEventListener("click",()=>{window.location.href+=encodeURIComponent(t.value)}),document.addEventListener("keypress",e=>{"Enter"===e.key&&(e.preventDefault(),window.location.href+=encodeURIComponent(t.value))}),async function(){const e=await async function(e,n="GET",t=null){const o={method:n,headers:{"Content-Type":"application/json; charset=utf-8"}};t&&(o.body=JSON.stringify(t));const r=await window.fetch(e,o);return await r.json()}("/data/count");r.innerHTML=`Online: ${e.online}`}()}]);
--------------------------------------------------------------------------------
/src/escape-html.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /*!
3 | * escape-html
4 | * Copyright(c) 2012-2013 TJ Holowaychuk
5 | * Copyright(c) 2015 Andreas Lubbe
6 | * Copyright(c) 2015 Tiancheng "Timothy" Gu
7 | * MIT Licensed
8 | */
9 |
10 |
11 | /**
12 | * Module variables.
13 | * @private
14 | */
15 |
16 | const matchHtmlRegExp = /["'&<>]/;
17 |
18 | /**
19 | * Module exports.
20 | * @public
21 | */
22 |
23 | // module.exports = escapeHtml
24 |
25 | /**
26 | * Escape special characters in the given string of text.
27 | *
28 | * @param {string} string The string to escape for inserting into HTML
29 | * @return {string}
30 | * @public
31 | */
32 |
33 | export default function escapeHtml(string) {
34 | const str = `${string}`;
35 | const match = matchHtmlRegExp.exec(str);
36 |
37 | if (!match) {
38 | return str;
39 | }
40 |
41 | let escape;
42 | let html = '';
43 | let index = 0;
44 | let lastIndex = 0;
45 |
46 | for (index = match.index; index < str.length; index++) {
47 | switch (str.charCodeAt(index)) {
48 | case 34: // "
49 | escape = '"';
50 | break;
51 | case 38: // &
52 | escape = '&';
53 | break;
54 | case 39: // '
55 | escape = ''';
56 | break;
57 | case 60: // <
58 | escape = '<';
59 | break;
60 | case 62: // >
61 | escape = '>';
62 | break;
63 | default:
64 | continue;
65 | }
66 |
67 | if (lastIndex !== index) {
68 | html += str.substring(lastIndex, index);
69 | }
70 |
71 | lastIndex = index + 1;
72 | html += escape;
73 | }
74 |
75 | return lastIndex !== index
76 | ? html + str.substring(lastIndex, index)
77 | : html;
78 | }
79 | /* eslint-enable */
80 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // require packages
2 | const express = require('express');
3 | let io = require('socket.io');
4 |
5 | // current count of online people
6 | let currentCount = 0;
7 |
8 | // get our port
9 | const port = process.env.PORT || 3000;
10 |
11 | // make an app
12 | const app = express({ static: true });
13 | app.use('/', express.static('public'));
14 |
15 | // redirect trailing slash
16 | app.use((req, res, next) => {
17 | if (req.url.substr(-1) === '/' && (req.url.split('/').length - 1 > 1)) res.redirect(301, req.url.slice(0, -1));
18 | else next();
19 | });
20 |
21 | // keep list of active namespaces so we know if a given name needs to be created
22 | const namespaces = [];
23 |
24 | // on a connection function
25 | function connection(socket) {
26 | // add to count
27 | currentCount += 1;
28 |
29 | // when the client diconnects
30 | socket.on('disconnect', () => {
31 | // remove 1 from count
32 | if (currentCount !== 0) {
33 | currentCount -= 1;
34 | }
35 | });
36 |
37 | // on every message sent from THIS socket
38 | socket.on('message', (data) => {
39 | // set up data
40 | const newData = {
41 | message: data.message,
42 | color: data.color,
43 | };
44 |
45 | // validate
46 | if (newData.color.charAt(0) !== '#') { return { status: 'error' }; }
47 | if (newData.color.length !== 4) { return { status: 'error' }; }
48 | if (Number.isNaN(parseInt(newData.color.substring(1), 16))) { return { status: 'error' }; }
49 |
50 | // emit the message to everyone but the client
51 | socket.broadcast.emit('message', newData);
52 |
53 | return { status: 'success' };
54 | });
55 | }
56 |
57 | // namespace paths
58 | app.get('/:namespace', (req, res) => {
59 | let { namespace } = req.params;
60 | namespace = namespace.toLowerCase();
61 |
62 | // if the namespace is not in the list, add it
63 | if (namespaces.indexOf(namespace) === -1) {
64 | namespaces.push(namespace);
65 | io.of(`/${namespace}`).on('connection', connection);
66 | }
67 |
68 | // serve
69 | res.sendFile(`${__dirname}/public/chatroom/index.html`);
70 | });
71 |
72 | // main path
73 | app.get('/', (req, res) => {
74 | res.sendFile(`${__dirname}/public/homepage/index.html`);
75 | });
76 |
77 | // get count
78 | app.get('/data/count', (req, res) => {
79 | res.json({ online: currentCount });
80 | });
81 |
82 | // listen on the port
83 | const server = app.listen(port);
84 |
85 | // setup sockets
86 | io = io(server);
87 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | body, html{
2 | font-family: Arial, Helvetica, sans-serif;
3 | padding: 0px;
4 | margin: 0px;
5 | box-sizing: border-box;
6 | }
7 |
8 | ::placeholder{
9 | color: #ccc;
10 | }
11 |
12 | main{
13 | position: absolute;
14 | top: 50%;
15 | left: 50%;
16 | transform: translate(-50%, -50%);
17 | }
18 |
19 | main a {
20 | color: #3f89ff;
21 | }
22 |
23 | .bubble{
24 | display: block;
25 | padding: 25px;
26 | margin: 20px;
27 | font-size: 48px;
28 | border-radius: 30px 30px 30px 0px;
29 | word-wrap: break-word;
30 | width: 75%;
31 | border-bottom: 2px solid #ccc;
32 | border-left: 2px solid #ccc;
33 | }
34 |
35 | .my-bubble{
36 | margin-left: auto;
37 | margin-right: 20px;
38 | border-radius: 30px 30px 0px 30px;
39 | }
40 |
41 | #history{
42 | position: fixed;
43 | top: 0;
44 | bottom: 120px;
45 | left: 0;
46 | right: 0;
47 | overflow-y: scroll;
48 | background-color: #efefef;
49 | }
50 |
51 | #input-space{
52 | position: fixed;
53 | min-height: 100px;
54 | left: 0;
55 | right: 0;
56 | bottom: 0;
57 | background-color: #f9f9f9;
58 | text-align: center;
59 | padding-top: 20px;
60 | padding-bottom: 10px;
61 | }
62 |
63 | #input{
64 | border: 3px solid #ccc;
65 | width: 70%;
66 | font-size: 48px;
67 | display: inline-block;
68 | margin-right: 20px;
69 | outline: none;
70 | text-align: left;
71 | padding: 12px;
72 | border-radius: 10px;
73 | }
74 |
75 |
76 | #send-button{
77 | font-size: 48px;
78 | display: inline-block;
79 | text-align: center;
80 | background-color: #3f89ff;
81 | padding: 15px;
82 | border-radius: 10px;
83 | color: white;
84 | }
85 |
86 | #share{
87 | font-size: 40px;
88 | z-index: 100;
89 | position: absolute;
90 | display: inline-block;
91 | padding: 10px;
92 | top: 10px;
93 | right: 10px;
94 | background-color: #ccc;
95 | border-radius: 10px;
96 | opacity: 0.5;
97 | }
98 |
99 | #share:hover{
100 | background-color: #bdbdbd;
101 | opacity: 1;
102 | }
103 |
104 | .input-shape{
105 | position: relative;
106 | border: 10px solid #3f89ff;
107 | width: 300px;
108 | height: 90px;
109 | font-size: 50px;
110 | text-align: center;
111 | line-height: 90px;
112 | border-radius: 10px;
113 | outline: none;
114 | }
115 |
116 | #room-name{
117 | color: #bbb;
118 | margin: auto;
119 | display: block;
120 | margin-top: 30px;
121 | }
122 |
123 | #enter{
124 | margin: auto;
125 | color: #fff;
126 | background-color: #3f89ff;
127 | margin-top: 15px;
128 | }
129 |
130 | #lobby-section{
131 | text-align: center;
132 | font-size: 24px;
133 | line-height: 40px;
134 | margin-top: 15px;
135 | }
136 |
137 | #online-count{
138 | text-align: center;
139 | font-size: 24px;
140 | }
141 |
--------------------------------------------------------------------------------
/public/js/chatroom.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t,n){"use strict";n.r(t);
2 | /*!
3 | * escape-html
4 | * Copyright(c) 2012-2013 TJ Holowaychuk
5 | * Copyright(c) 2015 Andreas Lubbe
6 | * Copyright(c) 2015 Tiancheng "Timothy" Gu
7 | * MIT Licensed
8 | */
9 | const o=/["'&<>]/;
10 | /*!
11 | * sendverse
12 | * Copyright(c) 2019 Liam Ilan
13 | * MIT Licensed
14 | */
15 | const r=io.connect(window.location.href.toLowerCase()),a=document.getElementById("share"),c=document.getElementById("input"),i=document.getElementById("send-button"),l=document.getElementById("history"),s=`#${Math.floor(4095*Math.random()).toString(16).padStart(3,"0")}`;function d(e,t,n=!1){const r=document.createElement("div");r.className=`bubble ${n?"my-bubble":""}`,r.innerHTML=function(e){const t=`${e}`,n=o.exec(t);if(!n)return t;let r,a="",c=0,i=0;for(c=n.index;c10?"black":"white"}(e),l.appendChild(r),l.scrollTop=l.scrollHeight}function u(){const e={message:c.innerText,color:s};""!==e.message&&(d(e.color,c.innerText,!0),r.emit("message",e),c.innerText="")}i.addEventListener("click",u),c.addEventListener("paste",e=>{e.preventDefault();const t=(e.originalEvent||e).clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,t)}),c.addEventListener("keypress",e=>{"Enter"===e.key&&(e.preventDefault(),u())}),a.addEventListener("click",()=>{const e=document.createElement("textarea");if(e.contentEditable="true",e.readOnly="false",e.innerHTML=window.location.href,document.body.appendChild(e),navigator.platform.match(/iPhone|iPod|iPad/)){const t=document.createRange();t.selectNodeContents(e);const n=window.getSelection();n.removeAllRanges(),n.addRange(t),e.setSelectionRange(0,999999)}else e.select();document.execCommand("copy"),document.body.removeChild(e),a.innerHTML="Copied",window.setTimeout(()=>{a.innerHTML="Copy Link"},1e3)}),r.on("message",e=>{d(e.color,e.message)})}]);
--------------------------------------------------------------------------------
/src/chatroom.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * sendverse
3 | * Copyright(c) 2019 Liam Ilan
4 | * MIT Licensed
5 | */
6 |
7 | /* global io */
8 | /* Main */
9 | import escapeHtml from './escape-html';
10 |
11 | const socket = io.connect(window.location.href.toLowerCase());
12 |
13 | const share = document.getElementById('share');
14 | const input = document.getElementById('input');
15 | const sendButton = document.getElementById('send-button');
16 | const history = document.getElementById('history');
17 |
18 | // color of your messages. Each participant has their color chosen at random.
19 | const ourColor = `#${Math.floor(Math.random() * 4095).toString(16).padStart(3, '0')}`;
20 |
21 | // calculate the color the font should be depending on a background color
22 | // for a hex with a length of 4
23 | function fontColor(color) {
24 | const r = parseInt(color.charAt(1), 16);
25 | const g = parseInt(color.charAt(2), 16);
26 | const b = parseInt(color.charAt(3), 16);
27 | return Math.max(r, g, b) > 10 ? 'black' : 'white';
28 | }
29 |
30 | // add a bubble
31 | function addBubble(color, message, myBubble = false) {
32 | const bubble = document.createElement('div');
33 | bubble.className = `bubble ${myBubble ? 'my-bubble' : ''}`;
34 |
35 | // escape everything, before putting it in the UI
36 | bubble.innerHTML = escapeHtml(message);
37 | bubble.style.backgroundColor = color;
38 | bubble.style.color = fontColor(color);
39 |
40 | history.appendChild(bubble);
41 |
42 | // scroll down to keep recent messages in view
43 | history.scrollTop = history.scrollHeight;
44 | }
45 |
46 | // post a message
47 | function postMessage() {
48 | const data = { message: input.innerText, color: ourColor };
49 |
50 | if (data.message !== '') {
51 | addBubble(data.color, input.innerText, true);
52 |
53 | // emit the message
54 | socket.emit('message', data);
55 | input.innerText = '';
56 | }
57 | }
58 |
59 | // when you click the sendbutton, post the message
60 | sendButton.addEventListener('click', postMessage);
61 |
62 | // remove styling from copy paste
63 | input.addEventListener('paste', (e) => {
64 | // cancel paste
65 | e.preventDefault();
66 |
67 | // get text representation of clipboard
68 | const text = (e.originalEvent || e).clipboardData.getData('text/plain');
69 |
70 | // insert text manually
71 | document.execCommand('insertHTML', false, text);
72 | });
73 |
74 | // prevent enter from adding a line break in input so that Enter sends message.
75 | input.addEventListener('keypress', (e) => {
76 | if (e.key === 'Enter') {
77 | e.preventDefault();
78 | postMessage(e);
79 | }
80 | });
81 |
82 | // Copy Link functionality.
83 | share.addEventListener('click', () => {
84 | const element = document.createElement('textarea'); // set temp element.
85 | element.contentEditable = 'true';
86 | element.readOnly = 'false';
87 | element.innerHTML = window.location.href; // this is what we want copied.
88 | document.body.appendChild(element);
89 |
90 | if (navigator.platform.match(/iPhone|iPod|iPad/)) {
91 | const range = document.createRange();
92 | range.selectNodeContents(element);
93 |
94 | const selection = window.getSelection();
95 | selection.removeAllRanges();
96 | selection.addRange(range);
97 |
98 | element.setSelectionRange(0, 999999);
99 | } else {
100 | element.select();
101 | }
102 |
103 | document.execCommand('copy');
104 | document.body.removeChild(element); // cleanup.
105 |
106 | // button UI
107 | share.innerHTML = 'Copied';
108 | window.setTimeout(() => { share.innerHTML = 'Copy Link'; }, 1000);
109 | });
110 |
111 | // Listen to server events.
112 | socket.on('message', (data) => {
113 | addBubble(data.color, data.message);
114 | });
115 |
--------------------------------------------------------------------------------