{"id":24838263,"url":"https://github.com/duckymomo20012/chat-grpc","last_synced_at":"2026-05-04T04:31:30.409Z","repository":{"id":177559029,"uuid":"656435679","full_name":"DuckyMomo20012/chat-grpc","owner":"DuckyMomo20012","description":"Chat app with gRPC Python server","archived":false,"fork":false,"pushed_at":"2024-06-25T16:24:20.000Z","size":277,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-26T04:14:57.857Z","etag":null,"topics":["grpc","grpc-interceptor","grpc-python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DuckyMomo20012.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-21T00:45:49.000Z","updated_at":"2024-06-30T06:41:27.000Z","dependencies_parsed_at":null,"dependency_job_id":"7e81a6e5-1098-4f82-9c46-65ed71018159","html_url":"https://github.com/DuckyMomo20012/chat-grpc","commit_stats":null,"previous_names":["duckymomo20012/chat-grpc"],"tags_count":0,"template":false,"template_full_name":"DuckyMomo20012/python-template","purl":"pkg:github/DuckyMomo20012/chat-grpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DuckyMomo20012%2Fchat-grpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DuckyMomo20012%2Fchat-grpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DuckyMomo20012%2Fchat-grpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DuckyMomo20012%2Fchat-grpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DuckyMomo20012","download_url":"https://codeload.github.com/DuckyMomo20012/chat-grpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DuckyMomo20012%2Fchat-grpc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32595054,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["grpc","grpc-interceptor","grpc-python"],"created_at":"2025-01-31T06:10:02.886Z","updated_at":"2026-05-04T04:31:30.393Z","avatar_url":"https://github.com/DuckyMomo20012.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n  \u003ch1\u003eChat gRPC\u003c/h1\u003e\n\n  \u003cp\u003e\n    Chat app with gRPC Python server\n  \u003c/p\u003e\n\n\u003c!-- Badges --\u003e\n\u003cp\u003e\n  \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/graphs/contributors\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/contributors/DuckyMomo20012/chat-grpc\" alt=\"contributors\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/last-commit/DuckyMomo20012/chat-grpc\" alt=\"last update\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/network/members\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/forks/DuckyMomo20012/chat-grpc\" alt=\"forks\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/stargazers\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/stars/DuckyMomo20012/chat-grpc\" alt=\"stars\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/issues/\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/issues/DuckyMomo20012/chat-grpc\" alt=\"open issues\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/blob/main/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/license/DuckyMomo20012/chat-grpc.svg\" alt=\"license\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003ch4\u003e\n    \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/\"\u003eView Demo\u003c/a\u003e\n  \u003cspan\u003e · \u003c/span\u003e\n    \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc\"\u003eDocumentation\u003c/a\u003e\n  \u003cspan\u003e · \u003c/span\u003e\n    \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/issues/\"\u003eReport Bug\u003c/a\u003e\n  \u003cspan\u003e · \u003c/span\u003e\n    \u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/issues/\"\u003eRequest Feature\u003c/a\u003e\n  \u003c/h4\u003e\n\u003c/div\u003e\n\n\u003cbr /\u003e\n\n\u003c!-- Table of Contents --\u003e\n\n# :notebook_with_decorative_cover: Table of Contents\n\n- [About the Project](#star2-about-the-project)\n  - [Description](#thought_balloon-description)\n  - [Screenshots](#camera-screenshots)\n  - [Tech Stack](#space_invader-tech-stack)\n  - [Features](#dart-features)\n  - [Environment Variables](#key-environment-variables)\n- [Getting Started](#toolbox-getting-started)\n  - [Prerequisites](#bangbang-prerequisites)\n  - [Run Locally](#running-run-locally)\n    - [Start Manually](#robot-start-manually)\n    - [Start with Docker Compose](#whale-start-with-docker-compose)\n- [Usage](#eyes-usage)\n  - [Generate protobuf](#generate-protobuf)\n  - [Application flow](#application-flow)\n    - [Server](#server)\n      - [Server initialization](#server-initialization)\n      - [Authentication](#authentication)\n      - [Message broadcasting](#message-broadcasting)\n      - [Logging](#logging)\n    - [Client](#client)\n      - [Client initialization](#client-initialization)\n      - [Authentication](#authentication)\n      - [Event listener](#event-listener)\n- [Roadmap](#compass-roadmap)\n- [Contributing](#wave-contributing)\n  - [Code of Conduct](#scroll-code-of-conduct)\n- [FAQ](#grey_question-faq)\n- [License](#warning-license)\n- [Contact](#handshake-contact)\n- [Acknowledgements](#gem-acknowledgements)\n\n\u003c!-- About the Project --\u003e\n\n## :star2: About the Project\n\n\u003c!-- Description --\u003e\n\n### :thought_balloon: Description:\n\n- Each user can send messages to other users, only if the previous message sent\n  by the user has been reacted by **two** other users. If not, the message will\n  be rejected.\n\n- Each user can react to messages sent by other users, and by the user itself.\n  However, the self reaction will not be counted to the message when checking if\n  the message can be sent to other users.\n\n\u003c!-- Screenshots --\u003e\n\n### :camera: Screenshots\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/DuckyMomo20012/chat-grpc/assets/64480713/043a0ec1-00c8-47a8-b232-8d1243fd86f8\" alt=\"screenshot\" /\u003e\n  \u003ci\u003eLast updated: Jun 29, 2023\u003c/i\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/DuckyMomo20012/chat-grpc/assets/64480713/54dc0736-67e0-42ec-a3db-3a92e3fc20e9\" alt=\"screenshot_1\" /\u003e\n  \u003ci\u003eLast updated: Jun 29, 2023\u003c/i\u003e\n\u003c/div\u003e\n\n\u003c!-- TechStack --\u003e\n\n### :space_invader: Tech Stack\n\n\u003cdetails\u003e\n  \u003csummary\u003eClient\u003c/summary\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"https://www.python.org/\"\u003ePython\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"https://grpc.io/\"\u003egRPC\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eServer\u003c/summary\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"https://grpc.io/\"\u003egRPC\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"https://tortoise.github.io/\"\u003eTortoise ORM\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eDatabase\u003c/summary\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"https://www.postgresql.org/\"\u003ePostgreSQL\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eDevOps\u003c/summary\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/details\u003e\n\n\u003c!-- Features --\u003e\n\n### :dart: Features\n\n- Basic authentication.\n- Chat with other users.\n- React to messages.\n\n\u003c!-- Env Variables --\u003e\n\n### :key: Environment Variables\n\nTo run this project, you will need to add the following environment variables to\nyour `.env` file:\n\n- **Server configs:**\n\n  `PORT`: Port to run the server.\n\n  `JWT_SECRET_KEY`: Secret key to sign JWT tokens.\n\n  `JWT_EXPIRATION_TIME`: Expiration time of JWT tokens, **in seconds** . Default\n  is `3600` seconds.\n\n  `DB_CONNECTION_STRING`: Postgres connection string to connect to the database.\n\n- **Client configs:**\n\n  `PORT`: Port to run the server.\n\n  `SERVER_HOST`: Host of the server.\n\nE.g:\n\n```\n# .env\nPORT=\"9000\"\nJWT_SECRET_KEY=\"secret\"\nJWT_EXPIRATION_TIME=\"3600\"\nDB_CONNECTION_STRING=\"postgres://postgres:postgres@localhost:65432/chat\"\n\nSERVER_HOST=\"localhost\"\n```\n\nYou can also check out the file `.env.example` to see all required environment\nvariables.\n\n\u003c!-- Getting Started --\u003e\n\n## :toolbox: Getting Started\n\n\u003c!-- Prerequisites --\u003e\n\n### :bangbang: Prerequisites\n\n- Python: `\u003e= 3.12`.\n\n- This project uses [Poetry](https://python-poetry.org/) as package manager:\n\n  Linux, macOS, Windows (WSL)\n\n  ```bash\n  curl -sSL https://install.python-poetry.org | python3 -\n  ```\n\n  Read more about installation on\n  [Poetry documentation](https://python-poetry.org/docs/master/#installation).\n\n\u003c!-- Run Locally --\u003e\n\n### :running: Run Locally\n\nClone the project:\n\n```bash\ngit clone https://github.com/DuckyMomo20012/chat-grpc.git\n```\n\nGo to the project directory:\n\n```bash\ncd chat-grpc\n```\n\nInstall dependencies:\n\n```bash\npoetry install\n\npre-commit install\n```\n\nOR:\n\nInstall dependencies with `pip`:\n\n```bash\npip install -r requirements.txt\n\npre-commit install\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eExport dependencies from \u003c/code\u003epyproject.toml\u003c/code\u003e\u003c/summary\u003e\n\nExport Poetry dependencies to file `requirements.txt`:\n\n```bash\npoetry export -f requirements.txt --output requirements.txt\n```\n\n\u003e **Note**: You can add option: `--dev` to include development dependencies.\n\n\u003c/details\u003e\n\n\u003c!-- Start Manually --\u003e\n\n#### :robot: Start Manually\n\n- Setup database:\n\n  ```bash\n  make compose-db\n  ```\n\n- Start the server:\n\n  - With Poetry:\n\n    - Activate the virtual environment:\n\n      ```bash\n      poetry shell\n      ```\n\n    - Start the server:\n\n      ```bash\n      poe dev\n      ```\n\n  - With Makefile:\n\n    ```bash\n    make server\n    ```\n\n  - With Docker compose:\n\n    ```bash\n    docker compose --profile server up -d\n    ```\n\n  - With Python:\n\n    ```bash\n    python cli.py server\n    ```\n\n- Stop the database:\n\n  ```bash\n  make compose-down\n  ```\n\n- Start the client:\n\n  - With Poetry:\n\n    - Activate the virtual environment:\n\n      ```bash\n      poetry shell\n      ```\n\n    - Start the client:\n\n      ```bash\n      poe dev client\n      ```\n\n  - With Makefile:\n\n    ```bash\n    make client\n    ```\n\n  - With Python:\n\n    ```bash\n    python cli.py client\n    ```\n\n\u003c!-- Start with Docker Compose --\u003e\n\n#### :whale: Start with Docker Compose\n\nStart the server and database:\n\n```bash\nmake compose-up\n```\n\nStop the server and database:\n\n```bash\nmake compose-down\n```\n\n\u003c!-- Usage --\u003e\n\n## :eyes: Usage\n\n### Generate protobuf\n\n```bash\nmake gen-proto\n```\n\nThis will generate the protobuf files in the `chat_grpc/proto` directory using\nthe file `buf.gen.yaml` as configuration.\n\n\u003cdetails\u003e\n\u003csummary\u003eThe auto-generated file problem\u003c/summary\u003e\n\nDue to the problem with the auto-generated python absolute imports in the files\n`.*_pb2_gprc.py`, you **HAVE TO nested** the proto directory in the `proto`\ndirectory.\n\nFor example:\n\n- If you move configure the `proto` directory with the\n  `proto/auth_service/auth_service.proto` or\n  `proto/chat_service/chat_service.proto`\n- Configure the `buf.gen.yaml` file with the `out` configure to\n  `../pkg/protobuf`.\n- Then the auto-generated files will be in the `pkg/protobuf` directory, just\n  like current configuration. However, the file `.*_pb2_gprc.py` will have the\n  import `from auth_service ...` instead of\n  `from pkg.protobuf.chat_service ...`.\n\n\u003c/details\u003e\n\n### Makefile\n\n- `make gen-proto`: Generate protobuf files.\n\n  Usage:\n\n  ```bash\n  make gen-proto\n  ```\n\n- `make server`: Start the server.\n\n  Usage:\n\n  ```bash\n  make server\n  ```\n\n- `make client`: Start the client.\n\n  Usage:\n\n  ```bash\n  make client\n  ```\n\n- `make compose-up`: Start the server and database with docker compose.\n\n  Usage:\n\n  ```bash\n  make compose-up\n  ```\n\n- `make compose-down`: Stop the server and database with docker compose.\n\n  Usage:\n\n  ```bash\n  make compose-down\n  ```\n\n- `make compose-db`: Start the database with docker compose (without the\n  server).\n\n  Usage:\n\n  ```bash\n  make compose-db\n  ```\n\n### CLI\n\n```bash\n$ python cli.py --help\n\nUsage: cli.py [OPTIONS] [SERVICE]:[server|client]\n\nArguments:\n  [SERVICE]:[server|client]  Service to run  [default: server]\n\nOptions:\n  --help  Show this message and exit.\n```\n\n\u003e **Note**: This is an entry point for all the services. Each service should be\n\u003e run from this entry point to make the absolute import work.\n\n### Application flow\n\n#### Server\n\n##### Server initialization\n\nFirst, the server is configured with **interceptors** to handle authentication\nand logging. The gRPC server is registered with two main services: `AuthService`\nand `ChatService`. Finally, the server is listening on the address `[::]:PORT`,\nwith `PORT` is the environment variable.\n\nThen, the server also init the Tortoise ORM to connect to the database. The\ntortoise ORM is configured with the environment variable `DB_CONNECTION_STRING`.\nThis library also automatically generate the database schema for the models,\nwhich are configured by specifying the `models` file paths while initializing\nthe ORM.\n\nThe server also create a `Server` object to hold the `set` of connected clients.\nWhile updating this set of clients, the server will use an `asyncio.Lock` to\nprevent concurrent access.\n\n##### Authentication\n\nEach client will have a **token** to authenticate with the server. The token is\ngenerated by the server when the client login. The token is a JWT token, which\nholds the `user_id` and `user_name` of the user, and the `exp` time. The token\nis signed with the `secret` key, which is configured by the environment variable\n`JWT_SECRET_KEY`. The token is valid for **1 hour**, which is configured by the\n`JWT_EXPIRE_TIME` environment variable.\n\nThe JWT interceptor will check the token in the metadata of the request. If the\ntoken is valid, the request will be passed to the next interceptor. Otherwise,\nthe interceptor will return an error to the client. The interceptor will ignore\nthe authentication for the `login` and `register` methods. Also, the interceptor\nwill **\"inject\" the `user_id` into the context** of the request.\n\nAfter the user logged in, the server will add the `user_id` to the `Server`\n**set** of connected clients.\n\nAfter the user signed out, the token will be revoked by putting into the\n**blacklist table**. The JWT interceptor will check the token in the blacklist\ntable. If the token is in the blacklist table, the interceptor will return an\nerror to the client.\n\n\u003e **Note**: Currently, the blacklist table has to be manually cleaned up.\n\n##### Message broadcasting\n\nEvery time a client sends a message or reacts to a message, the server will\ncreate an `Event` record in the database. The `Event` record will be used to\nstore history of the chat conversation. After the `Event` record is created, the\n`post_save` hook will be called to create another `EventQueue` record. The\nnumber of `EventQueue` records is equal to the number of connected clients,\nwhich is stored as a `set` in the `Server` object. The `EventQueue` record will\nbe used to broadcast the message to the clients.\n\nThe `EventQueue` record is send back to the client as a `Subscribe` route\nresponse, and the record will be marked as sent by setting the `is_sent` field\nto `True`. The client will use the `Subscribe` route to receive the `EventQueue`\nand then send back the `event_id` back to the server to **acknowledge** that it\nhas received the event queue. The server will then delete the `EventQueue`\nrecord from the database that matches the `event_id`.\n\nIf the client is logged in, but close the app **without logging out**, the\nserver is able to resend the `EventQueue` record to the client when it connects\nagain, as the `user_id` is still in the `Server` set of connected clients.\n\n\u003e **Note**: Sometimes, the client receives the `EventQueue` record, but the\n\u003e client don't send back the `event_id` to the server. This will cause the\n\u003e database to have a lot of `EventQueue` records that are not deleted. This\n\u003e problem is not solved yet, and the `EventQueue` records have to be manually be\n\u003e deleted.\n\n##### Logging\n\nThe server will log the request and response of each route by the logging\ninterceptor. The log is visible in the console, and also in the `logs`\ndirectory.\n\n#### Client\n\n##### Client initialization\n\nThe client is built with the `DearPyGui` library. The client will connect to the\nserver with the address `[::]:PORT`, with `PORT` is the environment variable.\n\n##### Authentication\n\nThe client will send the `login` request to the server with the `user_name` and\nthe `password`. The server will return the `access_token` and this is stored in\nthe `Client` object. The `access_token` is used to authenticate with the server.\n\nWhen the client sends the request to the server, the `access_token` is added to\nthe metadata of the request (as the `Bearer` authorization token), by the token\ninterceptor.\n\nAfter the client logged out, the access token will be deleted from the client.\n\n\u003e **Note**: Currently, the client will not automatically refresh the token.\n\n##### Event listener\n\nAfter login successfully, the client starts a **deamon thread** to send the\n`Subscribe` request to the server to receive the `EventQueue` record, within the\nloop. The client will send back to the server the `event_id` of the `EventQueue`\nrecord that it has just received.\n\n\u003c!-- Roadmap --\u003e\n\n## :compass: Roadmap\n\n- [x] Hash passwords.\n- [ ] Notification panel.\n- [ ] Refresh token to keep user logged in.\n- [ ] Improve ORM queries.\n- [ ] Improve error handling.\n\n\u003c!-- Contributing --\u003e\n\n## :wave: Contributing\n\n\u003ca href=\"https://github.com/DuckyMomo20012/chat-grpc/graphs/contributors\"\u003e\n  \u003cimg src=\"https://contrib.rocks/image?repo=DuckyMomo20012/chat-grpc\" /\u003e\n\u003c/a\u003e\n\nContributions are always welcome!\n\n\u003c!-- Code of Conduct --\u003e\n\n### :scroll: Code of Conduct\n\nPlease read the\n[Code of Conduct](https://github.com/DuckyMomo20012/chat-grpc/blob/main/CODE_OF_CONDUCT.md).\n\n\u003c!-- FAQ --\u003e\n\n## :grey_question: FAQ\n\n- The client app is not responding when closing the app.\n\n  - This is because the client still has working thread. Maybe, the client has\n    some errors, which causes the thread to not stop. You can try to close the\n    app again, or kill the app process, or **just spam the Ctrl+C**.\n\n- Cannot handle errors in event listener which run in a thread.\n\n  - Handle errors in thread is quite difficult. I will look into this later.\n    Currently, I can only handle the errors in the callback function of the\n    event listener.\n\n\u003c!-- License --\u003e\n\n## :warning: License\n\nDistributed under MIT license. See\n[LICENSE](https://github.com/DuckyMomo20012/chat-grpc/blob/main/LICENSE) for\nmore information.\n\n\u003c!-- Contact --\u003e\n\n## :handshake: Contact\n\nDuong Vinh - [@duckymomo20012](https://twitter.com/duckymomo20012) -\ntienvinh.duong4@gmail.com\n\nProject Link:\n[https://github.com/DuckyMomo20012/chat-grpc](https://github.com/DuckyMomo20012/chat-grpc).\n\n\u003c!-- Acknowledgments --\u003e\n\n## :gem: Acknowledgements\n\nHere are useful resources and libraries that we have used in our projects:\n\n- [Buf CLI](https://buf.build/product/cli/): Generate code, prevent breaking\n  changes, lint Protobuf schemas, enforce best practices, and invoke APIs with\n  the Buf CLI.\n- [grpc-interceptor](https://grpc-interceptor.readthedocs.io/en/latest/):\n  Simplified Python gRPC Interceptors.\n- [Dear PyGui](https://dearpygui.readthedocs.io/en/latest/): Dear PyGui is an\n  easy-to-use, dynamic, GPU-Accelerated, cross-platform graphical user interface\n  toolkit(GUI) for Python. It is “built with”\n  [Dear ImGui](https://github.com/ocornut/imgui).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fduckymomo20012%2Fchat-grpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fduckymomo20012%2Fchat-grpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fduckymomo20012%2Fchat-grpc/lists"}