Skip to content

Commit

Permalink
Allow handlers to describe URNs for when new contacts are created
Browse files Browse the repository at this point in the history
  • Loading branch information
nicpottier committed Jan 8, 2018
1 parent 4b95ea2 commit eecbcef
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 35 deletions.
24 changes: 12 additions & 12 deletions backends/rapidpro/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,13 @@ func (ts *BackendTestSuite) TestContact() {
now := time.Now()

// create our new contact
contact, err := contactForURN(ctx, ts.b.db, knChannel.OrgID(), knChannel, urn, "Ryan Lewis")
contact, err := contactForURN(ctx, ts.b, knChannel.OrgID(), knChannel, urn, "Ryan Lewis")
ts.NoError(err)

now2 := time.Now()

// load this contact again by URN, should be same contact, name unchanged
contact2, err := contactForURN(ctx, ts.b.db, knChannel.OrgID(), knChannel, urn, "Other Name")
contact2, err := contactForURN(ctx, ts.b, knChannel.OrgID(), knChannel, urn, "Other Name")
ts.NoError(err)

ts.Equal(contact.UUID, contact2.UUID)
Expand All @@ -210,7 +210,7 @@ func (ts *BackendTestSuite) TestContact() {
ts.True(contact2.CreatedOn.Before(now2))

// load a contact by URN instead (this one is in our testdata)
contact, err = contactForURN(ctx, ts.b.db, knChannel.OrgID(), knChannel, urns.NewTelURNForCountry("+12067799192", "US"), "")
contact, err = contactForURN(ctx, ts.b, knChannel.OrgID(), knChannel, urns.NewTelURNForCountry("+12067799192", "US"), "")
ts.NoError(err)
ts.NotNil(contact)

Expand All @@ -225,7 +225,7 @@ func (ts *BackendTestSuite) TestContactURN() {

ctx := context.Background()

contact, err := contactForURN(ctx, ts.b.db, knChannel.OrgID_, knChannel, urn, "")
contact, err := contactForURN(ctx, ts.b, knChannel.OrgID_, knChannel, urn, "")
ts.NoError(err)

tx, err := ts.b.db.Beginx()
Expand Down Expand Up @@ -258,11 +258,11 @@ func (ts *BackendTestSuite) TestContactURN() {
tgChannel := ts.getChannel("TG", "dbc126ed-66bc-4e28-b67b-81dc3327c98a")
tgURN := urns.NewTelegramURN(12345, "")

tgContact, err := contactForURN(ctx, ts.b.db, tgChannel.OrgID_, tgChannel, tgURN, "")
tgContact, err := contactForURN(ctx, ts.b, tgChannel.OrgID_, tgChannel, tgURN, "")
ts.NoError(err)

tgURNDisplay := urns.NewTelegramURN(12345, "Jane")
displayContact, err := contactForURN(ctx, ts.b.db, tgChannel.OrgID_, tgChannel, tgURNDisplay, "")
displayContact, err := contactForURN(ctx, ts.b, tgChannel.OrgID_, tgChannel, tgURNDisplay, "")

ts.Equal(tgContact.URNID, displayContact.URNID)
ts.Equal(tgContact.ID, displayContact.ID)
Expand All @@ -283,13 +283,13 @@ func (ts *BackendTestSuite) TestContactURN() {
wait.Add(2)
go func() {
var err2 error
contact2, err2 = contactForURN(ctx, ts.b.db, knChannel.OrgID(), knChannel, urn2, "")
contact2, err2 = contactForURN(ctx, ts.b, knChannel.OrgID(), knChannel, urn2, "")
ts.NoError(err2)
wait.Done()
}()
go func() {
var err3 error
contact3, err3 = contactForURN(ctx, ts.b.db, knChannel.OrgID(), knChannel, urn2, "")
contact3, err3 = contactForURN(ctx, ts.b, knChannel.OrgID(), knChannel, urn2, "")
ts.NoError(err3)
wait.Done()
}()
Expand All @@ -308,7 +308,7 @@ func (ts *BackendTestSuite) TestContactURNPriority() {

ctx := context.Background()

knContact, err := contactForURN(ctx, ts.b.db, knChannel.OrgID_, knChannel, knURN, "")
knContact, err := contactForURN(ctx, ts.b, knChannel.OrgID_, knChannel, knURN, "")
ts.NoError(err)

tx, err := ts.b.db.Beginx()
Expand All @@ -320,7 +320,7 @@ func (ts *BackendTestSuite) TestContactURNPriority() {

// ok, now looking up our contact should reset our URNs and their affinity..
// TwitterURN should be first all all URNs should now use Twitter channel
twContact, err := contactForURN(ctx, ts.b.db, twChannel.OrgID_, twChannel, twURN, "")
twContact, err := contactForURN(ctx, ts.b, twChannel.OrgID_, twChannel, twURN, "")
ts.NoError(err)

ts.Equal(twContact.ID, knContact.ID)
Expand Down Expand Up @@ -678,7 +678,7 @@ func (ts *BackendTestSuite) TestWriteMsg() {
ts.NotNil(m.ModifiedOn_)
ts.NotNil(m.QueuedOn_)

contact, err := contactForURN(ctx, ts.b.db, m.OrgID_, knChannel, urn, "")
contact, err := contactForURN(ctx, ts.b, m.OrgID_, knChannel, urn, "")
ts.Equal("test contact", contact.Name.String)
ts.Equal(m.OrgID_, contact.OrgID)
ts.Equal(m.ContactID_, contact.ID)
Expand Down Expand Up @@ -711,7 +711,7 @@ func (ts *BackendTestSuite) TestChannelEvent() {
err := ts.b.WriteChannelEvent(ctx, event)
ts.NoError(err)

contact, err := contactForURN(ctx, ts.b.db, channel.OrgID_, channel, urn, "")
contact, err := contactForURN(ctx, ts.b, channel.OrgID_, channel, urn, "")
ts.NoError(err)
ts.Equal("kermit frog", contact.Name.String)

Expand Down
2 changes: 1 addition & 1 deletion backends/rapidpro/channel_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ RETURNING id
// writeChannelEventToDB writes the passed in msg status to our db
func writeChannelEventToDB(ctx context.Context, b *backend, e *DBChannelEvent) error {
// grab the contact for this event
contact, err := contactForURN(ctx, b.db, e.OrgID_, e.channel, e.URN_, e.ContactName_)
contact, err := contactForURN(ctx, b, e.OrgID_, e.channel, e.URN_, e.ContactName_)
if err != nil {
return err
}
Expand Down
39 changes: 30 additions & 9 deletions backends/rapidpro/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"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"
Expand Down Expand Up @@ -58,10 +59,10 @@ WHERE u.identity = $1 AND u.contact_id = c.id AND u.org_id = $2 AND c.is_active
`

// 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, db *sqlx.DB, org OrgID, channel *DBChannel, urn urns.URN, name string) (*DBContact, error) {
func contactForURN(ctx context.Context, b *backend, org OrgID, channel *DBChannel, urn urns.URN, name string) (*DBContact, error) {
// try to look up our contact by URN
contact := &DBContact{}
err := db.GetContext(ctx, contact, lookupContactFromURNSQL, urn.Identity(), org)
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
Expand All @@ -70,7 +71,7 @@ func contactForURN(ctx context.Context, db *sqlx.DB, org OrgID, channel *DBChann
// we found it, return it
if err != sql.ErrNoRows {
// insert it
tx, err := db.BeginTxx(ctx, nil)
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
Expand All @@ -92,17 +93,37 @@ func contactForURN(ctx context.Context, db *sqlx.DB, org OrgID, channel *DBChann
contact.ModifiedOn = time.Now()
contact.IsNew = true

// if we have a name and we aren't anonymous, set it
if name != "" && !channel.OrgIsAnon() {
contact.Name = null.StringFrom(name)
// 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 := db.BeginTxx(ctx, nil)
tx, err := b.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
Expand All @@ -122,7 +143,7 @@ func contactForURN(ctx context.Context, db *sqlx.DB, org OrgID, channel *DBChann
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, db, org, channel, urn, name)
return contactForURN(ctx, b, org, channel, urn, name)
}
}
return nil, err
Expand All @@ -131,7 +152,7 @@ func contactForURN(ctx context.Context, db *sqlx.DB, org OrgID, channel *DBChann
// 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, db, org, channel, urn, name)
return contactForURN(ctx, b, org, channel, urn, name)
}

// all is well, we created the new contact, commit and move forward
Expand Down
2 changes: 1 addition & 1 deletion backends/rapidpro/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ RETURNING id

func writeMsgToDB(ctx context.Context, b *backend, m *DBMsg) error {
// grab the contact for this msg
contact, err := contactForURN(ctx, b.db, m.OrgID_, m.channel, m.URN_, m.ContactName_)
contact, err := contactForURN(ctx, b, m.OrgID_, m.channel, m.URN_, m.ContactName_)

// our db is down, write to the spool, we will write/queue this later
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,20 @@ type ChannelHandler interface {
SendMsg(context.Context, Msg) (MsgStatus, error)
}

// URNDescriber is the interface handlers which can look up URN metadata should satisfy
// URNDescriber is the interface handlers which can look up URN metadata for new contacts should satisfy
type URNDescriber interface {
DescribeURN(Channel, urns.URN) map[string]string
DescribeURN(context.Context, Channel, urns.URN) (map[string]string, error)
}

// RegisterHandler adds a new handler for a channel type, this is called by individual handlers when they are initialized
func RegisterHandler(handler ChannelHandler) {
registeredHandlers[handler.ChannelType()] = handler
}

// GetHandler returns the handler for the passed in channel type, or nil if not found
func GetHandler(ct ChannelType) ChannelHandler {
return registeredHandlers[ct]
}

var registeredHandlers = make(map[ChannelType]ChannelHandler)
var activeHandlers = make(map[ChannelType]ChannelHandler)
15 changes: 10 additions & 5 deletions handlers/facebook/facebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat
func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) {
// can't do anything with facebook refs, ignore them
if urn.IsFacebookRef() {
return nil, nil
return map[string]string{}, nil
}

accessToken := channel.StringConfigForKey(courier.ConfigAuthToken, "")
Expand All @@ -364,9 +364,14 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn
}

// build a request to lookup the stats for this contact
u, _ := url.Parse(fmt.Sprintf("%s%s", facebookGraphURL, urn.Path()))
u.Query().Set("fields", "first_name,last_name")
u.Query().Set("access_token", accessToken)
base, _ := url.Parse(facebookGraphURL)
path, _ := url.Parse(fmt.Sprintf("/%s", urn.Path()))
u := base.ResolveReference(path)

query := url.Values{}
query.Set("fields", "first_name,last_name")
query.Set("access_token", accessToken)
u.RawQuery = query.Encode()
req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
rr, err := utils.MakeHTTPRequest(req)
if err != nil {
Expand All @@ -377,5 +382,5 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn
firstName, _ := jsonparser.GetString(rr.Body, "first_name")
lastName, _ := jsonparser.GetString(rr.Body, "last_name")

return map[string]string{"name": utils.JoinNonEmpty(firstName, lastName)}, nil
return map[string]string{"name": utils.JoinNonEmpty(" ", firstName, lastName)}, nil
}
30 changes: 25 additions & 5 deletions handlers/facebook/facebook_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package facebook

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/nyaruka/courier"
. "github.com/nyaruka/courier/handlers"
"github.com/nyaruka/gocommon/urns"
)

var testChannels = []courier.Channel{
Expand Down Expand Up @@ -320,13 +324,14 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server {
defer r.Body.Close()

// invalid auth token
if accessToken != "auth_token" {
if accessToken != "a123" {
http.Error(w, "invalid auth token", 403)
}

// user has a name
if strings.HasSuffix(r.URL.String(), "1337") {
if strings.HasSuffix(r.URL.Path, "1337") {
w.Write([]byte(`{ "first_name": "John", "last_name": "Doe"}`))
return
}

// no name
Expand All @@ -337,10 +342,25 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server {
return server
}

func TestHandler(t *testing.T) {
fbService := buildMockFBGraph(testCases)
defer fbService.Close()
func TestDescribe(t *testing.T) {
fbGraph := buildMockFBGraph(testCases)
defer fbGraph.Close()

handler := NewHandler().(courier.URNDescriber)
tcs := []struct {
urn urns.URN
metadata map[string]string
}{{"facebook:1337", map[string]string{"name": "John Doe"}},
{"facebook:4567", map[string]string{"name": ""}},
{"facebook:ref:1337", map[string]string{}}}

for _, tc := range tcs {
metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn)
assert.Equal(t, metadata, tc.metadata)
}
}

func TestHandler(t *testing.T) {
RunChannelTestCases(t, testChannels, NewHandler(), testCases)
}

Expand Down

0 comments on commit eecbcef

Please sign in to comment.