{"id":20858679,"url":"https://github.com/thesephist/plume","last_synced_at":"2025-05-12T08:31:35.464Z","repository":{"id":57513392,"uuid":"231756145","full_name":"thesephist/plume","owner":"thesephist","description":"Small in-memory real time chat server with Go and WebSockets","archived":false,"fork":false,"pushed_at":"2020-06-22T21:10:27.000Z","size":125,"stargazers_count":32,"open_issues_count":0,"forks_count":5,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-06-20T10:16:55.157Z","etag":null,"topics":["chatroom","gorilla-websocket","messaging","websocket"],"latest_commit_sha":null,"homepage":"https://plume.chat","language":"Go","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/thesephist.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-04T12:03:51.000Z","updated_at":"2023-09-06T16:17:03.000Z","dependencies_parsed_at":"2022-08-31T23:00:49.324Z","dependency_job_id":null,"html_url":"https://github.com/thesephist/plume","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/thesephist%2Fplume","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thesephist%2Fplume/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thesephist%2Fplume/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thesephist%2Fplume/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thesephist","download_url":"https://codeload.github.com/thesephist/plume/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225130753,"owners_count":17425506,"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":["chatroom","gorilla-websocket","messaging","websocket"],"created_at":"2024-11-18T04:46:56.687Z","updated_at":"2024-11-18T04:46:56.764Z","avatar_url":"https://github.com/thesephist.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Plume\n\nPlume is a tiny in-memory chat server, running at [plume.chat](https://plume.chat).\n\nPlume is built on...\n\n- [Gorilla WebSocket](https://github.com/gorilla/websocket) for initiating and managing WebSocket connections\n- [Mailgun](https://mailgun.com) to send authentication emails\n- My own [blocks.css](https://thesephist.github.io/blocks.css/) to add some spice to the UI design\n\n![Plume.chat](docs/plume.png)\n\n## Design\n\nPlume's model consists of five types and makes substantial use of [Go channels](https://gobyexample.com/channels).\n\nIn the design of Plume's domain models, I tried to keep the chat-room related data structures (`Message`, `Client`, `Room`) separate from the protocol and lifecycle related data structures (`Server` and its related code). This leads to a bit of indirection, for example in not directly pairing WebSocket connections to `Client`s, but the abstraction boundary makes the code more readable and allows for potentially other protocols in the future to connect to a Plume chat room.\n\n### Message\n\n```go\ntype Message struct {\n\tType int    `json:\"type\"`\n\tUser User   `json:\"user\"`\n\tText string `json:\"text\"`\n}\n```\n\nThe core model behind Plume is `Message` -- a type that represents all kinds of messages exchanged between clients and servers in Plume, and channels owned by servers and clients that exchange those Messages.\n\nThe client (`static/js/plume.js`) and server (`pkg/plume/server.go`) have an isomorphic (equivalent) view of messages. Each message has a `type`, which encodes non-user-visible message types like Auth handshakes; a `user` (who sent the message), and some optional `text` content.\n\nA message represents any communication between the chat server and client (the browser). Most messages you send will be of type `msgText` (normal, text messages that are shown to everyone), but the `Message` type is also used to represent messages needed to join and authenticate to a session. You can read more about those message types below under \"Client lifecycle\".\n\n### User\n\n```go\ntype User struct {\n\tName  string `json:\"name\"`\n\tEmail string `json:\"-\"`\n}\n```\n\nA user is a data structure representing a person with the intent to join a session, and really just contains two simple pieces of data: their username and email. The `User` type is passed around Plume's code as a container for user data, and not used for much else.\n\n### Client\n\n```go\ntype Client struct {\n\tUser      User\n\tRoom      *Room\n\tOnMessage func(Message)\n\n\treceiver chan Message\n}\n```\n\nThe `Client` represents a browser tab connected to a chat session, and holds most of the data structures critical for messages to be sent and received.\n\nThere can be many clients linked to the same user. If, for example, the same person logs in on two different devices or browser tabs with the same username and email, Plume represents this as two Clients with equal User values.\n\nEach Client is linked to zero or one `Room`, which represents a collection of clients that all talk to each other. When a message is sent, that message comes through from one client, and is passed to all other clients in the Room (through `func (rm *Room) Broadcast()`). Other clients in the room will receive this message by listening to `receiver`, a channel of incoming messages.\n\n`OnMessage` is just an API hook for any code consuming the Client-Room-Message model, like the Plume server, to do something whenever a client receives a new message. In Plume.chat, the `OnMessage` handler forwards the message content onto the WebSocket connection corresponding to the client.\n\n### Room\n\n```go\ntype Room struct {\n\tSender chan\u003c- Message\n\t// map of usernames to emails\n\tverifiedNames   map[string]string\n\tclientReceivers map[*Client]chan Message\n}\n```\n\nA `Room` represents a collection of Clients (stored in `clientReceivers`). The Room also controls who can enter (if a new user wants to enter with an existing username claimed by someone else, for example, we'll reject.)\n\nMost importantly, a Room holds the main channel through which new messages are broadcast: the `Room.Sender`.\n\n### Server\n\n```go\ntype Server struct {\n\tRoom       *Room\n\tBotClient  *Client\n\tloginCodes map[string]User\n}\n```\n\nThe `Server` is tasked with keep track of most state about a running instance of a Plume web server. This includes the chat Room instance, a record of valid login codes and users they're associated with, and a special `@plumebot` user and client for each session, that claims the `@plumebot` username so nobody else can take it, and broadcasts any administrative messages to the whole Room.\n\nThe important state missing in `Server` is active WebSocket connections and Client lifecycle states, which are stored in closures that run as Goroutines whenever a WebSocket connection is initiated. You can find that closure defined in `server.go` as `func (srv *Server) connect()`.\n\n## Client lifecycle\n\nThe client lifecycle is the sequence of states a Plume client (the browser) goes through, to join a chat session:\n\n1. Every client starts out as just a simple HTTP client, no WebSocket connection.\n2. When the user tries to join with a username and email, the web client (`plume.js`) opens a new WebSocket connection by making a request to `/connect`. This [upgrades](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) the HTTP connection to a WebSocket connection, but no messages are sent at this point -- the client is still unauthenticated.\n3. The server either (a) sends a login code to the user through email, or (b) will respond with a `msgMayNotEnter` type message to the WebSocket client, which means that the username is taken.\n4. To authenticate, the client must send the server a `msgAuth` type message with the login code as the text content.\n5. If the server receives a matching login code, a `Client` instance will be created and paired to the right `Room`. At this point, the server starts forwarding any new messages between the `Client` and the WebSocket connection, and the chat can continue as normal. If the server receives an *incorrect* login code, it responds with `msgAuthRst`, which means that the code was not valid.\n6. To keep the WebSocket connection alive, the server sends a WebSocket ping message every minute -- when the connection dies, the Client leaves the room and no longer responds to incoming messages.\n\n## Deploy\n\nDeployment is managed by systemd. Copy the `plume.service` file to `/etc/systemd/system/plume.service` and update:\n\n- replace `plume-user` with your Linux user\n- replace `/home/plume-user/plume` with your working directory (path to repository or a copy of `static/`)\n\nThen start Plume as a service:\n\n```sh\nsystemctl daemon-reload # reload systemd script\nsystemctl start plume   # start Plume server as a service\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthesephist%2Fplume","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthesephist%2Fplume","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthesephist%2Fplume/lists"}