Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ── Node / Frontend ──────────────────────────────────────────
node_modules/
frontend/dist/
frontend/.env
frontend/.env.local
frontend/.env.*.local
*.tsbuildinfo

# ── Go / Backend ─────────────────────────────────────────────
backend/bin/
backend/*.exe
backend/*.test
*.out

# ── Environment & Secrets ────────────────────────────────────
.env
.env.*
!.env.example
infrastructure/.env
*.pem
*.key

# ── Terraform ────────────────────────────────────────────────
infrastructure/terraform/.terraform/
infrastructure/terraform/.terraform.lock.hcl
infrastructure/terraform/terraform.tfstate
infrastructure/terraform/terraform.tfstate.backup
infrastructure/terraform/*.tfvars
infrastructure/terraform/override.tf
infrastructure/terraform/override.tf.json

# ── Docker ───────────────────────────────────────────────────
.docker/

# ── OS & Editor ──────────────────────────────────────────────
.DS_Store
Thumbs.db
desktop.ini
.vscode/
.idea/
*.swp
*.swo
*~

# ── Logs & Temp ──────────────────────────────────────────────
*.log
logs/
tmp/
temp/
9 changes: 9 additions & 0 deletions backend/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func main() {
UserPasswords: make(map[string]string),
FriendRequests: make(map[string]*models.FriendRequest),
Locations: make(map[string]*models.Location),
Messages: make(map[string]*models.Message),
}

// Define our routes (Requires Go 1.22+ for method-based routing)
Expand All @@ -49,6 +50,14 @@ func main() {
mux.HandleFunc("POST /api/locations", api.EnableCORS(srv.HandleShareLocation))
mux.HandleFunc("GET /api/locations/nearby", api.EnableCORS(srv.HandleGetNearbyUsers))

// Message routes
mux.HandleFunc("POST /api/messages", api.EnableCORS(srv.HandleSendMessage))
mux.HandleFunc("GET /api/messages/conversations", api.EnableCORS(srv.HandleGetConversations))
mux.HandleFunc("GET /api/messages/conversation", api.EnableCORS(srv.HandleGetConversation))

// Trending route
mux.HandleFunc("GET /api/trending", api.EnableCORS(srv.HandleGetTrending))

// Start the server
port := ":8080"
log.Printf("Backend server starting on port %s...\n", port)
Expand Down
1 change: 1 addition & 0 deletions backend/internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Server struct {
UserPasswords map[string]string // In-memory password storage (demo only)
FriendRequests map[string]*models.FriendRequest
Locations map[string]*models.Location
Messages map[string]*models.Message
}

// HandleLogin authenticates a user
Expand Down
196 changes: 196 additions & 0 deletions backend/internal/api/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package api

import (
"encoding/json"
"net/http"
"sort"
"strings"
"time"

"github.com/mtepenner/brevity-sharing/internal/models"
)

// userByID looks up a user by their ID across the username-keyed Users map.
func (s *Server) userByID(id string) *models.User {
for _, u := range s.Users {
if u.ID == id {
return u
}
}
return nil
}

// HandleSendMessage sends a private message from one user to another.
func (s *Server) HandleSendMessage(w http.ResponseWriter, r *http.Request) {
var req struct {
SenderID string `json:"sender_id"`
RecipientID string `json:"recipient_id"`
Content string `json:"content"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}

if req.SenderID == "" || req.RecipientID == "" || req.Content == "" {
http.Error(w, "sender_id, recipient_id, and content are required", http.StatusBadRequest)
return
}

if len(req.Content) > 1000 {
http.Error(w, "Message too long (max 1000 characters)", http.StatusBadRequest)
return
}

if req.SenderID == req.RecipientID {
http.Error(w, "Cannot send a message to yourself", http.StatusBadRequest)
return
}

msg := &models.Message{
ID: generateID(),
SenderID: req.SenderID,
RecipientID: req.RecipientID,
Content: req.Content,
CreatedAt: time.Now(),
}

s.Messages[msg.ID] = msg

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(msg)
}

// Conversation summarises the last message and unread count for a conversation partner.
type Conversation struct {
PartnerID string `json:"partner_id"`
PartnerUsername string `json:"partner_username"`
LastMessage *models.Message `json:"last_message"`
UnreadCount int `json:"unread_count"`
}

// HandleGetConversations returns a list of conversations for a given user.
func (s *Server) HandleGetConversations(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "user_id required", http.StatusBadRequest)
return
}

convMap := make(map[string]*Conversation)

for _, msg := range s.Messages {
if msg.SenderID != userID && msg.RecipientID != userID {
continue
}

partnerID := msg.RecipientID
if msg.RecipientID == userID {
partnerID = msg.SenderID
}

conv, exists := convMap[partnerID]
if !exists {
partnerUsername := partnerID
if partner := s.userByID(partnerID); partner != nil {
partnerUsername = partner.Username
}
conv = &Conversation{
PartnerID: partnerID,
PartnerUsername: partnerUsername,
}
convMap[partnerID] = conv
}

if conv.LastMessage == nil || msg.CreatedAt.After(conv.LastMessage.CreatedAt) {
msgCopy := *msg
conv.LastMessage = &msgCopy
}

if msg.RecipientID == userID && msg.ReadAt == nil {
conv.UnreadCount++
}
}

conversations := make([]*Conversation, 0, len(convMap))
for _, conv := range convMap {
conversations = append(conversations, conv)
}

sort.Slice(conversations, func(i, j int) bool {
if conversations[i].LastMessage == nil {
return false
}
if conversations[j].LastMessage == nil {
return true
}
return conversations[i].LastMessage.CreatedAt.After(conversations[j].LastMessage.CreatedAt)
})

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(conversations)
}

// HandleGetConversation returns all messages exchanged between two users.
func (s *Server) HandleGetConversation(w http.ResponseWriter, r *http.Request) {
user1 := r.URL.Query().Get("user1")
user2 := r.URL.Query().Get("user2")

if user1 == "" || user2 == "" {
http.Error(w, "user1 and user2 required", http.StatusBadRequest)
return
}

messages := make([]models.Message, 0)
for _, msg := range s.Messages {
if (msg.SenderID == user1 && msg.RecipientID == user2) ||
(msg.SenderID == user2 && msg.RecipientID == user1) {
messages = append(messages, *msg)
}
}

sort.Slice(messages, func(i, j int) bool {
return messages[i].CreatedAt.Before(messages[j].CreatedAt)
})

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(messages)
}

// HandleGetTrending returns the top trending hashtags extracted from stored tweets.
func (s *Server) HandleGetTrending(w http.ResponseWriter, r *http.Request) {
tweets := s.DB.GetTimeline()

topicCounts := make(map[string]int)
for _, tweet := range tweets {
for _, word := range strings.Fields(tweet.Content) {
word = strings.Trim(word, ".,!?\"';:()")
if strings.HasPrefix(word, "#") && len(word) > 1 {
topicCounts[strings.ToLower(word)]++
}
}
}

type TrendingTopic struct {
Topic string `json:"topic"`
Count int `json:"count"`
}

topics := make([]TrendingTopic, 0, len(topicCounts))
for topic, count := range topicCounts {
topics = append(topics, TrendingTopic{Topic: topic, Count: count})
}

sort.Slice(topics, func(i, j int) bool {
return topics[i].Count > topics[j].Count
})

if len(topics) > 10 {
topics = topics[:10]
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(topics)
}
13 changes: 13 additions & 0 deletions backend/internal/models/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

import "time"

// Message represents a private message between two users
type Message struct {
ID string `json:"id"`
SenderID string `json:"sender_id"`
RecipientID string `json:"recipient_id"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
ReadAt *time.Time `json:"read_at,omitempty"`
}
Loading
Loading