|
- package alpscarddav
-
- import (
- "fmt"
- "net/http"
- "net/url"
- "path"
- "strings"
-
- "git.sr.ht/~migadu/alps"
- "github.com/emersion/go-vcard"
- "github.com/emersion/go-webdav/carddav"
- "github.com/google/uuid"
- "github.com/labstack/echo/v4"
- )
-
- type AddressBookRenderData struct {
- alps.BaseRenderData
- AddressBook *carddav.AddressBook
- AddressObjects []AddressObject
- Query string
- }
-
- type AddressObjectRenderData struct {
- alps.BaseRenderData
- AddressBook *carddav.AddressBook
- AddressObject AddressObject
- }
-
- type UpdateAddressObjectRenderData struct {
- alps.BaseRenderData
- AddressBook *carddav.AddressBook
- AddressObject *carddav.AddressObject // nil if creating a new contact
- Card vcard.Card
- }
-
- func parseObjectPath(s string) (string, error) {
- p, err := url.PathUnescape(s)
- if err != nil {
- err = fmt.Errorf("failed to parse path: %v", err)
- return "", echo.NewHTTPError(http.StatusBadRequest, err)
- }
- return string(p), nil
- }
-
- func registerRoutes(p *plugin) {
- p.GET("/contacts", func(ctx *alps.Context) error {
- queryText := ctx.QueryParam("query")
-
- c, addressBook, err := p.clientWithAddressBook(ctx.Session)
- if err != nil {
- return err
- }
-
- query := carddav.AddressBookQuery{
- DataRequest: carddav.AddressDataRequest{
- Props: []string{
- vcard.FieldFormattedName,
- vcard.FieldEmail,
- vcard.FieldUID,
- },
- },
- PropFilters: []carddav.PropFilter{{
- Name: vcard.FieldFormattedName,
- }},
- }
-
- if queryText != "" {
- query.PropFilters = []carddav.PropFilter{
- {
- Name: vcard.FieldFormattedName,
- TextMatches: []carddav.TextMatch{{Text: queryText}},
- },
- {
- Name: vcard.FieldEmail,
- TextMatches: []carddav.TextMatch{{Text: queryText}},
- },
- }
- }
-
- aos, err := c.QueryAddressBook(addressBook.Path, &query)
- if err != nil {
- return fmt.Errorf("failed to query CardDAV addresses: %v", err)
- }
-
- return ctx.Render(http.StatusOK, "address-book.html", &AddressBookRenderData{
- BaseRenderData: *alps.NewBaseRenderData(ctx),
- AddressBook: addressBook,
- AddressObjects: newAddressObjectList(aos),
- Query: queryText,
- })
- })
-
- p.GET("/contacts/:path", func(ctx *alps.Context) error {
- path, err := parseObjectPath(ctx.Param("path"))
- if err != nil {
- return err
- }
-
- c, addressBook, err := p.clientWithAddressBook(ctx.Session)
- if err != nil {
- return err
- }
-
- multiGet := carddav.AddressBookMultiGet{
- DataRequest: carddav.AddressDataRequest{
- Props: []string{
- vcard.FieldFormattedName,
- vcard.FieldEmail,
- vcard.FieldUID,
- },
- },
- }
- aos, err := c.MultiGetAddressBook(path, &multiGet)
- if err != nil {
- return fmt.Errorf("failed to query CardDAV address: %v", err)
- }
- if len(aos) != 1 {
- return fmt.Errorf("expected exactly one address object with path %q, got %v", path, len(aos))
- }
- ao := &aos[0]
-
- return ctx.Render(http.StatusOK, "address-object.html", &AddressObjectRenderData{
- BaseRenderData: *alps.NewBaseRenderData(ctx),
- AddressBook: addressBook,
- AddressObject: AddressObject{ao},
- })
- })
-
- updateContact := func(ctx *alps.Context) error {
- addressObjectPath, err := parseObjectPath(ctx.Param("path"))
- if err != nil {
- return err
- }
-
- c, addressBook, err := p.clientWithAddressBook(ctx.Session)
- if err != nil {
- return err
- }
-
- var ao *carddav.AddressObject
- var card vcard.Card
- if addressObjectPath != "" {
- ao, err := c.GetAddressObject(addressObjectPath)
- if err != nil {
- return fmt.Errorf("failed to query CardDAV address: %v", err)
- }
- card = ao.Card
- } else {
- card = make(vcard.Card)
- }
-
- if ctx.Request().Method == "POST" {
- fn := ctx.FormValue("fn")
- emails := strings.Split(ctx.FormValue("emails"), ",")
-
- if _, ok := card[vcard.FieldVersion]; !ok {
- // Some CardDAV servers (e.g. Google) don't support vCard 4.0
- var version = "4.0"
- if !addressBook.SupportsAddressData(vcard.MIMEType, version) {
- version = "3.0"
- }
- if !addressBook.SupportsAddressData(vcard.MIMEType, version) {
- return fmt.Errorf("upstream CardDAV server doesn't support vCard %v", version)
- }
- card.SetValue(vcard.FieldVersion, version)
- }
-
- if field := card.Preferred(vcard.FieldFormattedName); field != nil {
- field.Value = fn
- } else {
- card.Add(vcard.FieldFormattedName, &vcard.Field{Value: fn})
- }
-
- // TODO: Google wants a "N" field, fails with a 400 otherwise
-
- // TODO: params are lost here
- var emailFields []*vcard.Field
- for _, email := range emails {
- emailFields = append(emailFields, &vcard.Field{
- Value: strings.TrimSpace(email),
- })
- }
- card[vcard.FieldEmail] = emailFields
-
- id := uuid.New()
- if _, ok := card[vcard.FieldUID]; !ok {
- card.SetValue(vcard.FieldUID, id.URN())
- }
-
- var p string
- if ao != nil {
- p = ao.Path
- } else {
- p = path.Join(addressBook.Path, id.String()+".vcf")
- }
- ao, err = c.PutAddressObject(p, card)
- if err != nil {
- return fmt.Errorf("failed to put address object: %v", err)
- }
-
- return ctx.Redirect(http.StatusFound, AddressObject{ao}.URL())
- }
-
- return ctx.Render(http.StatusOK, "update-address-object.html", &UpdateAddressObjectRenderData{
- BaseRenderData: *alps.NewBaseRenderData(ctx),
- AddressBook: addressBook,
- AddressObject: ao,
- Card: card,
- })
- }
-
- p.GET("/contacts/create", updateContact)
- p.POST("/contacts/create", updateContact)
-
- p.GET("/contacts/:path/edit", updateContact)
- p.POST("/contacts/:path/edit", updateContact)
-
- p.POST("/contacts/:path/delete", func(ctx *alps.Context) error {
- path, err := parseObjectPath(ctx.Param("path"))
- if err != nil {
- return err
- }
-
- c, err := p.client(ctx.Session)
- if err != nil {
- return err
- }
-
- if err := c.RemoveAll(path); err != nil {
- return fmt.Errorf("failed to delete address object: %v", err)
- }
-
- return ctx.Redirect(http.StatusFound, "/contacts")
- })
- }
|