Compare commits

...

18 commits

Author SHA1 Message Date
d3c07e19b8 Add cryptalk.service
Unit file for the Claytonia server.
2024-02-01 19:06:53 -05:00
6b6a25f28a Update 'README.md'
Changed screenshot
2023-11-13 17:34:53 -05:00
d2b779540b Update 'client/public/js/cryptalk.min.js'
Claytonia branding.
Added more help
2023-11-13 17:33:52 -05:00
3dda921db2 Update 'client/source/settings.js'
Claytonia branding 
Added more help
2023-11-13 17:33:01 -05:00
Hexagon
a9c0e7592e
Update FUNDING.yml 2023-02-03 12:19:22 +01:00
Hexagon
8ceb715798
Dependency update. Bump. 2022-03-30 00:45:02 +02:00
Hexagon
558b2dff35 Use PM2 for docker main process. Update deps. Bump. 2022-02-21 00:00:27 +01:00
Hexagon
c91a1c105b Dependency update 2022-01-31 21:04:22 +01:00
Hexagon
2197a1ef46 Dependency update, close #36. 2022-01-02 21:27:58 +01:00
Hexagon
d6b5baeb7c
Update README.md 2021-12-31 17:37:29 +01:00
Hexagon
2b7e5bcffe Update dev dependencies 2021-12-25 21:35:32 +01:00
Hexagon
d4e6c5cdb1 Update dependencies, bump version. 2021-12-12 21:14:18 +01:00
Hexagon
82a498a4ff Cleanup 2021-11-09 23:43:21 +01:00
Hexagon
6e8816c467 Dependency bump (serve 12->13). Add dependency checks to local build pipeline, do not run on ci builds. 2021-11-07 13:52:45 +01:00
Hexagon
28a8cf8b31
Create dependabot.yml 2021-11-07 13:20:31 +01:00
Hexagon
cd4e146aa9 Fix issue #4 2021-11-03 21:40:15 +01:00
Hexagon
2f74ef5ca7 CRLF -> LF in docker-entrypoint 2021-11-03 21:29:56 +01:00
Hexagon
9087608087 Minor fix 2021-11-03 21:17:16 +01:00
18 changed files with 3758 additions and 4025 deletions

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
github: [hexagon] github: [hexagon]
ko_fi: hexagon_56k

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View file

@ -27,4 +27,4 @@ jobs:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build:ci

View file

@ -1,7 +1,6 @@
FROM node:16-alpine FROM keymetrics/pm2:16-alpine
COPY . /usr/src/app COPY . /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN npm install --no-cache RUN npm install --no-cache --production
EXPOSE 8080 EXPOSE 8080
RUN chmod +x /usr/src/app/docker-entrypoint.sh CMD [ "pm2-runtime", "start", "pm2.json" ]
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh", "npm", "start"]

287
README.md
View file

@ -1,150 +1,137 @@
![cryptalk](/screenshot.png) ![cryptalk](https://i.imgur.com/1sH36n5.png)
![Node.js CI](https://github.com/Hexagon/cryptalk/workflows/Node.js%20CI/badge.svg?branch=master) ![Node.js CI](https://github.com/Hexagon/cryptalk/workflows/Node.js%20CI/badge.svg?branch=master)
[![npm version](https://badge.fury.io/js/cryptalk.svg)](https://badge.fury.io/js/cryptalk) [![npm version](https://badge.fury.io/js/cryptalk.svg)](https://badge.fury.io/js/cryptalk)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/753ef40cec1747c2b5025f834635375b)](https://www.codacy.com/gh/Hexagon/cryptalk/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Hexagon/cryptalk&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/753ef40cec1747c2b5025f834635375b)](https://www.codacy.com/gh/Hexagon/cryptalk/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Hexagon/cryptalk&utm_campaign=Badge_Grade)
Cyptalk is a HTML5/Node.js based, client side (E2EE) encrypted instant chat # Cryptalk
Cyptalk is a HTML5/Node.js based, client side (E2EE) encrypted instant chat
Features
======== ## Features
* Client side AES-256-CBC encryption/decryption (the server is just a messenger) * Client side AES-256-CBC encryption/decryption (the server is just a messenger)
* 256 bit key derived from your passphrase using PBKDF2 * 256 bit key derived from your passphrase using PBKDF2
* Messages torched after a configurable delay, default is 600s. * Messages torched after a configurable delay, default is 600s.
* Simple setup using npm, Docker or Heroku * Simple setup using npm, Docker or Heroku
* Notification sounds (mutable) * Notification sounds (mutable)
* Native popup notifications * Native popup notifications
* Configurable page title * Configurable page title
* Nicknames, optional. * Nicknames, optional.
* Quick-links using http://server/#Room:Passphrase, optional and insecure * Quick-links using http://server/#Room:Passphrase, optional and insecure
## Installing
Docker setup
======== ### Docker setup
To run latest cryptalk with docker, exposed on host port 80, simply run the following command to pull it from docker hub To run latest cryptalk with docker, exposed on host port 80, simply run the following command to pull it from docker hub
```bash ```bash
sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk
``` ```
### Heroku setup
Heroku setup
======== Click the button below
Click the button below [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/hexagon/cryptalk)
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/hexagon/cryptalk) ### Docker setup without using docker hub
Clone this repo, enter the new directory.
Docker setup without using docker hub Build image
======== ```bash
docker build . --tag="hexagon/cryptalk"
Clone this repo, enter the new directory. ```
Build image Run container, enable start on boot, expose to port 80 at host
```bash ```bash
docker build . --tag="hexagon/cryptalk" sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk
``` ```
Run container, enable start on boot, expose to port 80 at host Browse to ```http://<ip-of-server>/```
```bash
sudo docker run -d --restart=always -p 80:8080 hexagon/cryptalk Done!
```
### npm setup
Browse to ```http://<ip-of-server>/```
Install node.js, exact procedure is dependant on platform and distribution.
Done!
Install the app from npm
```bash
npm install cryptalk -g
npm setup ````
========
Then issue the following to start the app
Install node.js, exact procedure is dependant on platform and distribution.
```bash
Install the app from npm cryptalk
```bash ```
npm install cryptalk -g
```` Browse to ```http://localhost:8080```
Then issue the following to start the app Done!
```bash ## Usage
cryptalk
``` ```
Browse to ```http://localhost:8080``` Available commands:
Done! Client:
/key StrongPassphrase Sets encryption key
/nick NickName Sets an optional nick
/mute Audio on
Developer setup /unmute Audio off
======== /clear Clear on-screen buffer
/help This
Install node.js (development require >=12.0), exact procedure is dependant on platform and distribution. /title Set your local page title
/torch AfterSeconds Console messages are torched
Clone this repo after this amount of seconds
```bash (default 600).
git clone https://github.com/Hexagon/cryptalk.git
cd cryptalk Room:
``` /join RoomId Join a room
/leave Leave the room
Pull dependencies from npm /count Count participants
```bash
npm install Host:
``` /connect Connect to host
/disconnect Disconnect from host
Start server
```bash You can select any of the five last commands/messages with up/down key.
npn run start
``` Due to security reasons, /key command is not saved, and command
history is automatically cleared after one minute of inactivity.
Browse to ```http://localhost:8080```
It is highly recommended to use incognito mode while chatting,
To work on the JavaScript, edit the code in ```client/source/```. To test the changes, first run ```npm run build``` to lint, build and minify the code. Then restart the server. to prevent browsers from keeping history or cache.
Usage ```
========
## Development
```
Install node.js (development require >=12.0), exact procedure is dependant on platform and distribution.
Available commands:
Clone this repo
Client: ```bash
/key StrongPassphrase Sets encryption key git clone https://github.com/Hexagon/cryptalk.git
/nick NickName Sets an optional nick cd cryptalk
/mute Audio on ```
/unmute Audio off
/clear Clear on-screen buffer Pull dependencies from npm
/help This ```bash
/title Set your local page title npm install
/torch AfterSeconds Console messages are torched ```
after this amount of seconds
(default 600). Start server
```bash
Room: npm run start
/join RoomId Join a room ```
/leave Leave the room
/count Count participants Browse to ```http://localhost:8080```
Host: To work on the JavaScript, edit the code in ```client/source/```. To test the changes, first run ```npm run build``` to lint, build and minify the code. Then restart the server.
/connect Connect to host
/disconnect Disconnect from host
You can select any of the five last commands/messages with up/down key.
Due to security reasons, /key command is not saved, and command
history is automatically cleared after one minute of inactivity.
It is highly recommended to use incognito mode while chatting,
to prevent browsers from keeping history or cache.
```

View file

@ -1,32 +1,31 @@
/*------------------------------------*\
GENERIC
\*------------------------------------*/
html { html {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */ -moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */ box-sizing: border-box; /* Opera/IE 8+ */
} }
*, *:before, *:after { *,
*:before,
*:after {
box-sizing: inherit; box-sizing: inherit;
font-family: monospace, 'Courier New'; font-family: monospace, 'Courier New';
font-size: 12px; font-size: 12px;
} }
body, html { body,
html {
min-height: 100%; min-height: 100%;
min-width: 600px; min-width: 600px;
background-color: #181A1D; background-color: #181a1d;
overflow: hidden; overflow: hidden;
padding: 0px; padding: 0px;
margin:0px; margin: 0px;
color: #00DD00; color: #00dd00;
} }
.good { color: #99FF99; } .good { color: #99ff99; }
.bad { color: #ff7777; } .bad { color: #ff7777; }
.info { color: #99FFFF; } .info { color: #99ffff; }
.neutral { color: #eeeeee; } .neutral { color: #eeeeee; }
/*------------------------------------*\ /*------------------------------------*\
@ -38,8 +37,8 @@ body, html {
bottom: 40px; bottom: 40px;
position: absolute; position: absolute;
list-style-type: none; list-style-type: none;
padding:0; padding: 0;
margin:0; margin: 0;
} }
/* Messages */ /* Messages */
@ -50,7 +49,7 @@ body, html {
} }
#chat li .timestamp { #chat li .timestamp {
color: #A0A00A; color: #a0a00a;
} }
/* Message types */ /* Message types */
@ -58,24 +57,27 @@ body, html {
#chat i { #chat i {
font-style: normal; font-style: normal;
} }
#chat i.motd { color: #99FF99; display:inline-block; line-height: 12px !important; } #chat i.motd {
color: #99ff99;
display:inline-block;
line-height: 12px !important;
}
#chat i.info { color: #999999; } #chat i.info { color: #999999; }
#chat i.server { color: #99FFFF; } #chat i.server { color: #99ffff; }
#chat i.error { color: #ff7777; } #chat i.error { color: #ff7777; }
#chat i.message { color: #eeeeee; } #chat i.message { color: #eeeeee; }
#chat i.nick { color: #99FF99; } #chat i.nick { color: #99ff99; }
#chat i.fatal { color: #ff7777; } #chat i.fatal { color: #ff7777; }
/*------------------------------------*\ /*------------------------------------*\
INPUT & LOADER INPUT & LOADER
\*------------------------------------*/ \*------------------------------------*/
#input_wrapper { #input_wrapper {
right:0; right: 0;
bottom:0; bottom: 0;
left:0; left: 0;
position: absolute; position: absolute;
height: 30px;
height:30px;
} }
#input, #input,
@ -90,26 +92,35 @@ body, html {
padding: 5px 5px 5px 15px; padding: 5px 5px 5px 15px;
color: #FFFFFF; color: #ffffff;
background-color:#272A2E; background-color:#272a2e;
height:30px; height:30px;
border-top: 2px solid #153315; border-top: 2px solid #153315;
} }
#input { z-index: 1; } #input {
#loader { z-index: 0; line-height: 20px; font-size: 14px; font-weight: 100; font-family: tahoma;} z-index: 1;
}
#loader {
z-index: 0;
line-height: 20px;
font-size: 14px;
font-weight: 100;
font-family: tahoma;
}
.loading #loader {
z-index: 2;
}
/*------------------------------------*\
SPINNER
\*------------------------------------*/
.loading #loader { z-index: 2; }
.loading #loader span { .loading #loader span {
margin-left:-2px; margin-left:-2px;
-webkit-animation: rotation 1s infinite linear; -webkit-animation: rotation 1s infinite linear;
} }
@-webkit-keyframes rotation { @-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);} from { -webkit-transform: rotate(0deg); }
to {-webkit-transform: rotate(359deg);} to { -webkit-transform: rotate(359deg); }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -9,8 +9,7 @@ for(var k in proto) ElementArray.prototype[k] = proto[k];
// Create to actual dollar function // Create to actual dollar function
function Dollar (selector) { function Dollar (selector) {
var match, let matches = new ElementArray();
matches = new ElementArray();
if (selector !== undefined) { if (selector !== undefined) {
if (selector === document) { if (selector === document) {
@ -18,7 +17,8 @@ function Dollar (selector) {
} else if (selector === window) { } else if (selector === window) {
matches.push(window); matches.push(window);
} else { } else {
if ((match = document.querySelectorAll(selector))) { let match = document.querySelectorAll(selector);
if (match) {
for( var i=0; i < match.length; i++) { for( var i=0; i < match.length; i++) {
matches.push(match[i]); matches.push(match[i]);
} }

View file

@ -57,9 +57,9 @@ export default function (mediator, settings, templates) {
// Make sure the nick meets the length requirements // Make sure the nick meets the length requirements
if (payload.length > settings.nick.maxLen) { if (payload.length > settings.nick.maxLen) {
return mediator('console:error', $.template(templates.messages.nick_to_long, { nick_maxLen: settings.nick.maxLen } )); return mediator.emit('console:error', $.template(templates.messages.nick_to_long, { nick_maxLen: settings.nick.maxLen } ));
} else if (payload.length < settings.nick.minLen) { } else if (payload.length < settings.nick.minLen) {
return mediator('console:error', $.template(templates.messages.nick_to_short, {nick_minLen: settings.nick.minLen } )); return mediator.emit('console:error', $.template(templates.messages.nick_to_short, {nick_minLen: settings.nick.minLen } ));
} }
// Set nick // Set nick

View file

@ -128,20 +128,6 @@ export default function(mediator,settings,templates, sounds) {
components.input[0].removeAttribute('disabled'); components.input[0].removeAttribute('disabled');
components.inputWrapper[0].className = ''; components.inputWrapper[0].className = '';
components.input.focus(); components.input.focus();
},
_require: function (filepath, done) {
commands.lockInput();
commands.post('info', 'Requiring ' + filepath + '...');
require([filepath], function () {
commands.post('info', 'Successfully required ' + filepath + '.');
commands.unlockInput();
done();
}, function (e) {
commands.post('error', 'An error occurred while trying to load "' + filepath + '":\n' + e);
commands.unlockInput();
done();
});
} }
}, },
@ -156,7 +142,8 @@ export default function(mediator,settings,templates, sounds) {
// If the active element is not the input, focus on it and exit the function. // If the active element is not the input, focus on it and exit the function.
// Ignore this when ctrl and/or alt is pressed! // Ignore this when ctrl and/or alt is pressed!
if (!e.ctrlKey && !e.altKey && components.input[0] !== $.activeElement()) { if (!e.ctrlKey && !e.altKey && components.input[0] !== $.activeElement()) {
return components.input.focus(); components.input.focus();
return;
} }
// Return immediatly if the buffer is empty or if the hit key was not <enter> // Return immediatly if the buffer is empty or if the hit key was not <enter>
@ -176,7 +163,8 @@ export default function(mediator,settings,templates, sounds) {
payload, payload,
function(retvals, recipients) { function(retvals, recipients) {
if(!recipients) { if(!recipients) {
return commands.post('error', $.template(templates.messages.unrecognized_command, { commandName: command })); commands.post('error', $.template(templates.messages.unrecognized_command, { commandName: command }));
return;
} else { } else {
commands.clearInput(); commands.clearInput();
} }
@ -217,13 +205,11 @@ export default function(mediator,settings,templates, sounds) {
// Connect events // Connect events
for (var commandName in commands) { for (var commandName in commands) {
if (commandName !== '_require' && commandName !== 'post') { if (commandName !== 'post') {
mediator.on('console:' + commandName, commands[commandName]); mediator.on('console:' + commandName, commands[commandName]);
} }
} }
mediator.on('console:require', commands._require);
mediator.on('console:post', function (data) { mediator.on('console:post', function (data) {
commands.post(data.type, data.data, data.nick); commands.post(data.type, data.data, data.nick);
}); });

View file

@ -1,42 +1,34 @@
export default { export default {
title: 'Cryptalk - Online', title: 'Claytonia Chat',
ttl: 600000, ttl: 600000,
motd: '<pre>\n\n' + motd: '<pre>\n\n' +
'▄████▄ ██▀███ ▓██ ██▓ ██▓███ ▄▄▄█████▓ ▄▄▄ ██▓ ██ ▄█▀ \n' + ' Welcome to Claytonia Chat \n' +
'▒██▀ ▀█ ▓██ ▒ ██▒▒██ ██▒▓██░ ██▒▓ ██▒ ▓▒▒████▄ ▓██▒ ██▄█▒ \n' + ' Tip of the day: /help \n' +
'▒▓█ ▄ ▓██ ░▄█ ▒ ▒██ ██░▓██░ ██▓▒▒ ▓██░ ▒░▒██ ▀█▄ ▒██░ ▓███▄░ \n' + ' Public Room: /join Claytonia \n' +
'▒▓▓▄ ▄██▒▒██▀▀█▄ ░ ▐██▓░▒██▄█▓▒ ▒░ ▓██▓ ░ ░██▄▄▄▄██ ▒██░ ▓██ █▄ \n' + ' Public Key: /key Claytonia \n' +
'▒ ▓███▀ ░░██▓ ▒██▒ ░ ██▒▓░▒██▒ ░ ░ ▒██▒ ░ ▓█ ▓██▒░██████▒▒██▒ █▄ \n' + ' Everyone in the room must have the same key to decrypt messages. \n' +
'░ ░▒ ▒ ░░ ▒▓ ░▒▓░ ██▒▒▒ ▒▓▒░ ░ ░ ▒ ░░ ▒▒ ▓▒█░░ ▒░▓ ░▒ ▒▒ ▓▒ \n' + '----------------------------------------------------------------------' +
' ░ ▒ ░▒ ░ ▒░▓██ ░▒░ ░▒ ░ ░ ▒ ▒▒ ░░ ░ ▒ ░░ ░▒ ▒░ \n' + '</pre>',
'░ ░░ ░ ▒ ▒ ░░ ░░ ░ ░ ▒ ░ ░ ░ ░░ ░ \n' +
'░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ \n' + nick: {
'░ ░ ░ \n' + maxLen: 20,
' https://github.com/hexagon/cryptalk \n' + minLen: 2,
' \n' + },
' Tip of the day: /help \n' +
'----------------------------------------------------------------------' + key: {
'</pre>', maxLen: 1024,
minLen: 8,
nick: { },
maxLen: 20,
minLen: 2, room: {
}, minLen: 1,
maxLen: 64
key: { },
maxLen: 1024,
minLen: 8, notifications: {
}, maxOnePerMs: 3000
}
room: { };
minLen: 1,
maxLen: 64
},
notifications: {
maxOnePerMs: 3000
}
};

17
cryptalk.service Normal file
View file

@ -0,0 +1,17 @@
[Unit]
Description=Cryptalk Node.js App
Documentation=https://github.com/Hexagon/cryptalk
After=network.target
[Service]
ExecStart=/usr/bin/npm run start
WorkingDirectory=/home/cryptochat/cryptalk
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PATH=/usr/bin
User=cryptochat
Group=cryptochat
[Install]
WantedBy=multi-user.target

View file

@ -1,11 +0,0 @@
#!/bin/sh
set -e
# /usr/src/app/external-public kan be mounted as a volume on host to expose
# statics to be hosted by host nginx
if [ -d /usr/src/app/external-public ]; then
cp -R /usr/src/app/client/public/* /usr/src/app/external-public/
fi
exec "$@"

7223
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,18 @@
{ {
"name": "cryptalk", "name": "cryptalk",
"version": "1.2.2", "version": "1.2.9",
"description": "Encrypted HTML5/Node.JS instant chat", "description": "Encrypted HTML5/Node.JS instant chat",
"main": "server/server.js", "main": "server/server.js",
"preferGlobal": true, "preferGlobal": true,
"private": false, "private": false,
"scripts": { "scripts": {
"test": "echo \"No tests written yet\" && exit 0", "test": "echo \"No tests written yet\" && exit 0",
"build": "npm run test:lint && npx rollup -c rollup.config.js && npm run build:minify && npm run build:cleanup", "build": "npm update && npm outdated && npm run test:lint && npx rollup -c rollup.config.js && npm run build:minify && npm run build:cleanup",
"build:ci": "npm run test:lint && npx rollup -c rollup.config.js && npm run build:minify && npm run build:cleanup",
"build:minify": "uglifyjs client/public/js/cryptalk.js --source-map -o client/public/js/cryptalk.min.js", "build:minify": "uglifyjs client/public/js/cryptalk.js --source-map -o client/public/js/cryptalk.min.js",
"build:cleanup": "(rm client/public/js/cryptalk.js || del client\\public\\js\\cryptalk.js)", "build:cleanup": "(rm client/public/js/cryptalk.js || del client\\public\\js\\cryptalk.js)",
"test:lint": "eslint ./client/source/**/*.js ./server/*.js", "test:lint": "eslint ./client/source/**/*.js ./server/*.js",
"start": "node server/server.js" "start": "node ./server/server.js"
}, },
"keywords": [ "keywords": [
"cryptalk", "cryptalk",
@ -38,7 +39,7 @@
}, },
"bin": "./server/server.js", "bin": "./server/server.js",
"dependencies": { "dependencies": {
"serve": "^12.0.1", "serve": "^13.0.2",
"socket.io": "^4.3.1" "socket.io": "^4.3.1"
}, },
"os": [ "os": [

11
pm2.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "cryptalk",
"script": "server/server.js",
"instances": "1",
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
}

View file

@ -2,13 +2,13 @@ import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
module.exports = { module.exports = {
input: 'client/source/cryptalk.js', input: 'client/source/cryptalk.js',
output: { output: {
file: 'client/public/js/cryptalk.js', file: 'client/public/js/cryptalk.js',
format: 'iife' format: 'iife'
}, },
plugins: [ plugins: [
nodeResolve(), nodeResolve(),
commonjs() commonjs()
] ]
}; };