mirror of
https://github.com/hanzoskill/sogcli.git
synced 2026-10-30 21:03:29 +00:00
436 lines
11 KiB
Go
436 lines
11 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/visionik/sogcli/internal/carddav"
|
|
"github.com/visionik/sogcli/internal/config"
|
|
)
|
|
|
|
// ContactsCmd handles contact operations.
|
|
type ContactsCmd struct {
|
|
List ContactsListCmd `cmd:"" help:"List contacts"`
|
|
Get ContactsGetCmd `cmd:"" help:"Get contact details"`
|
|
Search ContactsSearchCmd `cmd:"" help:"Search contacts"`
|
|
Create ContactsCreateCmd `cmd:"" help:"Create a contact"`
|
|
Update ContactsUpdateCmd `cmd:"" help:"Update a contact"`
|
|
Delete ContactsDeleteCmd `cmd:"" help:"Delete a contact"`
|
|
Books ContactsBooksCmd `cmd:"" name:"books" help:"List address books"`
|
|
}
|
|
|
|
// ContactsListCmd lists contacts in an address book.
|
|
type ContactsListCmd struct {
|
|
AddressBook string `arg:"" optional:"" help:"Address book path (default: primary)"`
|
|
Max int `help:"Maximum contacts to return" default:"100"`
|
|
}
|
|
|
|
// Run executes the contacts list command.
|
|
func (c *ContactsListCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
ctx := context.Background()
|
|
contacts, err := client.ListContacts(ctx, bookPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list contacts: %w", err)
|
|
}
|
|
|
|
if len(contacts) == 0 {
|
|
fmt.Println("No contacts found.")
|
|
return nil
|
|
}
|
|
|
|
// Limit results
|
|
if c.Max > 0 && len(contacts) > c.Max {
|
|
contacts = contacts[:c.Max]
|
|
}
|
|
|
|
if root.JSON {
|
|
return outputContactsJSON(contacts)
|
|
}
|
|
|
|
return outputContactsTable(contacts)
|
|
}
|
|
|
|
// ContactsGetCmd gets contact details.
|
|
type ContactsGetCmd struct {
|
|
UID string `arg:"" help:"Contact UID"`
|
|
AddressBook string `help:"Address book path (default: primary)"`
|
|
}
|
|
|
|
// Run executes the contacts get command.
|
|
func (c *ContactsGetCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
ctx := context.Background()
|
|
contact, err := client.GetContact(ctx, bookPath, c.UID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get contact: %w", err)
|
|
}
|
|
|
|
if root.JSON {
|
|
return outputContactsJSON([]carddav.Contact{*contact})
|
|
}
|
|
|
|
return outputContactDetail(contact)
|
|
}
|
|
|
|
// ContactsSearchCmd searches for contacts.
|
|
type ContactsSearchCmd struct {
|
|
Query string `arg:"" help:"Search query (name)"`
|
|
AddressBook string `help:"Address book path (default: primary)"`
|
|
}
|
|
|
|
// Run executes the contacts search command.
|
|
func (c *ContactsSearchCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
ctx := context.Background()
|
|
contacts, err := client.SearchContacts(ctx, bookPath, c.Query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to search contacts: %w", err)
|
|
}
|
|
|
|
if len(contacts) == 0 {
|
|
fmt.Println("No contacts found.")
|
|
return nil
|
|
}
|
|
|
|
if root.JSON {
|
|
return outputContactsJSON(contacts)
|
|
}
|
|
|
|
return outputContactsTable(contacts)
|
|
}
|
|
|
|
// ContactsCreateCmd creates a contact.
|
|
type ContactsCreateCmd struct {
|
|
Name string `arg:"" help:"Full name"`
|
|
Email []string `help:"Email addresses" short:"e"`
|
|
Phone []string `help:"Phone numbers" short:"p"`
|
|
Org string `help:"Organization"`
|
|
Title string `help:"Job title"`
|
|
Note string `help:"Note"`
|
|
AddressBook string `help:"Address book path (default: primary)"`
|
|
}
|
|
|
|
// Run executes the contacts create command.
|
|
func (c *ContactsCreateCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
// Parse name into first/last
|
|
firstName, lastName := parseName(c.Name)
|
|
|
|
contact := &carddav.Contact{
|
|
UID: generateContactUID(),
|
|
FullName: c.Name,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
Emails: c.Email,
|
|
Phones: c.Phone,
|
|
Org: c.Org,
|
|
Title: c.Title,
|
|
Note: c.Note,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err := client.CreateContact(ctx, bookPath, contact); err != nil {
|
|
return fmt.Errorf("failed to create contact: %w", err)
|
|
}
|
|
|
|
if root.JSON {
|
|
return outputContactsJSON([]carddav.Contact{*contact})
|
|
}
|
|
|
|
fmt.Printf("Created contact: %s (%s)\n", contact.FullName, contact.UID)
|
|
return nil
|
|
}
|
|
|
|
// ContactsUpdateCmd updates a contact.
|
|
type ContactsUpdateCmd struct {
|
|
UID string `arg:"" help:"Contact UID"`
|
|
Name string `help:"Full name"`
|
|
Email []string `help:"Email addresses (replaces existing)" short:"e"`
|
|
Phone []string `help:"Phone numbers (replaces existing)" short:"p"`
|
|
Org string `help:"Organization"`
|
|
Title string `help:"Job title"`
|
|
Note string `help:"Note"`
|
|
AddressBook string `help:"Address book path (default: primary)"`
|
|
}
|
|
|
|
// Run executes the contacts update command.
|
|
func (c *ContactsUpdateCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
ctx := context.Background()
|
|
contact, err := client.GetContact(ctx, bookPath, c.UID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get contact: %w", err)
|
|
}
|
|
|
|
// Apply updates
|
|
if c.Name != "" {
|
|
contact.FullName = c.Name
|
|
contact.FirstName, contact.LastName = parseName(c.Name)
|
|
}
|
|
if len(c.Email) > 0 {
|
|
contact.Emails = c.Email
|
|
}
|
|
if len(c.Phone) > 0 {
|
|
contact.Phones = c.Phone
|
|
}
|
|
if c.Org != "" {
|
|
contact.Org = c.Org
|
|
}
|
|
if c.Title != "" {
|
|
contact.Title = c.Title
|
|
}
|
|
if c.Note != "" {
|
|
contact.Note = c.Note
|
|
}
|
|
|
|
if err := client.UpdateContact(ctx, bookPath, contact); err != nil {
|
|
return fmt.Errorf("failed to update contact: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Updated contact: %s\n", c.UID)
|
|
return nil
|
|
}
|
|
|
|
// ContactsDeleteCmd deletes a contact.
|
|
type ContactsDeleteCmd struct {
|
|
UID string `arg:"" help:"Contact UID"`
|
|
AddressBook string `help:"Address book path (default: primary)"`
|
|
}
|
|
|
|
// Run executes the contacts delete command.
|
|
func (c *ContactsDeleteCmd) Run(root *Root) error {
|
|
client, bookPath, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
if c.AddressBook != "" {
|
|
bookPath = c.AddressBook
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if err := client.DeleteContact(ctx, bookPath, c.UID); err != nil {
|
|
return fmt.Errorf("failed to delete contact: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Deleted contact: %s\n", c.UID)
|
|
return nil
|
|
}
|
|
|
|
// ContactsBooksCmd lists available address books.
|
|
type ContactsBooksCmd struct{}
|
|
|
|
// Run executes the contacts books command.
|
|
func (c *ContactsBooksCmd) Run(root *Root) error {
|
|
client, _, err := getCardDAVClient(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
ctx := context.Background()
|
|
books, err := client.FindAddressBooks(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list address books: %w", err)
|
|
}
|
|
|
|
if len(books) == 0 {
|
|
fmt.Println("No address books found.")
|
|
return nil
|
|
}
|
|
|
|
if root.JSON {
|
|
return outputAddressBooksJSON(books)
|
|
}
|
|
|
|
fmt.Printf("%-50s %s\n", "PATH", "NAME")
|
|
for _, book := range books {
|
|
fmt.Printf("%-50s %s\n", book.Path, book.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getCardDAVClient creates a CardDAV client from config.
|
|
func getCardDAVClient(root *Root) (*carddav.Client, string, error) {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
|
|
email := root.Account
|
|
if email == "" {
|
|
email = cfg.DefaultAccount
|
|
}
|
|
if email == "" {
|
|
return nil, "", fmt.Errorf("no account specified. Use --account or set a default")
|
|
}
|
|
|
|
acct, err := cfg.GetAccount(email)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if acct.CardDAV.URL == "" {
|
|
return nil, "", fmt.Errorf("no CardDAV URL configured for %s. Run: sog auth add %s --carddav-url <url>", email, email)
|
|
}
|
|
|
|
password, err := cfg.GetPasswordForProtocol(email, config.ProtocolCardDAV)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to get password: %w", err)
|
|
}
|
|
|
|
client, err := carddav.Connect(carddav.Config{
|
|
URL: acct.CardDAV.URL,
|
|
Email: email,
|
|
Password: password,
|
|
})
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to connect to CardDAV: %w", err)
|
|
}
|
|
|
|
return client, acct.CardDAV.DefaultAddressBook, nil
|
|
}
|
|
|
|
// parseName splits a full name into first and last name.
|
|
func parseName(fullName string) (first, last string) {
|
|
parts := strings.Fields(fullName)
|
|
if len(parts) == 0 {
|
|
return "", ""
|
|
}
|
|
if len(parts) == 1 {
|
|
return parts[0], ""
|
|
}
|
|
return parts[0], strings.Join(parts[1:], " ")
|
|
}
|
|
|
|
// generateContactUID generates a unique identifier for a contact.
|
|
func generateContactUID() string {
|
|
return fmt.Sprintf("%d@sog", time.Now().UnixNano())
|
|
}
|
|
|
|
// outputContactsJSON outputs contacts as JSON.
|
|
func outputContactsJSON(contacts []carddav.Contact) error {
|
|
for _, c := range contacts {
|
|
emails := strings.Join(c.Emails, ",")
|
|
phones := strings.Join(c.Phones, ",")
|
|
fmt.Printf(`{"uid":"%s","full_name":"%s","emails":"%s","phones":"%s","org":"%s"}`+"\n",
|
|
c.UID, c.FullName, emails, phones, c.Org)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// outputContactsTable outputs contacts as a table.
|
|
func outputContactsTable(contacts []carddav.Contact) error {
|
|
fmt.Printf("%-30s %-30s %-20s\n", "NAME", "EMAIL", "PHONE")
|
|
for _, c := range contacts {
|
|
name := c.FullName
|
|
if len(name) > 30 {
|
|
name = name[:27] + "..."
|
|
}
|
|
email := ""
|
|
if len(c.Emails) > 0 {
|
|
email = c.Emails[0]
|
|
}
|
|
if len(email) > 30 {
|
|
email = email[:27] + "..."
|
|
}
|
|
phone := ""
|
|
if len(c.Phones) > 0 {
|
|
phone = c.Phones[0]
|
|
}
|
|
fmt.Printf("%-30s %-30s %-20s\n", name, email, phone)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// outputContactDetail outputs a single contact in detail.
|
|
func outputContactDetail(contact *carddav.Contact) error {
|
|
fmt.Printf("UID: %s\n", contact.UID)
|
|
fmt.Printf("Name: %s\n", contact.FullName)
|
|
if contact.FirstName != "" || contact.LastName != "" {
|
|
fmt.Printf(" (First: %s, Last: %s)\n", contact.FirstName, contact.LastName)
|
|
}
|
|
if len(contact.Emails) > 0 {
|
|
fmt.Printf("Email: %s\n", strings.Join(contact.Emails, ", "))
|
|
}
|
|
if len(contact.Phones) > 0 {
|
|
fmt.Printf("Phone: %s\n", strings.Join(contact.Phones, ", "))
|
|
}
|
|
if contact.Org != "" {
|
|
fmt.Printf("Org: %s\n", contact.Org)
|
|
}
|
|
if contact.Title != "" {
|
|
fmt.Printf("Title: %s\n", contact.Title)
|
|
}
|
|
if len(contact.Addresses) > 0 {
|
|
fmt.Printf("Address: %s\n", strings.Join(contact.Addresses, "; "))
|
|
}
|
|
if contact.Birthday != "" {
|
|
fmt.Printf("Birthday: %s\n", contact.Birthday)
|
|
}
|
|
if contact.Note != "" {
|
|
fmt.Printf("Note: %s\n", contact.Note)
|
|
}
|
|
if contact.URL != "" {
|
|
fmt.Printf("URL: %s\n", contact.URL)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// outputAddressBooksJSON outputs address books as JSON.
|
|
func outputAddressBooksJSON(books []carddav.AddressBook) error {
|
|
for _, b := range books {
|
|
fmt.Printf(`{"path":"%s","name":"%s","description":"%s"}`+"\n", b.Path, b.Name, b.Description)
|
|
}
|
|
return nil
|
|
}
|