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{}