diff --git a/cmd/termbox_server/main.go b/cmd/termbox_server/main.go index 7f44892..40882a2 100644 --- a/cmd/termbox_server/main.go +++ b/cmd/termbox_server/main.go @@ -30,6 +30,7 @@ func main() { protected.Use(handlers.AuthMiddleware) protected.POST("/send", handlers.HandleSendTermail) protected.GET("/inbox", handlers.HandleGetInbox) + protected.GET("/:id", handlers.HandleGetTermail) protected.POST("/:id/read", handlers.HandleMarkTermailRead) protected.DELETE("/:id", handlers.HandleDeleteTermail) diff --git a/internal/cli/root.go b/internal/cli/root.go index 1d183b1..ce950d5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -62,6 +63,14 @@ var inboxCmd = &cobra.Command{ Run: runInbox, } +var inboxViewCmd = &cobra.Command{ + Use: "view [termail_id]", + Short: "View termail content", + Long: "View the full content of a specific termail by ID.", + Args: cobra.ExactArgs(1), + Run: runInboxView, +} + var inboxReadCmd = &cobra.Command{ Use: "read [termail_id]", Short: "Mark termail as read", @@ -92,6 +101,7 @@ func init() { inboxCmd.Flags().IntP("offset", "o", 0, "Number of termails to skip") inboxCmd.Flags().Bool("unread", false, "Show only unread termails") + inboxCmd.AddCommand(inboxViewCmd) inboxCmd.AddCommand(inboxReadCmd) inboxCmd.AddCommand(inboxDeleteCmd) @@ -170,6 +180,64 @@ func makeAuthenticatedRequest(method, endpoint string, body []byte) (*http.Respo return http.DefaultClient.Do(req) } +func runInboxView(cmd *cobra.Command, args []string) { + termailID, err := strconv.Atoi(args[0]) + if err != nil { + fmt.Printf("Invalid termail ID: %s\n", args[0]) + return + } + + endpoint := fmt.Sprintf("/termail/%d", termailID) + resp, err := makeAuthenticatedRequest("GET", endpoint, nil) + if err != nil { + fmt.Printf("Error fetching termail: %v\n", err) + return + } + defer resp.Body.Close() + + var response map[string]interface{} + json.NewDecoder(resp.Body).Decode(&response) + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Error: %s\n", response["error"]) + return + } + + termailData := response["termail"].(map[string]interface{}) + + content := fmt.Sprintf("From: %s\nSubject: %s\nSent: %s\nRead: %t\n\n%s\n", + termailData["sender"].(string), + termailData["subject"].(string), + termailData["sent_at"].(string), + termailData["is_read"].(bool), + termailData["content"].(string), + ) + + if shouldUsePager(content) { + if err := displayWithLess(content); err != nil { + fmt.Print(content) + } + } else { + fmt.Print(content) + } + + markAsReadEndpoint := fmt.Sprintf("/termail/%d/read", termailID) + makeAuthenticatedRequest("POST", markAsReadEndpoint, nil) +} + +func shouldUsePager(content string) bool { + lines := strings.Count(content, "\n") + return lines > 20 +} + +func displayWithLess(content string) error { + lessCmd := exec.Command("less", "-R") + lessCmd.Stdin = strings.NewReader(content) + lessCmd.Stdout = os.Stdout + lessCmd.Stderr = os.Stderr + return lessCmd.Run() +} + func runInbox(cmd *cobra.Command, args []string) { search, _ := cmd.Flags().GetString("search") limit, _ := cmd.Flags().GetInt("limit") @@ -195,16 +263,27 @@ func runInbox(cmd *cobra.Command, args []string) { json.NewDecoder(resp.Body).Decode(&response) if resp.StatusCode == http.StatusOK { - termails := response["termails"].([]interface{}) - if len(termails) == 0 { + termails, exists := response["termails"] + if !exists || termails == nil { fmt.Println("No termails found.") return } - for _, t := range termails { + termailList, ok := termails.([]interface{}) + if !ok { + fmt.Println("No termails found.") + return + } + + if len(termailList) == 0 { + fmt.Println("No termails found.") + return + } + + for _, t := range termailList { termail := t.(map[string]interface{}) status := "READ" - if !termail["is_read"].(bool) { + if isRead, ok := termail["is_read"].(bool); ok && !isRead { status = "UNREAD" } fmt.Printf("[%s] ID: %.0f | From: %s | Subject: %s | Sent: %s\n", diff --git a/internal/db/user.go b/internal/db/user.go index b86136e..8ab27e0 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -191,6 +191,22 @@ type SendTermailRequest struct { Content string `json:"content"` } +func GetTermail(ctx context.Context, termailID, userID int) (*Termail, error) { + var t Termail + err := Pool.QueryRow(ctx, ` + SELECT t.id, t.sender_id, t.receiver_id, t.subject, t.content, t.is_read, t.sent_at, u.username as sender + FROM termails t + JOIN users u ON t.sender_id = u.id + WHERE t.id = $1 AND t.receiver_id = $2 + `, termailID, userID).Scan(&t.ID, &t.SenderID, &t.ReceiverID, &t.Subject, &t.Content, &t.IsRead, &t.SentAt, &t.Sender) + + if err != nil { + return nil, err + } + + return &t, nil +} + func SendTermail(ctx context.Context, senderID int, req SendTermailRequest) (*Termail, error) { receiver, err := GetUserByUsername(ctx, req.ReceiverUsername) if err != nil { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 2105558..60ded0e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -195,6 +195,26 @@ func HandleGetInbox(c echo.Context) error { }) } +func HandleGetTermail(c echo.Context) error { + userID := c.Get("user_id").(int) + termailID, err := strconv.Atoi(c.Param("id")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid termail ID"}) + } + + termail, err := db.GetTermail(c.Request().Context(), termailID, userID) + if err != nil { + if strings.Contains(err.Error(), "no rows") { + return c.JSON(http.StatusNotFound, map[string]string{"error": "Termail not found"}) + } + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch termail"}) + } + + return c.JSON(http.StatusOK, map[string]any{ + "termail": termail, + }) +} + func HandleMarkTermailRead(c echo.Context) error { userID := c.Get("user_id").(int) termailID, err := strconv.Atoi(c.Param("id"))