This commit is contained in:
Radon 2025-01-18 22:01:02 -06:00
commit 1bf98ab3eb
15 changed files with 2734 additions and 0 deletions

BIN
chatserver Normal file

Binary file not shown.

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module chat
go 1.23.4
require github.com/mattn/go-sqlite3 v1.14.24 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

627
main.go Normal file
View File

@ -0,0 +1,627 @@
package main
import (
"compress/gzip"
"database/sql"
"encoding/json"
"fmt"
_ "github.com/mattn/go-sqlite3"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
type Database struct {
db *sql.DB
}
func TimezoneToLocation(timezone string) *time.Location {
defaultLocation := time.FixedZone("UTC", 0)
location, err := time.LoadLocation(timezone)
if err != nil {
return defaultLocation
} else {
return location
}
}
func TimeStringToTime(timeString string) time.Time {
t, _ := time.Parse("2006-01-02 15:04:05", timeString)
return t
}
func TimeStringToTimeInLocation(timeString string, timezone string) string {
t := TimeStringToTime(timeString)
location := TimezoneToLocation(timezone)
return t.In(location).Format("2006-01-02 15:04:05")
}
func OpenDatabase(filepath string) *Database {
if db, err := sql.Open("sqlite3", filepath); err != nil {
return nil
} else {
return &Database{
db: db,
}
}
}
func (db *Database) Close() {
db.db.Close()
}
func (db *Database) DbCreateTableMessages() {
stmt := `CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
db.db.Exec(stmt)
}
func (db *Database) DbCreateTableUsers() {
stmt := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
timezone TEXT DEFAULT 'America/New_York',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`
db.db.Exec(stmt)
}
func (db *Database) UserTimezoneSet(ip_address, timezone string) {
stmt, err := db.db.Prepare("UPDATE users SET timezone = ? WHERE ip_address = ?")
if err != nil {
fmt.Println(err)
}
stmt.Exec(timezone, ip_address)
}
func (db *Database) UserAdd(ip_address, username string) {
stmt, err := db.db.Prepare("INSERT INTO users (username, ip_address) VALUES (?, ?)")
if err != nil {
fmt.Println(err)
}
stmt.Exec(username, ip_address)
}
func (db *Database) MessageAdd(ip_address string, content string) {
stmt, err := db.db.Prepare("INSERT INTO messages (ip_address, content) VALUES (?, ?)")
if err != nil {
fmt.Println(err)
}
stmt.Exec(ip_address, content)
}
func (db *Database) UserNameGet(ip_address string) string {
rows, err := db.db.Query("SELECT username FROM users WHERE ip_address = ?", ip_address)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var username string
rows.Next()
rows.Scan(&username)
return username
}
func (db *Database) UserGetTimezone(ip_address string) string {
rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var timezone string
rows.Next()
rows.Scan(&timezone)
return timezone
}
func (db *Database) UsersGet() []User {
rows, err := db.db.Query("SELECT * FROM users")
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var users []User
for rows.Next() {
var id string
var ip_address string
var username string
var created_at string
var timezone string
rows.Scan(&id, &ip_address, &username, &created_at, &timezone)
user := User{
Id: id,
Username: username,
IpAddress: ip_address,
Timezone: timezone,
}
users = append(users, user)
}
return users
}
func (db *Database) MessagesGet() []Message {
rows, err := db.db.Query(`
SELECT messages.id, messages.ip_address, messages.content,
strftime('%Y-%m-%d %H:%M:%S', messages.created_at) as created_at,
users.username
FROM messages
LEFT JOIN users ON messages.ip_address = users.ip_address;
`)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var messages []Message
for rows.Next() {
var id string
var content string
var ip_address string
var created_at string
var username string
rows.Scan(&id, &ip_address, &content, &created_at, &username)
message := Message{
Id: id,
Content: content,
SenderIp: ip_address,
SenderUsername: username,
Timestamp: created_at,
}
messages = append(messages, message)
}
return messages
}
func (db *Database) UserNameExists(username string) bool {
rows, err := db.db.Query("SELECT * FROM users WHERE username = ?", username)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
return rows.Next()
}
func (db *Database) UserExists(ip string) bool {
rows, err := db.db.Query("SELECT * FROM users WHERE ip_address = ?", ip)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
return rows.Next()
}
func (db *Database) UserNameChange(ip, newUsername string) {
stmt, err := db.db.Prepare("UPDATE users SET username = ? WHERE ip_address = ?")
if err != nil {
fmt.Println(err)
}
stmt.Exec(newUsername, ip)
}
func (db *Database) UserMessagesGet(ip string) []Message {
rows, err := db.db.Query(`
SELECT messages.*, users.username
FROM messages
LEFT JOIN users ON messages.ip_address = users.ip_address
WHERE messages.ip_address = ?
ORDER BY messages.created_at DESC;
`, ip)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var messages []Message
for rows.Next() {
var id string
var content string
var ip_address string
var created_at string
var username string
rows.Scan(&id, &ip_address, &content, &created_at, &username)
t, _ := time.Parse(created_at, created_at)
message := Message{
Id: id,
Content: content,
SenderIp: ip_address,
SenderUsername: username,
Timestamp: t.Format(created_at),
}
messages = append(messages, message)
}
return messages
}
func (db *Database) MessageDelete(id string) {
stmt, err := db.db.Prepare("DELETE FROM messages WHERE id = ?")
if err != nil {
fmt.Println(err)
}
stmt.Exec(id)
}
func (db *Database) UserDelete(ip string) {
stmt, err := db.db.Prepare("DELETE FROM users WHERE ip_address = ?")
if err != nil {
fmt.Println(err)
}
stmt.Exec(ip)
}
func (db *Database) UsersDelete() {
stmt, err := db.db.Prepare("DELETE FROM users")
if err != nil {
fmt.Println(err)
}
stmt.Exec()
}
func (db *Database) MessagesDelete() {
stmt, err := db.db.Prepare("DELETE FROM messages")
if err != nil {
fmt.Println(err)
}
stmt.Exec()
}
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.Writer.Write(data)
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
func getClientIP(r *http.Request) string {
if fwdIP := r.Header.Get("X-Forwarded-For"); fwdIP != "" {
return strings.Split(fwdIP, ",")[0]
}
clientIP := r.RemoteAddr
if host, _, err := net.SplitHostPort(clientIP); err == nil {
return host
}
return clientIP
}
type User struct {
Id string
Username string
IpAddress string
Timezone string
}
type Message struct {
Id string
Content string
SenderIp string
SenderUsername string
Timestamp string
}
type Server struct {
Ip string
Port int
Connected map[string]time.Time // Map IP -> Last activity time
Database *Database
mu sync.Mutex // For thread safety
}
func NewServer(ip string, port int, dbpath string) *Server {
return &Server{
Ip: ip,
Port: port,
Connected: make(map[string]time.Time),
Database: OpenDatabase(dbpath),
mu: sync.Mutex{},
}
}
func (s *Server) AddMessage(userip string, contents string) {
s.Database.MessageAdd(userip, contents)
}
func (s *Server) updateActivity(ip string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Connected[ip] = time.Now()
}
func (s *Server) cleanupActivity() {
s.mu.Lock()
defer s.mu.Unlock()
for ip, lastActivity := range s.Connected {
if time.Since(lastActivity) > 10*time.Second {
delete(s.Connected, ip)
}
}
}
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
w.WriteHeader(http.StatusOK)
}
func validUsername(username string) bool {
for _, c := range username {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
return false
}
}
return true
}
func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
var req struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
s.mu.Lock()
if len(req.Username) > 64 {
http.Error(w, fmt.Sprintf(`{"error": "Username too long (must be less than 64 characters)"}`), http.StatusRequestEntityTooLarge)
s.mu.Unlock()
return
}
if !validUsername(req.Username) {
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
s.mu.Unlock()
return
}
if s.Database.UserNameExists(req.Username) {
s.mu.Unlock()
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
return
}
if s.Database.UserExists(clientIP) {
s.Database.UserNameChange(clientIP, req.Username)
} else {
s.Database.UserAdd(clientIP, req.Username)
}
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
}
func getMessageTemplate(file string, body string) string {
contents, _ := os.ReadFile(file)
return strings.Replace(string(contents), "{{body}}", body, 1)
}
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "text/html")
var body string
messages := s.Database.MessagesGet()
for _, msg := range messages {
clientIP := getClientIP(r)
timeZone := s.Database.UserGetTimezone(clientIP)
timeLocal := TimeStringToTimeInLocation(msg.Timestamp, timeZone)
body += fmt.Sprintf(`<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
msg.SenderUsername, timeLocal, msg.Content)
}
w.Write([]byte(getMessageTemplate("messages.html", body)))
case http.MethodPut:
w.Header().Set("Content-Type", "application/json")
// Get client's IP
clientIP := getClientIP(r)
s.mu.Lock()
exists := s.Database.UserExists(clientIP)
username := s.Database.UserNameGet(clientIP)
s.mu.Unlock()
if !exists {
errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
http.Error(w, errorFmt, http.StatusUnauthorized)
return
}
var msg struct {
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
s.Database.MessageAdd(clientIP, msg.Message)
json.NewEncoder(w).Encode(map[string]string{
"status": "Message received",
"from": username,
})
default:
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
}
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.mu.Lock()
exists := s.Database.UserExists(clientIP)
username := s.Database.UserNameGet(clientIP)
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"hasUsername": exists,
"username": username,
})
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
s.mu.Lock()
var users []string
for ip := range s.Connected {
// for all connected, get their usernames
users = append(users, s.Database.UserNameGet(ip))
}
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"users": users,
})
}
func (s *Server) handleTimezone(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
var req struct {
Timezone string `json:"timezone"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
s.mu.Lock()
if !s.Database.UserExists(clientIP) {
http.Error(w, `{"error": "User not registered"}`, http.StatusUnauthorized)
s.mu.Unlock()
return
}
s.Database.UserTimezoneSet(clientIP, req.Timezone)
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
})
}
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(readFile("root.html"))
}
func readFile(filepath string) []byte {
contents, _ := os.ReadFile(filepath)
return contents
}
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "application/javascript")
w.Write(readFile("root.js"))
}
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "text/css")
w.Write(readFile("root.css"))
}
func (s *Server) Run() {
handler := http.NewServeMux()
handler.HandleFunc("/ping", s.handlePing)
handler.HandleFunc("/username", s.handleUsername)
handler.HandleFunc("/messages", s.handleMessages)
handler.HandleFunc("/username/status", s.handleUsernameStatus)
handler.HandleFunc("/users", s.handleUsers)
handler.HandleFunc("/", s.handleRoot)
handler.HandleFunc("/root.js", s.handleJs)
handler.HandleFunc("/root.css", s.handleCss)
handler.HandleFunc("/timezone", s.handleTimezone)
fmt.Printf("Server starting on %s:%d\n", s.Ip, s.Port)
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Ip, s.Port), GzipMiddleware(handler)); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func (s *Server) Stop() {
s.Database.Close()
}
func main() {
ip := os.Args[1]
port, _ := strconv.Atoi(os.Args[2])
databaseFile := os.Args[3]
server := NewServer(ip, port, databaseFile)
server.Database.DbCreateTableMessages()
server.Database.DbCreateTableUsers()
defer server.Stop()
server.Run()
}

7
messages.html Normal file
View File

@ -0,0 +1,7 @@
<html>
<body>
<div class="container">
{{body}}
</div>
</body>
</html>

17
notes/todo.md Normal file
View File

@ -0,0 +1,17 @@
# Changes To Make
## Frontend
### High Priority
- Nothing yet
### Mid Priority
- Nothing yet
### Low Priority
- Mobile formatting @media
## Backend
### High Priority
- Nothing yet
### Mid Priority
- Nothing yet
### Low Priority
- Search functionality?
- Delete messages? (maybe later)
- Old messages should be deleted? Or if database is over a certain size? Not sure if this is really necessary.

333
old/main.go Normal file
View File

@ -0,0 +1,333 @@
package main
import (
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.Writer.Write(data)
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
func getClientIP(r *http.Request) string {
if fwdIP := r.Header.Get("X-Forwarded-For"); fwdIP != "" {
return strings.Split(fwdIP, ",")[0]
}
clientIP := r.RemoteAddr
if host, _, err := net.SplitHostPort(clientIP); err == nil {
return host
}
return clientIP
}
type Message struct {
Content string
SenderIp string
SenderUsername string
Timestamp string
}
func NewMessage(username, ip, content string) *Message {
timestamp := time.Now().Format("2006-01-02 15:04:05")
return &Message{
Content: content,
SenderIp: ip,
SenderUsername: username,
Timestamp: timestamp,
}
}
type Server struct {
Ip string
Port int
Messages []Message
// Add mappings for IPs and usernames
Connected map[string]time.Time // Map IP -> Last activity time
Usernames map[string]string // Map IP -> Username
mu sync.Mutex // For thread safety
}
func NewServer(ip string, port int) *Server {
return &Server{
Ip: ip,
Port: port,
Messages: make([]Message, 0),
Connected: make(map[string]time.Time),
Usernames: make(map[string]string),
mu: sync.Mutex{},
}
}
func (s *Server) AddMessage(username string, userip string, contents string) {
message := NewMessage(username, userip, contents)
s.Messages = append(s.Messages, *message)
}
func (s *Server) updateActivity(ip string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Connected[ip] = time.Now()
}
func (s *Server) cleanupActivity() {
s.mu.Lock()
defer s.mu.Unlock()
for ip, lastActivity := range s.Connected {
if time.Since(lastActivity) > 10*time.Second {
delete(s.Connected, ip)
}
}
}
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
w.WriteHeader(http.StatusOK)
}
func validUsername(username string) bool {
for _, c := range username {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
return false
}
}
return true
}
func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
var req struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
s.mu.Lock()
if len(req.Username) > 64 {
http.Error(w, fmt.Sprintf(`{"error": "Username too long (must be less than 64 characters)"}`), http.StatusRequestEntityTooLarge)
s.mu.Unlock()
return
}
if !validUsername(req.Username) {
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
s.mu.Unlock()
return
}
for _, username := range s.Usernames {
if username == req.Username {
s.mu.Unlock()
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
return
}
}
if _, ok := s.Usernames[clientIP]; ok {
fmt.Println("Name change detected, fixing messages to reflect the change")
for i, msg := range s.Messages {
if msg.SenderIp == clientIP {
s.Messages[i].SenderUsername = req.Username
}
}
}
s.Usernames[clientIP] = req.Username
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
fmt.Println(clientIP, "->", req.Username)
}
func getMessageTemplate(file string, body string) string {
contents, _ := os.ReadFile(file)
return strings.Replace(string(contents), "{{body}}", body, 1)
}
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "text/html")
var body string
for _, msg := range s.Messages {
body += fmt.Sprintf(`<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
msg.SenderUsername, msg.Timestamp, msg.Content)
}
w.Write([]byte(getMessageTemplate("messages.html", body)))
case http.MethodPut:
w.Header().Set("Content-Type", "application/json")
// Get client's IP
clientIP := getClientIP(r)
// Check if IP is registered
s.mu.Lock()
username, exists := s.Usernames[clientIP]
s.mu.Unlock()
if !exists {
errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
http.Error(w, errorFmt, http.StatusUnauthorized)
return
}
var msg struct {
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
// Add message using the stored username and IP
s.AddMessage(username, clientIP, msg.Message)
json.NewEncoder(w).Encode(map[string]string{
"status": "Message received",
"from": username,
})
default:
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
}
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.mu.Lock()
username, exists := s.Usernames[clientIP]
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"hasUsername": exists,
"username": username,
})
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
s.mu.Lock()
var users []string
for ip := range s.Connected {
users = append(users, s.Usernames[ip])
}
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"users": users,
})
}
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(readFile("root.html"))
}
func readFile(filepath string) []byte {
contents, _ := os.ReadFile(filepath)
return contents
}
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "application/javascript")
w.Write(readFile("root.js"))
}
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "text/css")
w.Write(readFile("root.css"))
}
func (s *Server) Run() {
handler := http.NewServeMux()
handler.HandleFunc("/ping", s.handlePing)
handler.HandleFunc("/username", s.handleUsername)
handler.HandleFunc("/messages", s.handleMessages)
handler.HandleFunc("/username/status", s.handleUsernameStatus)
handler.HandleFunc("/users", s.handleUsers)
handler.HandleFunc("/", s.handleRoot)
handler.HandleFunc("/root.js", s.handleJs)
handler.HandleFunc("/root.css", s.handleCss)
fmt.Printf("Server starting on %s:%d\n", s.Ip, s.Port)
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Ip, s.Port), GzipMiddleware(handler)); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func main() {
ip := os.Args[1]
port, _ := strconv.Atoi(os.Args[2])
// ip := "localhost"
// port := 8080
server := NewServer(ip, port)
server.Run()
}

366
old/main.go.bak Normal file
View File

@ -0,0 +1,366 @@
package main
import (
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.Writer.Write(data)
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
func getClientIP(r *http.Request) string {
if fwdIP := r.Header.Get("X-Forwarded-For"); fwdIP != "" {
return strings.Split(fwdIP, ",")[0]
}
clientIP := r.RemoteAddr
if host, _, err := net.SplitHostPort(clientIP); err == nil {
return host
}
return clientIP
}
type Message struct {
Content string
SenderIp string
SenderUsername string
Timestamp string
}
func NewMessage(username, ip, content string) *Message {
timestamp := time.Now().Format("2006-01-02 15:04:05")
return &Message{
Content: content,
SenderIp: ip,
SenderUsername: username,
Timestamp: timestamp,
}
}
// TODO: Use it
func writeMessageToFile(msg Message) {
contents := fmt.Sprintf("%s,%s,%s,%s\n", msg.SenderUsername, msg.SenderIp, msg.Timestamp, msg.Content)
os.WriteFile("messages.txt", []byte(contents), os.ModeAppend)
}
// TODO: Use it
func writeMessagesToFile(messages []Message) {
var contents string
for _, msg := range messages {
contents += fmt.Sprintf("%s,%s,%s,%s\n", msg.SenderUsername, msg.SenderIp, msg.Timestamp, msg.Content)
}
os.WriteFile("messages.txt", []byte(contents), os.ModeAppend)
}
// TODO: Use it
func readMessagesFromFile(filepath string) []Message {
contents, _ := os.ReadFile(filepath)
lines := strings.Split(string(contents), "\n")
var messages []Message
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, ",")
username := parts[0]
timestamp := parts[1]
content := strings.Join(parts[2:], " ")
messages = append(messages, Message{username, "", content, timestamp})
}
return messages
}
type Server struct {
Ip string
Port int
Messages []Message
// Add mappings for IPs and usernames
Connected map[string]time.Time // Map IP -> Last activity time
Usernames map[string]string // Map IP -> Username
mu sync.Mutex // For thread safety
}
func NewServer(ip string, port int) *Server {
return &Server{
Ip: ip,
Port: port,
Messages: make([]Message, 0),
Connected: make(map[string]time.Time),
Usernames: make(map[string]string),
mu: sync.Mutex{},
}
}
func (s *Server) AddMessage(username string, userip string, contents string) {
message := NewMessage(username, userip, contents)
s.Messages = append(s.Messages, *message)
}
func (s *Server) updateActivity(ip string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Connected[ip] = time.Now()
}
func (s *Server) cleanupActivity() {
s.mu.Lock()
defer s.mu.Unlock()
for ip, lastActivity := range s.Connected {
if time.Since(lastActivity) > 10*time.Second {
delete(s.Connected, ip)
}
}
}
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
w.WriteHeader(http.StatusOK)
}
func validUsername(username string) bool {
for _, c := range username {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
return false
}
}
return true
}
func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
var req struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
s.mu.Lock()
if len(req.Username) > 64 {
http.Error(w, fmt.Sprintf(`{"error": "Username too long (must be less than 64 characters)"}`), http.StatusRequestEntityTooLarge)
s.mu.Unlock()
return
}
if !validUsername(req.Username) {
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
s.mu.Unlock()
return
}
for _, username := range s.Usernames {
if username == req.Username {
s.mu.Unlock()
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
return
}
}
if _, ok := s.Usernames[clientIP]; ok {
fmt.Println("Name change detected, fixing messages to reflect the change")
for i, msg := range s.Messages {
if msg.SenderIp == clientIP {
s.Messages[i].SenderUsername = req.Username
}
}
}
s.Usernames[clientIP] = req.Username
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
fmt.Println(clientIP, "->", req.Username)
}
func getMessageTemplate(file string, body string) string {
contents, _ := os.ReadFile(file)
return strings.Replace(string(contents), "{{body}}", body, 1)
}
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "text/html")
var body string
for _, msg := range s.Messages {
body += fmt.Sprintf(`<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
msg.SenderUsername, msg.Timestamp, msg.Content)
}
w.Write([]byte(getMessageTemplate("messages.html", body)))
case http.MethodPut:
w.Header().Set("Content-Type", "application/json")
// Get client's IP
clientIP := getClientIP(r)
// Check if IP is registered
s.mu.Lock()
username, exists := s.Usernames[clientIP]
s.mu.Unlock()
if !exists {
errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
http.Error(w, errorFmt, http.StatusUnauthorized)
return
}
var msg struct {
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
// Add message using the stored username and IP
s.AddMessage(username, clientIP, msg.Message)
json.NewEncoder(w).Encode(map[string]string{
"status": "Message received",
"from": username,
})
default:
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
}
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.mu.Lock()
username, exists := s.Usernames[clientIP]
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"hasUsername": exists,
"username": username,
})
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
clientIP := getClientIP(r)
s.updateActivity(clientIP)
s.cleanupActivity()
s.mu.Lock()
var users []string
for ip := range s.Connected {
users = append(users, s.Usernames[ip])
}
s.mu.Unlock()
json.NewEncoder(w).Encode(map[string]interface{}{
"users": users,
})
}
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(readFile("root.html"))
}
func readFile(filepath string) []byte {
contents, _ := os.ReadFile(filepath)
return contents
}
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "application/javascript")
w.Write(readFile("root.js"))
}
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
_ = r
w.Header().Set("Content-Type", "text/css")
w.Write(readFile("root.css"))
}
func (s *Server) Run() {
handler := http.NewServeMux()
handler.HandleFunc("/ping", s.handlePing)
handler.HandleFunc("/username", s.handleUsername)
handler.HandleFunc("/messages", s.handleMessages)
handler.HandleFunc("/username/status", s.handleUsernameStatus)
handler.HandleFunc("/users", s.handleUsers)
handler.HandleFunc("/", s.handleRoot)
handler.HandleFunc("/root.js", s.handleJs)
handler.HandleFunc("/root.css", s.handleCss)
fmt.Printf("Server starting on %s:%d\n", s.Ip, s.Port)
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Ip, s.Port), GzipMiddleware(handler)); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func main() {
ip := os.Args[1]
port, _ := strconv.Atoi(os.Args[2])
// ip := "localhost"
// port := 8080
server := NewServer(ip, port)
server.Run()
}

7
old/messages.html Normal file
View File

@ -0,0 +1,7 @@
<html>
<body>
<div class="container">
{{body}}
</div>
</body>
</html>

247
old/root.css Normal file
View File

@ -0,0 +1,247 @@
:root {
--radchat-color: #40a02b;
--main-bg-color: #11111b;
--pum-button-inactive-fg: #cdd6f4;
--pum-button-inactive-bg: #11111b;
--pum-button-active-fg: #89b4fa;
--pum-button-active-bg: #1e1e2e;
--pum-title-color: #cdd6f4;
--pum-bg-color: #1e1e2e;
--user-color: #89b4fa;
--timestamp-color: #313244;
--separator-color: #181825;
--message-color: #cdd6f4;
--message-bg-color: #1e1e2e;
--input-bg-color: #181825;
--input-text-color: #cdd6f4;
--input-button-inactive-bg: #b4befe;
--input-button-inactive-fg: #11111b;
--input-button-active-bg: #89b4fa;
--input-button-active-fg: #11111b;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0; /* Remove padding from body */
background-color: var(--main-bg-color);
color: var(--pum-title-color);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden; /* Prevent body scroll */
}
.container {
max-width: 800px;
margin: 0 auto;
width: 100%;
position: relative;
height: 100vh;
padding: 20px; /* Move padding here */
padding-bottom: 80px; /* Space for message input */
display: flex;
flex-direction: column;
}
.header-controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px; /* Space between buttons */
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.header-controls {
gap: 10px;
}
}
.settings-button, .users-button {
top: 20px;
background: none;
border: none;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--pum-button-inactive-bg);
}
.settings-button {
right: calc(48%);
}
.users-button {
right: calc(52%);
}
.settings-icon, .users-icon {
width: 24px;
height: 24px;
fill: var(--pum-button-inactive-fg);
}
.settings-button:hover, .users-button:hover {
background-color: var(--pum-button-active-bg);
}
.settings-button:hover .settings-icon,
.users-button:hover .users-icon {
fill: var(--pum-button-active-fg);
}
#users-list {
margin-top: 0px;
}
.current-user {
position: absolute;
right: 0;
top: 28px;
color: var(--user-color);
font-weight: bold;
}
.username-section, .users-section {
position: fixed;
transform: translateX(-50%);
top: 70px;
padding: 15px;
background-color: var(--pum-bg-color);
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 100;
display: none;
min-width: 250px;
}
.username-section {
right: 50%;
transform: translateX(10%);
}
.users-section {
right: 50%;
transform: translateX(-10%);
}
.users-section h3 {
margin-top: 0;
margin-bottom: 10px;
}
.user-item {
padding: 8px 0;
color: var(--user-color);
border-bottom: 1px solid var(--separator-color);
}
.user-item:last-child {
border-bottom: none;
}
.messages-section {
flex-grow: 1;
margin-top: 60px; /* Space for gear icon */
margin-bottom: 20px;
overflow-y: auto;
height: 0; /* Allow flex-grow to work */
}
.message {
padding: 10px 0;
}
.message .username {
color: var(--user-color);
font-weight: bold;
}
.message .timestamp {
color: var(--timestamp-color);
font-weight: thin;
font-size: 0.8em;
}
.message .content {
margin-top: 5px;
color: var(--message-color);
word-wrap: break-word; /* Handle long words */
word-break: break-word; /* Break words at arbitrary points if needed */
white-space: pre-wrap; /* Preserve whitespace and wraps */
max-width: 100%; /* Ensure it doesn't exceed container width */
}
.message-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background-color: var(--message-bg-color);
z-index: 100; /* Ensure it stays on top */
}
.message-container {
max-width: 800px;
margin: 0 auto;
display: flex;
gap: 10px;
}
input {
padding: 10px;
border: none;
border-radius: 4px;
background-color: var(--input-bg-color);
color: var(--input-text-color);
flex-grow: 1;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: var(--input-button-inactive-bg);
color: var(--input-button-inactive-fg);
cursor: pointer;
}
button:hover {
background-color: var(--input-button-active-bg);
color: var(--input-button-active-fg)
}
button.scroll {
padding: 10px 20px;
border-radius: 4px;
border: none;
background-color: var(--input-button-inactive-bg);
padding: 10px;
cursor: pointer;
}
button.scroll:hover {
background-color: var(--input-button-active-bg);
}
.scroll-icon {
width: 14px;
height: 14px;
fill: var(--var-input-button-inactive-fg);
}
.scroll-icon:hover {
fill: var(--var-input-button-active-fg);
}
.radchat {
position: absolute;
left: 0;
font-family: Arial, sans-serif;
font-size: 2em;
font-weight: bold;
color: var(--radchat-color);
}
#status, #username-status {
margin-top: 10px;
}

66
old/root.html Normal file
View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="Cache-Control" content="max-age=86400, must-revalidate">
<meta http-equiv="Pragma" content="cache">
<meta http-equiv="Expires" content="86400">
<title>RadChat</title>
<script src="root.js"></script>
<link rel="stylesheet" href="root.css">
</head>
<body>
<div class="container">
<div class="radchat">
RadChat
</div>
<div class="header-controls">
<button class="users-button" onclick="toggleUsers()">
<svg class="users-icon" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</button>
<button class="settings-button" onclick="toggleSettings()">
<svg class="settings-icon" viewBox="0 0 24 24">
<path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.65.07-.97 0-.32-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65z"/>
</svg>
</button>
</div>
<div id="current-user" class="current-user"></div>
<div class="users-section" id="users-panel">
<h3>Online Users</h3>
<div id="users-list">
<!-- Users will be loaded here -->
</div>
</div>
<div class="username-section" id="settings-panel">
<h3>Username</h3>
<div style="display: flex; gap: 10px;">
<input type="text" id="username" placeholder="Enter username">
<button onclick="setUsername()">Set</button>
</div>
<div id="username-status"></div>
</div>
<div class="messages-section" id="messages">
<!-- Messages will be loaded here -->
</div>
</div>
<div class="message-section">
<div class="message-container">
<input type="text" id="message" placeholder="Type a message">
<button onclick="sendMessage()">Send</button>
<button id="scroll" class="scroll"><svg class="scroll-icon" id="scroll-icon" viewBox="0 0 24 24">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
</svg></button>
</div>
<div id="status"></div>
</div>
</body>
</html>

244
old/root.js Normal file
View File

@ -0,0 +1,244 @@
function toggleSettings() {
const panel = document.getElementById('settings-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
if (panel.style.display === 'block') {
const username = document.getElementById('username');
username.focus();
username.selectionStart = username.selectionEnd = username.value.length
}
}
function toggleUsers() {
const panel = document.getElementById('users-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('click', function(event) {
const settingsPanel = document.getElementById('settings-panel');
const settingsButton = document.querySelector('.settings-button');
const usersPanel = document.getElementById('users-panel');
const usersButton = document.querySelector('.users-button');
if (!settingsPanel.contains(event.target) && !settingsButton.contains(event.target)) {
settingsPanel.style.display = 'none';
}
if (!usersPanel.contains(event.target) && !usersButton.contains(event.target)) {
usersPanel.style.display = 'none';
}
});
document.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
const settingsPanel = document.getElementById('settings-panel');
const inputPanel = document.getElementById('message');
if (settingsPanel.contains(event.target)) {
setUsername();
}
if (inputPanel.contains(event.target)) {
sendMessage();
}
}
});
async function loadUsers() {
try {
const response = await fetch('/users');
const data = await response.json();
const usersList = document.getElementById('users-list');
usersList.innerHTML = '';
data.users.sort().forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
userDiv.textContent = user;
usersList.appendChild(userDiv);
});
} catch (error) {
console.error('Error loading users:', error);
}
}
let lastMessageCount = 0;
async function loadMessages() {
try {
const messagesDiv = document.getElementById('messages');
const response = await fetch('/messages');
const text = await response.text();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = text;
if (messagesDiv.scrollTop != bottom) {
// show a button to scroll to the bottom
const scrollToBottomButton = document.getElementById('scroll');
scrollToBottomButton.style.display = 'block';
scrollToBottomButton.onclick = scrollToBottom;
} else {
// hide the button
const scrollToBottomButton = document.getElementById('scroll');
scrollToBottomButton.style.display = 'none';
}
const messages = tempDiv.getElementsByTagName('p');
const newMessageCount = messages.length;
if (newMessageCount > lastMessageCount || lastMessageCount === 0) {
messagesDiv.innerHTML = '';
Array.from(messages).forEach(msg => {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
const [username, content] = msg.innerHTML.split('<br>');
messageDiv.innerHTML = '<div class="username">' + username + '</div>' +
'<div class="content">' + content + '</div>';
messagesDiv.appendChild(messageDiv);
});
scrollToBottom();
lastMessageCount = newMessageCount;
}
} catch (error) {
console.error('Error loading messages:', error);
}
}
var bottom = 0;
function scrollToBottom() {
const messagesDiv = document.getElementById('messages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
bottom = messagesDiv.scrollTop;
}
async function checkUsername() {
try {
const response = await fetch('/username/status');
const data = await response.json();
if (!data.hasUsername) {
document.getElementById('settings-panel').style.display = 'block'
const username = document.getElementById('username');
username.focus();
username.selectionStart = username.selectionEnd = username.value.length
}
} catch (error) {
console.error('Error checking username status:', error);
}
}
async function updateCurrentUser() {
try {
const response = await fetch('/username/status');
const data = await response.json();
const userDiv = document.getElementById('current-user');
if (data.hasUsername) {
userDiv.textContent = data.username;
} else {
userDiv.textContent = '';
}
} catch (error) {
console.error('Error getting username:', error);
}
}
async function setUsername() {
const username = document.getElementById('username').value;
if (!username) {
showUsernameStatus('Please enter a username', 'red');
return;
}
try {
const response = await fetch('/username', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username: username })
});
const data = await response.json();
if (response.ok) {
showUsernameStatus('Username set successfully!', 'green');
updateCurrentUser();
setTimeout(() => {
document.getElementById('settings-panel').style.display = 'none';
}, 500);
location.reload();
} else {
showUsernameStatus(data.error || 'Failed to set username', 'red');
}
} catch (error) {
showUsernameStatus('Error connecting to server', 'red');
}
}
async function sendMessage() {
const message = document.getElementById('message').value;
if (!message) {
return;
}
try {
const response = await fetch('/messages', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: message })
});
const data = await response.json();
if (response.ok) {
document.getElementById('message').value = '';
// showStatus('Message sent!', '#64b5f6');
loadMessages();
} else {
showStatus(data.error || 'Failed to send message', 'red');
}
} catch (error) {
showStatus('Error connecting to server', 'red');
}
}
function showStatus(message, color) {
const status = document.getElementById('status');
status.textContent = message;
status.style.color = color;
setTimeout(() => {
status.textContent = '';
}, 3000);
}
function showUsernameStatus(message, color) {
const status = document.getElementById('username-status');
status.textContent = message;
status.style.color = color;
setTimeout(() => {
status.textContent = '';
}, 3000);
}
async function pingCheck() {
try {
await fetch('/ping', {method: 'POST'});
} catch (error) {
console.error('Ping failed:', error);
}
}
async function initialize() {
await loadMessages();
document.getElementById('users-panel').style.display = 'none';
document.getElementById('settings-panel').style.display = 'none';
checkUsername();
updateCurrentUser();
setInterval(loadMessages, 1000);
setInterval(loadUsers, 1000);
setInterval(pingCheck, 3000);
scrollToBottom();
}
initialize();

321
root.css Normal file
View File

@ -0,0 +1,321 @@
[data-theme="dark"] {
--radchat-color: #40a02b;
--main-bg-color: #11111b;
--pum-button-inactive-fg: #cdd6f4;
--pum-button-inactive-bg: #11111b;
--pum-button-active-fg: #89b4fa;
--pum-button-active-bg: #1e1e2e;
--pum-title-color: #cdd6f4;
--pum-bg-color: #1e1e2e;
--user-color: #89b4fa;
--timestamp-color: #313244;
--separator-color: #181825;
--message-color: #cdd6f4;
--message-bg-color: #1e1e2e;
--input-bg-color: #181825;
--input-text-color: #cdd6f4;
--input-button-inactive-bg: #b4befe;
--input-button-inactive-fg: #11111b;
--input-button-active-bg: #89b4fa;
--input-button-active-fg: #11111b;
}
[data-theme="light"] {
--radchat-color: #40a02b;
--main-bg-color: #dce0e8;
--pum-button-inactive-fg: #4c4f69;
--pum-button-inactive-bg: #dce0e8;
--pum-button-active-fg: #1e66f5;
--pum-button-active-bg: #eff1f5;
--pum-title-color: #4c4f69;
--pum-bg-color: #eff1f5;
--user-color: #1e66f5;
--timestamp-color: #8c8fa1;
--separator-color: #e6e9ef;
--message-color: #4c4f69;
--message-bg-color: #eff1f5;
--input-bg-color: #e6e9ef;
--input-text-color: #4c4f69;
--input-button-inactive-bg: #7287fd;
--input-button-inactive-fg: #dce0e8;
--input-button-active-bg: #1e66f5;
--input-button-active-fg: #dce0e8;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0; /* Remove padding from body */
background-color: var(--main-bg-color);
color: var(--pum-title-color);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden; /* Prevent body scroll */
}
.container {
max-width: 800px;
margin: 0 auto;
width: 100%;
position: relative;
height: 100vh;
padding: 20px; /* Move padding here */
padding-bottom: 80px; /* Space for message input */
display: flex;
flex-direction: column;
}
.header-controls {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px; /* Space between buttons */
align-items: center;
justify-content: center;
}
/* Show/hide appropriate icon based on theme */
[data-theme="light"] .theme-icon .sun { display: none; }
[data-theme="dark"] .theme-icon .moon { display: none; }
@media (max-width: 100px) {
.header-controls {
gap: 10px;
}
}
.settings-button, .users-button, .theme-button {
top: 20px;
background: none;
border: none;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--pum-button-inactive-bg);
}
.settings-button {
right: calc(50%);
}
.users-button {
right: calc(60%);
}
.theme-button {
right: calc(30%);
}
.settings-icon, .users-icon, .theme-icon{
width: 24px;
height: 24px;
fill: var(--pum-button-inactive-fg);
}
.settings-button:hover, .users-button:hover, .theme-button:hover {
background-color: var(--pum-button-active-bg);
}
.settings-button:hover .settings-icon,
.users-button:hover .users-icon,
.theme-button:hover .theme-icon {
fill: var(--pum-button-active-fg);
}
#users-list {
margin-top: 0px;
}
.current-user {
position: absolute;
right: 0;
top: 28px;
color: var(--user-color);
font-weight: bold;
}
.username-section, .users-section {
position: fixed;
transform: translateX(-50%);
top: 70px;
padding: 15px;
background-color: var(--pum-bg-color);
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 100;
display: none;
min-width: 250px;
}
.username-section {
right: 50%;
transform: translateX(10%);
}
.username-section h3 {
margin-top: 0;
margin-bottom: 20px;
}
.users-section {
right: 50%;
transform: translateX(-10%);
}
.users-section h3 {
margin-top: 0;
margin-bottom: 10px;
}
.user-item {
padding: 8px 0;
color: var(--user-color);
border-bottom: 1px solid var(--separator-color);
}
.user-item:last-child {
border-bottom: none;
}
.messages-section {
flex-grow: 1;
margin-top: 60px; /* Space for gear icon */
margin-bottom: 60px;
overflow-y: auto;
height: 0; /* Allow flex-grow to work */
}
.message {
padding: 10px 0;
}
.message .username {
color: var(--user-color);
font-weight: bold;
}
.message .timestamp {
color: var(--timestamp-color);
font-weight: thin;
font-size: 0.8em;
}
.message .content {
margin-top: 5px;
color: var(--message-color);
word-wrap: break-word; /* Handle long words */
word-break: break-word; /* Break words at arbitrary points if needed */
white-space: pre-wrap; /* Preserve whitespace and wraps */
max-width: 100%; /* Ensure it doesn't exceed container width */
}
.message-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background-color: var(--message-bg-color);
z-index: 100; /* Ensure it stays on top */
}
.message-container {
max-width: 800px;
margin: 0 auto;
display: flex;
gap: 10px;
}
.youtube-embed {
position: relative;
padding-top: 10px;
width: 100%;
max-width: 560px; /* Standard YouTube width */
}
.youtube-embed iframe {
border-radius: 4px;
}
.image-embed {
padding-top: 10px;
max-width: 560px; /* Match YouTube width */
}
.image-embed img {
max-width: 100%;
height: auto;
border-radius: 4px;
display: block;
}
input {
padding: 10px;
border: none;
border-radius: 4px;
background-color: var(--input-bg-color);
color: var(--input-text-color);
flex-grow: 1;
}
textarea {
padding: 10px;
border: none;
border-radius: 4px;
background-color: var(--input-bg-color);
color: var(--input-text-color);
flex-grow: 1;
resize: none; /* Prevents manual resizing */
min-height: 38px; /* Match your previous input height */
line-height: 1.4;
font-family: Arial, sans-serif; /* Match your body font */
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: var(--input-button-inactive-bg);
color: var(--input-button-inactive-fg);
cursor: pointer;
}
button:hover {
background-color: var(--input-button-active-bg);
color: var(--input-button-active-fg)
}
button.scroll {
padding: 10px 20px;
border-radius: 4px;
border: none;
background-color: var(--input-button-inactive-bg);
padding: 10px;
cursor: pointer;
}
button.scroll:hover {
background-color: var(--input-button-active-bg);
}
.scroll-icon {
width: 14px;
height: 14px;
fill: var(--var-input-button-inactive-fg);
}
.scroll-icon:hover {
fill: var(--var-input-button-active-fg);
}
.radchat {
position: absolute;
left: 0;
font-family: Arial, sans-serif;
font-size: 2em;
font-weight: bold;
color: var(--radchat-color);
}
#status, #username-status {
margin-top: 10px;
}

74
root.html Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta http-equiv="Cache-Control" content="max-age=86400, must-revalidate">
<meta http-equiv="Pragma" content="cache">
<meta http-equiv="Expires" content="86400">
<title>RadChat</title>
<script src="root.js"></script>
<link rel="stylesheet" href="root.css">
</head>
<body>
<div class="container">
<div class="radchat">
RadChat
</div>
<div class="header-controls">
<button class="users-button" onclick="toggleUsers()">
<svg class="users-icon" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</button>
<button class="settings-button" onclick="toggleSettings()">
<svg class="settings-icon" viewBox="0 0 24 24">
<path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.65.07-.97 0-.32-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65z"/>
</svg>
</button>
<button class="theme-button" onclick="toggleTheme()">
<svg class="theme-icon" viewBox="0 0 24 24">
<!-- Moon icon for dark mode -->
<path class="moon" d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-3.03 0-5.5-2.47-5.5-5.5 0-1.82.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/>
<!-- Sun icon for light mode -->
<path class="sun" d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
</svg>
</button>
</div>
<div id="current-user" class="current-user"></div>
<div class="users-section" id="users-panel">
<h3>Online Users</h3>
<div id="users-list">
<!-- Users will be loaded here -->
</div>
</div>
<div class="username-section" id="settings-panel">
<h3>Username</h3>
<div style="display: flex; gap: 10px;">
<input type="text" id="username" placeholder="Enter username">
<button onclick="setUsername()">Set</button>
</div>
<div id="username-status"></div>
</div>
<div class="messages-section" id="messages">
<!-- Messages will be loaded here -->
</div>
</div>
<div class="message-section">
<div class="message-container">
<textarea id="message" placeholder="Type a message" rows="1"></textarea>
<button onclick="sendMessage()">Send</button>
<button id="scroll" class="scroll"><svg class="scroll-icon" id="scroll-icon" viewBox="0 0 24 24">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
</svg></button>
</div>
<div id="status"></div>
</div>
</body>
</html>

418
root.js Normal file
View File

@ -0,0 +1,418 @@
function toggleSettings() {
const panel = document.getElementById("settings-panel");
panel.style.display = panel.style.display === "none" ? "block" : "none";
if (panel.style.display === "block") {
const username = document.getElementById("username");
username.focus();
username.selectionStart = username.selectionEnd = username.value
.length;
}
}
function toggleUsers() {
const panel = document.getElementById("users-panel");
panel.style.display = panel.style.display === "none" ? "block" : "none";
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute(
"data-theme",
);
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
document.addEventListener("click", function (event) {
const settingsPanel = document.getElementById("settings-panel");
const settingsButton = document.querySelector(".settings-button");
const usersPanel = document.getElementById("users-panel");
const usersButton = document.querySelector(".users-button");
if (
!settingsPanel.contains(event.target) &&
!settingsButton.contains(event.target)
) {
settingsPanel.style.display = "none";
}
if (
!usersPanel.contains(event.target) &&
!usersButton.contains(event.target)
) {
usersPanel.style.display = "none";
}
});
document.addEventListener("keypress", function (event) {
if (event.key === "Enter" && !event.shiftKey) {
const settingsPanel = document.getElementById("settings-panel");
const inputPanel = document.getElementById("message");
if (settingsPanel.contains(event.target)) {
setUsername();
}
if (inputPanel.contains(event.target)) {
event.preventDefault();
sendMessage();
}
}
});
document.addEventListener("input", function (_) {
const msg = document.getElementById("message");
msg.style.height = "auto";
msg.style.height = (msg.scrollHeight) + "px";
});
document.addEventListener("blur", function (_) {
const msg = document.getElementById("message");
msg.style.height = "auto";
}, true);
async function loadUsers() {
try {
const response = await fetch("/users");
const data = await response.json();
const usersList = document.getElementById("users-list");
usersList.innerHTML = "";
data.users.sort().forEach((user) => {
const userDiv = document.createElement("div");
userDiv.className = "user-item";
userDiv.textContent = user;
usersList.appendChild(userDiv);
});
} catch (error) {
console.error("Error loading users:", error);
}
}
let lastMessageCount = 0;
async function loadMessages() {
try {
let messagesDiv = document.getElementById("messages");
const response = await fetch("/messages");
const text = await response.text();
const tempDiv = document.createElement("div");
tempDiv.innerHTML = text;
// getting an error right here that messagesDiv is null
// but only on the first load, so maybe we can just hold off?
while (messagesDiv === null) {
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
messagesDiv = document.getElementById("messages");
}
if (messagesDiv.scrollTop != bottom) {
// show a button to scroll to the bottom
const scrollToBottomButton = document.getElementById(
"scroll",
);
scrollToBottomButton.style.display = "block";
scrollToBottomButton.onclick = scrollToBottom;
} else {
// hide the button
const scrollToBottomButton = document.getElementById(
"scroll",
);
scrollToBottomButton.style.display = "none";
}
const messages = tempDiv.getElementsByTagName("p");
const newMessageCount = messages.length;
if (
newMessageCount > lastMessageCount ||
lastMessageCount === 0
) {
messagesDiv.innerHTML = "";
Array.from(messages).forEach((msg) => {
const messageDiv = document.createElement(
"div",
);
messageDiv.className = "message";
const [username, content] = msg.innerHTML.split(
"<br>",
);
const linkedContent = content.replace(
/(?![^<]*>)(https?:\/\/[^\s<]+)/g,
function (url) {
console.log(
"Processing URL:",
url,
); // Debug log
const videoId = getYouTubeID(
url,
);
if (videoId) {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>
<div class="youtube-embed">
<iframe
width="100%"
height="315"
src="https://www.youtube.com/embed/${videoId}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>`;
} else if (isImageUrl(url)) {
console.log(
"Attempting to embed image:",
url,
);
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>
<div class="image-embed">
<img
src="${url}"
alt="Embedded image"
loading="lazy"
onerror="console.log('Image failed to load:', this.src); this.style.display='none'"
onload="console.log('Image loaded successfully:', this.src)">
</div>`;
}
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
},
);
messageDiv.innerHTML =
'<div class="username">' + username +
"</div>" +
'<div class="content">' +
linkedContent + "</div>";
messagesDiv.appendChild(messageDiv);
});
scrollToBottom();
lastMessageCount = newMessageCount;
}
} catch (error) {
console.error("Error loading messages:", error);
}
}
function isImageUrl(url) {
return url.match(/\.(jpeg|jpg|gif|png|webp|bmp)($|\?)/i) != null;
}
function getYouTubeID(url) {
// First check if it's a Shorts URL
if (url.includes("/shorts/")) {
const shortsMatch = url.match(/\/shorts\/([^/?]+)/);
return shortsMatch ? shortsMatch[1] : false;
}
// Otherwise check regular YouTube URLs
const regExp =
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[7].length == 11) ? match[7] : false;
}
var bottom = 0;
function scrollToBottom() {
const messagesDiv = document.getElementById("messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
bottom = messagesDiv.scrollTop;
}
async function checkUsername() {
try {
const response = await fetch("/username/status");
const data = await response.json();
if (!data.hasUsername) {
document.getElementById("settings-panel").style
.display = "block";
const username = document.getElementById("username");
username.focus();
username.selectionStart =
username.selectionEnd =
username.value.length;
}
} catch (error) {
console.error("Error checking username status:", error);
}
}
async function updateCurrentUser() {
try {
const response = await fetch("/username/status");
const data = await response.json();
const userDiv = document.getElementById("current-user");
if (data.hasUsername) {
userDiv.textContent = data.username;
} else {
userDiv.textContent = "";
}
} catch (error) {
console.error("Error getting username:", error);
}
}
async function setUsername() {
const username = document.getElementById("username").value;
if (!username) {
showUsernameStatus("Please enter a username", "red");
return;
}
try {
const response = await fetch("/username", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: username }),
});
const data = await response.json();
if (response.ok) {
showUsernameStatus(
"Username set successfully!",
"green",
);
updateCurrentUser();
setTimeout(() => {
document.getElementById("settings-panel").style
.display = "none";
}, 500);
location.reload();
} else {
showUsernameStatus(
data.error || "Failed to set username",
"red",
);
}
} catch (error) {
showUsernameStatus("Error connecting to server", "red");
}
}
async function sendMessage() {
const messageInput = document.getElementById("message");
const message = messageInput.value;
if (!message) {
return;
}
try {
const response = await fetch("/messages", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message: message }),
});
const data = await response.json();
if (response.ok) {
messageInput.value = "";
messageInput.style.height = "auto";
loadMessages();
} else {
showStatus(
data.error || "Failed to send message",
"red",
);
}
} catch (error) {
showStatus("Error connecting to server", "red");
}
}
function showStatus(message, color) {
const status = document.getElementById("status");
status.textContent = message;
status.style.color = color;
setTimeout(() => {
status.textContent = "";
}, 3000);
}
function showUsernameStatus(message, color) {
const status = document.getElementById("username-status");
status.textContent = message;
status.style.color = color;
setTimeout(() => {
status.textContent = "";
}, 3000);
}
async function pingCheck() {
try {
await fetch("/ping", { method: "POST" });
} catch (error) {
console.error("Ping failed:", error);
}
}
async function timeZoneCheck() {
try {
const timeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
const response = await fetch("/timezone", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ timezone: timeZone }),
});
const data = await response.json();
if (!response.ok) {
console.error("Failed to set timezone:", data.error);
}
} catch (error) {
console.error("Error checking timezone:", error);
}
}
function initializeTheme() {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
document.documentElement.setAttribute("data-theme", savedTheme);
} else {
// Check system preference
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: light)")
.matches
) {
document.documentElement.setAttribute(
"data-theme",
"light",
);
} else {
document.documentElement.setAttribute(
"data-theme",
"dark",
);
}
}
}
async function initialize() {
usersPanel = document.getElementById("users-panel");
if (usersPanel) {
usersPanel.style.display = "none";
}
settingsPanel = document.getElementById("settings-panel");
if (settingsPanel) {
settingsPanel.style.display = "none";
}
initializeTheme();
checkUsername();
updateCurrentUser();
timeZoneCheck();
setInterval(loadMessages, 1000);
setInterval(loadUsers, 1000);
setInterval(pingCheck, 3000);
await loadMessages();
scrollToBottom();
}
initialize();