summaryrefslogtreecommitdiff
path: root/blogs/discord-sucks-so-i-made-my-own.md
diff options
context:
space:
mode:
Diffstat (limited to 'blogs/discord-sucks-so-i-made-my-own.md')
-rw-r--r--blogs/discord-sucks-so-i-made-my-own.md186
1 files changed, 186 insertions, 0 deletions
diff --git a/blogs/discord-sucks-so-i-made-my-own.md b/blogs/discord-sucks-so-i-made-my-own.md
new file mode 100644
index 0000000..bfd4057
--- /dev/null
+++ b/blogs/discord-sucks-so-i-made-my-own.md
@@ -0,0 +1,186 @@
+---
+title: "Discord sucks! ...so I made my own"
+description: How I built Eko - THE discord alternative for terminal nerds
+date: 2025-08-08
+---
+
+## Discord sucks
+
+I have been disatisfied with discord for a few years by now, the main reasons that most people will
+agree with me, are it's slow, sluggish, bloated, and that it gets worse over time.
+
+- Reduced upload size from 50mb to 10mb
+- Ugly UI revamps
+- Literally ships with an entire browser (chromium due to electron)
+
+But there are a few more personal reasons for me.
+
+Ignoring the fact that I am in 100 servers and literally can't join any more,
+as someone who spends most of his day in the terminal, writing code with NVIM (btw),
+I really missed vim motions in discord, they are great and I'd probably talk about them in the future.
+
+## Searching for alternatives
+
+Vim motions are so ubiquitous they are practically everywhere, not just coding editors like Zed and
+vsc\*de but also note taking apps like Obsidian, and heck, even google docs have an extension for them.
+
+Surely an app as popular as Discord will have them right? well after some searching I found
+[Cordless](https://github.com/Bios-Marcel/cordless), a discord client in the terminal, great!
+
+Reading the readme, I immediately got dissapointed, the project has been shutdown.
+The reason was due to a high risk of Discord banning users for ToS violation, so I couldn't even make my own client!
+
+## The idea
+
+So I thought to myself, if Discord doesn't let me make a client, I'll write my own Discord, complete
+with a backend, frontend and most importantly, vim motions!
+
+Great, now that I have the idea, what languages do I use for the backend and the frontend?
+
+I heard that golang is a really good language for the backend, due to it's concurrency model making
+it easy to handle a ton of connections simultaneously.
+
+And what about the frontend? I'd need to find some library to help me make a TUI (terminal UI),
+After some searching, I found [bubbletea](https://github.com/charmbracelet/bubbletea) from [charm](https://github.com/charmbracelet),
+a golang library for creating TUIs, perfect.
+
+This has the added benefit of the backend and frontend being able to share types as they are
+written in the same language.
+
+## The implementation
+
+### The frontend
+
+Bubbletea uses the Elm architecture to create a UI, it is made out of 3 components:
+
+- The model - the state of the UI, it can contain UI elements, buttons, styles, and even other models
+- The view - a function that takes a model and returns the UI, in the case of a TUI, it just returns a string
+- The update - a function that takes the model and a message, and computes a new model based on that message
+
+The way it works, is a new message comes, for example a user resized the window, this message
+gets propagated to the model via the update method, which produces a new model, then
+the view method takes the new model and renders the new state to the UI.
+
+The concept itself is really great, although Bubbletea, partly due to golang had to make a few
+tradeoffs which made me like it less.
+
+First, golang doesn't have the concept of "const", so the view must take a copy of the model,
+FOR EVERY RE-RENDER, this is extremely inefficient, especially that most models can take thousands
+of bytes if not more, and that the copy is being pased on the stack.
+
+This doesn't only happen on view, this also happens in the update function, every update, the model
+also gets copied and passed on the stack, then the function modifies the stack version and returns
+a new version of it that is then also, gets copied on the stack.
+
+This is due to the Elm architecture originating from Elm, a functional programming language, in
+such languages mutating is discouraged, and everything gets copied.
+
+### The backend
+
+The server itself is fairly boring, it has a loop that accepts new TCP/TLS connections, and it
+spawns a new goroutine ("Green thread") to handle the client requests.
+
+So instead, let's talk about how IDs are handled!
+
+My first idea was to use a UUID (universally unique identifier), but it takes 128 bits, that seems
+way too much, 32 bits already allow for 4 billion permutations, but just in case (see IPv4 lol),
+64 bits should be plenty, that's 18 quintillion!
+
+For the format, is it fine to just randomly generate them? 18 quintillion is alot, but the chance
+of collissions is not as low as I'd like it to be, I wonder what Discord uses for their IDs...
+
+#### Snowflake IDs
+
+Discord uses snowflake IDs, originally invented by Twitter, they are 64-bit with a special format
+that guranteed uniqueness across multiple machines and data centers, here's how they work:
+
+```
+ 0 1 2 3 4 5 6
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|s| Milliseconds since Epoch | Node ID | Mac ID | Incremented |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+```
+
+- s - The sign bit when storing the ID as an int64, unused (always 0)
+- Milliseconds since Epoch - The number of milliseconds since some date, in Twitter's case this is 2010, for Discord it's 2015, and for Eko it's 2025
+- Node ID - A unique identifier for the data center or cluster, in case of Eko this is always just 1 as I run eko on a single VPS server
+- Mac ID - The specific machine ID within a cluster, in Eko this is combined with the NodeID into a single 10-bit field, with a value of 1
+- Incremented - If 2 IDs are generated in the same millisecond, this is incremented, up to 4095 times,
+allowing a single machine to generate 4 million IDs per second (1000 * 4096)
+
+The snowflake ID also has the nice benefits of embedding a timestamp in it, this allows to save
+even more space by not needing to store a separate unix timestamp for creation dates, the creation
+date of a user is when their ID was first generated.
+
+If you are interested in knowing when your discord account was created, there are [websites](https://snowsta.mp/)
+that allow you to check this.
+
+Because everything in Eko uses snowflake IDs (users, networks, frequencies, messages) this trivially
+allows us to view the timestamp, the client just shifts 22 bits to get the milliseconds, adds the
+hardcoded Epoch, formats the unix timestamp, and now you got timestamps of messages.
+
+### The protocol
+
+Ok, so we have the backend, we have the frontend, but how will they communicate over the network?
+
+The first thought that came to mind, was to use whatever Discord uses, which is HTTPS and websockets,
+but this felt wrong, using such a bloated protocol just for sending a few text messages.
+
+I decided it'd be better to design my own protocol from scratch, that'd be efficient and small.
+After a while of planning, I came up with this packet format:
+
+```
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| Version |En.| Type | Payload Length |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| Payload... Payload Length bytes ... |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+```
+
+- Version (8 bits) - the most important thing when designing a protocol is to have a version field,
+ so that when you inevitably introduce breaking changes, the server and client will notice a mismatch
+ and know how to deal with it
+- Encoding (2 bits) - Indicates the format of the payload, more on that later
+- Type (6 bits) - The type of the payload, what kind of message it is
+- Payload Length (16 bits) - How long is the payload, 16 bits gives us a maximum payload size of 65,536 bytes
+- Payload (0-65k bytes) - The actual body of the message, formatted per the encoding
+
+I was thinking about how to encode the payload in an efficient format, JSON was just too verbose
+for this, after taking a look at protobufs, cap'n proto and MsgPack, I chose MsgPack, mostly
+because of it's ease of use while being almost as fast as the other 2.
+
+But I still wanted to support JSON, so I introduced a 2-bit encoding into my packet,
+the idea is the packet sender specifies 0 for JSON, 1 for MsgPack, and the other 2 are reserved.
+That way the server and client can communicate with MsgPack, but if a developer writes their own
+client that uses JSON, the server can handle that just as fine.
+
+Another thing to mention is how the type thing is used as a "tagged union", it specifies the
+structure of the payload, unlike a tagged union though, it doesn't need to store the size of the
+largest packet, that's what the payload length field is for.
+
+## The release
+
+After months of work, I am happy to present...
+
+[Eko](https://github.com/kyren223/eko) - a terminal-native discord alternative for terminal enthusiasts!
+
+<img
+ src="/eko-screenshot.png"
+ alt="Description"
+ style={{ width: "1000px", paddingBottom: "20px" }}
+/>
+
+For more information head over to the [README](https://github.com/kyren223/eko), or install it now with
+
+```
+curl -sS https://eko.kyren.codes/install.sh | sh
+```
+
+Hope to see you there!
+
+As always if you have questions or want to chat, feel free to contact me on discord at Kyren223 or email me at `contact at kyren.codes`.
+
+You can also add me on Eko, here's my ID: `2102554263552`