Skip to content

Commit

Permalink
Merge pull request #32 from nyaruka/schemes-identity
Browse files Browse the repository at this point in the history
Add support for schemes and identity, proper null handling
  • Loading branch information
norkans7 authored Aug 16, 2017
2 parents bc8b042 + 2c95148 commit 4f03630
Show file tree
Hide file tree
Showing 21 changed files with 1,000 additions and 69 deletions.
48 changes: 48 additions & 0 deletions backends/rapidpro/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func (ts *MsgTestSuite) getChannel(cType string, cUUID string) *DBChannel {

channel, err := ts.b.GetChannel(courier.ChannelType(cType), channelUUID)
ts.NoError(err, "error building channel uuid")
ts.NotNil(channel)

return channel.(*DBChannel)
}
Expand Down Expand Up @@ -195,6 +196,53 @@ func (ts *MsgTestSuite) TestContactURN() {

// and channel should be set to twitter
ts.Equal(twURN.ChannelID, twChannel.ID())

// test that we don't use display when looking up URNs
tgChannel := ts.getChannel("TG", "dbc126ed-66bc-4e28-b67b-81dc3327c98a")
tgURN := courier.NewTelegramURN(12345, "")

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

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

ts.Equal(tgContact.URNID, displayContact.URNID)
ts.Equal(tgContact.ID, displayContact.ID)

tgContactURN, err := contactURNForURN(ts.b.db, tgChannel.OrgID_, tgChannel.ID_, tgContact.ID, tgURNDisplay)
ts.Equal(tgContact.URNID, tgContactURN.ID)
ts.Equal("jane", tgContactURN.Display.String)
}

func (ts *MsgTestSuite) TestContactURNPriority() {
knChannel := ts.getChannel("KN", "dbc126ed-66bc-4e28-b67b-81dc3327c95d")
twChannel := ts.getChannel("TW", "dbc126ed-66bc-4e28-b67b-81dc3327c96a")
knURN := courier.NewTelURNForCountry("12065551111", "US")
twURN := courier.NewTelURNForCountry("12065552222", "US")

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

_, err = contactURNForURN(ts.b.db, knChannel.OrgID_, twChannel.ID_, knContact.ID, twURN)
ts.NoError(err)

// 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(ts.b.db, twChannel.OrgID_, twChannel.ID_, twURN, "")
ts.NoError(err)

ts.Equal(twContact.ID, knContact.ID)

// get all the URNs for this contact
urns, err := contactURNsForContact(ts.b.db, twContact.ID)
ts.NoError(err)

ts.Equal("tel:+12065552222", urns[0].Identity)
ts.Equal(twChannel.ID(), urns[0].ChannelID)

ts.Equal("tel:+12065551111", urns[1].Identity)
ts.Equal(twChannel.ID(), urns[1].ChannelID)
}

func (ts *MsgTestSuite) TestStatus() {
Expand Down
66 changes: 62 additions & 4 deletions backends/rapidpro/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package rapidpro

import (
"database/sql"
"database/sql/driver"
"encoding/csv"
"errors"
"regexp"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -45,7 +50,7 @@ func getChannel(b *backend, channelType courier.ChannelType, channelUUID courier
}

const lookupChannelFromUUIDSQL = `
SELECT org_id, id, uuid, channel_type, scheme, address, country, config
SELECT org_id, id, uuid, channel_type, schemes, address, country, config
FROM channels_channel
WHERE uuid = $1 AND is_active = true AND org_id IS NOT NULL`

Expand Down Expand Up @@ -127,7 +132,7 @@ type DBChannel struct {
OrgID_ OrgID `db:"org_id"`
ID_ courier.ChannelID `db:"id"`
ChannelType_ courier.ChannelType `db:"channel_type"`
Scheme_ string `db:"scheme"`
Schemes_ StringSlice `db:"schemes"`
UUID_ courier.ChannelUUID `db:"uuid"`
Address_ sql.NullString `db:"address"`
Country_ sql.NullString `db:"country"`
Expand All @@ -142,8 +147,8 @@ func (c *DBChannel) OrgID() OrgID { return c.OrgID_ }
// ChannelType returns the type of this channel
func (c *DBChannel) ChannelType() courier.ChannelType { return c.ChannelType_ }

// Scheme returns the scheme of the URNs this channel deals with
func (c *DBChannel) Scheme() string { return c.Scheme_ }
// Schemes returns the schemes this channels supports
func (c *DBChannel) Schemes() []string { return []string(c.Schemes_) }

// ID returns the id of this channel
func (c *DBChannel) ID() courier.ChannelID { return c.ID_ }
Expand Down Expand Up @@ -180,3 +185,56 @@ func (c *DBChannel) StringConfigForKey(key string, defaultValue string) string {
}
return str
}

// supportsScheme returns whether the passed in channel supports the passed in scheme
func (c *DBChannel) supportsScheme(scheme string) bool {
for _, s := range c.Schemes_ {
if s == scheme {
return true
}
}
return false
}

// StringSlice is our custom implementation of a string array to support Postgres coding / encoding of schemes
type StringSlice []string

var quoteEscapeRegex = regexp.MustCompile(`([^\\]([\\]{2})*)\\"`)

// Scan convert a SQL value into our string array
// http://www.postgresql.org/docs/9.1/static/arrays.html#ARRAYS-IO
func (s *StringSlice) Scan(src interface{}) error {
asBytes, ok := src.([]byte)
if !ok {
return error(errors.New("Scan source was not []bytes"))
}
str := string(asBytes)

// change quote escapes for csv parser
str = quoteEscapeRegex.ReplaceAllString(str, `$1""`)
str = strings.Replace(str, `\\`, `\`, -1)
// remove braces
str = str[1 : len(str)-1]
csvReader := csv.NewReader(strings.NewReader(str))

slice, err := csvReader.Read()

if err != nil {
return err
}

(*s) = StringSlice(slice)

return nil
}

// Value returns the SQL encoded version of our StringSlice
func (s StringSlice) Value() (driver.Value, error) {
// string escapes.
// \ => \\\
// " => \"
for i, elem := range s {
s[i] = `"` + strings.Replace(strings.Replace(elem, `\`, `\\\`, -1), `"`, `\"`, -1) + `"`
}
return "{" + strings.Join(s, ",") + "}", nil
}
13 changes: 7 additions & 6 deletions backends/rapidpro/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,22 @@ func insertContact(db *sqlx.DB, contact *DBContact) error {
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.urn = $1 AND u.contact_id = c.id AND u.org_id = $2 AND c.is_active = TRUE AND c.is_test = FALSE
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(db *sqlx.DB, org OrgID, channelID courier.ChannelID, urn courier.URN, name string) (*DBContact, error) {
// try to look up our contact by URN
contact := DBContact{}
err := db.Get(&contact, lookupContactFromURNSQL, urn, org)
contact := &DBContact{}
err := db.Get(contact, lookupContactFromURNSQL, urn.Identity(), org)
if err != nil && err != sql.ErrNoRows {
return nil, err
}

// we found it, return it
if err != sql.ErrNoRows {
return &contact, nil
err := setDefaultURN(db, channelID, contact, urn)
return contact, err
}

// didn't find it, we need to create it instead
Expand All @@ -70,7 +71,7 @@ func contactForURN(db *sqlx.DB, org OrgID, channelID courier.ChannelID, urn cour
contact.ModifiedBy = 1

// Insert it
err = insertContact(db, &contact)
err = insertContact(db, contact)
if err != nil {
return nil, err
}
Expand All @@ -85,7 +86,7 @@ func contactForURN(db *sqlx.DB, org OrgID, channelID courier.ChannelID, urn cour
contact.URNID = contactURN.ID

// and return it
return &contact, err
return contact, err
}

// DBContact is our struct for a contact in the database
Expand Down
5 changes: 3 additions & 2 deletions backends/rapidpro/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ CREATE TABLE channels_channel (
modified_on timestamp with time zone NOT NULL,
uuid character varying(36) NOT NULL,
channel_type character varying(3) NOT NULL,
scheme character varying(16) NOT NULL,
name character varying(64),
schemes character varying(16)[] NOT NULL,
address character varying(64),
country character varying(2),
config text,
Expand All @@ -41,9 +41,10 @@ CREATE TABLE contacts_contact (
DROP TABLE IF EXISTS contacts_contacturn CASCADE;
CREATE TABLE contacts_contacturn (
id serial primary key,
urn character varying(255) NOT NULL,
identity character varying(255) NOT NULL,
path character varying(255) NOT NULL,
scheme character varying(128) NOT NULL,
display character varying(128) NULL,
priority integer NOT NULL,
channel_id integer references channels_channel(id) on delete cascade,
contact_id integer references contacts_contact(id) on delete cascade,
Expand Down
17 changes: 10 additions & 7 deletions backends/rapidpro/testdata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ INSERT INTO orgs_org("id", "name", "language")

/* Channel with id 10, 11, 12 */
DELETE FROM channels_channel;
INSERT INTO channels_channel("id", "scheme", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('10', 'tel', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c95d', 'KN', '2500', 1, 'RW', '{ "encoding": "smart", "use_national": true }');
INSERT INTO channels_channel("id", "schemes", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('10', '{"tel"}', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c95d', 'KN', '2500', 1, 'RW', '{ "encoding": "smart", "use_national": true }');

INSERT INTO channels_channel("id", "scheme", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('11', 'tel', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c96a', 'TW', '4500', 1, 'US', NULL);
INSERT INTO channels_channel("id", "schemes", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('11', '{"tel"}', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c96a', 'TW', '4500', 1, 'US', NULL);

INSERT INTO channels_channel("id", "scheme", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('12', 'tel', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c97a', 'DM', '4500', 1, 'US', NULL);
INSERT INTO channels_channel("id", "schemes", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('12', '{"tel"}', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c97a', 'DM', '4500', 1, 'US', NULL);

INSERT INTO channels_channel("id", "schemes", "is_active", "created_on", "modified_on", "uuid", "channel_type", "address", "org_id", "country", "config")
VALUES('13', '{"telegram"}', 'Y', NOW(), NOW(), 'dbc126ed-66bc-4e28-b67b-81dc3327c98a', 'TG', 'courierbot', 1, NULL, NULL);

/* Contact with id 100 */
DELETE FROM contacts_contact;
Expand All @@ -21,7 +24,7 @@ INSERT INTO contacts_contact("id", "is_active", "created_on", "modified_on", "uu

/** ContactURN with id 1000 */
DELETE FROM contacts_contacturn;
INSERT INTO contacts_contacturn("id", "urn", "path", "scheme", "priority", "channel_id", "contact_id", "org_id")
INSERT INTO contacts_contacturn("id", "identity", "path", "scheme", "priority", "channel_id", "contact_id", "org_id")
VALUES(1000, 'tel:+12067799192', '+12067799192', 'tel', 50, 10, 100, 1);

/** Msg with id 10,000 */
Expand Down
Loading

0 comments on commit 4f03630

Please sign in to comment.