changes
This commit is contained in:
parent
8185bb0559
commit
7e617eb1af
@ -5,10 +5,9 @@
|
|||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"databasePath": "/home/radon/Documents/chattest.db",
|
"databasePath": "/home/radon/Documents/chattest.db",
|
||||||
"indexJsPath": "./content/root.js",
|
"indexJsPath": "./public/index.js",
|
||||||
"indexCssPath": "./content/root.css",
|
"indexCssPath": "./public/style.css",
|
||||||
"indexHtmlPath": "./content/root.html",
|
"indexHtmlPath": "./public/index.html"
|
||||||
"messagesHtmlPath": "./content/messages.html"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"messageMaxAge": 259200,
|
"messageMaxAge": 259200,
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
{{body}}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
326
db/db.go
Normal file
326
db/db.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id string
|
||||||
|
Username string
|
||||||
|
IpAddress string
|
||||||
|
Timezone string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Id string
|
||||||
|
SenderIp string
|
||||||
|
SenderUsername string
|
||||||
|
Content string
|
||||||
|
Timestamp string
|
||||||
|
Edited bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
edited INTEGER DEFAULT 0,
|
||||||
|
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) {
|
||||||
|
_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) UserAdd(ip_address, username string) {
|
||||||
|
_, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) MessageAdd(ip_address string, content string) {
|
||||||
|
_, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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 {
|
||||||
|
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, messages.edited
|
||||||
|
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
|
||||||
|
var edited int
|
||||||
|
rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
|
||||||
|
|
||||||
|
editedBool := false
|
||||||
|
if edited == 1 {
|
||||||
|
editedBool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
message := Message{
|
||||||
|
Id: id,
|
||||||
|
Content: content,
|
||||||
|
SenderIp: ip_address,
|
||||||
|
SenderUsername: username,
|
||||||
|
Edited: editedBool,
|
||||||
|
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) {
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var edited int
|
||||||
|
rows.Scan(&id, &ip_address, &content, &created_at, &username, &edited)
|
||||||
|
t, _ := time.Parse(created_at, created_at)
|
||||||
|
editedBool := false
|
||||||
|
if edited == 1 {
|
||||||
|
editedBool = true
|
||||||
|
}
|
||||||
|
message := Message{
|
||||||
|
Id: id,
|
||||||
|
Content: content,
|
||||||
|
SenderIp: ip_address,
|
||||||
|
SenderUsername: username,
|
||||||
|
Edited: editedBool,
|
||||||
|
Timestamp: t.Format(created_at),
|
||||||
|
}
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) MessageDeleteId(id string) {
|
||||||
|
_, err := db.db.Exec("DELETE FROM messages WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
|
||||||
|
res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
affected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(affected), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) MessageEditIfOwner(id string, content string, ip string) (int, error) {
|
||||||
|
res, err := db.db.Exec("UPDATE messages SET content = ?, edited = 1 WHERE id = ? AND ip_address = ?", content, id, ip)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
affected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(affected), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) UsersDelete() {
|
||||||
|
_, err := db.db.Exec("DELETE FROM users")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) MessagesDelete() {
|
||||||
|
_, err := db.db.Exec("DELETE FROM messages")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
779
main.go
779
main.go
@ -1,789 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"chat/srv"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Database struct {
|
var config srv.Config
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func TimezoneToLocation(timezone string) *time.Location {
|
func init() {
|
||||||
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) {
|
|
||||||
_, err := db.db.Exec("UPDATE users SET timezone = ? WHERE ip_address = ?", timezone, ip_address)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) UserAdd(ip_address, username string) {
|
|
||||||
_, err := db.db.Exec("INSERT INTO users (username, ip_address) VALUES (?, ?)", username, ip_address)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) MessageAdd(ip_address string, content string) {
|
|
||||||
_, err := db.db.Exec("INSERT INTO messages (ip_address, content) VALUES (?, ?)", ip_address, content)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) 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 {
|
|
||||||
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) {
|
|
||||||
_, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) MessageDeleteId(id string) {
|
|
||||||
_, err := db.db.Exec("DELETE FROM messages WHERE id = ?", id)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (db *Database) MessageDeleteIfOwner(id string, ip string) (int, error) {
|
|
||||||
res, err := db.db.Exec("DELETE FROM messages WHERE id = ? AND ip_address = ?", id, ip)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(affected), nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) MessageEditIfOwner(id string, content string, ip string) (int, error) {
|
|
||||||
res, err := db.db.Exec("UPDATE messages SET content = ? WHERE id = ? AND ip_address = ?", content, id, ip)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(affected), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) UsersDelete() {
|
|
||||||
_, err := db.db.Exec("DELETE FROM users")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Database) MessagesDelete() {
|
|
||||||
_, err := db.db.Exec("DELETE FROM messages")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Connected map[string]time.Time // Map IP -> Last activity time
|
|
||||||
Database *Database
|
|
||||||
Config Config
|
|
||||||
mu sync.Mutex // For thread safety
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(config Config) *Server {
|
|
||||||
|
|
||||||
return &Server{
|
|
||||||
Connected: make(map[string]time.Time),
|
|
||||||
Database: OpenDatabase(config.Paths.DatabasePath),
|
|
||||||
Config: config,
|
|
||||||
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) > s.Config.Options.NameMaxLength {
|
|
||||||
http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), 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) {
|
|
||||||
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
|
|
||||||
s.mu.Unlock()
|
|
||||||
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(filepath string, body string) string {
|
|
||||||
contents, _ := os.ReadFile(filepath)
|
|
||||||
return strings.Replace(string(contents), "{{body}}", body, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodPatch:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
var req struct {
|
|
||||||
MessageId string `json:"messageId"`
|
|
||||||
MessageContent string `json:"messageContent"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clientIP := getClientIP(r)
|
|
||||||
if affected, err := s.Database.MessageEditIfOwner(req.MessageId, req.MessageContent, clientIP); err != nil {
|
|
||||||
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
} else if affected == 0 {
|
|
||||||
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"status": "Message edited successfully",
|
|
||||||
})
|
|
||||||
|
|
||||||
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>%s<br><span class="username">%s</span><br><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
|
|
||||||
msg.Id, msg.SenderUsername, timeLocal, msg.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(getMessageTemplate(s.Config.Paths.MessagesHtmlPath, 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,
|
|
||||||
})
|
|
||||||
case http.MethodDelete:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
MessageId string `json:"messageId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clientIP := getClientIP(r)
|
|
||||||
if affected, err := s.Database.MessageDeleteIfOwner(req.MessageId, clientIP); err != nil {
|
|
||||||
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
} else if affected == 0 {
|
|
||||||
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"status": "Message deleted",
|
|
||||||
})
|
|
||||||
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")
|
|
||||||
file := readFile(s.Config.Paths.IndexHtmlPath)
|
|
||||||
w.Write(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
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")
|
|
||||||
file := readFile(s.Config.Paths.IndexCssPath)
|
|
||||||
w.Write(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleMessagesLength(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// should return the number of messages in the database
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
messages := s.Database.MessagesGet()
|
|
||||||
json.NewEncoder(w).Encode(map[string]int{
|
|
||||||
"length": len(messages),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Run() {
|
|
||||||
s.Database.DbCreateTableMessages()
|
|
||||||
s.Database.DbCreateTableUsers()
|
|
||||||
s.Database.DeleteOldMessages(s.Config.Options.MessageMaxAge)
|
|
||||||
handler := http.NewServeMux()
|
|
||||||
handler.HandleFunc("/ping", s.handlePing)
|
|
||||||
handler.HandleFunc("/username", s.handleUsername)
|
|
||||||
handler.HandleFunc("/messages", s.handleMessages)
|
|
||||||
handler.HandleFunc("/messages/length", s.handleMessagesLength)
|
|
||||||
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.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Stop() {
|
|
||||||
s.Database.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Printf("Usage: %s <config.json>\n", os.Args[0])
|
fmt.Printf("Usage: %s <config.json>\n", os.Args[0])
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
_, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0)
|
config = srv.LoadConfig(os.Args[1])
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error opening config file: ", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
config := LoadConfig(os.Args[1])
|
|
||||||
fmt.Println("Config loaded")
|
fmt.Println("Config loaded")
|
||||||
server := NewServer(config)
|
|
||||||
server.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
func main() {
|
||||||
Server struct {
|
srv.NewServer(config).Run()
|
||||||
IpAddress string `json:"ipAddress"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
} `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"`
|
|
||||||
Options struct {
|
|
||||||
MessageMaxAge int `json:"messageMaxAge"`
|
|
||||||
NameMaxLength int `json:"nameMaxLength"`
|
|
||||||
} `json:"options"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,26 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="theme-button" onclick="toggleTheme()">
|
<button class="theme-button" onclick="toggleTheme()">
|
||||||
<svg class="theme-icon" viewBox="0 0 24 24">
|
<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"/>
|
<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"/>
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="search-trigger">
|
||||||
|
<div class="search-container" id="searchContainer">
|
||||||
|
<button class="search-button" id="searchButton">
|
||||||
|
<svg class="search-icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
id="searchInput"
|
||||||
|
placeholder="Type to search page..."
|
||||||
|
>
|
||||||
|
<span class="search-count" id="searchCount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="current-user" class="current-user"></div>
|
<div id="current-user" class="current-user"></div>
|
||||||
|
|
||||||
@ -58,7 +72,6 @@
|
|||||||
<!-- Messages will be loaded here -->
|
<!-- Messages will be loaded here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-section">
|
<div class="message-section">
|
||||||
<div class="message-container">
|
<div class="message-container">
|
||||||
<textarea id="message" placeholder="Type a message (use shift+return for a new line)" rows="1"></textarea>
|
<textarea id="message" placeholder="Type a message (use shift+return for a new line)" rows="1"></textarea>
|
||||||
@ -67,22 +80,7 @@
|
|||||||
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
|
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
|
||||||
</svg></button>
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-trigger">
|
|
||||||
<div class="search-container" id="searchContainer">
|
|
||||||
<button class="search-button" id="searchButton">
|
|
||||||
<div class="search-icon"></div>
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
id="searchInput"
|
|
||||||
placeholder="Type to search page..."
|
|
||||||
>
|
|
||||||
<span class="search-count" id="searchCount"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -27,17 +27,42 @@ function toggleTheme() {
|
|||||||
localStorage.setItem("theme", newTheme);
|
localStorage.setItem("theme", newTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editMessage(messageId, content) {
|
function replaceDiv(oldDiv, newDiv) {
|
||||||
const newContent = prompt("Edit message:", content);
|
oldDiv.parentNode.replaceChild(newDiv, oldDiv);
|
||||||
|
}
|
||||||
|
|
||||||
if (newContent === null) {
|
function cancelEditMessage(messageId) {
|
||||||
|
const editMessageDiv = document.getElementById(
|
||||||
|
".edit-message@" + messageId,
|
||||||
|
);
|
||||||
|
replaceDiv(editMessageDiv, editMessageOriginalDiv);
|
||||||
|
editMessageOriginalDiv = null;
|
||||||
|
editing = false;
|
||||||
|
editMessageNumber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEditMessage(messageId) {
|
||||||
|
const editMessageDiv = document.getElementById(
|
||||||
|
".edit-message@" + messageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content =
|
||||||
|
editMessageOriginalDiv.querySelector(".content").textContent;
|
||||||
|
|
||||||
|
const newContent =
|
||||||
|
editMessageDiv.querySelector(".edit-message-textarea").value;
|
||||||
|
|
||||||
|
if (content === null) {
|
||||||
|
cancelEditMessage(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newContent === "") {
|
if (content === "") {
|
||||||
|
cancelEditMessage(messageId);
|
||||||
deleteMessage(messageId);
|
deleteMessage(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newContent === content) {
|
if (newContent === content) {
|
||||||
|
cancelEditMessage(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +79,9 @@ async function editMessage(messageId, content) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
editing = false;
|
||||||
|
editMessageNumber = null;
|
||||||
|
editMessageOriginalDiv = null;
|
||||||
updateMessagesInPlace();
|
updateMessagesInPlace();
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to edit message");
|
console.error("Failed to edit message");
|
||||||
@ -63,6 +91,61 @@ async function editMessage(messageId, content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editMessageOriginalDiv = null;
|
||||||
|
editMessageNumber = null;
|
||||||
|
editing = false;
|
||||||
|
|
||||||
|
async function editMessage(messageId) {
|
||||||
|
if (editing) {
|
||||||
|
cancelEditMessage(editMessageNumber);
|
||||||
|
}
|
||||||
|
editing = true;
|
||||||
|
editMessageNumber = messageId;
|
||||||
|
const originalMessageDiv = document.getElementById(
|
||||||
|
".message@" + messageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const height = originalMessageDiv.scrollHeight;
|
||||||
|
|
||||||
|
const queryContent = originalMessageDiv.querySelector(".content");
|
||||||
|
|
||||||
|
// we need to replace all the html elements with their text content, this includes links like img, a, iframe
|
||||||
|
queryContent.querySelectorAll("a").forEach((link) => {
|
||||||
|
link.textContent = link.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
queryContent.querySelectorAll("img").forEach((img) => {
|
||||||
|
img.textContent = img.src;
|
||||||
|
});
|
||||||
|
|
||||||
|
queryContent.querySelectorAll("iframe").forEach((iframe) => {
|
||||||
|
iframe.textContent = iframe.src;
|
||||||
|
});
|
||||||
|
|
||||||
|
const content =
|
||||||
|
originalMessageDiv.querySelector(".content").textContent;
|
||||||
|
|
||||||
|
editMessageOriginalDiv = originalMessageDiv.cloneNode(true);
|
||||||
|
|
||||||
|
const editMessageDiv = document.createElement("div");
|
||||||
|
const editTextArea = document.createElement("textarea");
|
||||||
|
editTextArea.className = "edit-message-textarea";
|
||||||
|
editTextArea.id = "edit-message-textarea";
|
||||||
|
editTextArea.value = content;
|
||||||
|
editTextArea.style.height = height + "px";
|
||||||
|
editMessageDiv.className = "edit-message";
|
||||||
|
editMessageDiv.id = ".edit-message@" + messageId;
|
||||||
|
|
||||||
|
editMessageDiv.innerHTML += `<div class="button-container">` +
|
||||||
|
`<button class="edit-message-button" onclick="submitEditMessage('${messageId}')">Submit</button>` +
|
||||||
|
`<button class="edit-message-button" onclick="cancelEditMessage('${messageId}')">Cancel</button>` +
|
||||||
|
`</div>`;
|
||||||
|
|
||||||
|
editMessageDiv.appendChild(editTextArea);
|
||||||
|
|
||||||
|
replaceDiv(originalMessageDiv, editMessageDiv);
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteMessage(messageId) {
|
async function deleteMessage(messageId) {
|
||||||
if (confirm("Delete this message?")) {
|
if (confirm("Delete this message?")) {
|
||||||
try {
|
try {
|
||||||
@ -101,7 +184,7 @@ function setScrollLocation(height) {
|
|||||||
messagesDiv.scrollTop = height;
|
messagesDiv.scrollTop = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", function (event) {
|
document.addEventListener("click", function(event) {
|
||||||
const settingsPanel = document.getElementById("settings-panel");
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
const settingsButton = document.querySelector(".settings-button");
|
const settingsButton = document.querySelector(".settings-button");
|
||||||
const usersPanel = document.getElementById("users-panel");
|
const usersPanel = document.getElementById("users-panel");
|
||||||
@ -122,7 +205,7 @@ document.addEventListener("click", function (event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keypress", function (event) {
|
document.addEventListener("keypress", function(event) {
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
const settingsPanel = document.getElementById("settings-panel");
|
const settingsPanel = document.getElementById("settings-panel");
|
||||||
const inputPanel = document.getElementById("message");
|
const inputPanel = document.getElementById("message");
|
||||||
@ -136,13 +219,13 @@ document.addEventListener("keypress", function (event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("input", function (_) {
|
document.addEventListener("input", function(_) {
|
||||||
const msg = document.getElementById("message");
|
const msg = document.getElementById("message");
|
||||||
msg.style.height = "auto";
|
msg.style.height = "auto";
|
||||||
msg.style.height = (msg.scrollHeight) + "px";
|
msg.style.height = (msg.scrollHeight) + "px";
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("blur", function (_) {
|
document.addEventListener("blur", function(_) {
|
||||||
const msg = document.getElementById("message");
|
const msg = document.getElementById("message");
|
||||||
msg.style.height = "auto";
|
msg.style.height = "auto";
|
||||||
}, true);
|
}, true);
|
||||||
@ -178,6 +261,9 @@ async function getMessageCount() {
|
|||||||
|
|
||||||
let lastMessageCount = 0;
|
let lastMessageCount = 0;
|
||||||
async function loadMessages(forceUpdate = false, scrollLocation) {
|
async function loadMessages(forceUpdate = false, scrollLocation) {
|
||||||
|
if (editing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const newMessageCount = await getMessageCount();
|
const newMessageCount = await getMessageCount();
|
||||||
|
|
||||||
@ -232,6 +318,7 @@ async function loadMessages(forceUpdate = false, scrollLocation) {
|
|||||||
"<br>",
|
"<br>",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
messageDiv.id = ".message@" + messageId;
|
||||||
const usernameDiv = document.createElement(
|
const usernameDiv = document.createElement(
|
||||||
"div",
|
"div",
|
||||||
);
|
);
|
||||||
@ -255,23 +342,19 @@ async function loadMessages(forceUpdate = false, scrollLocation) {
|
|||||||
doc.querySelector("span").textContent;
|
doc.querySelector("span").textContent;
|
||||||
|
|
||||||
const isMultiline = contentString.match(
|
const isMultiline = contentString.match(
|
||||||
"\\n",
|
"\\n",
|
||||||
) && true || false;
|
) && true || false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
compareUsername ===
|
compareUsername ===
|
||||||
document.getElementById(
|
document.getElementById(
|
||||||
"current-user",
|
"current-user",
|
||||||
).textContent
|
).textContent
|
||||||
) {
|
) {
|
||||||
deleteHtml =
|
deleteHtml =
|
||||||
`<button class="delete-button" title="Delete message" onclick="deleteMessage('${messageId}')" style="display: inline;">🗑️</button>`;
|
`<button class="delete-button" title="Delete message" onclick="deleteMessage('${messageId}')" style="display: inline;">🗑️</button>`;
|
||||||
editHtml =
|
editHtml =
|
||||||
`<button class="edit-button" title="Edit message" onclick="editMessage('${messageId}', '${contentString}')" style="display: inline;">📝</button>`;
|
`<button class="edit-button" title="Edit message" onclick="editMessage('${messageId}')" style="display: inline;">📝</button>`;
|
||||||
}
|
|
||||||
// TODO: Add edit for multiline messages
|
|
||||||
if (isMultiline) {
|
|
||||||
editHtml = "";
|
|
||||||
}
|
}
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
@ -296,7 +379,7 @@ async function loadMessages(forceUpdate = false, scrollLocation) {
|
|||||||
function contentEmbedding(content) {
|
function contentEmbedding(content) {
|
||||||
return content.replace(
|
return content.replace(
|
||||||
/(?![^<]*>)(https?:\/\/[^\s<]+)/g,
|
/(?![^<]*>)(https?:\/\/[^\s<]+)/g,
|
||||||
function (url) {
|
function(url) {
|
||||||
const videoId = getYouTubeID(
|
const videoId = getYouTubeID(
|
||||||
url,
|
url,
|
||||||
);
|
);
|
||||||
@ -306,15 +389,15 @@ function contentEmbedding(content) {
|
|||||||
height="315"
|
height="315"
|
||||||
src="https://www.youtube.com/embed/${videoId}"
|
src="https://www.youtube.com/embed/${videoId}"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
onerror="console.log('Video failed to load:', this.src); this.style.display='none'"
|
onerror="console.log('Video failed to load:', this.src); this.value=this.src"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowfullscreen></iframe></div>`;
|
allowfullscreen></iframe></div>`;
|
||||||
} else if (isImageUrl(url)) {
|
} else if (isImageUrl(url)) {
|
||||||
return `<div class="image-embed"><img
|
return `<div class="image-embed"><img
|
||||||
src="${url}"
|
src="${url}"
|
||||||
alt="Embedded image"
|
alt="${url}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onerror="console.log('Image failed to load:', this.src); this.style.display='none'"></div>`;
|
onerror="console.log('Image failed to load:', this.src); this.value=this.src"></div>`;
|
||||||
}
|
}
|
||||||
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
|
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
|
||||||
},
|
},
|
||||||
@ -356,7 +439,7 @@ async function checkUsername() {
|
|||||||
username.focus();
|
username.focus();
|
||||||
username.selectionStart =
|
username.selectionStart =
|
||||||
username.selectionEnd =
|
username.selectionEnd =
|
||||||
username.value.length;
|
username.value.length;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking username status:", error);
|
console.error("Error checking username status:", error);
|
||||||
@ -523,7 +606,7 @@ function initializeTheme() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keyup", function (event) {
|
document.addEventListener("keyup", function(event) {
|
||||||
const inputPanel = document.getElementById("message");
|
const inputPanel = document.getElementById("message");
|
||||||
if (inputPanel.contains(event.target) && event.key === "ArrowUp") {
|
if (inputPanel.contains(event.target) && event.key === "ArrowUp") {
|
||||||
if (inputPanel.value === "") {
|
if (inputPanel.value === "") {
|
||||||
@ -681,9 +764,8 @@ function initializeSearchBox() {
|
|||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "center",
|
block: "center",
|
||||||
});
|
});
|
||||||
searchCount.textContent = `${
|
searchCount.textContent = `${currentMatchIndex + 1
|
||||||
currentMatchIndex + 1
|
}/${matches.length}`;
|
||||||
}/${matches.length}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchButton.addEventListener("click", () => {
|
searchButton.addEventListener("click", () => {
|
||||||
@ -710,7 +792,7 @@ function initializeSearchBox() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Debug function to check search coverage (call from console: checkSearchCoverage())
|
// Debug function to check search coverage (call from console: checkSearchCoverage())
|
||||||
window.checkSearchCoverage = function () {
|
window.checkSearchCoverage = function() {
|
||||||
const textNodes = findTextNodes(document.body);
|
const textNodes = findTextNodes(document.body);
|
||||||
console.log(
|
console.log(
|
||||||
"Total searchable text nodes found:",
|
"Total searchable text nodes found:",
|
@ -42,47 +42,60 @@
|
|||||||
--input-button-active-fg: #dce0e8;
|
--input-button-active-fg: #dce0e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-message textarea {
|
||||||
|
order: 1; /* Make textarea come first */
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--input-bg-color);
|
||||||
|
color: var(--input-text-color);
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-message .button-container {
|
||||||
|
order: 2; /* Make buttons container come second */
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-message button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--input-button-inactive-bg);
|
||||||
|
color: var(--input-button-inactive-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.search-trigger {
|
.search-trigger {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
position: flex;
|
position: absolute;
|
||||||
display: flex;
|
display: inline;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--pum-button-active-bg);
|
background: var(--pum-button-inactive-bg);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
transition: width 0.2s ease;
|
transition: width 0.2s ease;
|
||||||
overflow: hidden;
|
width: 24px;
|
||||||
width: 48px;
|
height: 24px;
|
||||||
height: 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container.expanded {
|
.search-container.expanded {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--pum-button-active-bg);
|
|
||||||
color: var(--pum-button-inactive-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-button:hover {
|
|
||||||
background: var(--pum-button-active-bg);
|
|
||||||
color: var(--pum-button-active-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
display: none;
|
display: none;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
@ -107,25 +120,6 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search icon using CSS */
|
|
||||||
.search-icon {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border: 2px solid var(--pum-button-inactive-fg);
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 2px;
|
|
||||||
height: 10px;
|
|
||||||
background: var(--pum-button-inactive-fg);
|
|
||||||
bottom: -8px;
|
|
||||||
right: -3px;
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-count {
|
.search-count {
|
||||||
display: none;
|
display: none;
|
||||||
@ -166,20 +160,25 @@ body {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-65%);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px; /* Space between buttons */
|
gap: 20px; /* Space between buttons */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-controls .search-trigger {
|
||||||
|
position: relative;
|
||||||
|
right: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Show/hide appropriate icon based on theme */
|
/* Show/hide appropriate icon based on theme */
|
||||||
[data-theme="light"] .theme-icon .sun { display: none; }
|
[data-theme="light"] .theme-icon .sun { display: none; }
|
||||||
[data-theme="dark"] .theme-icon .moon { display: none; }
|
[data-theme="dark"] .theme-icon .moon { display: none; }
|
||||||
|
|
||||||
|
|
||||||
.settings-button, .users-button, .theme-button {
|
.settings-button, .users-button, .theme-button, .search-button {
|
||||||
top: 20px;
|
top: 20px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@ -194,18 +193,6 @@ body {
|
|||||||
background-color: var(--pum-button-inactive-bg);
|
background-color: var(--pum-button-inactive-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button {
|
|
||||||
right: calc(50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.users-button {
|
|
||||||
right: calc(60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-button {
|
|
||||||
right: calc(30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-header {
|
.message-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -251,19 +238,20 @@ body {
|
|||||||
background: var(--timestamp-color);
|
background: var(--timestamp-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-icon, .users-icon, .theme-icon{
|
.settings-icon, .users-icon, .theme-icon, .search-icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
fill: var(--pum-button-inactive-fg);
|
fill: var(--pum-button-inactive-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button:hover, .users-button:hover, .theme-button:hover {
|
.settings-button:hover, .users-button:hover, .theme-button:hover, .search-button:hover {
|
||||||
background-color: var(--pum-button-active-bg);
|
background-color: var(--pum-button-active-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button:hover .settings-icon,
|
.settings-button:hover .settings-icon,
|
||||||
.users-button:hover .users-icon,
|
.users-button:hover .users-icon,
|
||||||
.theme-button:hover .theme-icon {
|
.theme-button:hover .theme-icon,
|
||||||
|
.search-button:hover .search-icon {
|
||||||
fill: var(--pum-button-active-fg);
|
fill: var(--pum-button-active-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +476,7 @@ button.scroll:hover {
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-button, .users-button, .theme-button {
|
.settings-button, .users-button, .theme-button {
|
||||||
width: 36px;
|
width: 36px;
|
@ -2,17 +2,15 @@
|
|||||||
## Frontend
|
## Frontend
|
||||||
### High Priority
|
### High Priority
|
||||||
- Lazy load with pagination (frontend and backend)
|
- Lazy load with pagination (frontend and backend)
|
||||||
- Edit support for multiline message
|
- Clicking edit message should reposition you at the message with it centered or where the textarea edit box is at the top of the screen at least
|
||||||
### Mid Priority
|
### Mid Priority
|
||||||
- Other embeds (Twitter posts, spotify tracks, soundcloud, github repos, instagram posts, other video platforms)
|
- Other embeds (Twitter posts, spotify tracks, soundcloud, github repos, instagram posts, other video platforms)
|
||||||
### Low Priority
|
### Low Priority
|
||||||
- Reposition the search button
|
- Fix mobile views instead of hiding elements that you don't want to position properly (search, radchat title, username)
|
||||||
- Fix mobile views instead of hiding elements that you don't want to position properly
|
|
||||||
## Backend
|
## Backend
|
||||||
### High Priority
|
### High Priority
|
||||||
- Lazy load with pagination (frontend and backend)
|
- Lazy load with pagination (frontend and backend)
|
||||||
### Mid Priority
|
### Mid Priority
|
||||||
- Nothing yet
|
- Nothing yet
|
||||||
### Low Priority
|
### Low Priority
|
||||||
- Tidy up
|
- Nothing yet
|
||||||
- Perhaps a boolean value in the database to be able to display whether or not a message has been edited or not
|
|
||||||
|
285
srv/handle.go
Normal file
285
srv/handle.go
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package srv
|
||||||
|
|
||||||
|
import (
|
||||||
|
tu "chat/tu"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientIP := getClientIP(r)
|
||||||
|
s.updateActivity(clientIP)
|
||||||
|
s.cleanupActivity()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) > s.Config.Options.NameMaxLength {
|
||||||
|
http.Error(w, fmt.Sprintf(`{"error": "Username too long (%v out of %v characters maximum)"}`, len(req.Username), s.Config.Options.NameMaxLength), 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) {
|
||||||
|
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
|
||||||
|
s.mu.Unlock()
|
||||||
|
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 (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPatch:
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
var req struct {
|
||||||
|
MessageId string `json:"messageId"`
|
||||||
|
MessageContent string `json:"messageContent"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIP := getClientIP(r)
|
||||||
|
if affected, err := s.Database.MessageEditIfOwner(req.MessageId, req.MessageContent, clientIP); err != nil {
|
||||||
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else if affected == 0 {
|
||||||
|
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "Message edited successfully",
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := tu.TimeStringToTimeInLocation(msg.Timestamp, timeZone)
|
||||||
|
edited := ""
|
||||||
|
if msg.Edited {
|
||||||
|
edited = "(edited)"
|
||||||
|
}
|
||||||
|
body += fmt.Sprintf(`<p>%s<br><span class="username">%s</span><br><span class="timestamp">%s %s</span><br><span class="message">%s</span><br></p>`,
|
||||||
|
msg.Id, msg.SenderUsername, timeLocal, edited, msg.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(getMessageTemplate(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,
|
||||||
|
})
|
||||||
|
case http.MethodDelete:
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
MessageId string `json:"messageId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIP := getClientIP(r)
|
||||||
|
if affected, err := s.Database.MessageDeleteIfOwner(req.MessageId, clientIP); err != nil {
|
||||||
|
http.Error(w, `{"error": "Unauthorized"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else if affected == 0 {
|
||||||
|
http.Error(w, `{"error": "Message not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "Message deleted",
|
||||||
|
})
|
||||||
|
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")
|
||||||
|
file := readFile(s.Config.Paths.IndexHtmlPath)
|
||||||
|
w.Write(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = r
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
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")
|
||||||
|
file := readFile(s.Config.Paths.IndexCssPath)
|
||||||
|
w.Write(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleMessagesLength(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// should return the number of messages in the database
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
messages := s.Database.MessagesGet()
|
||||||
|
json.NewEncoder(w).Encode(map[string]int{
|
||||||
|
"length": len(messages),
|
||||||
|
})
|
||||||
|
}
|
181
srv/srv.go
Normal file
181
srv/srv.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package srv
|
||||||
|
|
||||||
|
import (
|
||||||
|
db "chat/db"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
IpAddress string `json:"ipAddress"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `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"`
|
||||||
|
Options struct {
|
||||||
|
MessageMaxAge int `json:"messageMaxAge"`
|
||||||
|
NameMaxLength int `json:"nameMaxLength"`
|
||||||
|
} `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getMessageTemplate(body string) string {
|
||||||
|
template := `<html>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{{body}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
return strings.Replace(template, "{{body}}", body, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(filepath string) []byte {
|
||||||
|
contents, _ := os.ReadFile(filepath)
|
||||||
|
return contents
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Connected map[string]time.Time // Map IP -> Last activity time
|
||||||
|
Database *db.Database
|
||||||
|
Config Config
|
||||||
|
mu sync.Mutex // For thread safety
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(config Config) *Server {
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
Connected: make(map[string]time.Time),
|
||||||
|
Database: db.OpenDatabase(config.Paths.DatabasePath),
|
||||||
|
Config: config,
|
||||||
|
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) Run() {
|
||||||
|
s.Database.DbCreateTableMessages()
|
||||||
|
s.Database.DbCreateTableUsers()
|
||||||
|
s.Database.DeleteOldMessages(s.Config.Options.MessageMaxAge)
|
||||||
|
handler := http.NewServeMux()
|
||||||
|
handler.HandleFunc("/", s.handleRoot)
|
||||||
|
handler.HandleFunc("/root.js", s.handleJs)
|
||||||
|
handler.HandleFunc("/root.css", s.handleCss)
|
||||||
|
handler.HandleFunc("/ping", s.handlePing)
|
||||||
|
handler.HandleFunc("/timezone", s.handleTimezone)
|
||||||
|
handler.HandleFunc("/username/status", s.handleUsernameStatus)
|
||||||
|
handler.HandleFunc("/messages/length", s.handleMessagesLength)
|
||||||
|
handler.HandleFunc("/users", s.handleUsers)
|
||||||
|
handler.HandleFunc("/username", s.handleUsername)
|
||||||
|
handler.HandleFunc("/messages", s.handleMessages)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
s.Database.Close()
|
||||||
|
}
|
26
tu/tu.go
Normal file
26
tu/tu.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package tu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user