Skip to content

Commit

Permalink
Add support for schemes and identity, proper null handling
Browse files Browse the repository at this point in the history
  • Loading branch information
nicpottier committed Aug 15, 2017
1 parent 21c6026 commit 2d1000e
Show file tree
Hide file tree
Showing 16 changed files with 805 additions and 31 deletions.
56 changes: 52 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,46 @@ func (c *DBChannel) StringConfigForKey(key string, defaultValue string) string {
}
return str
}

// 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
}
2 changes: 1 addition & 1 deletion backends/rapidpro/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ 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
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
14 changes: 7 additions & 7 deletions backends/rapidpro/testdata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ 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);

/* Contact with id 100 */
DELETE FROM contacts_contact;
Expand All @@ -21,7 +21,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
26 changes: 15 additions & 11 deletions backends/rapidpro/urn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rapidpro

import (
"database/sql"
"strings"

null "gopkg.in/guregu/null.v3"

Expand All @@ -20,17 +19,21 @@ var NilContactURNID = ContactURNID{null.NewInt(0, false)}

// NewDBContactURN returns a new ContactURN object for the passed in org, contact and string urn, this is not saved to the DB yet
func newDBContactURN(org OrgID, channelID courier.ChannelID, contactID ContactID, urn courier.URN) *DBContactURN {
offset := strings.Index(string(urn), ":")
scheme := string(urn)[:offset]
path := string(urn)[offset+1:]

return &DBContactURN{OrgID: org, ChannelID: channelID, ContactID: contactID, URN: urn, Scheme: scheme, Path: path}
return &DBContactURN{
OrgID: org,
ChannelID: channelID,
ContactID: contactID,
Identity: urn.Identity(),
Scheme: urn.Scheme(),
Path: urn.Path(),
Display: urn.Display(),
}
}

const selectOrgURN = `
SELECT org_id, id, urn, scheme, path, priority, channel_id, contact_id
SELECT org_id, id, identity, scheme, path, display, priority, channel_id, contact_id
FROM contacts_contacturn
WHERE org_id = $1 AND urn = $2
WHERE org_id = $1 AND identity = $2
ORDER BY priority desc LIMIT 1
`

Expand Down Expand Up @@ -62,8 +65,8 @@ func contactURNForURN(db *sqlx.DB, org OrgID, channelID courier.ChannelID, conta
}

const insertURN = `
INSERT INTO contacts_contacturn(org_id, urn, path, scheme, priority, channel_id, contact_id)
VALUES(:org_id, :urn, :path, :scheme, :priority, :channel_id, :contact_id)
INSERT INTO contacts_contacturn(org_id, identity, path, scheme, display, priority, channel_id, contact_id)
VALUES(:org_id, :identity, :path, :scheme, :display, :priority, :channel_id, :contact_id)
RETURNING id
`

Expand Down Expand Up @@ -102,9 +105,10 @@ func updateContactURN(db *sqlx.DB, urn *DBContactURN) error {
type DBContactURN struct {
OrgID OrgID `db:"org_id"`
ID ContactURNID `db:"id"`
URN courier.URN `db:"urn"`
Identity string `db:"identity"`
Scheme string `db:"scheme"`
Path string `db:"path"`
Display null.String `db:"display"`
Priority int `db:"priority"`
ChannelID courier.ChannelID `db:"channel_id"`
ContactID ContactID `db:"contact_id"`
Expand Down
2 changes: 1 addition & 1 deletion channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ var ErrChannelWrongType = errors.New("channel type wrong")
type Channel interface {
UUID() ChannelUUID
ChannelType() ChannelType
Scheme() string
Schemes() []string
Country() string
Address() string
ConfigForKey(key string, defaultValue interface{}) interface{}
Expand Down
2 changes: 1 addition & 1 deletion handlers/external/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (h *handler) ReceiveMessage(channel courier.Channel, w http.ResponseWriter,
}

// create our URN
urn, err := courier.NewURNFromParts(channel.Scheme(), sender)
urn, err := courier.NewURNFromParts(channel.Schemes()[0], sender)
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func init() {
type MockChannel struct {
uuid ChannelUUID
channelType ChannelType
scheme string
schemes []string
address string
country string
config map[string]interface{}
Expand All @@ -217,8 +217,8 @@ func (c *MockChannel) UUID() ChannelUUID { return c.uuid }
// ChannelType returns the type of this channel
func (c *MockChannel) ChannelType() ChannelType { return c.channelType }

// Scheme returns the scheme of this channel
func (c *MockChannel) Scheme() string { return c.scheme }
// Schemes returns the schemes for this channel
func (c *MockChannel) Schemes() []string { return c.schemes }

// Address returns the address of this channel
func (c *MockChannel) Address() string { return c.address }
Expand Down Expand Up @@ -257,7 +257,7 @@ func NewMockChannel(uuid string, channelType string, address string, country str
channel := &MockChannel{
uuid: cUUID,
channelType: ChannelType(channelType),
scheme: TelScheme,
schemes: []string{TelScheme},
address: address,
country: country,
config: config,
Expand Down
27 changes: 27 additions & 0 deletions urn.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"regexp"
"strings"

null "gopkg.in/guregu/null.v3"

"github.com/nyaruka/phonenumbers"
)

Expand All @@ -29,6 +31,10 @@ type URN string
func (u URN) Path() string {
parts := strings.SplitN(string(u), ":", 2)
if len(parts) == 2 {
pathParts := strings.SplitN(parts[1], "#", 2)
if len(pathParts) == 2 {
return pathParts[0]
}
return parts[1]
}
return string(u)
Expand All @@ -43,6 +49,27 @@ func (u URN) Scheme() string {
return ""
}

// Display returns the display portion for the URN (if any)
func (u URN) Display() null.String {
parts := strings.SplitN(string(u), ":", 2)
if len(parts) == 2 {
pathParts := strings.SplitN(parts[1], "#", 2)
if len(pathParts) == 2 {
return null.NewString(pathParts[1], true)
}
}
return null.NewString("", false)
}

// Identity returns the URN with any display attributes stripped
func (u URN) Identity() string {
parts := strings.SplitN(string(u), "#", 2)
if len(parts) == 2 {
return parts[0]
}
return string(u)
}

// String returns a string representation of our URN
func (u URN) String() string {
return string(u)
Expand Down
10 changes: 10 additions & 0 deletions vendor/gopkg.in/guregu/null.v3/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions vendor/gopkg.in/guregu/null.v3/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2d1000e

Please sign in to comment.