Browse Source

New integration: phone calls. Fixes #403

pull/409/head
Pēteris Caune 4 years ago
parent
commit
ee9ac0ffef
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
20 changed files with 292 additions and 16 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +4
    -1
      hc/api/models.py
  3. +38
    -0
      hc/api/tests/test_notify.py
  4. +25
    -0
      hc/api/transports.py
  5. +59
    -0
      hc/front/tests/test_add_call.py
  6. +1
    -0
      hc/front/urls.py
  7. +28
    -0
      hc/front/views.py
  8. +9
    -5
      static/css/icomoon.css
  9. BIN
      static/fonts/icomoon.eot
  10. +1
    -0
      static/fonts/icomoon.svg
  11. BIN
      static/fonts/icomoon.ttf
  12. BIN
      static/fonts/icomoon.woff
  13. BIN
      static/img/integrations/call.png
  14. +1
    -1
      templates/emails/sms-limit-subject.html
  15. +15
    -1
      templates/front/channels.html
  16. +5
    -0
      templates/front/event_summary.html
  17. +13
    -1
      templates/front/welcome.html
  18. +84
    -0
      templates/integrations/add_call.html
  19. +1
    -0
      templates/integrations/call_message.html
  20. +7
    -7
      templates/payments/pricing.html

+ 1
- 0
CHANGELOG.md View File

@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
- Updated Discord integration to use discord.com instead of discordapp.com
- Add "Failure Keyword" filtering for inbound emails (#396)
- Add support for multiple, comma-separated keywords (#396)
- New integration: phone calls (#403)
### Bug Fixes
- Removing Pager Team integration, project appears to be discontinued


+ 4
- 1
hc/api/models.py View File

@ -50,6 +50,7 @@ CHANNEL_KINDS = (
("shell", "Shell Command"),
("zulip", "Zulip"),
("spike", "Spike"),
("call", "Phone Call"),
)
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
@ -460,6 +461,8 @@ class Channel(models.Model):
return transports.Zulip(self)
elif self.kind == "spike":
return transports.Spike(self)
elif self.kind == "call":
return transports.Call(self)
else:
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
@ -640,7 +643,7 @@ class Channel(models.Model):
@property
def sms_number(self):
assert self.kind in ("sms", "whatsapp")
assert self.kind in ("call", "sms", "whatsapp")
if self.value.startswith("{"):
doc = json.loads(self.value)
return doc["value"]


+ 38
- 0
hc/api/tests/test_notify.py View File

@ -754,6 +754,44 @@ class NotifyTestCase(BaseTestCase):
self.assertEqual(email.to[0], "[email protected]")
self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached")
@patch("hc.api.transports.requests.request")
def test_call(self, mock_post):
value = {"label": "foo", "value": "+1234567890"}
self._setup_data("call", json.dumps(value))
self.check.last_ping = now() - td(hours=2)
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertEqual(payload["To"], "+1234567890")
@patch("hc.api.transports.requests.request")
def test_call_limit(self, mock_post):
# At limit already:
self.profile.last_sms_date = now()
self.profile.sms_sent = 50
self.profile.save()
definition = {"value": "+1234567890"}
self._setup_data("call", json.dumps(definition))
self.channel.notify(self.check)
self.assertFalse(mock_post.called)
n = Notification.objects.get()
self.assertTrue("Monthly phone call limit exceeded" in n.error)
# And email should have been sent
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertEqual(email.to[0], "[email protected]")
self.assertEqual(email.subject, "Monthly Phone Call Limit Reached")
@patch("apprise.Apprise")
@override_settings(APPRISE_ENABLED=True)
def test_apprise_enabled(self, mock_apprise):


+ 25
- 0
hc/api/transports.py View File

@ -478,6 +478,31 @@ class Sms(HttpTransport):
return self.post(url, data=data, auth=auth)
class Call(HttpTransport):
URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Calls.json"
def is_noop(self, check):
return check.status != "down"
def notify(self, check):
profile = Profile.objects.for_user(self.channel.project.owner)
if not profile.authorize_sms():
profile.send_sms_limit_notice("phone call")
return "Monthly phone call limit exceeded"
url = self.URL % settings.TWILIO_ACCOUNT
auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH)
twiml = tmpl("call_message.html", check=check, site_name=settings.SITE_NAME)
data = {
"From": settings.TWILIO_FROM,
"To": self.channel.sms_number,
"Twiml": twiml,
}
return self.post(url, data=data, auth=auth)
class WhatsApp(HttpTransport):
URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"


+ 59
- 0
hc/front/tests/test_add_call.py View File

@ -0,0 +1,59 @@
from django.test.utils import override_settings
from hc.api.models import Channel
from hc.test import BaseTestCase
@override_settings(TWILIO_ACCOUNT="foo", TWILIO_AUTH="foo", TWILIO_FROM="123")
class AddCallTestCase(BaseTestCase):
def setUp(self):
super(AddCallTestCase, self).setUp()
self.url = "/projects/%s/add_call/" % self.project.code
def test_instructions_work(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Get a phone call")
@override_settings(USE_PAYMENTS=True)
def test_it_warns_about_limits(self):
self.profile.sms_limit = 0
self.profile.save()
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "upgrade to a")
def test_it_creates_channel(self):
form = {"label": "My Phone", "value": "+1234567890"}
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, self.channels_url)
c = Channel.objects.get()
self.assertEqual(c.kind, "call")
self.assertEqual(c.sms_number, "+1234567890")
self.assertEqual(c.name, "My Phone")
self.assertEqual(c.project, self.project)
def test_it_rejects_bad_number(self):
form = {"value": "not a phone number"}
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertContains(r, "Invalid phone number format.")
def test_it_trims_whitespace(self):
form = {"value": " +1234567890 "}
self.client.login(username="[email protected]", password="password")
self.client.post(self.url, form)
c = Channel.objects.get()
self.assertEqual(c.sms_number, "+1234567890")
@override_settings(TWILIO_AUTH=None)
def test_it_requires_credentials(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)

+ 1
- 0
hc/front/urls.py View File

@ -55,6 +55,7 @@ channel_urls = [
project_urls = [
path("add_apprise/", views.add_apprise, name="hc-add-apprise"),
path("add_call/", views.add_call, name="hc-add-call"),
path("add_discord/", views.add_discord, name="hc-add-discord"),
path("add_email/", views.add_email, name="hc-add-email"),
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),


+ 28
- 0
hc/front/views.py View File

@ -269,6 +269,7 @@ def index(request):
"enable_shell": settings.SHELL_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_call": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
@ -702,6 +703,7 @@ def channels(request, code):
"enable_shell": settings.SHELL_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_call": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
@ -1515,6 +1517,32 @@ def add_sms(request, code):
return render(request, "integrations/add_sms.html", ctx)
@require_setting("TWILIO_AUTH")
@login_required
def add_call(request, code):
project = _get_project_for_user(request, code)
if request.method == "POST":
form = forms.AddSmsForm(request.POST)
if form.is_valid():
channel = Channel(project=project, kind="call")
channel.name = form.cleaned_data["label"]
channel.value = json.dumps({"value": form.cleaned_data["value"]})
channel.save()
channel.assign_all_checks()
return redirect("hc-p-channels", project.code)
else:
form = forms.AddSmsForm()
ctx = {
"page": "channels",
"project": project,
"form": form,
"profile": project.owner_profile,
}
return render(request, "integrations/add_call.html", ctx)
@require_setting("TWILIO_USE_WHATSAPP")
@login_required
def add_whatsapp(request, code):


+ 9
- 5
static/css/icomoon.css View File

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('../fonts/icomoon.eot?37tb6f');
src: url('../fonts/icomoon.eot?37tb6f#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?37tb6f') format('truetype'),
url('../fonts/icomoon.woff?37tb6f') format('woff'),
url('../fonts/icomoon.svg?37tb6f#icomoon') format('svg');
src: url('../fonts/icomoon.eot?e4bee3');
src: url('../fonts/icomoon.eot?e4bee3#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?e4bee3') format('truetype'),
url('../fonts/icomoon.woff?e4bee3') format('woff'),
url('../fonts/icomoon.svg?e4bee3#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-call:before {
content: "\e91a";
color: #e81a34;
}
.icon-spike:before {
content: "\e919";
color: #007bff;


BIN
static/fonts/icomoon.eot View File


+ 1
- 0
static/fonts/icomoon.svg View File

@ -44,4 +44,5 @@
<glyph unicode="&#xe917;" glyph-name="shell" d="M109.229 960c-60.512 0-109.229-48.717-109.229-109.229v-805.546c0-60.512 48.717-109.223 109.229-109.223h805.546c60.512 0 109.223 48.712 109.223 109.223v805.546c0 60.512-48.712 109.229-109.223 109.229h-805.546zM293.258 784.868h56.588v-76.746c21.22-2.829 40.317-8.607 57.293-17.33s31.36-20.161 43.149-34.308c12.025-13.911 21.217-30.412 27.583-49.51 6.602-18.862 9.905-40.082 9.905-63.66h-98.32c0 28.529-6.485 50.1-19.452 64.718-12.968 14.854-30.533 22.28-52.696 22.28-12.025 0-22.515-1.649-31.474-4.95-8.724-3.065-15.916-7.544-21.575-13.439-5.659-5.659-9.904-12.377-12.733-20.158-2.594-7.781-3.892-16.271-3.892-25.466s1.298-17.446 3.892-24.755c2.829-7.073 7.425-13.675 13.791-19.805 6.602-6.13 15.209-12.024 25.819-17.683 10.61-5.423 23.813-10.966 39.61-16.625 23.813-8.96 45.39-18.269 64.724-27.936 19.334-9.431 35.835-20.517 49.51-33.249 13.911-12.496 24.524-27.115 31.833-43.855 7.545-16.504 11.316-36.070 11.316-58.704 0-20.748-3.421-39.495-10.258-56.235-6.838-16.504-16.62-30.766-29.352-42.791s-28.058-21.696-45.977-29.005c-17.919-7.073-37.964-11.669-60.127-13.791v-69.315h-56.229v69.315c-20.041 1.886-39.495 6.248-58.357 13.086-18.862 7.073-35.603 17.213-50.221 30.416-14.382 13.204-25.931 29.827-34.655 49.868-8.724 20.277-13.086 44.561-13.086 72.854h98.673c0-16.976 2.474-31.243 7.425-42.796 4.951-11.317 11.319-20.393 19.1-27.23 8.016-6.602 17.092-11.315 27.23-14.144s20.512-4.244 31.122-4.244c25.228 0 44.326 5.894 57.293 17.683s19.452 26.992 19.452 45.619c0 9.903-1.532 18.627-4.597 26.172-2.829 7.781-7.542 14.856-14.144 21.222-6.366 6.366-14.856 12.143-25.466 17.33-10.374 5.423-22.987 10.726-37.841 15.914-23.813 8.488-45.507 17.446-65.076 26.877s-36.31 20.395-50.221 32.891c-13.675 12.496-24.282 26.998-31.827 43.502-7.545 16.74-11.316 36.545-11.316 59.415 0 20.041 3.415 38.314 10.253 54.818 6.838 16.74 16.509 31.241 29.005 43.502s27.583 22.16 45.266 29.705c17.683 7.545 37.371 12.38 59.063 14.502v76.040zM571.594 187.88h322.544v-76.746h-322.544v76.746z" />
<glyph unicode="&#xe918;" glyph-name="zulip" d="M512.002 960c-0.001 0-0.001 0-0.001 0-282.77 0-512.001-229.23-512.001-512.001 0-0.001 0-0.001 0-0.002v0.001c0.002-282.769 229.231-511.998 512.001-511.998 0.001 0 0.001 0 0.002 0h-0.001c0.001 0 0.001 0 0.001 0 282.769 0 512 229.229 512.001 511.999v0c0 0.001 0 0.001 0 0.001 0 282.77-229.23 512.001-512.001 512.001-0.001 0-0.001 0-0.002 0h0.001zM284.075 714.286h394.776l58.098-119.923-268.809-280.809h209.963l63.314-131.839h-397.006l-61.825 131.839 276.205 279.32h-219.596z" />
<glyph unicode="&#xe919;" glyph-name="spike" d="M109.229 960c-60.512 0-109.229-48.717-109.229-109.229v-805.546c0-60.512 48.717-109.223 109.229-109.223h805.546c60.512 0 109.223 48.712 109.223 109.223v805.546c0 60.512-48.712 109.229-109.223 109.229h-805.546zM519.994 785.921c26.731 0 48.899-9.736 66.504-29.225 18.255-19.486 27.384-43.496 27.384-72.027s-9.132-52.538-27.384-72.022c-17.605-19.486-39.773-29.231-66.504-29.231-27.386 0-50.206 9.74-68.461 29.231-17.605 19.484-26.403 43.49-26.403 72.022s8.804 52.541 26.403 72.027c18.255 19.484 41.075 29.225 68.461 29.225zM520.32 507.623c6.096-0.139 12.331-0.933 18.708-2.381l0.011 0.006c28.061-6.363 48.535-22.392 61.416-48.083 13.515-25.828 16.278-58.825 8.279-98.987-15.724-78.957-55.879-141.702-120.468-188.229-11.595-8.035-23.78-10.623-36.535-7.728s-22.963 10.188-30.631 21.894c-7.802 11.022-10.558 22.314-8.252 33.889 2.167 10.883 8.281 19.813 18.328 26.784 27.336 20.121 48.064 43.881 62.183 71.267-23.827 7.539-41.767 21.209-53.821 41.021-11.92 20.489-15.293 43.669-10.153 69.536 5.558 27.906 18.846 49.43 39.858 64.586 15.763 11.367 32.786 16.844 51.076 16.426z" />
<glyph unicode="&#xe91a;" glyph-name="call" d="M511.999 960c-282.768-0.001-511.998-229.229-511.999-511.998v-0.001c0.001-282.768 229.229-511.998 511.998-511.999h0.001c282.768 0.001 511.998 229.229 511.999 511.998v0.001c-0.001 282.768-229.229 511.998-511.998 511.999h-0.001zM523.135 718.551c0.212 0.008 0.461 0.012 0.71 0.012 1.228 0 2.431-0.105 3.601-0.307l-0.125 0.018c63.598-9.762 111.38-29.198 148.064-60.881s61.269-74.421 82.321-127.903c0.921-2.278 1.455-4.919 1.455-7.686 0-11.609-9.411-21.019-21.019-21.019-8.842 0-16.409 5.46-19.514 13.192l-0.050 0.141c-19.763 50.209-41.118 85.954-70.69 111.495s-68.379 42.13-126.95 51.12c-10.294 1.472-18.121 10.229-18.121 20.815 0 11.361 9.015 20.616 20.281 21.002l0.035 0.001zM335.388 700.417c28.506 0 56.475-2.279 61.81-8.072 8.866-33.089 17.681-87.508 30.79-150.924-1.22-23.476-66.153-77.434-66.153-77.434 17.073-28.659 66.462-80.495 88.414-97.873s83.536-60.363 120.121-67.070c0 0 44.507 80.799 54.873 83.238 14.634 0 129.576 4.573 143.6-11.281s34.151-114.946 28.969-134.153c-5.183-19.207-24.828-41.032-43.121-51.398-19.512-9.147-126.82-15.236-202.43 9.459-37.805 12.347-128.683 52.31-196.289 119.12s-111.52 160.031-117.925 196.301c-12.811 72.538-31.148 153.47 32.323 184.453 7.47 3.354 36.513 5.634 65.019 5.634zM516.113 634.010c0.212 0.008 0.461 0.012 0.71 0.012 1.228 0 2.431-0.105 3.601-0.307l-0.125 0.018c42.287-6.491 74.931-19.61 100.057-41.311s41.701-50.751 55.633-86.146c1.134-2.501 1.795-5.423 1.795-8.499 0-11.606-9.409-21.015-21.015-21.015-9.145 0-16.925 5.841-19.814 13.996l-0.045 0.147c-12.644 32.122-26 54.166-44.014 69.725s-41.719 25.843-78.979 31.563c-10.294 1.472-18.121 10.229-18.121 20.815 0 11.361 9.015 20.616 20.281 21.002l0.035 0.001z" />
</font></defs></svg>

BIN
static/fonts/icomoon.ttf View File


BIN
static/fonts/icomoon.woff View File


BIN
static/img/integrations/call.png View File

Before After
Width: 128  |  Height: 128  |  Size: 3.4 KiB

+ 1
- 1
templates/emails/sms-limit-subject.html View File

@ -1 +1 @@
Monthly {{ transport }} Limit Reached
Monthly {% if transport == "phone call" %}Phone Call{% else %}{{ transport }}{% endif %} Limit Reached

+ 15
- 1
templates/front/channels.html View File

@ -68,6 +68,8 @@
{% endif %}
{% elif ch.kind == "sms" %}
SMS to <span>{{ ch.sms_number }}</span>
{% elif ch.kind == "call" %}
Phone call to <span>{{ ch.sms_number }}</span>
{% elif ch.kind == "trello" %}
Trello
board <span>{{ ch.trello_board_list|first }}</span>,
@ -122,7 +124,7 @@
{% else %}
Never
{% endif %}
{% if ch.kind == "sms" or ch.kind == "whatsapp" %}
{% if ch.kind == "sms" or ch.kind == "whatsapp" or ch.kind == "call" %}
<p>Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.</p>
{% endif %}
</td>
@ -191,6 +193,17 @@
<a href="{% url 'hc-add-email' project.code %}" class="btn btn-primary">Add Integration</a>
</li>
{% if enable_call %}
<li>
<img src="{% static 'img/integrations/call.png' %}"
class="icon" alt="Phone icon" />
<h2>Phone Call</h2>
<p>Get a phone call when a check goes down.</p>
<a href="{% url 'hc-add-call' project.code %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
<li>
<img src="{% static 'img/integrations/webhook.png' %}"
class="icon" alt="Webhook icon" />
@ -199,6 +212,7 @@
<p>Receive a HTTP callback when a check goes down.</p>
<a href="{% url 'hc-add-webhook' project.code %}" class="btn btn-primary">Add Integration</a>
</li>
{% if enable_apprise %}
<li>
<img src="{% static 'img/integrations/apprise.png' %}"


+ 5
- 0
templates/front/event_summary.html View File

@ -35,6 +35,11 @@
{% if event.channel.name %}
({{ event.channel.name }})
{% endif %}
{% elif event.channel.kind == "call" %}
Made a phone call to {{ event.channel.sms_number }}
{% if event.channel.name %}
({{ event.channel.name }})
{% endif %}
{% else %}
Sent alert to {{ event.channel.get_kind_display }}
{% if event.channel.name %}


+ 13
- 1
templates/front/welcome.html View File

@ -507,6 +507,18 @@
</div>
</div>
{% if enable_call %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">
<div class="integration">
<img src="{% static 'img/integrations/call.png' %}" class="icon" alt="" />
<h3>
{% trans "Phone Call" %}<br>
<small>&nbsp;</small>
</h3>
</div>
</div>
{% endif %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">
<a href="{% url 'hc-serve-doc' 'configuring_prometheus' %}" class="integration">
<img src="{% static 'img/integrations/prometheus.png' %}" class="icon" alt="" />
@ -565,7 +577,7 @@
</div>
{% endif %}
<div class="col-md-2 col-sm-4 col-xs-6">
<div class="col-md-2 col-sm-4 col-xs-6">
<div class="integration">
<img src="{% static 'img/integrations/spike.png' %}" class="icon" alt="Spike.sh icon" />
<h3>


+ 84
- 0
templates/integrations/add_call.html View File

@ -0,0 +1,84 @@
{% extends "base.html" %}
{% load humanize static hc_extras %}
{% block title %}Add Phone Call Integration - {{ site_name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Phone Call</h1>
<p>
Get a phone call when a check goes down. When you pick up the call,
a text-to-speech engine will read out a message and then hang up.
</p>
{% if show_pricing and profile.sms_limit == 0 %}
<p class="alert alert-info">
<strong>Paid plan required.</strong>
Phone call notifications are not available on the free plan–they
cost too much! Please upgrade to a
<a href="{% url 'hc-billing' %}">paid plan</a>
to enable phone call notifications.
</p>
{% endif %}
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group {{ form.label.css_classes }}">
<label for="id_label" class="col-sm-2 control-label">Label</label>
<div class="col-sm-6">
<input
id="id_label"
type="text"
class="form-control"
name="label"
placeholder="Alice's Phone"
value="{{ form.label.value|default:"" }}">
{% if form.label.errors %}
<div class="help-block">
{{ form.label.errors|join:"" }}
</div>
{% else %}
<span class="help-block">
Optional. If you add multiple phone numbers,
the labels will help you tell them apart.
</span>
{% endif %}
</div>
</div>
<div class="form-group {{ form.value.css_classes }}">
<label for="id_number" class="col-sm-2 control-label">Phone Number</label>
<div class="col-sm-3">
<input
id="id_number"
type="tel"
class="form-control"
name="value"
placeholder="+1234567890"
value="{{ form.value.value|default:"" }}">
{% if form.value.errors %}
<div class="help-block">
{{ form.value.errors|join:"" }}
</div>
{% else %}
<span class="help-block">
Make sure the phone number starts with "+" and has the
country code.
</span>
{% endif %}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">Save Integration</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

+ 1
- 0
templates/integrations/call_message.html View File

@ -0,0 +1 @@
<Response><Say>Hello! A message from {{ site_name }}: The check "{{ check.name_then_code }}" is down.</Say></Response>

+ 7
- 7
templates/payments/pricing.html View File

@ -87,7 +87,7 @@
</li>
<li class="list-group-item">API access</li>
<li class="list-group-item">
<span data-help="sms-help">5 SMS &amp; WhatsApp credits</span>
<span data-help="sms-help">5 SMS, WhatsApp and call credits</span>
</li>
<li class="list-group-item">&nbsp;</li>
</ul>
@ -120,7 +120,7 @@
</li>
<li class="list-group-item">API access</li>
<li class="list-group-item">
<span data-help="sms-help">5 SMS &amp; WhatsApp credits</span>
<span data-help="sms-help">5 SMS, WhatsApp and call credits</span>
</li>
<li class="list-group-item">Email support</li>
</ul>
@ -156,7 +156,7 @@
</li>
<li class="list-group-item">API access</li>
<li class="list-group-item">
<span data-help="sms-help">50 SMS &amp; WhatsApp credits</span>
<span data-help="sms-help">50 SMS, WhatsApp and call credits</span>
</li>
<li class="list-group-item">Email support</li>
</ul>
@ -192,7 +192,7 @@
</li>
<li class="list-group-item">API access</li>
<li class="list-group-item">
<span data-help="sms-help">500 SMS &amp; WhatsApp credits</span>
<span data-help="sms-help">500 SMS, WhatsApp and call credits</span>
</li>
<li class="list-group-item">Priority email support</li>
</ul>
@ -303,10 +303,10 @@
</div>
<div id="sms-help" class="hidden">
<p>The maximum number of SMS &amp; WhatsApp notifications per month.</p>
<p>The maximum number of SMS, WhatsApp and phone call notifications per month.</p>
<p>The limit is applied to the combined number of sent SMS and
WhatsApp notifications. </p>
<p>The limit is applied to the combined number of sent SMS, WhatsApp and phone
call notifications. </p>
</div>
{% if not request.user.is_authenticated %}


Loading…
Cancel
Save