changes
This commit is contained in:
commit
1bf98ab3eb
BIN
chatserver
Normal file
BIN
chatserver
Normal file
Binary file not shown.
5
go.mod
Normal file
5
go.mod
Normal 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
2
go.sum
Normal 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
627
main.go
Normal 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
7
messages.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{{body}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
notes/todo.md
Normal file
17
notes/todo.md
Normal 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
333
old/main.go
Normal 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
366
old/main.go.bak
Normal 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
7
old/messages.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{{body}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
247
old/root.css
Normal file
247
old/root.css
Normal 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
66
old/root.html
Normal 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
244
old/root.js
Normal 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
321
root.css
Normal 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
74
root.html
Normal 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
418
root.js
Normal 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();
|
Loading…
x
Reference in New Issue
Block a user