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,31 +149,29 @@ async function loadMessages() {
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>`;
return `<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 `<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>`;
},

176
main.go
View File

@ -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(`<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
msg.SenderUsername, timeLocal, msg.Content)
body += fmt.Sprintf(`%s<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
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 <config.json>\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
}

View File

@ -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)