feat: initial commit

This commit is contained in:
Konstantin Zhigaylo 2025-06-23 21:47:24 +03:00
commit 3b27e15421
No known key found for this signature in database
GPG Key ID: DD1C2780F0E05B5C
33 changed files with 5718 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Konstantin Zhigaylo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Racinfo
An unofficial webpage with information about Real Address Chat protocol.

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

16
eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "rac-hub",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-tooltip": "^1.2.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.522.0",
"next": "15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@next/eslint-plugin-next": "^15.3.4",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.4",
"typescript": "^5"
}
}

4273
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- sharp
- unrs-resolver

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

122
src/app/globals.css Normal file
View File

@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-geist: var(--font-geist);
--font-rokkitt: var(--font-rokkitt);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
}
.dark {
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

34
src/app/layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Rokkitt } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist",
subsets: ["latin"],
});
const rokkitt = Rokkitt({
variable: "--font-rokkitt",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Racinfo",
description: "A website related to Real Address Chat protocol.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${rokkitt.variable} bg-stone-900 font-geist antialiased`}
>
{children}
</body>
</html>
);
}

25
src/app/page.tsx Normal file
View File

@ -0,0 +1,25 @@
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import Text from "@/components/blocks/Text";
export default function Home() {
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<Text>
Welcome to an unofficial webpage about the Real Address Chat protocol.
Here you can find clients, servers, and documentation on how to use
RAC and its successor, WRAC.
</Text>
<Text>
Want to add your project to Racinfo? Make a pull request on our
official GitHub repository, and well review it.
</Text>
<Footer />
</main>
</div>
);
}

178
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,178 @@
import { ProjectCard, ProjectProps } from "@/components/ProjectCard";
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import Text from "@/components/blocks/Text";
import PageTitle from "@/components/blocks/PageTitle";
import Title from "@/components/blocks/Title";
export default function Projects() {
const clients: ProjectProps[] = [
{
name: "Tower",
authorId: "kostya-zero",
authorGit: "https://github.com/kostya-zero",
projectGit: "https://github.com/kostya-zero/tower",
description: "A modern desktop client for RAC protocol built with Tauri.",
tags: ["Unreleased", "WRAC", "RAC", "v2", "TLS"],
},
{
name: "bRAC",
authorId: "MeexReay",
authorGit: "https://github.com/MeexReay",
projectGit: "https://github.com/MeexReay/bRAC",
description: "Better RAC client.",
tags: ["Active", "WRAC", "RAC", "v2", "TLS"],
},
{
name: "clRAC",
externalDownload: true,
authorId: "ghost",
authorGit: "https://github.com/ghost",
projectGit: "https://github.com/MeexReay/bRAC",
description: "The official RAC client.",
tags: ["Unknown", "RAC", "v2"],
},
{
name: "Mefedroniy",
authorId: "OctoBanon-Main",
authorGit: "https://github.com/OctoBanon-Main",
projectGit: "https://github.com/OctoBanon-Main/mefedroniy-client",
description: "TUI client for Real Address Chat protocol. ",
tags: ["Active", "RAC", "v1.99.2"],
},
{
name: "Snowdrop",
authorId: "Forbirdden",
authorGit: "https://github.com/Forbirdden",
projectGit: "https://github.com/Forbirdden/Snowdrop",
description: "Coming Soon™",
tags: ["Unreleased", "WRAC", "RAC", "v2"],
},
{
name: "cRACk",
authorId: "pansangg",
authorGit: "https://github.com/pansangg",
projectGit: "https://github.com/pansangg/cRACk",
description: "TUI RAC client on Python.",
tags: ["Active", "RAC", "v2"],
},
{
name: "CRAB",
authorId: "pixtated",
authorGit: "https://gitea.bedohswe.eu.org/pixtaded",
projectGit: "https://gitea.bedohswe.eu.org/pixtaded/crab",
description: "Crimean RAC Bundle ",
tags: ["Abandoned", "RAC", "v1.99.2"],
},
{
name: "dobroho_vechora",
authorId: "bedohswe",
authorGit: "https://gitea.bedohswe.eu.org/bedohswe",
projectGit: "https://gitea.bedohswe.eu.org/bedohswe/dobroho_vechora",
description: "RAC client made with Bash script.",
tags: ["Abandoned", "RAC", "v1.99.2"],
},
];
const servers: ProjectProps[] = [
{
name: "sRAC",
authorId: "MeexReay",
authorGit: "https://github.com/MeexReay",
projectGit: "https://github.com/MeexReay/sRAC",
description: "Simple RAC server.",
tags: ["Active", "WRAC", "RAC", "v2", "TLS"],
},
{
name: "Gashishnik",
authorId: "OctoBanon-Main",
authorGit: "https://github.com/OctoBanon-Main",
projectGit: "https://github.com/OctoBanon-Main/mefedroniy-client",
description: "A WRAC server.",
tags: ["Unreleased", "WRAC", "v2"],
},
{
name: "lRACd",
authorId: "ghost",
externalDownload: true,
authorGit: "https://github.com/MeexReay",
projectGit: "https://github.com/MeexReay/bRAC",
description: "The official implementation of RAC server.",
tags: ["Unknown", "RAC", "v2"],
},
{
name: "CRAB",
authorId: "pixtated",
authorGit: "https://gitea.bedohswe.eu.org/pixtaded",
projectGit: "https://gitea.bedohswe.eu.org/pixtaded/crab",
description: "Crimean RAC Bundle ",
tags: ["Abandoned", "RAC", "v1.99.2"],
},
{
name: "AlmatyD",
authorId: "bedohswe",
authorGit: "https://gitea.bedohswe.eu.org/bedohswe",
projectGit: "https://gitea.bedohswe.eu.org/bedohswe/almatyd",
description: "Open source server for Sugoma's RAC protocol ",
tags: ["Abandoned", "RAC", "v1.0"],
},
];
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<PageTitle id={"projects"}>Projects</PageTitle>
<Text>
This is a curated list of projects that is related to RAC protocol.
There is a client and server implementations of RAC. Note that some
projects are not available on Git services and should be downloaded
from external sources.
</Text>
<Title id={"clients"}>Clients</Title>
<Text>
Here is a grid of all available client for RAC protocol with their
respective repository link. Also, you can hover over the author
username to go to it&apos;s GitHub profile page.
</Text>
<div className={"grid grid-cols-2 gap-4 w-full"}>
{clients.map((client) => (
<ProjectCard
key={client.name}
name={client.name}
externalDownload={client.externalDownload || undefined}
externalLink={client.externalLink || undefined}
authorId={client.authorId}
authorGit={client.projectGit}
projectGit={client.projectGit}
description={client.description}
tags={client.tags}
/>
))}
</div>
<Title id={"servers"}>Servers</Title>
<Text>
And here are the servers implementation! You can choose whatever you
want and setup your own RAC server.
</Text>
<div className={"grid grid-cols-2 gap-4 w-full"}>
{servers.map((server) => (
<ProjectCard
key={server.name}
name={server.name}
externalDownload={server.externalDownload}
authorId={server.authorId}
authorGit={server.projectGit}
projectGit={server.projectGit}
description={server.description}
tags={server.tags}
/>
))}
</div>
<Footer />
</main>
</div>
);
}

81
src/app/protocol/page.tsx Normal file
View File

@ -0,0 +1,81 @@
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import Link from "next/link";
import Text from "@/components/blocks/Text";
import PageTitle from "@/components/blocks/PageTitle";
import Title from "@/components/blocks/Title";
export default function Protocol() {
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<PageTitle id={"protocol"}>Protocol</PageTitle>
<Text>
Real Address Chat is a protocol based on TCP intended for chatting,
like IRC. It was supposed to be an IRC killer, but in reality, its
implementation is poor. Theres also a community-made successor called
WRAC. Its basically the same as RAC, but it uses WebSockets instead
of TCP for connections.
</Text>
<Text>
If you want to experiment with or implement RAC in your client or
server, use the documentation below to understand how it works.
</Text>
<Title id={"articles"}>Articles</Title>
<Text>Click on article down below that you want to read.</Text>
<div className={"flex flex-col gap-2"}>
<Link
href={"/protocol/rac"}
className={
"flex flex-col bg-stone-900 border border-stone-800 p-4 transition-all duration-200 hover:border-neutral-600 hover:bg-stone-800 rounded-lg"
}
>
<h1 className={"text-stone-300 font-rokkitt text-2xl font-bold"}>
Real Address Chat Protocol
</h1>
<p className={"text-stone-500"}>
This article explains the Real Address Chat protocol and how the
client and server interact with each other using it.
</p>
<small className={"text-stone-600"}>Curated by @kostya-zero</small>
</Link>
<Link
href={"/protocol/wrac"}
className={
"flex flex-col bg-stone-900 border border-stone-800 p-4 transition-all duration-200 hover:border-neutral-600 hover:bg-stone-800 rounded-lg"
}
>
<h1 className={"text-stone-300 font-rokkitt text-2xl font-bold"}>
WebSocket Real Address Chat Protocol
</h1>
<p className={"text-stone-500"}>
WRAC is a WebSocket-based implementation of the RAC protocol, made
by the community. It works the same as RAC but includes some
additions.
</p>
<small className={"text-stone-600"}>Curated by @kostya-zero</small>
</Link>
<Link
href={"/protocol/user-agents"}
className={
"flex flex-col bg-stone-900 border border-stone-800 p-4 transition-all duration-200 hover:border-neutral-600 hover:bg-stone-800 rounded-lg"
}
>
<h1 className={"text-stone-300 font-rokkitt text-2xl font-bold"}>
User Agents
</h1>
<p className={"text-stone-500"}>
A community-made solution to identify clients by a unique symbol
in front of their username.
</p>
<small className={"text-stone-600"}>Curated by @kostya-zero</small>
</Link>
</div>
<Footer />
</main>
</div>
);
}

View File

@ -0,0 +1,206 @@
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Text from "@/components/blocks/Text";
import PageTitle from "@/components/blocks/PageTitle";
import Title from "@/components/blocks/Title";
import InlineCode from "@/components/blocks/InlineCode";
import Code from "@/components/blocks/Code";
export default function Rac() {
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<Link
href={"/protocol"}
className={
"text-stone-500 flex flex-row items-center transition duration-200 hover:text-stone-50 gap-2"
}
>
<ArrowLeft size={16} /> Go Back
</Link>
<PageTitle id={"real-address-chat-protocol"}>
Real Address Chat Protocol
</PageTitle>
<Text>
As mentioned earlier, RAC is a TCP-based protocol, so the server and
client communicate by sending TCP packets to each other. The
implementation is pretty simple, so dont worry.
</Text>
<Text>
All RAC servers use port <InlineCode>42666</InlineCode> by default,
but servers that implement RACS (Real Address Chat Secure) use port{" "}
<InlineCode>42667</InlineCode>.
</Text>
<Text>
Keep in mind that the RAC server closes the connection after each
message sent from the client. In some cases, it might receive two
packets, but it will still close the connection.
</Text>
<Title id={"receiving-messages"}>Receiving messages</Title>
<Text>
Receiving messages from the server is implemented in an unusual way.
There are two ways to receive messages from the server.
</Text>
<Text>
But before you try to receive new messages, you need to get the total
size of all messages. The messages are stored on the server in a
single file, and to get new messages, you should send an offset.
</Text>
<Text>
To receive the current size of the messages on the server, you need to
send a <InlineCode>0x00</InlineCode> byte to the server. In response,
the server will return the current size of the messages. Now, lets
begin receiving those messages.
</Text>
<Text>
The first way is to receive all messages stored on the server. The
client should send a <InlineCode>0x00</InlineCode> byte and then, in
the same stream, send a <InlineCode>0x01</InlineCode> byte to the
server. The server will respond with all messages, separated by{" "}
<InlineCode>\n</InlineCode>.
</Text>
<Text>
The second way is to get messages in chunks. Again, the client sends a{" "}
<InlineCode>0x00</InlineCode> byte and then sends a{" "}
<InlineCode>0x02</InlineCode> byte, along with the size of the
messages it wants to receive. The number of messages to receive is
calculated using this formula:
</Text>
<Code>new_received_size - last_known_size</Code>
<Text>
The server will send messages matching the requested size, separated
by <InlineCode>\n</InlineCode>.
</Text>
<Title id={"sending-messages"}>Sending messages</Title>
<Text>
Sending messages in RAC is implemented simply, but with some
interesting details. The server doesnt identify clients or users, so
clients have to handle that themselves. This means the client must
send the user agent, username, and message all in one packet. For
example:
</Text>
<Code>{"▲<zero> Hello, world!"}</Code>
<Text>
Did you notice the <InlineCode></InlineCode> symbol? This is called a
User Agent. In this example, the message was sent by the Tower client,
because <InlineCode></InlineCode> is its respective user agent. As
mentioned earlier, the server itself cannot identify clients, so
clients have to identify each other using these user agents.
</Text>
<Text>
Also, note that <InlineCode>zero</InlineCode> is wrapped inside{" "}
<InlineCode>{"<>"}</InlineCode>. This approach makes parsing messages
easier with regex. Learn more about user agents and how to create your
own in the user agents article.
</Text>
<Text>
Note that the server should store the IP address with each sent
message. However, messages sent by clients in authorized mode should
be ignored for this. This idea, introduced by Sugoma, means that
clients shouldnt be anonymous unless theyre authorized. Still, most
clients ignore the IP address and dont display it.
</Text>
<Text>
Back to sending messages: To send a message to the server, the client
should send a <InlineCode>0x01</InlineCode> byte, followed by the
message in the same packetnot separately. After that, you wont
receive anything from the server, even if theres an error. So your
final message should look like this:
</Text>
<Code>{"<user> Hello, everyone!"}</Code>
<Text>
Also, server that are implements RAC v2 allows to send messages in
authorized mode. To send message in authorized mode, you need to send{" "}
<InlineCode>0x02</InlineCode> and after that, separating by{" "}
<InlineCode>\n</InlineCode>, send user&apos;s user name, password and
message (<b>do not</b> include <InlineCode>\n</InlineCode> in the
end).
</Text>
<Text>
If the message was sent successfully, the server wont send any
response. If not, the server will send a <InlineCode>0x01</InlineCode>{" "}
byte if the user doesnt exist, and a <InlineCode>0x02</InlineCode>{" "}
byte if the password is incorrect.
</Text>
<Title id={"authorization"}>Authorization</Title>
<Text>
Authorization is only available on servers that implement the RAC v2
protocol, which adds an authorization system. This feature was added
by Sugoma for people who dont want to expose their IP address to
everyone.
</Text>
<Text>
Before sending messages in authorized mode, you need to register a
user on the server. To do this, the client should send a{" "}
<InlineCode>0x03</InlineCode> byte and, just like when sending
messages, include the username and password separated by{" "}
<InlineCode>\n</InlineCode>. If the user is created, the client will
receive nothing in response. If the user already exists, the server
will respond with a <InlineCode>0x01</InlineCode> byte.
</Text>
<Title id={"tls-connection"}>TLS Connection</Title>
<Text>
You can wrap your connection with TLS encryption. Theres nothing
special about it.
</Text>
<Title id={"special-packets"}>Special Packets</Title>
<Text>
Some servers can implement special packets, but most of the time they
dont add much functionality.
</Text>
<Text>
One of them is the <InlineCode>0x69</InlineCode> byte. You can send
this byte to the server to get technical information about it. For
example, after sending this byte, you might receive these bytes in
response:
</Text>
<Table>
<TableHeader>
<TableRow>
<TableHead className={"w-[150px]"}>Byte</TableHead>
<TableHead>Meaning</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className={"font-semibold"}>
<InlineCode>0x01</InlineCode>
</TableCell>
<TableCell>The server supports RAC v1.0</TableCell>
</TableRow>
<TableRow>
<TableCell className={"font-semibold"}>
<InlineCode>0x02</InlineCode>
</TableCell>
<TableCell>The server supports RAC v1.99</TableCell>
</TableRow>
<TableRow>
<TableCell className={"font-semibold"}>
<InlineCode>0x03</InlineCode>
</TableCell>
<TableCell>The server supports RAC v2.0</TableCell>
</TableRow>
</TableBody>
</Table>
<Text>
Also, right after this byte, the server will send information about
itself, such as the server software name and its version.
</Text>
<Footer />
</main>
</div>
);
}

View File

@ -0,0 +1,128 @@
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Text from "@/components/blocks/Text";
import PageTitle from "@/components/blocks/PageTitle";
import Code from "@/components/blocks/Code";
import InlineCode from "@/components/blocks/InlineCode";
type UserAgent = {
client: string;
regex: string;
color: string;
};
export default function Wrac() {
const userAgents: UserAgent[] = [
{
client: "Tower",
regex: "\\u25B2<(.*?)> (.*)",
color: "White",
},
{
client: "bRAC",
regex: "\\uB9AC\\u3E70<(.*?)> (.*)",
color: "Green",
},
{
client: "CRAB",
regex: "\\u2550\\u2550\\u2550<(.*?)> (.*)",
color: "Light Red",
},
{
client: "Mefedroniy",
regex: "\\u00B0\\u0298<(.*?)> (.*)",
color: "Light Magenta",
},
{
client: "cRACk",
regex: "\\u2042<(.*?)> (.*)",
color: "Gold",
},
{
client: "Snowdrop",
regex: "\\u0D9E<(.*?)> (.*)",
color: "Light Green",
},
{
client: "clRAC",
regex: "<(.*?)> (.*)",
color: "Cyan",
},
];
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<Link
href={"/protocol"}
className={
"text-stone-500 flex flex-row items-center transition duration-200 hover:text-stone-50 gap-2"
}
>
<ArrowLeft size={16} /> Go Back
</Link>
<PageTitle id={"user-agents"}>User Agents</PageTitle>
<Text>
The RAC protocol doesnt have any functionality to identify clients,
so the community decided to use their own solution called{" "}
<b>User Agents</b>.
</Text>
<Text>
User Agents in RAC are implemented by adding an extra Unicode symbol
as a prefix to usernames. For example, a message sent with the Tower
client:
</Text>
<Code>{"▲<zero> Hello, world!"}</Code>
<Text>
The <InlineCode></InlineCode> symbol in front of the username
indicates that this message was sent using the Tower client. The
client should use regex to parse these messages and determine the
client, username, and message.
</Text>
<h3 className={"text-3xl text-stone-300 font-semibold font-rokkitt"}>
Known Agents
</h3>
<Text>
Below is a table of known user agents with their client names and
regular expressions. Clients can also optionally use colored usernames
for each client. You can add your own user agent via a pull request.
</Text>
<Table>
<TableHeader>
<TableRow>
<TableHead className={"w-[150px]"}>Client Name</TableHead>
<TableHead>Regular Expression</TableHead>
<TableHead>Color</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{userAgents.map((agent) => (
<TableRow key={agent.client}>
<TableCell className={"font-semibold"}>
{agent.client}
</TableCell>
<TableCell>
<code>{agent.regex}</code>
</TableCell>
<TableCell>{agent.color}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Footer />
</main>
</div>
);
}

View File

@ -0,0 +1,35 @@
import NavBar from "@/components/NavBar";
import Footer from "@/components/Footer";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import Text from "@/components/blocks/Text";
import PageTitle from "@/components/blocks/PageTitle";
export default function Wrac() {
return (
<div className={"bg-stone-900 min-h-screen font-geist"}>
<main
className={"max-w-[700px] pt-15 flex flex-col gap-4 mx-auto w-full p-4"}
>
<NavBar />
<Link
href={"/protocol"}
className={
"text-stone-500 flex flex-row items-center transition duration-200 hover:text-stone-50 gap-2"
}
>
<ArrowLeft size={16} /> Go Back
</Link>
<PageTitle id={"websocket-real-address-chat-protocol"}>
WebSocket Real Address Chat Protocol
</PageTitle>
<Text>
WRAC (WebSocket Real Address Chat) is a community-made successor to
RAC that uses WebSockets instead of TCP. It uses the same requests as
RAC, but you should send data in binary format.
</Text>
<Footer />
</main>
</div>
);
}

68
src/components/Badge.tsx Normal file
View File

@ -0,0 +1,68 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Props = {
text: string;
};
export default function Badge({ text }: Props) {
let color: string = "";
let description: string;
switch (text.trim()) {
case "Active":
color = "#006045";
description = "Actively developed and maintained.";
break;
case "Unreleased":
color = "#733e0a";
description = "Not released, but in development";
break;
case "Abandoned":
color = "#82181a";
description = "Abandoned by author.";
break;
case "Unknown":
color = "#024a70";
description = "The project is in unknown state.";
break;
case "WRAC":
description = "Supports connection via WebSocket-base RAC protocol.";
break;
case "RAC":
description = "Supports connection via TCP RAC protocol.";
break;
case "v2":
description = "Compatible with RAC v2.";
break;
case "v1.99.2":
description = "Compatible with RAC v1.99.2.";
break;
case "v1.0":
description = "Compatible with RAC v1.0.";
break;
case "TLS":
description = "Supports connections via TLS.";
break;
default:
description = "???";
break;
}
return (
<Tooltip>
<TooltipTrigger asChild>
<figure
className={`text-sm py-1 px-2 font-medium font-geist text-stone-300 bg-stone-800 rounded-lg`}
style={{ backgroundColor: color }}
>
{text}
</figure>
</TooltipTrigger>
<TooltipContent>
<p className={"text-stone-300"}>{description}</p>
</TooltipContent>
</Tooltip>
);
}

24
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,24 @@
import Link from "next/link";
export default function Footer() {
return (
<footer className="flex flex-col md:flex-row justify-between items-center mb-[15px] text-sm">
<Link
href="https://kostyazero.com"
className="ml-1 text-stone-500 hover:text-stone-50 transition duration-200"
>
© 2025 Konstantin Zhigaylo
</Link>
<div className="flex text-stone-500 items-center">
<p>The source code is available on</p>
<Link
href="https://github.com/kostya-zero/racinfo"
className="ml-1 text-stone-500 hover:text-stone-50 transition duration-200"
>
{" "}
GitHub
</Link>
</div>
</footer>
);
}

View File

@ -0,0 +1,26 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
type HoverTextProps = {
children: React.ReactNode;
href: string;
className?: string;
};
export default function HoverLink({
children,
href,
className = "",
}: HoverTextProps) {
return (
<Link
href={href}
className={cn(
"relative hover:text-stone-50 cursor-pointer transition-all ease-in-out before:transition-[width] before:ease-in-out before:duration-300 before:absolute before:bg-neutral-50 before:origin-center before:h-[1px] before:w-0 hover:before:w-[50%] before:bottom-0 before:left-[50%] after:transition-[width] after:ease-in-out after:duration-300 after:absolute after:bg-neutral-50 after:origin-center after:h-[1px] after:w-0 hover:after:w-[50%] after:bottom-0 after:right-[50%]",
className,
)}
>
<span>{children}</span>
</Link>
);
}

42
src/components/NavBar.tsx Normal file
View File

@ -0,0 +1,42 @@
"use client";
import HoverLink from "@/components/HoverLink";
import { usePathname } from "next/navigation";
export default function NavBar() {
const pathname = usePathname();
return (
<header className={"w-full flex flex-row items-end justify-between"}>
<h1 className={"text-5xl text-stone-200 font-rokkitt font-bold"}>
Racinfo
</h1>
<nav className={"flex flex-row items-center gap-4 mb-2"}>
<HoverLink
href={"/"}
className={`hover:text-stone-50 transition duration-200 ${
pathname === "/" ? "text-stone-50" : "text-stone-500"
}`}
>
Home
</HoverLink>
<HoverLink
href={"/protocol"}
className={`hover:text-stone-50 transition duration-200 ${
pathname === "/protocol" ? "text-stone-50" : "text-stone-500"
}`}
>
Protocol
</HoverLink>
<HoverLink
href={"/projects"}
className={`hover:text-stone-50 transition duration-200 ${
pathname === "/projects" ? "text-stone-50" : "text-stone-500"
}`}
>
Projects
</HoverLink>
</nav>
</header>
);
}

View File

@ -0,0 +1,63 @@
import Badge from "@/components/Badge";
import Link from "next/link";
type ProjectProps = {
name: string;
authorId: string;
authorGit: string;
projectGit: string;
description: string;
externalLink?: string;
externalDownload?: boolean;
tags: string[];
};
function ProjectCard({
name,
authorGit,
externalDownload,
projectGit,
externalLink,
description,
authorId,
tags,
}: ProjectProps) {
return (
<Link
href={
externalDownload && externalDownload ? externalLink || "" : projectGit
}
className={
"w-full flex flex-col gap-1 bg-stone-900 border select-none cursor-pointer border-stone-800 transition-all duration-200 hover:border-neutral-600 hover:bg-stone-800 rounded-lg p-4 shadow-[0px_4px_6px_-1px_rgba(0,0,0,0.1),0px_2px_4px_-1px_rgba(0,0,0,0.06)]"
}
>
<div className={"flex flex-row justify-between items-center"}>
<h4 className={"text-2xl text-stone-300 font-rokkitt font-semibold"}>
{name}
</h4>
{externalDownload ? (
<p className={"text-stone-500"}>external download</p>
) : (
<Link
href={authorGit}
className={
"text-stone-500 transition-colors duration-200 hover:text-stone-50"
}
>
@{authorId}
</Link>
)}
</div>
<p className={"text-stone-400 mb-1"}>{description}</p>
<div className={"flex flex-row mt-auto gap-1 items-center"}>
{tags.map((tag) => (
<Badge text={tag} key={tag} />
))}
</div>
</Link>
);
}
export { ProjectCard, type ProjectProps };

View File

@ -0,0 +1,11 @@
type Props = {
children?: React.ReactNode;
};
export default function Code({ children }: Props) {
return (
<code className={"bg-stone-800 text-stone-400 px-3 py-2 rounded-sm"}>
{children}
</code>
);
}

View File

@ -0,0 +1,7 @@
type Props = {
children: React.ReactNode;
};
export default function InlineCode({ children }: Props) {
return <code className={"bg-stone-800 px-1 rounded-sm"}>{children}</code>;
}

View File

@ -0,0 +1,13 @@
export default function PageTitle({
children,
id,
}: {
children: React.ReactNode;
id: string;
}) {
return (
<h2 id={id} className={"text-4xl text-stone-300 font-bold font-rokkitt"}>
{children}
</h2>
);
}

View File

@ -0,0 +1,7 @@
type Props = {
children?: React.ReactNode;
};
export default function Text({ children }: Props) {
return <p className={"text-stone-400 text-lg"}>{children}</p>;
}

View File

@ -0,0 +1,15 @@
type Props = {
children?: React.ReactNode;
id: string;
};
export default function Title({ children, id }: Props) {
return (
<h3
id={id}
className={"text-3xl text-stone-300 font-semibold font-rokkitt"}
>
{children}
</h3>
);
}

112
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,112 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm border-0", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead data-slot="table-header" className={cn("", className)} {...props} />
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"data-[state=selected]:bg-muted transition-colors",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-stone-300 font-rokkitt text-lg h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle text-stone-400 whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -0,0 +1,61 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary select-none text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}