├── src └── app │ └── config │ └── config.js ├── .gitignore ├── package.json ├── .dockerignore ├── README.md └── index.js /src/app/config/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | 4 | module.exports = { 5 | socket_port: process.env.SOCKET_PORT || 8080, 6 | origin: process.env.LOCALHOST_ORIGIN || "https://www.amahanechat.org", 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #.env 26 | .env 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.jpchat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dotenv": "^16.0.3", 14 | "express": "^4.18.2", 15 | "nodemon": "^2.0.22", 16 | "socket.io": "^4.6.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # The direction of \ is different in windows and mac 5 | # Windows: \, Mac:/ 6 | 7 | # dependencies 8 | node_modules 9 | .pnp 10 | **\.pnp.js 11 | 12 | # testing 13 | coverage 14 | 15 | # production 16 | build 17 | 18 | # misc 19 | **\.DS_Store 20 | **\.env.local 21 | **\.env.development.local 22 | **\.env.test.local 23 | **\.env.production.local 24 | 25 | **\npm-debug.log* 26 | **\yarn-debug.log* 27 | **\yarn-error.log* 28 | 29 | #fly.io 30 | **\fly.toml 31 | 32 | #.env 33 | **\.env 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmahaneChat - Socket Server 2 | [Official Website](https://amahanechat.org/register) 3 | 4 | # Description 5 | This repository serves as the WebSocket server for Amahane Chat, a chat platform designed for anime and manga enthusiasts. The server is responsible for real-time messaging, handling WebSocket connections, and video call signaling. 6 | 7 | # Features 8 | - `User Management`: Keeps track of connected users. 9 | - `Real-Time Messaging`: Allows text and sticker messages. 10 | - `Conversation Management`: Supports the addition of new conversations. 11 | - `Video Calls`: Manages signaling for peer-to-peer video calls. 12 | 13 | # Technologies Used 14 | - `Node.js` 15 | - `Express` 16 | - `Socket.io` 17 | 18 | # Project Structure 19 | The Amahane Chat project is split across multiple repositories: 20 | 21 | - **Frontend**: [fe.jpchat](https://github.com/animedaisuki/fe.jpchat) 22 | - **WebSocket Server**: [so.amahanechat](https://github.com/animedaisuki/socket.amahanechat) (Current repository) 23 | - **Backend**: be.jpchat (Private, not publicly accessible at this time) 24 | 25 | # Installation 26 | Clone the Repository 27 | ```bash 28 | git clone git@github.com:animedaisuki/so.amahanechat.git 29 | ``` 30 | 31 | Install Dependencies 32 | Navigate to the project directory and run: 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | Edit the .env file to include necessary environment variables like SOCKET_PORT and CORS_ORIGIN. 38 | 39 | Run the Server 40 | To start the WebSocket server, navigate to the project directory and run: 41 | ```bash 42 | npm start 43 | ``` 44 | 45 | The server should now be running and waiting for WebSocket connections on the specified port. 46 | 47 | # Events Handled 48 | - `addUser`: Adds a new user and their socket ID to a list of connected users. 49 | - `sendMessage`: Handles sending messages between users. 50 | - `addConversation`: Notifies users when a new conversation has been initiated. 51 | - `callUser`: Handles the initial signaling for a video call. 52 | - `answerCall`: Manages the handshake between the calling parties. 53 | - `declineCall`: Handles a call being declined. 54 | - `leaveCall`: Manages the process when a user leaves a call. 55 | - `disconnect`: Removes users from the list when they disconnect. 56 | 57 | # License 58 | MIT License 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const config = require("./src/app/config/config"); 2 | const express = require("express"); 3 | const http = require("http"); 4 | const { Server } = require("socket.io"); 5 | 6 | const app = express(); 7 | const server = http.createServer(app); 8 | 9 | const io = new Server(server, { cors: { origin: config.origin } }); 10 | 11 | app.use(express.static(__dirname + "../../build")); 12 | 13 | let users = {}; 14 | const addUsers = (userId, socketId) => { 15 | users[userId] = socketId; 16 | }; 17 | const removeUsers = (socketId) => { 18 | const copyOfUsers = { ...users }; 19 | for (const userId in copyOfUsers) { 20 | if (copyOfUsers[userId] === socketId) { 21 | delete users[userId]; 22 | return userId; 23 | } 24 | } 25 | }; 26 | const findReceiver = (userId) => { 27 | return users[userId]; 28 | }; 29 | 30 | let videoTalks = []; 31 | 32 | const addVideoTalks = (senderId, receiverId) => { 33 | videoTalks.push([senderId, receiverId]); 34 | }; 35 | 36 | const checkIfIsBusy = (userId) => { 37 | const result = videoTalks.find((subArray) => { 38 | return subArray.includes(userId); 39 | }); 40 | if (result) { 41 | return result.length !== 0; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | 47 | const removeVideoTalks = (userId) => { 48 | const filteredVideoTalks = videoTalks.filter((subArray) => { 49 | return !subArray.includes(userId); 50 | }); 51 | videoTalks = filteredVideoTalks; 52 | }; 53 | 54 | const findOtherUserInVideoTalks = (userId) => { 55 | if (findOtherUserInVideoTalks.length > 0) { 56 | const filteredVideoTalks = videoTalks.filter((subArray) => { 57 | return subArray.includes(userId); 58 | }); 59 | if (filteredVideoTalks.length > 0) { 60 | const anotherUserId = filteredVideoTalks[0].filter((id) => id !== userId); 61 | return anotherUserId[0]; 62 | } 63 | return null; 64 | } 65 | return null; 66 | }; 67 | 68 | io.on("connection", (socket) => { 69 | console.log("a user connected"); 70 | 71 | socket.on("addUser", (userId) => { 72 | addUsers(userId, socket.id); 73 | io.emit("getUsers", users); 74 | }); 75 | 76 | socket.on("sendMessage", (data) => { 77 | const { senderId, receiverId, text, isSticker } = data; 78 | const receiver = findReceiver(receiverId); 79 | if (receiver) { 80 | senderId._id = senderId.id; 81 | io.to(receiver).emit("getMessages", { 82 | senderId, 83 | text, 84 | isSticker, 85 | }); 86 | } 87 | }); 88 | 89 | socket.on("addConversation", (data) => { 90 | const { receiverId, newConversation } = data; 91 | const receiver = findReceiver(receiverId); 92 | if (receiver) { 93 | io.to(receiver).emit("getArrivalConversation", { 94 | newConversation, 95 | }); 96 | } 97 | }); 98 | 99 | socket.on("callUser", (data) => { 100 | const { senderId, receiverId } = data; 101 | const isBusy = checkIfIsBusy(receiverId); 102 | if (!isBusy) { 103 | addVideoTalks(senderId, receiverId); 104 | const receiver = findReceiver(receiverId); 105 | if (receiver) { 106 | io.to(receiver).emit("incomingCall", data); 107 | } 108 | } else { 109 | io.to(socket.id).emit("callIsBusy"); 110 | } 111 | }); 112 | 113 | socket.on("answerCall", (data) => { 114 | const { senderId, signalData } = data; 115 | const initiator = findReceiver(senderId); 116 | console.log("answered"); 117 | socket.to(initiator).emit("callAccepted", signalData); 118 | }); 119 | 120 | socket.on("declineCall", (data) => { 121 | const senderId = data; 122 | const initiator = findReceiver(senderId); 123 | removeVideoTalks(senderId); 124 | io.to(initiator).emit("callDeclined"); 125 | }); 126 | 127 | socket.on("leaveCall", (userId) => { 128 | const anotherUserId = findOtherUserInVideoTalks(userId); 129 | if (anotherUserId) { 130 | const socketId = findReceiver(anotherUserId); 131 | io.to(socketId).emit("callLeaved"); 132 | } 133 | removeVideoTalks(userId); 134 | }); 135 | 136 | socket.on("disconnect", () => { 137 | console.log("a user disconnected"); 138 | const userId = removeUsers(socket.id); 139 | io.emit("getUsers", users); 140 | const anotherUserId = findOtherUserInVideoTalks(userId); 141 | if (anotherUserId) { 142 | const socketId = findReceiver(anotherUserId); 143 | io.to(socketId).emit("callEnded"); 144 | } 145 | removeVideoTalks(userId); 146 | }); 147 | }); 148 | 149 | server.listen(config.socket_port, () => { 150 | console.log(`socket server is running at ${config.socket_port}`); 151 | }); 152 | --------------------------------------------------------------------------------