package gemlog import ( "context" "fmt" "strings" "time" _ "github.com/go-kivik/couchdb/v3" // CouchDB driver "github.com/go-kivik/kivik/v3" ) // Post represents a blog post document in CouchDB type Post struct { ID string `json:"_id,omitempty"` Rev string `json:"_rev,omitempty"` Title string `json:"title"` Date time.Time `json:"date"` Slug string `json:"slug"` Tags []string `json:"tags"` Content string `json:"content"` Created time.Time `json:"created"` Modified time.Time `json:"modified"` } // CouchDBClient wraps the CouchDB connection and operations type CouchDBClient struct { client *kivik.Client db *kivik.DB } // NewCouchDBClient creates a new CouchDB client connection func NewCouchDBClient(url, dbName string) (*CouchDBClient, error) { client, err := kivik.New("couch", url) if err != nil { return nil, fmt.Errorf("failed to create CouchDB client: %w", err) } // Check if database exists, create if it doesn't exists, err := client.DBExists(context.TODO(), dbName) if err != nil { return nil, fmt.Errorf("failed to check if database exists: %w", err) } if !exists { err = client.CreateDB(context.TODO(), dbName) if err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } } db := client.DB(context.TODO(), dbName) return &CouchDBClient{ client: client, db: db, }, nil } // SavePost saves a post to CouchDB func (c *CouchDBClient) SavePost(post *Post) error { ctx := context.TODO() // Set timestamps now := time.Now() if post.Created.IsZero() { post.Created = now } post.Modified = now // Generate ID if not set (use slug-timestamp) if post.ID == "" { post.ID = fmt.Sprintf("%s-%d", post.Slug, now.Unix()) } // Save the document rev, err := c.db.Put(ctx, post.ID, post) if err != nil { return fmt.Errorf("failed to save post: %w", err) } // Update the revision post.Rev = rev return nil } // GetPost retrieves a post by ID func (c *CouchDBClient) GetPost(id string) (*Post, error) { ctx := context.TODO() row := c.db.Get(ctx, id) if row.Err != nil { return nil, fmt.Errorf("failed to get post: %w", row.Err) } var post Post if err := row.ScanDoc(&post); err != nil { return nil, fmt.Errorf("failed to scan post document: %w", err) } return &post, nil } // GetAllPosts retrieves all posts from the database func (c *CouchDBClient) GetAllPosts() ([]*Post, error) { ctx := context.TODO() rows, err := c.db.AllDocs(ctx, kivik.Options{ "include_docs": true, }) if err != nil { return nil, fmt.Errorf("failed to get all posts: %w", err) } defer rows.Close() var posts []*Post for rows.Next() { var post Post if err := rows.ScanDoc(&post); err != nil { continue // Skip invalid documents } posts = append(posts, &post) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating posts: %w", err) } return posts, nil } // UpdatePost updates an existing post func (c *CouchDBClient) UpdatePost(post *Post) error { if post.ID == "" || post.Rev == "" { return fmt.Errorf("post ID and revision are required for updates") } post.Modified = time.Now() return c.SavePost(post) } // DeletePost deletes a post by ID and revision func (c *CouchDBClient) DeletePost(id, rev string) error { ctx := context.TODO() _, err := c.db.Delete(ctx, id, rev) if err != nil { return fmt.Errorf("failed to delete post: %w", err) } return nil } // ParseGeminiPost parses a gemini post content and returns a Post struct func ParseGeminiPost(content string) (*Post, error) { lines := strings.Split(content, "\n") var post Post var contentStart int inFrontmatter := false for i, line := range lines { line = strings.TrimSpace(line) // Check for frontmatter separator if line == "---" { if !inFrontmatter { inFrontmatter = true continue } else { // End of frontmatter contentStart = i + 1 break } } if !inFrontmatter { continue } // Parse frontmatter if strings.Contains(line, ":") { parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "title": post.Title = value case "date": if date, err := time.Parse("2006-01-02", value); err == nil { post.Date = date } case "slug": post.Slug = value case "tags": // Parse comma-separated tags if value != "" { tagList := strings.Split(value, ",") for _, tag := range tagList { post.Tags = append(post.Tags, strings.TrimSpace(tag)) } } } } } // Extract content after frontmatter if contentStart < len(lines) { post.Content = strings.Join(lines[contentStart:], "\n") } return &post, nil }