github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/contact/contacts.go (about)

     1  // Package contact is for managing the io.cozy.contacts documents and their
     2  // groups.
     3  package contact
     4  
     5  import (
     6  	"encoding/json"
     7  	"strings"
     8  
     9  	"github.com/cozy/cozy-stack/pkg/consts"
    10  	"github.com/cozy/cozy-stack/pkg/couchdb"
    11  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    12  	"github.com/cozy/cozy-stack/pkg/mail"
    13  	"github.com/cozy/cozy-stack/pkg/prefixer"
    14  )
    15  
    16  // Contact is a struct containing all the informations about a contact.
    17  // We are using maps/slices/interfaces instead of structs, as it is a doctype
    18  // that can also be used in front applications and they can add new fields. It
    19  // would be complicated to maintain a up-to-date mapping, and failing to do so
    20  // means that we can lose some data on JSON round-trip.
    21  type Contact struct {
    22  	couchdb.JSONDoc
    23  }
    24  
    25  // New returns a new blank contact.
    26  func New() *Contact {
    27  	return &Contact{
    28  		JSONDoc: couchdb.JSONDoc{
    29  			M: make(map[string]interface{}),
    30  		},
    31  	}
    32  }
    33  
    34  // DocType returns the contact document type
    35  func (c *Contact) DocType() string { return consts.Contacts }
    36  
    37  // ToMailAddress returns a struct that can be used by cozy-stack to send an
    38  // email to this contact
    39  func (c *Contact) ToMailAddress() (*mail.Address, error) {
    40  	emails, ok := c.Get("email").([]interface{})
    41  	if !ok || len(emails) == 0 {
    42  		return nil, ErrNoMailAddress
    43  	}
    44  	var email string
    45  	for i := range emails {
    46  		obj, ok := emails[i].(map[string]interface{})
    47  		if !ok {
    48  			continue
    49  		}
    50  		address, ok := obj["address"].(string)
    51  		if !ok {
    52  			continue
    53  		}
    54  		if primary, ok := obj["primary"].(bool); ok && primary {
    55  			email = address
    56  		}
    57  		if email == "" {
    58  			email = address
    59  		}
    60  	}
    61  	name := c.PrimaryName()
    62  	return &mail.Address{Name: name, Email: email}, nil
    63  }
    64  
    65  // PrimaryName returns the name of the contact
    66  func (c *Contact) PrimaryName() string {
    67  	if fullname, ok := c.Get("fullname").(string); ok && fullname != "" {
    68  		return fullname
    69  	}
    70  	name, ok := c.Get("name").(map[string]interface{})
    71  	if !ok {
    72  		return ""
    73  	}
    74  	var primary string
    75  	if given, ok := name["givenName"].(string); ok && given != "" {
    76  		primary = given
    77  	}
    78  	if family, ok := name["familyName"].(string); ok && family != "" {
    79  		if primary != "" {
    80  			primary += " "
    81  		}
    82  		primary += family
    83  	}
    84  	return primary
    85  }
    86  
    87  // SortingKey returns a string that can be used for sorting the contacts like
    88  // in the contacts app.
    89  func (c *Contact) SortingKey() string {
    90  	indexes, ok := c.Get("indexes").(map[string]interface{})
    91  	if !ok {
    92  		return c.PrimaryName()
    93  	}
    94  	str, ok := indexes["byFamilyNameGivenNameEmailCozyUrl"].(string)
    95  	if !ok {
    96  		return c.PrimaryName()
    97  	}
    98  	return str
    99  }
   100  
   101  // PrimaryPhoneNumber returns the preferred phone number,
   102  // or a blank string if the contact has no known phone number.
   103  func (c *Contact) PrimaryPhoneNumber() string {
   104  	phones, ok := c.Get("phone").([]interface{})
   105  	if !ok || len(phones) == 0 {
   106  		return ""
   107  	}
   108  	var number string
   109  	for i := range phones {
   110  		phone, ok := phones[i].(map[string]interface{})
   111  		if !ok {
   112  			continue
   113  		}
   114  		n, ok := phone["number"].(string)
   115  		if !ok {
   116  			continue
   117  		}
   118  		if primary, ok := phone["primary"].(bool); ok && primary {
   119  			number = n
   120  		}
   121  		if number == "" {
   122  			number = n
   123  		}
   124  	}
   125  	return number
   126  }
   127  
   128  // PrimaryCozyURL returns the URL of the primary cozy,
   129  // or a blank string if the contact has no known cozy.
   130  func (c *Contact) PrimaryCozyURL() string {
   131  	cozys, ok := c.Get("cozy").([]interface{})
   132  	if !ok || len(cozys) == 0 {
   133  		return ""
   134  	}
   135  	var url string
   136  	for i := range cozys {
   137  		cozy, ok := cozys[i].(map[string]interface{})
   138  		if !ok {
   139  			continue
   140  		}
   141  		u, ok := cozy["url"].(string)
   142  		if !ok {
   143  			continue
   144  		}
   145  		if primary, ok := cozy["primary"].(bool); ok && primary {
   146  			url = u
   147  		}
   148  		if url == "" {
   149  			url = u
   150  		}
   151  	}
   152  	if url != "" && !strings.HasPrefix(url, "http") {
   153  		url = "https://" + url
   154  	}
   155  	return url
   156  }
   157  
   158  // GroupIDs returns the list of the group identifiers that this contact belongs to.
   159  func (c *Contact) GroupIDs() []string {
   160  	rels, ok := c.Get("relationships").(map[string]interface{})
   161  	if !ok {
   162  		return nil
   163  	}
   164  
   165  	var groupIDs []string
   166  
   167  	for _, groups := range rels {
   168  		if groups, ok := groups.(map[string]interface{}); ok {
   169  			if data, ok := groups["data"].([]interface{}); ok {
   170  				for _, item := range data {
   171  					if item, ok := item.(map[string]interface{}); ok {
   172  						if item["_type"] == consts.Groups {
   173  							if id, ok := item["_id"].(string); ok {
   174  								groupIDs = append(groupIDs, id)
   175  							}
   176  						}
   177  					}
   178  				}
   179  			}
   180  		}
   181  	}
   182  
   183  	return groupIDs
   184  }
   185  
   186  // AddNameIfMissing can be used to add a name if there was none. We need the
   187  // email address to ignore it if the displayName was updated with it by a
   188  // service of the contacts application.
   189  func (c *Contact) AddNameIfMissing(db prefixer.Prefixer, name, email string) error {
   190  	was, ok := c.Get("displayName").(string)
   191  	if ok && len(was) > 0 && was != email {
   192  		return nil
   193  	}
   194  	was, ok = c.Get("fullname").(string)
   195  	if ok && len(was) > 0 {
   196  		return nil
   197  	}
   198  	c.M["displayName"] = name
   199  	c.M["fullname"] = name
   200  	return couchdb.UpdateDoc(db, c)
   201  }
   202  
   203  // AddCozyURL adds a cozy URL to this contact (unless the contact has already
   204  // this cozy URL) and saves the contact.
   205  func (c *Contact) AddCozyURL(db prefixer.Prefixer, cozyURL string) error {
   206  	cozys, ok := c.Get("cozy").([]interface{})
   207  	if !ok {
   208  		cozys = []interface{}{}
   209  	}
   210  	for i := range cozys {
   211  		cozy, ok := cozys[i].(map[string]interface{})
   212  		if !ok {
   213  			continue
   214  		}
   215  		u, ok := cozy["url"].(string)
   216  		if ok && cozyURL == u {
   217  			return nil
   218  		}
   219  	}
   220  	cozy := map[string]interface{}{"url": cozyURL}
   221  	c.M["cozy"] = append([]interface{}{cozy}, cozys...)
   222  	return couchdb.UpdateDoc(db, c)
   223  }
   224  
   225  // ChangeCozyURL is used when a contact has moved their Cozy to a new URL.
   226  func (c *Contact) ChangeCozyURL(db prefixer.Prefixer, cozyURL string) error {
   227  	cozy := map[string]interface{}{"url": cozyURL}
   228  	c.M["cozy"] = []interface{}{cozy}
   229  	return couchdb.UpdateDoc(db, c)
   230  }
   231  
   232  // Find returns the contact stored in database from a given ID
   233  func Find(db prefixer.Prefixer, contactID string) (*Contact, error) {
   234  	doc := &Contact{}
   235  	err := couchdb.GetDoc(db, consts.Contacts, contactID, doc)
   236  	return doc, err
   237  }
   238  
   239  // FindByEmail returns the contact with the given email address, when possible
   240  func FindByEmail(db prefixer.Prefixer, email string) (*Contact, error) {
   241  	var res couchdb.ViewResponse
   242  	err := couchdb.ExecView(db, couchdb.ContactByEmail, &couchdb.ViewRequest{
   243  		Key:         email,
   244  		IncludeDocs: true,
   245  		Limit:       1,
   246  	}, &res)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	if len(res.Rows) == 0 {
   251  		return nil, ErrNotFound
   252  	}
   253  	doc := &Contact{}
   254  	err = json.Unmarshal(res.Rows[0].Doc, &doc)
   255  	return doc, err
   256  }
   257  
   258  // CreateMyself creates the myself contact document from the instance settings.
   259  func CreateMyself(db prefixer.Prefixer, settings *couchdb.JSONDoc) (*Contact, error) {
   260  	doc := New()
   261  	doc.JSONDoc.M["me"] = true
   262  	if name, ok := settings.M["public_name"]; ok {
   263  		doc.JSONDoc.M["fullname"] = name
   264  	}
   265  	if email, ok := settings.M["email"]; ok {
   266  		doc.JSONDoc.M["email"] = []map[string]interface{}{
   267  			{"address": email, "primary": true},
   268  		}
   269  	}
   270  	if err := couchdb.CreateDoc(db, doc); err != nil {
   271  		return nil, err
   272  	}
   273  	return doc, nil
   274  }
   275  
   276  // GetMyself returns the myself contact document, or an ErrNotFound error.
   277  func GetMyself(db prefixer.Prefixer) (*Contact, error) {
   278  	var docs []*Contact
   279  	req := &couchdb.FindRequest{
   280  		UseIndex: "by-me",
   281  		Selector: mango.Equal("me", true),
   282  		Limit:    1,
   283  	}
   284  	err := couchdb.FindDocs(db, consts.Contacts, req, &docs)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	if len(docs) == 0 {
   289  		return nil, ErrNotFound
   290  	}
   291  	return docs[0], nil
   292  }
   293  
   294  var _ couchdb.Doc = &Contact{}