diff --git a/config.json b/config.json new file mode 100644 index 0000000..6b612ec --- /dev/null +++ b/config.json @@ -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" + } +} diff --git a/messages.html b/content/messages.html similarity index 100% rename from messages.html rename to content/messages.html diff --git a/root.css b/content/root.css similarity index 100% rename from root.css rename to content/root.css diff --git a/root.html b/content/root.html similarity index 100% rename from root.html rename to content/root.html diff --git a/root.js b/content/root.js similarity index 89% rename from root.js rename to content/root.js index b5a4c92..77df7ba 100644 --- a/root.js +++ b/content/root.js @@ -149,31 +149,29 @@ async function loadMessages() { url, ); if (videoId) { - return `${url} -
- -
`; + return `
+ +
`; } else if (isImageUrl(url)) { console.log( "Attempting to embed image:", url, ); - return `${url} -
- Embedded image -
`; + return `
+ Embedded image +
`; } return `${url}`; }, diff --git a/main.go b/main.go index b0a60bb..bcc1133 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "os" + "path/filepath" "strconv" "strings" "sync" @@ -77,27 +78,24 @@ func (db *Database) DbCreateTableUsers() { } 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 { 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 (?, ?)") + _, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address) 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 (?, ?)") + _, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content) if err != nil { fmt.Println(err) } - stmt.Exec(ip_address, content) } func (db *Database) UserNameGet(ip_address string) string { @@ -112,6 +110,18 @@ func (db *Database) UserNameGet(ip_address string) string { 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 { rows, err := db.db.Query("SELECT timezone FROM users WHERE ip_address = ?", ip_address) if err != nil { @@ -171,6 +181,27 @@ func (db *Database) MessagesGet() []Message { var created_at string var username string 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{ Id: id, Content: content, @@ -178,6 +209,7 @@ func (db *Database) MessagesGet() []Message { SenderUsername: username, Timestamp: created_at, } + messages = append(messages, message) } return messages @@ -202,11 +234,17 @@ func (db *Database) UserExists(ip string) bool { } 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 { fmt.Println(err) } - stmt.Exec(newUsername, ip) } func (db *Database) UserMessagesGet(ip string) []Message { @@ -244,36 +282,43 @@ func (db *Database) UserMessagesGet(ip string) []Message { return messages } -func (db *Database) MessageDelete(id string) { - stmt, err := db.db.Prepare("DELETE FROM messages WHERE id = ?") +func (db *Database) MessageDeleteId(id string) { + _, err := db.db.Exec("DELETE FROM messages WHERE id = ?", 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 = ?") +func (db *Database) DeleteOldMessages(ageMinutes int) { + 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 { fmt.Println(err) } - stmt.Exec(ip) } func (db *Database) UsersDelete() { - stmt, err := db.db.Prepare("DELETE FROM users") + _, err := db.db.Exec("DELETE FROM users") if err != nil { fmt.Println(err) } - stmt.Exec() } func (db *Database) MessagesDelete() { - stmt, err := db.db.Prepare("DELETE FROM messages") + _, err := db.db.Exec("DELETE FROM messages") if err != nil { fmt.Println(err) } - stmt.Exec() } type gzipResponseWriter struct { @@ -328,19 +373,18 @@ type Message struct { } type Server struct { - Ip string - Port int Connected map[string]time.Time // Map IP -> Last activity time Database *Database + Config Config mu sync.Mutex // For thread safety } -func NewServer(ip string, port int, dbpath string) *Server { +func NewServer(config Config) *Server { + return &Server{ - Ip: ip, - Port: port, Connected: make(map[string]time.Time), - Database: OpenDatabase(dbpath), + Database: OpenDatabase(config.Paths.DatabasePath), + Config: config, 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"}) } -func getMessageTemplate(file string, body string) string { - contents, _ := os.ReadFile(file) +func getMessageTemplate(filepath string, body string) string { + contents, _ := os.ReadFile(filepath) 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) timeZone := s.Database.UserGetTimezone(clientIP) timeLocal := TimeStringToTimeInLocation(msg.Timestamp, timeZone) - body += fmt.Sprintf(`

%s %s
%s

`, - msg.SenderUsername, timeLocal, msg.Content) + body += fmt.Sprintf(`%s

%s %s
%s

`, + 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: w.Header().Set("Content-Type", "application/json") @@ -574,7 +618,8 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { return } 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 { @@ -585,16 +630,21 @@ func readFile(filepath string) []byte { func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) { _ = r 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) { _ = r 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() { + s.Database.DbCreateTableMessages() + s.Database.DbCreateTableUsers() + s.Database.DeleteOldMessages(s.Config.Server.MessageMaxAge) handler := http.NewServeMux() handler.HandleFunc("/ping", s.handlePing) handler.HandleFunc("/username", s.handleUsername) @@ -605,8 +655,9 @@ func (s *Server) Run() { 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 starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port) + 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) } } @@ -616,12 +667,55 @@ func (s *Server) Stop() { } 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() + if len(os.Args) < 2 { + fmt.Printf("Usage: %s \n", os.Args[0]) + os.Exit(1) + } + _, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0) + if err != nil { + fmt.Println("Error opening config file: ", err) + os.Exit(1) + } + config := LoadConfig(os.Args[1]) + fmt.Println("Config loaded") + server := NewServer(config) 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 +} diff --git a/readme.md b/readme.md index 94ac04b..4e19dab 100644 --- a/readme.md +++ b/readme.md @@ -1,19 +1,17 @@ # Changes To Make ## Frontend ### High Priority -- Nothing yet +- Change light mode scroll down arrow svg fill color? to the buttom inactive fg color ### Mid Priority -- Nothing yet +- Up arrow in textarea should bring up last input ### Low Priority - Mobile formatting @media - First click of user list does not register ## Backend ### High Priority -- Updating messages should lazily load prior messages -- Separate all configuration out to a config file +- Updating messages should lazily load prior messages (pagination?) ### 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. +- Delete messages? (in progress, capability exists, flesh this out a bit)