This commit is contained in:
Radon 2025-01-20 12:23:27 -06:00
parent 5420fbb7a9
commit 2428f69df8
7 changed files with 171 additions and 67 deletions

14
config.json Normal file
View File

@ -0,0 +1,14 @@
{
"server": {
"ipAddress": "192.168.1.222",
"port": 8080,
"messageMaxAge": 259200
},
"paths": {
"databasePath": "/home/radon/Documents/chattest.db",
"indexJsPath": "./content/root.js",
"indexCssPath": "./content/root.css",
"indexHtmlPath": "./content/root.html",
"messagesHtmlPath": "./content/messages.html"
}
}

View File

@ -149,8 +149,7 @@ async function loadMessages() {
url, url,
); );
if (videoId) { if (videoId) {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a> return `<div class="youtube-embed">
<div class="youtube-embed">
<iframe <iframe
width="100%" width="100%"
height="315" height="315"
@ -165,8 +164,7 @@ async function loadMessages() {
"Attempting to embed image:", "Attempting to embed image:",
url, url,
); );
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a> return `<div class="image-embed">
<div class="image-embed">
<img <img
src="${url}" src="${url}"
alt="Embedded image" alt="Embedded image"

176
main.go
View File

@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -77,27 +78,24 @@ func (db *Database) DbCreateTableUsers() {
} }
func (db *Database) UserTimezoneSet(ip_address, timezone string) { func (db *Database) UserTimezoneSet(ip_address, timezone string) {
stmt, err := db.db.Prepare("UPDATE users SET timezone = ? WHERE ip_address = ?") _, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(timezone, ip_address)
} }
func (db *Database) UserAdd(ip_address, username string) { func (db *Database) UserAdd(ip_address, username string) {
stmt, err := db.db.Prepare("INSERT INTO users (username, ip_address) VALUES (?, ?)") _, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(username, ip_address)
} }
func (db *Database) MessageAdd(ip_address string, content string) { func (db *Database) MessageAdd(ip_address string, content string) {
stmt, err := db.db.Prepare("INSERT INTO messages (ip_address, content) VALUES (?, ?)") _, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(ip_address, content)
} }
func (db *Database) UserNameGet(ip_address string) string { func (db *Database) UserNameGet(ip_address string) string {
@ -112,6 +110,18 @@ func (db *Database) UserNameGet(ip_address string) string {
return username return username
} }
func (db *Database) UserIpGet(username string) string {
rows, err := db.db.Query("SELECT ip_address FROM users WHERE username = ?", username)
if err != nil {
fmt.Println(err)
}
defer rows.Close()
var ip_address string
rows.Next()
rows.Scan(&ip_address)
return ip_address
}
func (db *Database) UserGetTimezone(ip_address string) string { func (db *Database) UserGetTimezone(ip_address string) string {
rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address) rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address)
if err != nil { if err != nil {
@ -171,6 +181,27 @@ func (db *Database) MessagesGet() []Message {
var created_at string var created_at string
var username string var username string
rows.Scan(&id, &ip_address, &content, &created_at, &username) rows.Scan(&id, &ip_address, &content, &created_at, &username)
// TODO: Implement better message parsing for admin commands
// TESTING:
// if msgId, ok := strings.CutPrefix(content, "@delete message id"); ok {
// msgId = strings.TrimSpace(msgId)
// go func() {
// db.MessageDeleteId(msgId)
// // db.MessageDeleteId(id)
// }()
// continue
// }
// TESTING:
// if msgId, ok := strings.CutPrefix(content, "@delete user messages"); ok {
// msgId = strings.TrimSpace(msgId)
// go func() {
// db.UserMessagesDelete(ip_address)
// // db.MessageDeleteId(id)
// }()
// continue
// }
message := Message{ message := Message{
Id: id, Id: id,
Content: content, Content: content,
@ -178,6 +209,7 @@ func (db *Database) MessagesGet() []Message {
SenderUsername: username, SenderUsername: username,
Timestamp: created_at, Timestamp: created_at,
} }
messages = append(messages, message) messages = append(messages, message)
} }
return messages return messages
@ -202,11 +234,17 @@ func (db *Database) UserExists(ip string) bool {
} }
func (db *Database) UserNameChange(ip, newUsername string) { func (db *Database) UserNameChange(ip, newUsername string) {
stmt, err := db.db.Prepare("UPDATE users SET username = ? WHERE ip_address = ?") _, err := db.db.Exec("UPDATE users SET username = ? WHERE ip_address = ?", newUsername, ip)
if err != nil {
fmt.Println(err)
}
}
func (db *Database) UserMessagesDelete(ip string) {
_, err := db.db.Exec("DELETE FROM messages WHERE ip_address = ?", ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(newUsername, ip)
} }
func (db *Database) UserMessagesGet(ip string) []Message { func (db *Database) UserMessagesGet(ip string) []Message {
@ -244,36 +282,43 @@ func (db *Database) UserMessagesGet(ip string) []Message {
return messages return messages
} }
func (db *Database) MessageDelete(id string) { func (db *Database) MessageDeleteId(id string) {
stmt, err := db.db.Prepare("DELETE FROM messages WHERE id = ?") _, err := db.db.Exec("DELETE FROM messages WHERE id = ?", id)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(id)
} }
func (db *Database) UserDelete(ip string) { func (db *Database) DeleteOldMessages(ageMinutes int) {
stmt, err := db.db.Prepare("DELETE FROM users WHERE ip_address = ?") if ageMinutes <= 0 {
return
}
age := strconv.Itoa(ageMinutes)
_, err := db.db.Exec("DELETE FROM messages WHERE created_at < datetime('now', ? || ' minutes')", "-"+age)
if err != nil {
fmt.Println(err)
}
}
func (db *Database) UserDeleteIp(ip string) {
_, err := db.db.Exec("DELETE FROM users WHERE ip_address = ?", ip)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec(ip)
} }
func (db *Database) UsersDelete() { func (db *Database) UsersDelete() {
stmt, err := db.db.Prepare("DELETE FROM users") _, err := db.db.Exec("DELETE FROM users")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec()
} }
func (db *Database) MessagesDelete() { func (db *Database) MessagesDelete() {
stmt, err := db.db.Prepare("DELETE FROM messages") _, err := db.db.Exec("DELETE FROM messages")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
stmt.Exec()
} }
type gzipResponseWriter struct { type gzipResponseWriter struct {
@ -328,19 +373,18 @@ type Message struct {
} }
type Server struct { type Server struct {
Ip string
Port int
Connected map[string]time.Time // Map IP -> Last activity time Connected map[string]time.Time // Map IP -> Last activity time
Database *Database Database *Database
Config Config
mu sync.Mutex // For thread safety mu sync.Mutex // For thread safety
} }
func NewServer(ip string, port int, dbpath string) *Server { func NewServer(config Config) *Server {
return &Server{ return &Server{
Ip: ip,
Port: port,
Connected: make(map[string]time.Time), Connected: make(map[string]time.Time),
Database: OpenDatabase(dbpath), Database: OpenDatabase(config.Paths.DatabasePath),
Config: config,
mu: sync.Mutex{}, mu: sync.Mutex{},
} }
} }
@ -431,8 +475,8 @@ func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"}) json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
} }
func getMessageTemplate(file string, body string) string { func getMessageTemplate(filepath string, body string) string {
contents, _ := os.ReadFile(file) contents, _ := os.ReadFile(filepath)
return strings.Replace(string(contents), "{{body}}", body, 1) return strings.Replace(string(contents), "{{body}}", body, 1)
} }
@ -447,11 +491,11 @@ func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
clientIP := getClientIP(r) clientIP := getClientIP(r)
timeZone := s.Database.UserGetTimezone(clientIP) timeZone := s.Database.UserGetTimezone(clientIP)
timeLocal := TimeStringToTimeInLocation(msg.Timestamp, timeZone) 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>`, body += fmt.Sprintf(`%s<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
msg.SenderUsername, timeLocal, msg.Content) msg.Id, msg.SenderUsername, timeLocal, msg.Content)
} }
w.Write([]byte(getMessageTemplate("messages.html", body))) w.Write([]byte(getMessageTemplate(s.Config.Paths.MessagesHtmlPath, body)))
case http.MethodPut: case http.MethodPut:
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -574,7 +618,8 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write(readFile("root.html")) file := readFile(s.Config.Paths.IndexHtmlPath)
w.Write(file)
} }
func readFile(filepath string) []byte { func readFile(filepath string) []byte {
@ -585,16 +630,21 @@ func readFile(filepath string) []byte {
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) { func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
_ = r _ = r
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
w.Write(readFile("root.js")) file := readFile(s.Config.Paths.IndexJsPath)
w.Write(file)
} }
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
_ = r _ = r
w.Header().Set("Content-Type", "text/css") w.Header().Set("Content-Type", "text/css")
w.Write(readFile("root.css")) file := readFile(s.Config.Paths.IndexCssPath)
w.Write(file)
} }
func (s *Server) Run() { func (s *Server) Run() {
s.Database.DbCreateTableMessages()
s.Database.DbCreateTableUsers()
s.Database.DeleteOldMessages(s.Config.Server.MessageMaxAge)
handler := http.NewServeMux() handler := http.NewServeMux()
handler.HandleFunc("/ping", s.handlePing) handler.HandleFunc("/ping", s.handlePing)
handler.HandleFunc("/username", s.handleUsername) handler.HandleFunc("/username", s.handleUsername)
@ -605,8 +655,9 @@ func (s *Server) Run() {
handler.HandleFunc("/root.js", s.handleJs) handler.HandleFunc("/root.js", s.handleJs)
handler.HandleFunc("/root.css", s.handleCss) handler.HandleFunc("/root.css", s.handleCss)
handler.HandleFunc("/timezone", s.handleTimezone) handler.HandleFunc("/timezone", s.handleTimezone)
fmt.Printf("Server starting on %s:%d\n", s.Ip, s.Port) fmt.Printf("Server starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port)
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Ip, s.Port), GzipMiddleware(handler)); err != nil { defer s.Stop()
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil {
fmt.Printf("Server error: %v\n", err) fmt.Printf("Server error: %v\n", err)
} }
} }
@ -616,12 +667,55 @@ func (s *Server) Stop() {
} }
func main() { func main() {
ip := os.Args[1] if len(os.Args) < 2 {
port, _ := strconv.Atoi(os.Args[2]) fmt.Printf("Usage: %s <config.json>\n", os.Args[0])
databaseFile := os.Args[3] os.Exit(1)
server := NewServer(ip, port, databaseFile) }
server.Database.DbCreateTableMessages() _, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0)
server.Database.DbCreateTableUsers() if err != nil {
defer server.Stop() fmt.Println("Error opening config file: ", err)
os.Exit(1)
}
config := LoadConfig(os.Args[1])
fmt.Println("Config loaded")
server := NewServer(config)
server.Run() server.Run()
} }
type Config struct {
Server struct {
IpAddress string `json:"ipAddress"`
Port int `json:"port"`
MessageMaxAge int `json:"messageMaxAge"`
} `json:"server"`
Paths struct {
DatabasePath string `json:"databasePath"`
IndexJsPath string `json:"indexJsPath"`
IndexCssPath string `json:"indexCssPath"`
IndexHtmlPath string `json:"indexHtmlPath"`
MessagesHtmlPath string `json:"messagesHtmlPath"`
} `json:"paths"`
}
func LoadConfig(filepath string) Config {
contents, _ := os.ReadFile(filepath)
var config Config
err := json.Unmarshal(contents, &config)
config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath)
config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath)
config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath)
config.Paths.MessagesHtmlPath = pathMaker(config.Paths.MessagesHtmlPath)
config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath)
if err != nil {
fmt.Println("Error parsing config file: ", err)
os.Exit(1)
}
return config
}
func pathMaker(path string) string {
absPath, _ := filepath.Abs(path)
absPath = filepath.Clean(absPath)
fmt.Println(absPath)
return absPath
}

View File

@ -1,19 +1,17 @@
# Changes To Make # Changes To Make
## Frontend ## Frontend
### High Priority ### High Priority
- Nothing yet - Change light mode scroll down arrow svg fill color? to the buttom inactive fg color
### Mid Priority ### Mid Priority
- Nothing yet - Up arrow in textarea should bring up last input
### Low Priority ### Low Priority
- Mobile formatting @media - Mobile formatting @media
- First click of user list does not register - First click of user list does not register
## Backend ## Backend
### High Priority ### High Priority
- Updating messages should lazily load prior messages - Updating messages should lazily load prior messages (pagination?)
- Separate all configuration out to a config file
### Mid Priority ### Mid Priority
- Nothing yet - Nothing yet
### Low Priority ### Low Priority
- Search functionality? - Search functionality?
- Delete messages? (maybe later) - Delete messages? (in progress, capability exists, flesh this out a bit)
- Old messages should be deleted? Or if database is over a certain size? Not sure if this is really necessary.