forked from nyaruka/courier
/
contact.go
190 lines (159 loc) · 5.5 KB
/
contact.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package rapidpro
import (
"context"
"strconv"
"time"
null "gopkg.in/guregu/null.v3"
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/nyaruka/courier"
"github.com/nyaruka/gocommon/urns"
uuid "github.com/satori/go.uuid"
"github.com/sirupsen/logrus"
)
// ContactID is our representation of our database contact id
type ContactID struct {
null.Int
}
// NilContactID represents our nil value for ContactID
var NilContactID = ContactID{null.NewInt(0, false)}
// String returns a string representation of this ContactID
func (c *ContactID) String() string {
if c.Valid {
strconv.FormatInt(c.Int64, 10)
}
return "null"
}
const insertContactSQL = `
INSERT INTO contacts_contact(org_id, is_active, is_blocked, is_test, is_stopped, uuid, created_on, modified_on, created_by_id, modified_by_id, name)
VALUES(:org_id, TRUE, FALSE, FALSE, FALSE, :uuid, :created_on, :modified_on, :created_by_id, :modified_by_id, :name)
RETURNING id
`
// insertContact inserts the passed in contact, the id field will be populated with the result on success
func insertContact(tx *sqlx.Tx, contact *DBContact) error {
rows, err := tx.NamedQuery(insertContactSQL, contact)
if err != nil {
return err
}
defer rows.Close()
if rows.Next() {
err = rows.Scan(&contact.ID_)
}
return err
}
const lookupContactFromURNSQL = `
SELECT c.org_id, c.id, c.uuid, c.modified_on, c.created_on, c.name, u.id as "urn_id"
FROM contacts_contact AS c, contacts_contacturn AS u
WHERE u.identity = $1 AND u.contact_id = c.id AND u.org_id = $2 AND c.is_active = TRUE AND c.is_test = FALSE
`
// contactForURN first tries to look up a contact for the passed in URN, if not finding one then creating one
func contactForURN(ctx context.Context, b *backend, org OrgID, channel *DBChannel, urn urns.URN, auth string, name string) (*DBContact, error) {
// try to look up our contact by URN
contact := &DBContact{}
err := b.db.GetContext(ctx, contact, lookupContactFromURNSQL, urn.Identity(), org)
if err != nil && err != sql.ErrNoRows {
logrus.WithError(err).WithField("urn", urn.Identity()).WithField("org_id", org).Error("error looking up contact")
return nil, err
}
// we found it, return it
if err != sql.ErrNoRows {
// insert it
tx, err := b.db.BeginTxx(ctx, nil)
if err != nil {
logrus.WithError(err).WithField("urn", urn.Identity()).WithField("org_id", org).Error("error looking up contact")
return nil, err
}
err = setDefaultURN(tx, channel.ID(), contact, urn)
if err != nil {
logrus.WithError(err).WithField("urn", urn.Identity()).WithField("org_id", org).Error("error looking up contact")
tx.Rollback()
return nil, err
}
return contact, tx.Commit()
}
// didn't find it, we need to create it instead
contact.OrgID_ = org
contact.UUID_, _ = courier.NewContactUUID(uuid.NewV4().String())
contact.CreatedOn_ = time.Now()
contact.ModifiedOn_ = time.Now()
contact.IsNew_ = true
// if we aren't an anonymous org, we want to look up a name if possible and set it
if !channel.OrgIsAnon() {
// no name was passed in, see if our handler can look up information for this URN
if name == "" {
handler := courier.GetHandler(channel.ChannelType())
if handler != nil {
describer, isDescriber := handler.(courier.URNDescriber)
if isDescriber {
atts, err := describer.DescribeURN(ctx, channel, urn)
// in the case of errors, we log the error but move onwards anyways
if err != nil {
logrus.WithField("channel_uuid", channel.UUID()).WithField("channel_type", channel.ChannelType()).WithField("urn", urn).WithError(err).Error("unable to describe URN")
} else {
name = atts["name"]
}
}
}
}
if name != "" {
contact.Name_ = null.StringFrom(name)
}
}
// TODO: Set these to a system user
contact.CreatedBy_ = 1
contact.ModifiedBy_ = 1
// insert it
tx, err := b.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
err = insertContact(tx, contact)
if err != nil {
tx.Rollback()
return nil, err
}
// associate our URN
// If we've inserted a duplicate URN then we'll get a uniqueness violation.
// That means this contact URN was written by someone else after we tried to look it up.
contactURN, err := contactURNForURN(tx, org, channel.ID(), contact.ID_, urn, auth)
if err != nil {
tx.Rollback()
if pqErr, ok := err.(*pq.Error); ok {
// if this was a duplicate URN, start over with a contact lookup
if pqErr.Code.Name() == "unique_violation" {
return contactForURN(ctx, b, org, channel, urn, auth, name)
}
}
return nil, err
}
// if the returned URN is for a different contact, then we were in a race as well, rollback and start over
if contactURN.ContactID.Int64 != contact.ID_.Int64 {
tx.Rollback()
return contactForURN(ctx, b, org, channel, urn, auth, name)
}
// all is well, we created the new contact, commit and move forward
err = tx.Commit()
if err != nil {
return nil, err
}
// store this URN on our contact
contact.URNID_ = contactURN.ID
// and return it
return contact, nil
}
// DBContact is our struct for a contact in the database
type DBContact struct {
OrgID_ OrgID `db:"org_id"`
ID_ ContactID `db:"id"`
UUID_ courier.ContactUUID `db:"uuid"`
Name_ null.String `db:"name"`
URNID_ ContactURNID `db:"urn_id"`
CreatedOn_ time.Time `db:"created_on"`
ModifiedOn_ time.Time `db:"modified_on"`
CreatedBy_ int `db:"created_by_id"`
ModifiedBy_ int `db:"modified_by_id"`
IsNew_ bool
}
// UUID returns the UUID for this contact
func (c *DBContact) UUID() courier.ContactUUID { return c.UUID_ }