{"id":18718789,"url":"https://github.com/denis-source/socket_chat","last_synced_at":"2026-05-07T06:35:25.732Z","repository":{"id":63739652,"uuid":"532399052","full_name":"Denis-Source/socket_chat","owner":"Denis-Source","description":"Websocket realtime chat application.","archived":false,"fork":false,"pushed_at":"2022-11-27T05:31:06.000Z","size":866,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-28T11:12:09.846Z","etag":null,"topics":["chat","python","react","real-time","redux","server","sqlalchemy","websocket"],"latest_commit_sha":null,"homepage":"https://chat.zoloto.cx.ua","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Denis-Source.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-09-03T23:54:40.000Z","updated_at":"2022-12-05T04:55:52.000Z","dependencies_parsed_at":"2023-01-23T19:00:41.367Z","dependency_job_id":null,"html_url":"https://github.com/Denis-Source/socket_chat","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Denis-Source%2Fsocket_chat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Denis-Source%2Fsocket_chat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Denis-Source%2Fsocket_chat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Denis-Source%2Fsocket_chat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Denis-Source","download_url":"https://codeload.github.com/Denis-Source/socket_chat/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239581759,"owners_count":19662960,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chat","python","react","real-time","redux","server","sqlalchemy","websocket"],"created_at":"2024-11-07T13:23:01.276Z","updated_at":"2025-11-10T16:30:21.572Z","avatar_url":"https://github.com/Denis-Source.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Websocket Chat\n\nWebsocket realtime chat application.\nhttps://chat.zoloto.cx.ua/\nhttps://chat.zoloto.cx.ua/info\n\n## Installation\n```sh\ngit clone https://github.com/Denis-Source/socket_chat.git\n```\n\n#### backend\n\nAfter cloning the repository, go to the `backend` folder, configure the virtual environment  and install dependencies.\n\n```sh\ncd .\\socket_chat\\backend\\\n\npython -m venv env\n.\\env\\Scripts\\Activate.ps1\npip install -r .\\requirements.txt\n```\n\nYou can specify the port and IP address in the configs.\n\n```python\nIP = \"127.0.0.1\"\nPORT = 9000\n```\n\nThere is an option to specify the storage solution. In the example below, the mock memory storage is used.\n\n```python\nfrom storage.memory_storage import MemoryStorage\n\nSTORAGE_CLS = MemoryStorage\n```\n\nAfter the initial configuration, you will be able to run the backend part of the application.\n\n```python\npython .\\main.py\n```\n\n```sh\n2022-09-12 18:08:33,964 DEBUG   asyncio                 Using proactor: IocpProactor\n2022-09-12 18:08:33,965 INFO    server                  starting server\n2022-09-12 18:08:33,966 INFO    websockets.server       server listening on 127.0.0.1:9000\n```\n\n#### Frontend\nThe frontend configuration is more straight forward.\n```\ncd ..\\frontend\\\nnpm i\n\nnpm start\n```\n***\n\n## Showcase\nIn a nutshell, it is a simple one-page application with a minimalistic design and a Two-tab layout:\n![01 homepage](https://user-images.githubusercontent.com/58669569/189720077-34f986a1-b610-4b4f-9deb-bca8995c7f4e.png)\n\nThe mobile version is also available but it lacks the functionality if compared to the desktop one:\n![mobile](https://user-images.githubusercontent.com/58669569/189720117-6cac8786-c9ae-40b7-b446-d559e115ad0a.png)\n\nAll of the changes, incoming messages and other things can be viewed in a realtime, as the application is based on websocket communication protocol.\n***\n\n### Logs\nBoth frontend and backend provide verbose logging. The frontend one is visible by a user in the left tab and displays all of the communications between him and the server:\n![04 logs](https://user-images.githubusercontent.com/58669569/189720143-4c16bd19-97c7-4a39-84f9-0fed24232bd4.png)\n***\n\n### Usernames\nThe application does not require registration of any sort as the user name is generated at the start:\n![02 user](https://user-images.githubusercontent.com/58669569/189720158-4abaa8b3-8b81-433b-bd6a-f3a435a6c51a.png)\n\nThe application is completely anonymous and does not store any user data as it is not needed. Also, there is no form of roles or admin privileges, anyone can do anything.\n\nMost of the names are changeable, the username as well.\n\nAll entities of the application are distinguished by their universally unique identifier (UUID), so all of the names initially have that `00000000-0000-0000-0000-000000000000` look.\n***\n\n### Rooms\nThe main objective of the application is to provide an ability to exchange messages between people, which takes place in rooms. The room can be created or deleted by anyone and it is fully customizable:\n![03 room](https://user-images.githubusercontent.com/58669569/189720181-7dbd8b26-0d67-4922-afe2-411c80863cb5.png)\n\nThe room list is sorted by the room creation time, to solve the problem of navigating a potential overwhelmingly large number of them, the search is provided.\n***\n\n### Messages\nAfter the room is selected, the user is provided with an unlimited message history and an ability to create their own messages:\n![05 messages](https://user-images.githubusercontent.com/58669569/189720206-b0e0d9a0-3673-43a0-9358-d6e95db85e3d.png)\nThe amount of rooms and messages are **NOT LIMITED**.\n***\n\n### Side tabs\nWhen entered the room, there is still an option to see rooms, by switching the left tab to the room list:\n![07 roomMini](https://user-images.githubusercontent.com/58669569/189720568-b46db0c2-d072-425c-a16c-71fe9f4c1098.png)\nBoth left and right tabs can be selected. The one consists of logging and room list, the right one – a list of messages, a list of users entered the room and a drawing. \n***\n\n### Drawings\nAs a bonus, every room has a tab with a simple drawing board:\n![08 drawing](https://user-images.githubusercontent.com/58669569/189720244-4429fd68-4a93-4d76-a9b0-2c5acbdf4e3e.png)\n***\n\nThere is a simple selection of tools and colors that can be used. The drawing is shared between all of the room attendants.\n![09 amogus](https://user-images.githubusercontent.com/58669569/189720268-8bf7de86-0b10-40f2-8583-3c5430976174.png)\n***\n\n### Themes\nHaving a simple design allows the application to easily change its looks with a theme slider:\n![10 themes](https://user-images.githubusercontent.com/58669569/189720282-cd7f4b3a-0bf7-4d25-b8bf-011674ea2de0.png)\n\nThere are 8 different themes available with the following color schemes:\n![themes](https://user-images.githubusercontent.com/58669569/189720318-2fcc11ad-b98b-4eb2-9491-85b51443bdd5.png)\n***\n\n## Backend\nThe backend of the applications is written using [WebSockets](https://websockets.readthedocs.io/en/stable/) library. As it is a relatively simple demo, the application was written from scratch. The data travels through the internal structure:\n![Func Scheme (1)](https://user-images.githubusercontent.com/58669569/189756284-ebe04879-31bd-4508-9f8c-39aa4ad16523.png)\n\nIt is parsed with the `Utils` class, then goes to the `Server` class which decides what to do with it. All of the actions are done with the `Model` classes which are internally mapped with a database via the `Storage` class. \n\n#### Statements\nAll of the communications of the applications are done in the following format:\n```json\n{\n    \"type\": \"result\",\n    \"payload\": {\n        \"message\": \"user_created\",\n        \"object\": {\n            \"uuid\": \"02687b26-8620-4936-9675-00c2a06f43c8\",\n            \"name\": \"user-02687b26-8620-4936-9675-00c2a06f43c8\",\n            \"room_uuid\": null\n        }\n    }\n}\n```\n\n\u003e This statement comes first from the server and tells the client it's identity.\n\nThe statement has one of the following types:\n- `result` that comes from the server and tells a client what to do;\n- `call` that comes from the client and tells the server what to do;\n- `error`.\n\nAll of the statement have `payload` field. \n`message` specifies the type of actions that were or should be performed. Both the backend and frontend side have enumerations that list all of the available `message` types.\nTo send a data, `payload` could have other fields, such as `object`, `list`.\n\nThe statement used to change a room color:\n```json\n{\n    \"type\": \"call\",\n    \"payload\": {\n        \"message\": \"change_color\",\n        \"uuid\": \"45843334-b2ea-4673-86d1-6c8aab920b74\",\n        \"color\": \"#ff0000\"\n    }\n}\n```\n***\n\n### Utils class\nAll of the incoming messages are filtered and cleaned with the `Utils` class `parse_statement` method with the specified validators. \n\nThe `prepare_statement` method is on the other hand is used to construct a statement based on type, message and other additional parameters.\n***\n\n### Server class\nThe main logic is done in `Server` class that based on the incoming messages. To avoid messiness, all of the callable methods are grouped in the dictionary:\n```python\nself.call_methods = {\n\tRoomCallStatements.CREATE_ROOM: self.create_room,\n\tRoomCallStatements.DELETE_ROOM: self.delete_room,\n\tRoomCallStatements.LIST_ROOMS: self.list_rooms,\n\tRoomCallStatements.ENTER_ROOM: self.enter_room,\n\tRoomCallStatements.LEAVE_ROOM: self.leave_room,\n\tRoomCallStatements.CHANGE_ROOM_COLOR: self.change_room_color,\n\tRoomCallStatements.CHANGE_ROOM_NAME: self.change_room_name,\n\n\tUserCallStatements.CHANGE_USER_NAME: self.change_user_name,\n\n\tMessageCallStatements.CREATE_MESSAGE: self.create_message,\n\tMessageCallStatements.LIST_MESSAGES: self.list_messages,\n\n\tDrawingCallStatements.CHANGE_DRAW_LINE: self.change_draw_line,\n\tDrawingCallStatements.GET_DRAWING: self.get_drawing,\n\tDrawingCallStatements.RESET_DRAWING: self.reset_drawing\n}\n```\nTo handle the connection with a user, there is also `self.connections` dictionary, that maps user uuid with the corresponding websocket connection.\n\nThe websocket is based on a generator, so the for loop is used, to handle `user` statements:\n```python\nasync for raw_message in self.connections.get(user.uuid):\n\tmethod_type, payload = Utils.parse_statement(raw_message)\n\tmethod = self.call_methods[method_type]\n\n\tawait method(user, payload)\n```\nIn a case of calling the non existing method or not providing needed data, server sends the corresponding error statement.\n***\n### Model classes\nTo divide the server logic and direct data manipulation, the `Model` classes are implemented.\n\nAs we can see from the `AbstractModel` all of the classes should implement the following class methods:\n- `create` to create a model instance\n- `list` to list all of the model instances\n- `get` to get a specific model instance (based on `uuid`)\t\t\t\n- `delete` to delete a model instance\n\nAnd instance `get_dict` to construct a dictionary representation of an instance that is easily JSON serializable.\n\nThe models uses `Storage` class to save it's contents to the database. Since the `websockets` library is *asynchronous*, the `Storage` is made as well. That means that the model can not use default `__init__()` constructor to create an instance, so the `create()` class method is needed.\n\nAs said earlier, all of the `Model` logic is mapped with the `Storage` class, that is specified in the configs.\n\nFor example model class method `get` simply gets the model from the storage:\n```python\n@classmethod\nasync def get(cls, uuid: str):\n\tcls.logger.debug(f\"getting {cls.TYPE} ({uuid})\")\n\treturn await cls.storage.get(cls.TYPE, uuid)\n```\n\nAll of those methods are defined in the `BaseModel` class and overriden if needed in the other children classes.\n\nThe `BaseModel` also provides the `uuid` generation which defines instance uniquness:\n```\nclass BaseModel(AbstractModel):\n    def __init__(self):\n        self.uuid = str(uuid4())\n        self.name = f\"{self.TYPE}-{self.uuid}\"\n```\n\nAll of the models should have a `TYPE` that is defined in the `ModelTypes` enumeration. This is crusial as it is used by the `Storage` class.\n```python\nclass ModelTypes(str, Enum):\n    BASE = \"base\"\n    USER = \"user\"\n    ROOM = \"room\"\n    MESSAGE = \"message\"\n    DRAWING = \"drawing\"\n    LINE = \"line\"\n    COLOR = \"color\"\n```\n***\n\n### Storage class\nTo separate mandate file or SQL operaions from the `Model` class, the `Storage` class was implemented. It's created using *repository* pattern, as it provides only **4** methods:\n- `get()`;\n- `list()`;\n- `put()`;\n- `delete()`.\n\nThere are also 2 onetime-run static methods that are needed in some cases:\n- `prepare()`;\n- `close()`.\n\nAll of those method are defined in the `BaseStorage` class.\nThe `BaseStorage` class implements those methods by using dictionary with the not yet implemented model type specific methods:\n\n```python\nself._get_methods = {\n\tModelTypes.USER: self._get_user,\n\tModelTypes.ROOM: self._get_room,\n\tModelTypes.MESSAGE: self._get_message,\n\tModelTypes.DRAWING: self._get_drawing,\n}\nself._list_methods = {\n\tModelTypes.ROOM: self._list_rooms\n}\nself._put_methods = {\n\tModelTypes.USER: self._put_user,\n\tModelTypes.ROOM: self._put_room,\n\tModelTypes.MESSAGE: self._put_message,\n\tModelTypes.DRAWING: self._put_drawing,\n\tModelTypes.LINE: self._put_line,\n}\nself._delete_methods = {\n\tModelTypes.USER: self._delete_user,\n\tModelTypes.ROOM: self._delete_room,\n}\n```\nSo the child class should only implement those methods or the `NotImplementedError` will be used.\n\n#### MemoryStorage\nFor testing and developing purpouses the memory storage was created. It uses 5 different dictionaries to store models in memory:\n\n```python\nclass MemoryStorage(BaseStorage):\n    _users = {}\n    _rooms = {}\n    _messages = {}\n    _drawings = {}\n    _lines = {}\n```\n\nAll of the methods are implemented by dictionary access or `pop()` method.\n***\n#### AlchemyStorage\nTo store data properly the `AlchemyStorage` class was implemented that is based on the [SQLAlchemy](https://www.sqlalchemy.org/). The [ORM](https://ru.wikipedia.org/wiki/ORM) models were used they are not the same models that were described previously. The ORM models provided by the library are then reconstructed in the appropriate `Model` classes. \n\n\u003e That decision unsures that the project does not depend on the SQLAlchemy storage solution.\n\nTo store data in the specified way, the model tables were created using [declarative mapping](https://docs.sqlalchemy.org/en/13/orm/mapping_styles.html) (the imperative mappings did not work for me with the [asynchronous extension](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html)).\n\nUser model table for example:\n```python\nBase = declarative_base()\n\nclass UserDB(Base):\n\t__tablename__ = \"user\"\n\t__mapper_args__ = {'eager_defaults': True}\n\n\tname = Column(String(64))\n\tuuid = Column(String(36), primary_key=True)\n\troom_uuid = Column(String, ForeignKey(\"room.uuid\"))\n\troom = relationship(\"RoomDB\", back_populates=\"users\")\n\tmessages = relationship(\"MessageDB\", back_populates=\"user\")\n```\n\n\u003e The primary key is `uuid` as it satisfies the uniqueness constraint. There are also relations with the other tables as should be.\n\nAll of the database manipulations are done with [ORM Queries in 2.0 style](https://docs.sqlalchemy.org/en/14/glossary.html#term-2.0-stylehttps://docs.sqlalchemy.org/en/14/glossary.html#term-2.0-style) using `select`, `update` and \n`delete` query factories that are used with the session. Results are converted to classes with `scallars()` method.\n\nMethod to query a room for example:\n```python\nasync def _query_room(self, uuid: str) -\u003e RoomDB:\n\tquery = select(RoomDB).options(selectinload(RoomDB.users), selectinload(RoomDB.drawing),\n\t\t\t\t\t\t\t\t   selectinload(RoomDB.messages)).filter_by(uuid=uuid)\n\tresult = await self._session.scalars(query)\n\treturn result.first()\n```\n\n\u003e Considering the nature of asynchronous sqlachemy extension, we should specify loading of the related fields by calling `options` with the `selectinload`.\n\nAfter the data is recieved from the DB, the `Model` `from_data()` methods come in handy.\nExapmle of the `_get_user()` method:\n```python\nasync def _get_user(self, uuid: str) -\u003e \"models.user.User\":\n\tuser_db = await self._query_user(uuid)\n\n\tif user_db:\n\t\tuser = models.user.User.from_data(\n\t\t\tuser_db.name,\n\t\t\tuser_db.uuid,\n\t\t\tuser_db.room_uuid,\n\t\t)\n\t\treturn user\n\telse:\n\t\traise NotFoundException(uuid, ModelTypes.USER)\n```\n\n#### PostgreSQL\nGiven the ORM nature of SQLAlchemy, it is not that difficult to expand the previously mentioned `AlchemyStorage` class to the one that has an ability to connect to various \"real\" databases. In fact the only difference is the declaration of the database `engine`:\n```py\nclass PostgreStorage(AlchemyStorage):\n    _db_credentials = {\n        \"user\": \"user_name\",\n        \"dbname\": \"chat_db\",\n        \"password\": \"passwd\",\n        \"address\": \"localhost\"\n    }\n\n    NAME = \"postgres_storage\"\n    _engine = create_async_engine(\n        f\"postgresql+asyncpg://\"\n        f\"{_db_credentials.get('user')}:\"\n        f\"{_db_credentials.get('password')}\"\n        f\"@{_db_credentials.get('address')}\"\n        f\"/{_db_credentials.get('dbname')}\",\n    )\n    _session = AsyncSession(_engine, expire_on_commit=False)\n```\n\nGiven the postgresql is installed, a database is created and the `config.py` has `STORAGE_CLS = PostgreStorage`, the backend will attempt to connect to the local postgreSQL server with the provided credentials.\n***\n\n## Frontend\nForntend is based on the [React](https://reactjs.org/)  framework. The looks and feels were designed from scratch.\n\n### Project structure\nThe project was written using [TypeScript](https://www.typescriptlang.org/) and follows the basic react project structure. All of the components are styled with [sass](https://sass-lang.com/).\n\nAll of the source files are located in `src` folder. The application is split into components:\n- `Buttons` that consists of the button template and all of the functional buttons, including navigation ones;\n- `ColorPicker` different custom color pickers for rooms and drawing\n- `Drawing`\tdrawing component based on [react-konva](https://konvajs.org/docs/react/index.html) `Stage`;\n- `Header` that displays the heading of the application and username;\n- `Input` components related to input fields;\n- `Log`\tcomponents related to logging;\n- `Message`\tcomponents related to messages;\n- `Room` components related to rooms;\n- `Spinner`;\n- `Tabs` components that group other components into a layout.\n\n#### Models and Enumerations\nConsidering that the project is written with TypeScript, there is `Model` folder that provides interfaces for the `Drawing`, `Log`, `Message`, `Room` and `User` interfaces.\n\nSimple example of a `user` interface:\n```ts\nexport interface UserModel {\n    name: string;\n    room_uuid?: string;\n    uuid: string;\n}\n```\nThere are also Enumerations related to the statement types used to communicate with the server located in the `StatementTypes` folder.\n***\n#### Reducers\nTaking into account that the project has a complex internal state (message history, drawing, log lists, etc.), the [redux](https://react-redux.js.org/) state manager was used. For every model, the corresponding [redux slice](https://redux-toolkit.js.org/api/createSlice) was implemented.\n\nThe application calls the reducers with `dispatch` to avoid infinite callback passing.\n***\n\n### Libraries used\n#### [redux](https://redux.js.org/)\nAs mentioned previously.\n\n#### [react-use-websocket](https://www.npmjs.com/package/react-use-websocket)\nThe application relies on the websocket communication protocol. All of the communications were implemented using the library with the statement constructor `prepareStatement()` in the `api.ts` file.\n\n#### [react-scroll-to-bottom](https://stackoverflow.com/questions/37620694/how-to-scroll-to-bottom-in-react)\nTo implement automatic scrolling animations on new messages, log items room etc.\n\n#### [react-viewport-list](https://www.npmjs.com/package/react-viewport-list)\nThe application has several lists that are potentially infinite in size. To avoid any lags related to it, this library was used to render only the viewed elements.\n\n#### [react-konva](https://konvajs.org/docs/react/index.html)\nTo have a drawing board\n\n#### [react-slider](https://www.npmjs.com/package/react-slider)\nTo have a theme switcher in the form of a slider.\n\n#### [react-cookie](https://www.npmjs.com/package/react-cookie)\nTo save a preferred theme in cookies, selected with a previously mentioned slider.\n\n\u003e Only the theme number is stored in the cookies, there is no other data especially related to the user profile.\n\n#### [use-sound](https://www.npmjs.com/package/use-sound)\nThe application has sound notifications, so the appropriate library was used.\n\u003e TypeScript does not recognize the `mp3` format with the `ES6` format, so the *good old* `require` was used for that. \n\n#### [react-router](https://reactrouter.com/en/main)\nEven though it is a one page application, it still uses sever routes such as homepage, room page, information page, etc.\n\u003e Rooms have dynamic routing so it is possible to enter a room given its URL.\n\n***\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenis-source%2Fsocket_chat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdenis-source%2Fsocket_chat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenis-source%2Fsocket_chat/lists"}