Need to send a message to many people at once? Want to personalise it so that it doesn't seem so, well, impersonal? Can't send a group message as you don't want your mum seeing you sent the same heartfelt text to every one of your relations and not just her? We're going to look at how to create an SMS client which supports bulk messages and templates for personalisation.
Getting Started
You'll need a bit of experience with Python to run the CLI, and at least Python 3.6 as we're going to be using f-strings.
Install the dependencies into your virtual environment; I recommend using pipenv and installing the package in editing mode:
pipenv install -e .
Create the following environmental variables;
NEXMO_KEY
,NEXMO_SECRET
, andMY_NUMBER
the first two are available from your Nexmo Dashboard.MY_NUMBER
is your mobile number in the international E.164 format.
Managing SMS Contacts
Our application is going to need a basic CRM to hold our contacts. We're going to use tinydb for our persistence layer as we don't need anything too complicated and as a bonus, it stores the data as a JSON file, which makes backups and manual edits of the data straightforward.
As well as the contact's name and telephone number we have a few multiple choice values we collect.
@contact.command("create")
def contact_create():
"""Creates a new contact"""
questions = [
{"type": "input", "name": "name", "message": "Contact name"},
{
"type": "input",
"name": "phonenumber",
"message": "Contact phone number (E.164 international format)",
},
{
"type": "checkbox",
"qmark": "?",
"message": "Select diminutives (select at least 1)",
"name": "diminutive",
"choices": [
Separator("= The Bros ="),
{"name": "Bro"},
{"name": "Buddy"},
{"name": "Dude"},
{"name": "Matey"},
{"name": "Pal"},
Separator("= The Sweethearts ="),
{"name": "Baby"},
{"name": "Bae"},
{"name": "Darling"},
{"name": "Sweetheart"},
{"name": "Sugar"},
Separator("= The Scots/Irish/Aussies ="),
{"name": "████"},
{"name": "Eejit"},
{"name": "█████████"},
{"name": "Numpty"},
],
"validate": lambda answer: "You must choose at least one diminutive."
if len(answer) == 0
else True,
},
{
"type": "checkbox",
"qmark": "?",
"message": "Select greetings (select at least 1)",
"name": "greeting",
"choices": [
{"name": "Alright"},
{"name": "Greetings"},
{"name": "Hello"},
{"name": "Hey"},
{"name": "Hi"},
{"name": "Oi"},
{"name": "Wasssssup"},
{"name": "Yo"},
],
"validate": lambda answer: "You must choose at least one greeting."
if len(answer) == 0
else True,
},
{
"type": "checkbox",
"qmark": "?",
"message": "Select valediction (select at least 1)",
"name": "valediction",
"choices": [
{"name": "Bye"},
{"name": "Cya"},
{"name": "Love you x"},
{"name": "Peace"},
{"name": "xox"},
],
"validate": lambda answer: "You must choose at least one valediction."
if len(answer) == 0
else True,
},
]
answers = prompt(questions, style=questions_style)
contacts_db.insert(answers)
click.secho(
f"New contact {answers['name']} created", fg="black", bg="cyan", bold=True
)
When we render the template for a message, we'll supply a random value from these lists in the context, give the messages some variation.
Managing Our SMS Templates
Our templates are going to be rendered using Jinja2 giving us access to all of Jinja's built-in filters and features. We also supply information about each contact in the context when rendering the template.
I've made liberal use of Jinja's random
filter so that there is some variation in my messages, even if I resend the same person the same message multiple times it should look as if I've taken the time to write it personally each time. For added authenticity, you can randomly add in the odd typo from time-to-time.
{{ greeting }} {{ diminutive }} {{ ["gah", "sorry", "I suck", "soz"]|random }}, I'm {{ ["running", "runnin", "runing"]|random }} about {{ range(5,25)|random }} {{ ["mins", "minutes", "mintes"]|random }} late. {{ valediction }}
The rest of our template management code looks very similar to the contacts, although we do have a crude drill-down/expand so that you can quickly view the template contents.
@template.command("list")
def template_list():
"""View all templates"""
viewing_templates = True
all_templates = templates_db.all()
while viewing_templates:
questions = [
{
"type": "list",
"message": "View template",
"name": "template",
"choices": [
{"name": f"{template['name']}", "value": template}
for template in all_templates
],
}
]
answers = prompt(questions, style=questions_style)
click.echo(answers["template"]["name"])
click.echo("---")
click.echo(answers["template"]["template"])
if not click.confirm("View another template?"):
viewing_templates = False
Sending Multiple SMS with Python
With the Nexmo Python Client sending an SMS is a single function call; most of our SMS sending code is to allow the user to select which numbers are going to receive the message, and which template we should use.
@click.command()
def send():
"""Send SMS"""
questions = [
{
"type": "checkbox",
"qmark": "?",
"message": "Select contacts to message",
"name": "contacts",
"choices": [
{
"name": f"{contact['name']} - {contact['phonenumber']}",
"value": contact,
}
for contact in contacts_db.all()
],
"validate": lambda answer: "You must choose at least one contact."
if len(answer) == 0
else True,
},
{
"type": "list",
"message": "Select template",
"name": "template",
"choices": [
{"name": f"{template['name']}", "value": template["template"]}
for template in templates_db.all()
],
},
]
answers = prompt(questions, style=questions_style)
template = Template(answers["template"])
with click.progressbar(answers["contacts"], label="Sending messages") as contacts:
for contact in contacts:
message = template.render(
name=contact["name"],
phonenumber=contact["phonenumber"],
diminutive=random.choice(contact["diminutive"]),
greeting=random.choice(contact["greeting"]),
valediction=random.choice(contact["valediction"]),
)
nexmo_client.send_message(
{
"from": os.environ["MY_NUMBER"],
"to": contact["phonenumber"],
"text": message,
}
)
I modified the code above to log the results of my Christmas message so you can see the sort of variation it produces across different contacts.
Aaron was a developer advocate at Nexmo. A seasoned software engineer and wannabe digital artist Aaron is frequently found creating things with code, or electronics; sometimes both. You can customarily tell when he's working on something new by the smell of burning components in the air.