summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.prettierrc8
-rw-r--r--README.md16
-rw-r--r--accounts.js71
-rw-r--r--client.html97
-rw-r--r--commands.js207
-rw-r--r--commands/motd.js8
-rw-r--r--config.json9
-rw-r--r--index.js210
-rw-r--r--jsconfig.json44
-rw-r--r--lib.js2
-rw-r--r--package-lock.json120
-rw-r--r--package.json45
12 files changed, 594 insertions, 243 deletions
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..1491ea5
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+    "trailingComma": "es5",
+    "tabWidth": 4,
+    "semi": true,
+    "singleQuote": false,
+    "printWidth": 9999,
+    "endOfLine": "lf"
+}
diff --git a/README.md b/README.md
index e08f31f..87e2ecc 100644
--- a/README.md
+++ b/README.md
@@ -18,14 +18,14 @@ node index.js
 
 ## TODO
 
-- ~~Make commands be in an object instead of a switch statement~~
-- ~~/help command~~
-- ~~/about command~~
-- ~~Config (JSON)~~
-- ~~MOTD~~
-- ~~Custom commands (plugins)~~
-- ~~Auth (optional)~~
+-   ~~Make commands be in an object instead of a switch statement~~
+-   ~~/help command~~
+-   ~~/about command~~
+-   ~~Config (JSON)~~
+-   ~~MOTD~~
+-   ~~Custom commands (plugins)~~
+-   ~~Auth (optional)~~
 
 ## IN PROGRESS
 
-- JSON support (for clients and stuff)
+-   JSON support (for clients and stuff)
diff --git a/accounts.js b/accounts.js
index f45ab6e..27de609 100644
--- a/accounts.js
+++ b/accounts.js
@@ -1,31 +1,74 @@
-import { createHash } from "crypto"
-import cuid from 'cuid';
-import fs from "fs"
+import { createHash } from "crypto";
+import cuid from "cuid";
+import fs from "fs";
 
-if(!fs.existsSync("db")) fs.mkdirSync("db");
-if(!fs.existsSync("db/users.json")) fs.writeFileSync("db/users.json", "{}");
+if (!fs.existsSync("db")) fs.mkdirSync("db");
+if (!fs.existsSync("db/users.json")) fs.writeFileSync("db/users.json", "{}");
 
-const db = JSON.parse(fs.readFileSync("db/users.json").toString())
+const db = JSON.parse(fs.readFileSync("db/users.json").toString());
 
+/**
+ * Syncs DB to file
+ */
 function syncDB() {
-    fs.writeFileSync("db/users.json", JSON.stringify(db))
+    fs.writeFileSync("db/users.json", JSON.stringify(db));
 }
 
+/**
+ * Checks if account exists
+ * @param {String} username Username to check
+ * @returns {Boolean}
+ */
 export function checkAccount(username) {
-    return db[username] != undefined
+    return db[username] != undefined;
 }
 
-export function createAccount(username, password) {
-    let hashedPassword = createHash('sha256').update(password).digest('hex');
+/**
+ * Create an account
+ * @param {String} username The username
+ * @param {String} password The password
+ */
+export function createAccount(username, password, admin = false) {
+    let hashedPassword = createHash("sha256").update(password).digest("hex");
     db[username] = {
+        admin,
         id: cuid(),
         password: hashedPassword,
-        t: Number(new Date()) / 1000
+        ips: [],
+        t: Number(new Date()) / 1000,
     };
     syncDB();
 }
 
+/**
+ * Log IP address (for IP bans)
+ * @param {String} username Username
+ * @param {String} ip IP address
+ */
+export function logIP(username, ip) {
+    if (!db[username].ips) db[username].ips = [];
+    if (!db[username].ips.includes(ip)) db[username].ips.push(ip);
+    syncDB();
+}
+
+/**
+ * Check if password is correct
+ * @param {String} username The username
+ * @param {String} password The password
+ * @returns {Boolean}
+ */
 export function checkPassword(username, password) {
-    let hashedPassword = createHash('sha256').update(password).digest('hex');
-    return db[username]?.password === hashedPassword
-}
\ No newline at end of file
+    let hashedPassword = createHash("sha256").update(password).digest("hex");
+    return db[username]?.password === hashedPassword;
+}
+
+/**
+ * Get account data
+ * @param {String} username The username
+ * @returns {Object}
+ */
+export function getAccountData(username) {
+    let returnData = JSON.parse(JSON.stringify(db[username]));
+    returnData.password = null;
+    return returnData;
+}
diff --git a/client.html b/client.html
index db7a4d3..ed5f9d4 100644
--- a/client.html
+++ b/client.html
@@ -1,34 +1,63 @@
 <div class="container">
-    <div class="title">wsChat</div>
-    <div class="logs">
+    <div class="sidebar">
+        Channels
+        <ul id="channels"></ul>
     </div>
-    <div class="msg">
-        <form id="msg">
-            <input type="text" id="msg-content" autocomplete="off">
-            <input type="submit">
-        </form>
+    <div class="flex-col">
+        <div class="title">wsChat</div>
+        <div class="logs"></div>
+        <div class="msg">
+            <form id="msg">
+                <input type="text" id="msg-content" autocomplete="off" />
+                <input type="submit" />
+            </form>
+        </div>
     </div>
 </div>
 <script>
     const logs = document.querySelector(".logs");
     const msgForm = document.querySelector("#msg");
-    const server = prompt("Enter server URL", "ws://127.0.0.1:9933")
+    const server = prompt("Enter server URL", "ws://127.0.0.1:9933");
     const ws = new WebSocket(server); // CHANGE ME
-    ws.onopen = openEv => {
-        if(logs.innerText.length != 0) logs.innerText += "\n";
+    let gettingChannels = false;
+    let leavingAChannel = false;
+    ws.onopen = (openEv) => {
+        if (logs.innerText.length != 0) logs.innerText += "\n";
         logs.innerText += `Connected to wsChat server at ${ws.url}`;
         document.querySelector(".title").innerHTML += ` - ${new URL(ws.url).host}`;
-    }
-    ws.onmessage = msg => {;
+        gettingChannels = true;
+        ws.send(":jsonGet channels");
+    };
+    ws.onmessage = (msg) => {
         console.log(msg);
-        if(logs.innerText.length != 0) logs.innerText += "\n";
+        if (msg.data.startsWith(":json.channels>")) {
+            gettingChannels = false;
+            let channels = JSON.parse(String(msg.data).replace(":json.channels>", ""));
+            console.log(msg.data, "gh", channels);
+            channels.forEach((ch) => {
+                let elem = document.createElement("li");
+                elem.innerText = `#${ch}`;
+                elem.addEventListener("click", function (ev) {
+                    ws.send(`/join #${ch}`);
+                    leavingAChannel = true;
+                });
+                document.getElementById("channels").appendChild(elem);
+            });
+            return;
+        }
+        if (leavingAChannel) {
+            logs.innerText = "";
+            leavingAChannel = false;
+            return;
+        }
+        if (logs.innerText.length != 0) logs.innerText += "\n";
         logs.innerText += msg.data;
     };
-    ws.onclose = cls => {
-        if(logs.innerText.length != 0) logs.innerText += "\n";
-        logs.innerText += `Connection closed with code ${cls.code}, reason: ${cls.reason}`;
-    }
-    msgForm.addEventListener('submit', ev => {
+    ws.onclose = (cls) => {
+        if (logs.innerText.length != 0) logs.innerText += "\n";
+        logs.innerText += `Connection closed with code ${cls.code}, reason: ${cls.reason}. reload the page to reconnect`;
+    };
+    msgForm.addEventListener("submit", (ev) => {
         ev.preventDefault();
         const message = document.querySelector("#msg-content");
         ws.send(message.value);
@@ -39,31 +68,52 @@
     * {
         box-sizing: border-box;
     }
+
     body {
         margin: 0;
         width: 100%;
         min-height: 100vh;
         position: absolute;
     }
+
+    .sidebar {
+        padding: 1em;
+        border-right: 2px red solid;
+    }
+
+    .sidebar ul {
+        margin: 0;
+        padding: 0;
+        list-style: none;
+    }
+
     .container {
-        position: relative;
         display: flex;
-        flex-direction: column;
+        position: relative;
         height: calc(100vh - 3em);
         border: 2px red solid;
         margin: 1.5em;
     }
+
+    .flex-col {
+        flex-direction: column;
+        display: flex;
+        flex-grow: 1;
+    }
+
     .logs {
         flex-grow: 1;
         padding: 1em;
     }
+
     .title {
         border-bottom: 2px red solid;
         padding: 1em;
-        font-size:  larger;
-        font-weight:  bold;
+        font-size: larger;
+        font-weight: bold;
         text-align: center;
     }
+
     #msg {
         border-top: 2px red solid;
         padding: 1em;
@@ -72,7 +122,8 @@
         height: 2em;
         box-sizing: content-box;
     }
+
     #msg-content {
         flex-grow: 1;
     }
-</style>
\ No newline at end of file
+</style>
diff --git a/commands.js b/commands.js
index 3fbd63e..456c6a7 100644
--- a/commands.js
+++ b/commands.js
@@ -1,108 +1,161 @@
-import fs from "fs"
+import fs from "fs";
 
 export const commands = {
-    'join': {
-        name: 'join',
+    join: {
+        name: "join",
+        usage: "/join <channel>",
+        description: "Join a different channel",
         aliases: [],
-        command: function({user, server, args, sendInChannel}) {
-            if(args.length < 1) return user.socket.send("Error: You need to specify a channel (example: /join #home).");
-            if(!args[0].startsWith("#")) return user.socket.send("Error: Channel not found, run /channels to see a list of channels.");
-            if(!server.channels.includes(args[0].replace("#", ""))) return user.socket.send("Error: Channel not found, run /channels to see a list of channels.");
-            sendInChannel(`${user.name()} left #${user.channel}.`, user.channel)
+        command({ user, server, args, sendInChannel }) {
+            if (args.length < 1) return user.socket.send("Error: You need to specify a channel (example: /join #home).");
+            if (!args[0].startsWith("#")) return user.socket.send("Error: Channel not found, run /channels to see a list of channels.");
+            if (!server.channels.includes(args[0].replace("#", ""))) return user.socket.send("Error: Channel not found, run /channels to see a list of channels.");
+            sendInChannel(`${user.name()} left #${user.channel}.`, user.channel);
             user.channel = args[0].replace("#", "");
-            console.info(`${user.name()} went to #${user.channel}`)
-            sendInChannel(`${user.name()} joined #${user.channel}!`, user.channel)
-        }
+            server.updateUsers();
+            console.info(`${user.name()} went to #${user.channel}`);
+            sendInChannel(`${user.name()} joined #${user.channel}!`, user.channel);
+        },
     },
-    'channels': {
-        name: 'channels',
+    channels: {
+        name: "channels",
+        usage: "/channels",
+        description: "Shows all channels",
         aliases: [],
-        command: function({user, server}) {
-            user.socket.send(`Channels:\n${server.channels.map(ch => ` * #${ch}`).join("\n")}`)
-        }
+        command({ user, server }) {
+            user.socket.send(`Channels:\n${server.channels.map((ch) => ` * #${ch}`).join("\n")}`);
+        },
     },
-    'nick': {
-        name: 'nick',
-        aliases: ['nickname', 'name'],
-        command: function({user, server, args, sendInChannel}) {
-            if(args.length < 1) return user.socket.send("Error: You need to specify a nick (example: /nick WlodekM).");
-            if(args[0].length < 3 ) return user.socket.send("Error: Nick too short.");
-            if(args[0].length > 20) return user.socket.send("Error: Nick too long.");
-            if(Object.values(server.users).find(usr => usr.username == args[0])) return user.socket.send("Error: Nick already used.");
-            sendInChannel(`${user.name()} changed their nick to ${args[0]}!`, user.channel)
+    nick: {
+        name: "nick",
+        usage: "/nick <nickname>",
+        description: "Change your nickname",
+        aliases: ["nickname", "name"],
+        command({ user, server, args, sendInChannel }) {
+            if (args.length < 1) return user.socket.send("Error: You need to specify a nick (example: /nick WlodekM).");
+            if (args[0].length < 3) return user.socket.send("Error: Nick too short.");
+            if (args[0].length > 20) return user.socket.send("Error: Nick too long.");
+            if (Object.values(server.users).find((usr) => usr.username == args[0])) return user.socket.send("Error: Nick already used.");
+            sendInChannel(`${user.name()} changed their nick to ${args[0]}!`, user.channel);
+            server.updateUsers();
             user.nickname = args[0];
-        }
+        },
     },
-    'about': {
-        name: 'about',
+    about: {
+        name: "about",
+        usage: "/about",
+        description: "Shows info about wsChat",
         aliases: [],
-        command: function({user, args, sendInChannel}) {
-            user.socket.send(`wsChat v${JSON.parse(String(fs.readFileSync("package.json"))).version}\nGithub: https://github.com/WlodekM/wsChat`)
-        }
+        command({ user, args, sendInChannel }) {
+            user.socket.send(`wsChat v${JSON.parse(String(fs.readFileSync("package.json"))).version}\nGithub: https://github.com/WlodekM/wsChat`);
+        },
     },
-    'whois': {
-        name: 'whois',
+    whois: {
+        name: "whois",
+        usage: "/whois <username>",
+        description: "Shows info about a person",
         aliases: [],
-        command: function({user, server, args}) {
-            if(args.length < 1) return user.socket.send('Please provide username');
-            if(!Object.values(server.users).find(usr => usr.username == args[0])) return user.socket.send('User not found');
-            let userFound = Object.values(server.users).find(usr => usr.username == args[0])
-            userFound.id = Object.keys(server.users).find(usr => server.users[usr].username == args[0])
-            user.socket.send(`${userFound.username}\nClient: ${userFound.client ?? "<Unknown>"}\nID: ${userFound.id}`)
-        }
+        command({ user, server, args }) {
+            if (args.length < 1) return user.socket.send("Please provide username");
+            if (!Object.values(server.users).find((usr) => usr.username == args[0])) return user.socket.send("User not found");
+            let userFound = Object.values(server.users).find((usr) => usr.username == args[0]);
+            userFound.id = Object.keys(server.users).find((usr) => server.users[usr].username == args[0]);
+            user.socket.send(`${userFound.username}\nClient: ${userFound.client ?? "<Unknown>"}\nID: ${userFound.id}`);
+        },
     },
-    'users': {
-        name: 'users',
+    users: {
+        name: "users",
+        usage: "/users",
+        description: "Shows all users online",
         aliases: [],
-        command: function({user, server, args}) {
-            user.socket.send(`Users${args[0] != "global" ? ` in ${user.channel}` : ""}:\n${Object.values(server.users).filter(usr => (usr.channel == user.channel) || args[0] == "global").map(usr => ` * ${usr.name()}`).join("\n")}`)
-        }
+        command({ user, server, args }) {
+            user.socket.send(
+                `Users${args[0] != "global" ? ` in ${user.channel}` : ""}:\n${Object.values(server.users)
+                    .filter((usr) => usr.channel == user.channel || args[0] == "global")
+                    .map((usr) => ` * ${usr.name()}`)
+                    .join("\n")}`
+            );
+        },
     },
-    'help': {
-        name: 'help',
-        aliases: ['?'],
-        command: function({user, args, commands}) {
-            user.socket.send(`Commands available:\n${Object.values(commands).map(cmd => `* /${cmd.name} (Aliases: ${(cmd.aliases.join(", ")) || "<None>"})`).join("\n")}`)
-        }
+    help: {
+        name: "help",
+        usage: "/?",
+        description: "Shows all commands",
+        aliases: ["?"],
+        command({ user, args, commands }) {
+            user.socket.send(
+                `Commands available:\n${Object.values(commands)
+                    .map((cmd) => `* /${cmd.name} (Aliases: ${cmd.aliases.join(", ") || "<None>"})`)
+                    .join("\n")}`
+            );
+        },
     },
-    'login': {
-        name: 'login',
-        aliases: ['signin'],
-        command: function({server, args, user, sendInChannel}) {
-            if(args.length < 2) return user.socket.send(`Usage: /login <username> <password>`);
-            if(!server.accounts.checkAccount(args[0])) return user.socket.send(`Account "${args[0]}" not found!`);
-            if(!server.accounts.checkPassword(args[0], args[1])) return user.socket.send(`Password incorrect.`);
+    login: {
+        name: "login",
+        usage: "/login <username> <password>",
+        description: "Log into an account",
+        aliases: ["signin"],
+        command({ server, args, user, sendInChannel }) {
+            if (args.length < 2) return user.socket.send(`Usage: /login <username> <password>`);
+            if (!server.accounts.checkAccount(args[0])) return user.socket.send(`Account "${args[0]}" not found!`);
+            if (!server.accounts.checkPassword(args[0], args[1])) return user.socket.send(`Password incorrect.`);
+            if (server.config.saveIP) server.accounts.logIP(args[0], user.ip);
+            sendInChannel(`${user.name()} logged in as ${args[0]}!`, user.channel);
             user.username = args[0];
             user.nickname = "";
             user.guest = false;
-            sendInChannel(`${user.name()} logged in as ${args[0]}!`, user.channel)
-        }
+            user.admin = server.accounts.getAccountData(user.username).admin ?? false;
+            server.updateUsers();
+        },
     },
-    'register': {
-        name: 'register',
-        aliases: ['signup'],
-        command: function({server, args, user, sendInChannel}) {
-            if(args.length < 2) return user.socket.send(`Usage: /register <username> <password>`);
-            if(args[0].length < 3)  return user.socket.send(`Username too short!`);
-            if(args[0].length > 20) return user.socket.send(`Username too long!`);
-            if(args[1].length < 6)  return user.socket.send(`Password too short!`);
-            if(server.accounts.checkAccount(args[0])) return user.socket.send(`User with username "${args[0]}" already exists!`);
+    register: {
+        name: "register",
+        usage: "/register <username> <password>",
+        description: "Make a new account",
+        aliases: ["signup"],
+        command({ server, args, user, sendInChannel }) {
+            if (args.length < 2) return user.socket.send(`Usage: /register <username> <password>`);
+            if (args[0].length < 3) return user.socket.send(`Username too short!`);
+            if (args[0].length > 20) return user.socket.send(`Username too long!`);
+            if (args[1].length < 6) return user.socket.send(`Password too short!`);
+            if (server.accounts.checkAccount(args[0])) return user.socket.send(`User with username "${args[0]}" already exists!`);
             server.accounts.createAccount(args[0], args[1]);
+            if (server.config.saveIP) server.accounts.logIP(args[0], user.ip);
+            sendInChannel(`${user.name()} logged in as ${args[0]}!`, user.channel);
             user.username = args[0];
             user.nickname = "";
             user.guest = false;
-            sendInChannel(`${user.name()} logged in as ${args[0]}!`, user.channel)
-        }
+            user.admin = server.accounts.getAccountData(user.username).admin ?? false;
+            server.updateUsers();
+        },
     },
-}
+    pm: {
+        name: "pm",
+        usage: "/pm <user> [message]",
+        description: "Send a private message to a user",
+        aliases: [],
+        command({ server, args, user, sendInChannel }) {
+            if (args.length < 1) return user.socket.send("Please provide username");
+            if (!Object.values(server.users).find((usr) => usr.username == args[0])) return user.socket.send("User not found");
+            let userFound = Object.values(server.users).find((usr) => usr.username == args[0]);
+            userFound.id = Object.keys(server.users).find((usr) => server.users[usr].username == args[0]);
+            args.shift();
+            userFound.socket.send(`${user.username} -> You : ${args.join(" ")}`);
+            user.socket.send(`You -> ${userFound.username} : ${args.join(" ")}`);
+        },
+    },
+};
 
 export function register(cmd, data) {
-    commands[cmd] = data
+    commands[cmd] = data;
 }
 
-let commandFiles = fs.readdirSync("commands").filter(filename => filename.endsWith(".js")).map(file => file.replace(/\.js$/gmi, ''))
+let commandFiles = fs
+    .readdirSync("commands")
+    .filter((filename) => filename.endsWith(".js"))
+    .map((file) => file.replace(/\.js$/gim, ""));
 for (let i = 0; i < commandFiles.length; i++) {
     const cmdName = commandFiles[i];
-    const cmd = (await import(`./commands/${cmdName}.js`)).default
-    register(cmdName, cmd)
-}
\ No newline at end of file
+    const cmd = (await import(`./commands/${cmdName}.js`)).default;
+    register(cmdName, cmd);
+}
diff --git a/commands/motd.js b/commands/motd.js
index 740cb07..a4e7fde 100644
--- a/commands/motd.js
+++ b/commands/motd.js
@@ -1,7 +1,7 @@
 export default {
-    name: 'motd',
+    name: "motd",
     aliases: [],
-    command: function({user, server}) {
+    command: function ({ user, server }) {
         user.socket.send("MOTD: " + server.format(server.config.motd));
-    }
-}
\ No newline at end of file
+    },
+};
diff --git a/config.json b/config.json
index 884e06d..e8b3745 100644
--- a/config.json
+++ b/config.json
@@ -1,7 +1,12 @@
 {
     "name": "wsChat server",
-    "motd": "Welcome to $(serverName)$\nRun /help to see a list of commands",
+    "motd": "Welcome to $(serverName)$\nRun /help to see a list of commands\nHUGE CLIENT UPDATE\n- Client now supports links!\n- You can now {b}style{/b} your {i}messages{/i} with \\{b} or \\{i} tags!\n- {c#ff1111}Color support{/c} with {c#22aaff}\\{c#HEXCOLOR}{/c} tags",
     "max": 20,
+    "owner": "WlodekM",
+    "saveIP": false,
+    "requireLogin": true,
     "profanity": false,
+    "profanityRemoveWords": ["butt", "arse", "balls", "dick"],
+    "profanityAddWords": ["kys"],
     "fullMessage": "Sorry, but the server is full right now, come back later"
-}
\ No newline at end of file
+}
diff --git a/index.js b/index.js
index ed778b0..10c7a88 100644
--- a/index.js
+++ b/index.js
@@ -1,18 +1,20 @@
 import { WebSocketServer } from "ws";
-import { getRandomInt } from "./lib.js"
-import { profanity } from '@2toad/profanity';
+import { getRandomInt } from "./lib.js";
+import { profanity } from "@2toad/profanity";
 import { commands } from "./commands.js";
-import * as accounts from "./accounts.js"
-import cuid from 'cuid';
-import fs from 'fs';
-profanity.options.grawlixChar = "*"
+import * as accounts from "./accounts.js";
+import cuid from "cuid";
+import fs from "fs";
 
 const server = {
     config: JSON.parse(String(fs.readFileSync("config.json"))),
     channels: ["home", "off-topic"],
     users: {},
     accounts: accounts,
-}
+};
+
+if (server.config.profanityRemoveWords) profanity.removeWords(server.config.profanityRemoveWords);
+if (server.config.profanityAddWords) profanity.addWords(server.config.profanityAddWords);
 
 const ws = new WebSocketServer({
     port: 9933,
@@ -21,64 +23,106 @@ const ws = new WebSocketServer({
 function sendInChannel(msg, channel) {
     for (const userID in server.users) {
         const user = server.users[userID];
-        if (user.channel == channel) user.socket.send(msg)
+        if (user.channel == channel) user.socket.send(msg);
     }
 }
 
 function format(txt) {
-    txt = String(txt)
-    txt = txt.replaceAll("$(serverName)$", server.config.name)
-    txt = txt.replaceAll("$(userCount)$", Object.keys(server.users).length)
-    txt = txt.replaceAll("$(max)$", server.config.max)
-    return txt
+    txt = String(txt);
+    txt = txt.replaceAll("$(serverName)$", server.config.name);
+    txt = txt.replaceAll("$(userCount)$", Object.keys(server.users).length);
+    txt = txt.replaceAll("$(max)$", server.config.max);
+    return txt;
+}
+
+function updateUsers() {
+    Object.keys(server.users).forEach((user) => {
+        if (user.subscribedToUsers) {
+            user.socket.send(
+                `:json.sub<users>:${JSON.stringify(
+                    Object.values(server.users).map((usr) => {
+                        return {
+                            username: usr.username,
+                            nickname: usr.nickname,
+                            t: usr.t,
+                            channel: user.channel,
+                            displayName: user.name(),
+                        };
+                    })
+                )}`
+            );
+        }
+    });
 }
 
-server.format = format
+server.updateUsers = updateUsers;
+server.format = format;
 
-ws.on('connection', (socket, request) => {
+ws.on("connection", (socket, request) => {
     if (server.config.max && Object.keys(server.users).length >= server.config.max) {
-        socket.send(format(server.config.fullMessage ?? "Sorry, but the server is full right now, come back later"))
-        socket.close(1001, "Server full")
+        socket.send(format(server.config.fullMessage ?? "Sorry, but the server is full right now, come back later"));
+        socket.close(1001, "Server full");
+        return;
     }
-    let userID = cuid()
-    console.info(`${userID} joined the server.`)
-    socket.send(format(server.config.motd))
-    let anonID = getRandomInt(0, 99999)
+    let userID = cuid();
+    console.info(`${userID} joined the server.`);
+    socket.send(format(server.config.motd));
+    let ip = request.headers["x-forwarded-for"] || request.socket.remoteAddress;
+    console.log(request.headers["x-forwarded-for"], request.socket.remoteAddress);
+    let anonID = getRandomInt(0, 99999);
     server.users[userID] = {
         username: `Anonymous${"0".repeat(5 - anonID.toString().length) + anonID.toString()}`,
         nickname: `Anonymous${"0".repeat(5 - anonID.toString().length) + anonID.toString()}`,
         guest: true,
         socket: socket,
         joinReq: request,
+        ip: ip,
         t: {
             js: Number(new Date()),
             unix: Math.floor(new Date().getTime() / 1000),
-            str: String(new Date())
+            str: String(new Date()),
         },
-        channel: 'home',
-        name: function(){return this.nickname != "" ? this.nickname : this.username}
+        channel: "home",
+        name: function () {
+            return this.nickname != "" ? this.nickname : this.username;
+        },
+    };
+    const user = server.users[userID];
+    let ipBanList = JSON.parse(String(fs.readFileSync("db/bannedIps.json")));
+    if (ipBanList[user.ip]) {
+        socket.send("Your IP is banned for " + ipBanList[user.ip]);
+        socket.close(1002, "Banned");
+        return;
     }
-    const user = server.users[userID]
-    console.info(`${user.name()} joined the server!`)
-    sendInChannel(`${user.name()} joined #${server.users[userID].channel}!`, server.users[userID].channel)
-    socket.on('close', function (code, reason) {
-        sendInChannel(`${user.name()} left.`, server.users[userID].channel)
-        delete server.users[userID]
-    })
-    socket.on('message', function (rawData) {
+    console.info(`${user.name()} joined the server!`);
+    sendInChannel(`${user.name()} joined.`, server.users[userID].channel);
+    server.updateUsers();
+    socket.on("close", function (code, reason) {
+        sendInChannel(`${user.name()} left.`, server.users[userID].channel);
+        server.updateUsers();
+        delete server.users[userID];
+    });
+    socket.on("message", function (rawData) {
         if (rawData.toString().startsWith("/")) {
             let args = String(rawData).replace("/", "").split(" ");
             let command = args.shift();
-            let commandObj = Object.values(commands).find(cmd => cmd.name == command || cmd.aliases.includes(command))
-            console.log(`${user.name()} used /${command}`)
+            let commandObj = Object.values(commands).find((cmd) => cmd.name == command || cmd.aliases.includes(command));
+            console.log(`${user.name()} used /${command}`);
             if (!commandObj) return socket.send(`Error: Command "${command}" not found!`);
             try {
-                commandObj.command({ user, command, args, sendInChannel, server, commands })
+                commandObj.command({
+                    user,
+                    command,
+                    args,
+                    sendInChannel,
+                    server,
+                    commands,
+                });
             } catch (error) {
-                console.error(error)
-                user.socket.send(`Unexpected error ocurred while running the command`)
+                console.error(error);
+                user.socket.send(`Unexpected error ocurred while running the command`);
             }
-            return
+            return;
         }
         if (rawData.toString().startsWith(":client")) {
             let client = String(rawData).replace(":client", "");
@@ -86,53 +130,79 @@ ws.on('connection', (socket, request) => {
             if (client.length < 2) return socket.send("Error: client info too short!");
             if (client.length >= 100) return socket.send("Error: client info too long!");
             server.users[userID].client = client;
-            return
+            return;
         }
         if (rawData.toString().startsWith(":jsonGet")) {
             let params = String(rawData).split(" ");
-            params.shift()
+            params.shift();
             switch (params[0]) {
-                case 'channels':
-                    socket.send(JSON.stringify(server.channels));
+                case "channels":
+                    socket.send(":json.channels>" + JSON.stringify(server.channels));
                     break;
-                case 'users':
-                    socket.send(JSON.stringify(Object.values(server.users).map(usr => {return {
-                        username: usr.username,
-                        nickname: usr.nickname,
-                        t: usr.t,
-                        channel: user.channel,
-                        displayName: user.name()
-                    }})));
+                case "users":
+                    socket.send(
+                        ":json.users>" +
+                            JSON.stringify(
+                                Object.values(server.users).map((usr) => {
+                                    return {
+                                        username: usr.username,
+                                        nickname: usr.nickname,
+                                        t: usr.t,
+                                        channel: user.channel,
+                                        displayName: user.name(),
+                                    };
+                                })
+                            )
+                    );
                     break;
-                case 'usersLocal':
+                case "usersLocal":
                     socket.send(
                         JSON.stringify(
                             Object.values(server.users)
-                            .filter(usr => (usr.channel == user.channel))
-                            .map(usr => {return {
-                                username: usr.username,
-                                nickname: usr.nickname,
-                                t: usr.t,
-                                channel: user.channel,
-                                displayName: user.name()
-                            }})
-                        ));
+                                .filter((usr) => usr.channel == user.channel)
+                                .map((usr) => {
+                                    return {
+                                        username: usr.username,
+                                        nickname: usr.nickname,
+                                        t: usr.t,
+                                        channel: user.channel,
+                                        displayName: user.name(),
+                                    };
+                                })
+                        )
+                    );
                     break;
-            
+
                 default:
                     socket.send(`unknown "${params[0]}"`);
                     break;
             }
-            return
+            return;
         }
+        if (rawData.toString().startsWith(":jsonSubscribe")) {
+            let params = String(rawData).split(" ");
+            params.shift();
+            switch (params[0]) {
+                case "users":
+                    user.subscribedToUsers = true;
+                    break;
+
+                default:
+                    socket.send(`unknown "${params[0]}"`);
+                    break;
+            }
+            return;
+        }
+        if (server.config.requireLogin) return socket.send("This server requires you to log in, use /login <username> <password> to log in or /register <username> <password> to make an account.");
+        profanity.options.grawlixChar = "*";
+        if (!server.config.profanity) rawData = profanity.censor(String(rawData));
         if (rawData.length < 1) return socket.send("Error: message too short!");
         if (rawData.length >= 2000) return socket.send("Error: message too long!");
-        if (!server.profanity) rawData = profanity.censor(String(rawData));
-        sendInChannel(`<${user.name()}${user.guest ? " (guest)" : ""}> ${rawData}`, server.users[userID].channel)
-        console.log(`(#${server.users[userID].channel}) <${user.name()}> ${rawData}`)
-    })
-})
+        sendInChannel(`${user.admin ? '[ADMIN] ' : ''}<${user.name()}${user.guest ? " (guest)" : ""}> ${rawData}`, server.users[userID].channel);
+        console.log(`(#${server.users[userID].channel}) <${user.name()}> ${rawData}`);
+    });
+});
 
-ws.on('listening', () => {
-    console.info("[INFO] Server started")
-})
+ws.on("listening", () => {
+    console.info("[INFO] Server started");
+});
diff --git a/jsconfig.json b/jsconfig.json
index 238655f..dfef62d 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -1,27 +1,27 @@
 {
-  "compilerOptions": {
-    // Enable latest features
-    "lib": ["ESNext", "DOM"],
-    "target": "ESNext",
-    "module": "ESNext",
-    "moduleDetection": "force",
-    "jsx": "react-jsx",
-    "allowJs": true,
+    "compilerOptions": {
+        // Enable latest features
+        "lib": ["ESNext", "DOM"],
+        "target": "ESNext",
+        "module": "ESNext",
+        "moduleDetection": "force",
+        "jsx": "react-jsx",
+        "allowJs": true,
 
-    // Bundler mode
-    "moduleResolution": "bundler",
-    "allowImportingTsExtensions": true,
-    "verbatimModuleSyntax": true,
-    "noEmit": true,
+        // Bundler mode
+        "moduleResolution": "bundler",
+        "allowImportingTsExtensions": true,
+        "verbatimModuleSyntax": true,
+        "noEmit": true,
 
-    // Best practices
-    "strict": true,
-    "skipLibCheck": true,
-    "noFallthroughCasesInSwitch": true,
+        // Best practices
+        "strict": true,
+        "skipLibCheck": true,
+        "noFallthroughCasesInSwitch": true,
 
-    // Some stricter flags (disabled by default)
-    "noUnusedLocals": false,
-    "noUnusedParameters": false,
-    "noPropertyAccessFromIndexSignature": false
-  }
+        // Some stricter flags (disabled by default)
+        "noUnusedLocals": false,
+        "noUnusedParameters": false,
+        "noPropertyAccessFromIndexSignature": false
+    }
 }
diff --git a/lib.js b/lib.js
index 64b0d6c..9038650 100644
--- a/lib.js
+++ b/lib.js
@@ -12,4 +12,4 @@ export function getRandomInt(min, max) {
     min = Math.ceil(min);
     max = Math.floor(max);
     return Math.floor(Math.random() * (max - min + 1)) + min;
-}
\ No newline at end of file
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..120eefe
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,120 @@
+{
+    "name": "wschat",
+    "version": "1.3.0",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "wschat",
+            "version": "1.3.0",
+            "dependencies": {
+                "@2toad/profanity": "^2.2.0",
+                "cuid": "^3.0.0",
+                "ws": "^8.17.1"
+            },
+            "devDependencies": {
+                "@types/bun": "latest",
+                "prettier": "3.3.3"
+            },
+            "peerDependencies": {
+                "typescript": "^5.0.0"
+            }
+        },
+        "node_modules/@2toad/profanity": {
+            "version": "2.2.0",
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@types/bun": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.1.6.tgz",
+            "integrity": "sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA==",
+            "dev": true,
+            "dependencies": {
+                "bun-types": "1.1.17"
+            }
+        },
+        "node_modules/@types/node": {
+            "version": "20.12.14",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "undici-types": "~5.26.4"
+            }
+        },
+        "node_modules/@types/ws": {
+            "version": "8.5.10",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/bun-types": {
+            "version": "1.1.17",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "~20.12.8",
+                "@types/ws": "~8.5.10"
+            }
+        },
+        "node_modules/cuid": {
+            "version": "3.0.0",
+            "license": "MIT"
+        },
+        "node_modules/prettier": {
+            "version": "3.3.3",
+            "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+            "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+            "dev": true,
+            "bin": {
+                "prettier": "bin/prettier.cjs"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/prettier/prettier?sponsor=1"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "5.5.3",
+            "license": "Apache-2.0",
+            "peer": true,
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=14.17"
+            }
+        },
+        "node_modules/undici-types": {
+            "version": "5.26.5",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/ws": {
+            "version": "8.17.1",
+            "license": "MIT",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "peerDependencies": {
+                "bufferutil": "^4.0.1",
+                "utf-8-validate": ">=5.0.2"
+            },
+            "peerDependenciesMeta": {
+                "bufferutil": {
+                    "optional": true
+                },
+                "utf-8-validate": {
+                    "optional": true
+                }
+            }
+        }
+    }
+}
diff --git a/package.json b/package.json
index a81284c..62fed1c 100644
--- a/package.json
+++ b/package.json
@@ -1,23 +1,24 @@
 {
-  "name": "wschat",
-  "description": "A simple IRC-like chat made using nodejs and webSockets",
-  "version": "1.2.5",
-  "author": {
-    "name": "WlodekM",
-    "email": "[email protected]",
-    "url": "https://wlodekm.nekoweb.org"
-  },
-  "module": "index.js",
-  "type": "module",
-  "devDependencies": {
-    "@types/bun": "latest"
-  },
-  "peerDependencies": {
-    "typescript": "^5.0.0"
-  },
-  "dependencies": {
-    "@2toad/profanity": "^2.2.0",
-    "cuid": "^3.0.0",
-    "ws": "^8.17.1"
-  }
-}
\ No newline at end of file
+    "name": "wschat",
+    "description": "A simple IRC-like chat made using nodejs and webSockets",
+    "version": "1.3.0",
+    "author": {
+        "name": "WlodekM",
+        "email": "[email protected]",
+        "url": "https://wlodekm.nekoweb.org"
+    },
+    "module": "index.js",
+    "type": "module",
+    "devDependencies": {
+        "@types/bun": "latest",
+        "prettier": "3.3.3"
+    },
+    "peerDependencies": {
+        "typescript": "^5.0.0"
+    },
+    "dependencies": {
+        "@2toad/profanity": "^2.2.0",
+        "cuid": "^3.0.0",
+        "ws": "^8.17.1"
+    }
+}