forked from GithubBackups/healthchecks
Add WhatsApp integration (uses Twilio same as the SMS integration)
This commit is contained in:
parent
5f0b02845e
commit
40f4adf78b
@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Webhooks support HTTP PUT (#249)
|
||||
- Webhooks can use different req. bodies and headers for "up" and "down" events. (#249)
|
||||
- Show check's code instead of full URL on 992px - 1200px wide screens. (#253)
|
||||
- Add WhatsApp integration (uses Twilio same as the SMS integration)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix badges for tags containing special characters (#240, #237)
|
||||
|
@ -127,6 +127,7 @@ Configurations settings loaded from environment variables:
|
||||
| TWILIO_ACCOUNT | `None`
|
||||
| TWILIO_AUTH | `None`
|
||||
| TWILIO_FROM | `None`
|
||||
| TWILIO_USE_WHATSAPP | `"False"`
|
||||
| PD_VENDOR_KEY | `None`
|
||||
| TRELLO_APP_KEY | `None`
|
||||
| MATRIX_HOMESERVER | `None`
|
||||
|
@ -39,6 +39,7 @@ CHANNEL_KINDS = (
|
||||
("zendesk", "Zendesk"),
|
||||
("trello", "Trello"),
|
||||
("matrix", "Matrix"),
|
||||
("whatsapp", "WhatsApp"),
|
||||
)
|
||||
|
||||
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
|
||||
@ -328,6 +329,8 @@ class Channel(models.Model):
|
||||
return transports.Trello(self)
|
||||
elif self.kind == "matrix":
|
||||
return transports.Matrix(self)
|
||||
elif self.kind == "whatsapp":
|
||||
return transports.WhatsApp(self)
|
||||
else:
|
||||
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
|
||||
|
||||
@ -495,7 +498,7 @@ class Channel(models.Model):
|
||||
|
||||
@property
|
||||
def sms_number(self):
|
||||
assert self.kind == "sms"
|
||||
assert self.kind in ("sms", "whatsapp")
|
||||
if self.value.startswith("{"):
|
||||
doc = json.loads(self.value)
|
||||
return doc["value"]
|
||||
@ -556,6 +559,18 @@ class Channel(models.Model):
|
||||
doc = json.loads(self.value)
|
||||
return doc.get("down")
|
||||
|
||||
@property
|
||||
def whatsapp_notify_up(self):
|
||||
assert self.kind == "whatsapp"
|
||||
doc = json.loads(self.value)
|
||||
return doc["up"]
|
||||
|
||||
@property
|
||||
def whatsapp_notify_down(self):
|
||||
assert self.kind == "whatsapp"
|
||||
doc = json.loads(self.value)
|
||||
return doc["down"]
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
class Meta:
|
||||
|
@ -522,7 +522,7 @@ class NotifyTestCase(BaseTestCase):
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
self.channel.notify(self.check)
|
||||
assert Notification.objects.count() == 1
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["data"]
|
||||
@ -575,3 +575,51 @@ class NotifyTestCase(BaseTestCase):
|
||||
|
||||
self.channel.notify(self.check)
|
||||
self.assertTrue(mock_post.called)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_whatsapp(self, mock_post):
|
||||
definition = {"value": "+1234567890", "up": True, "down": True}
|
||||
|
||||
self._setup_data("whatsapp", json.dumps(definition))
|
||||
self.check.last_ping = now() - td(hours=2)
|
||||
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
self.channel.notify(self.check)
|
||||
self.assertEqual(Notification.objects.count(), 1)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["data"]
|
||||
self.assertEqual(payload["To"], "whatsapp:+1234567890")
|
||||
|
||||
# sent SMS counter should go up
|
||||
self.profile.refresh_from_db()
|
||||
self.assertEqual(self.profile.sms_sent, 1)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_whatsapp_obeys_up_down_flags(self, mock_post):
|
||||
definition = {"value": "+1234567890", "up": True, "down": False}
|
||||
|
||||
self._setup_data("whatsapp", json.dumps(definition))
|
||||
self.check.last_ping = now() - td(hours=2)
|
||||
|
||||
self.channel.notify(self.check)
|
||||
self.assertEqual(Notification.objects.count(), 0)
|
||||
|
||||
self.assertFalse(mock_post.called)
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_whatsapp_limit(self, mock_post):
|
||||
# At limit already:
|
||||
self.profile.last_sms_date = now()
|
||||
self.profile.sms_sent = 50
|
||||
self.profile.save()
|
||||
|
||||
definition = {"value": "+1234567890", "up": True, "down": True}
|
||||
self._setup_data("whatsapp", json.dumps(definition))
|
||||
|
||||
self.channel.notify(self.check)
|
||||
self.assertFalse(mock_post.called)
|
||||
|
||||
n = Notification.objects.get()
|
||||
self.assertTrue("Monthly message limit exceeded" in n.error)
|
||||
|
@ -415,6 +415,33 @@ class Sms(HttpTransport):
|
||||
return self.post(url, data=data, auth=auth)
|
||||
|
||||
|
||||
class WhatsApp(HttpTransport):
|
||||
URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json"
|
||||
|
||||
def is_noop(self, check):
|
||||
if check.status == "down":
|
||||
return not self.channel.whatsapp_notify_down
|
||||
else:
|
||||
return not self.channel.whatsapp_notify_up
|
||||
|
||||
def notify(self, check):
|
||||
profile = Profile.objects.for_user(self.channel.project.owner)
|
||||
if not profile.authorize_sms():
|
||||
return "Monthly message limit exceeded"
|
||||
|
||||
url = self.URL % settings.TWILIO_ACCOUNT
|
||||
auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH)
|
||||
text = tmpl("whatsapp_message.html", check=check, site_name=settings.SITE_NAME)
|
||||
|
||||
data = {
|
||||
"From": "whatsapp:%s" % settings.TWILIO_FROM,
|
||||
"To": "whatsapp:%s" % self.channel.sms_number,
|
||||
"Body": text,
|
||||
}
|
||||
|
||||
return self.post(url, data=data, auth=auth)
|
||||
|
||||
|
||||
class Trello(HttpTransport):
|
||||
URL = "https://api.trello.com/1/cards"
|
||||
|
||||
|
@ -133,6 +133,8 @@ class AddSmsForm(forms.Form):
|
||||
error_css_class = "has-error"
|
||||
label = forms.CharField(max_length=100, required=False)
|
||||
value = forms.CharField(max_length=16, validators=[phone_validator])
|
||||
down = forms.BooleanField(required=False, initial=True)
|
||||
up = forms.BooleanField(required=False, initial=True)
|
||||
|
||||
|
||||
class ChannelNameForm(forms.Form):
|
||||
|
70
hc/front/tests/test_add_whatsapp.py
Normal file
70
hc/front/tests/test_add_whatsapp.py
Normal file
@ -0,0 +1,70 @@
|
||||
from django.test.utils import override_settings
|
||||
from hc.api.models import Channel
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
TEST_CREDENTIALS = {
|
||||
"TWILIO_ACCOUNT": "foo",
|
||||
"TWILIO_AUTH": "foo",
|
||||
"TWILIO_FROM": "123",
|
||||
"TWILIO_USE_WHATSAPP": True,
|
||||
}
|
||||
|
||||
|
||||
@override_settings(**TEST_CREDENTIALS)
|
||||
class AddWhatsAppTestCase(BaseTestCase):
|
||||
url = "/integrations/add_whatsapp/"
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Get a WhatsApp message")
|
||||
|
||||
@override_settings(USE_PAYMENTS=True)
|
||||
def test_it_warns_about_limits(self):
|
||||
self.profile.sms_limit = 0
|
||||
self.profile.save()
|
||||
|
||||
self.client.login(username="alice@example.org", 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",
|
||||
"down": "true",
|
||||
"up": "true",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, "/integrations/")
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.kind, "whatsapp")
|
||||
self.assertEqual(c.sms_number, "+1234567890")
|
||||
self.assertEqual(c.name, "My Phone")
|
||||
self.assertTrue(c.whatsapp_notify_down)
|
||||
self.assertTrue(c.whatsapp_notify_up)
|
||||
self.assertEqual(c.project, self.project)
|
||||
|
||||
def test_it_obeys_up_down_flags(self):
|
||||
form = {"label": "My Phone", "value": "+1234567890"}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, "/integrations/")
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.kind, "whatsapp")
|
||||
self.assertEqual(c.sms_number, "+1234567890")
|
||||
self.assertEqual(c.name, "My Phone")
|
||||
self.assertFalse(c.whatsapp_notify_down)
|
||||
self.assertFalse(c.whatsapp_notify_up)
|
||||
self.assertEqual(c.project, self.project)
|
||||
|
||||
@override_settings(TWILIO_USE_WHATSAPP=False)
|
||||
def test_it_obeys_use_whatsapp_flag(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 404)
|
@ -39,6 +39,7 @@ channel_urls = [
|
||||
path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"),
|
||||
path("add_telegram/", views.add_telegram, name="hc-add-telegram"),
|
||||
path("add_sms/", views.add_sms, name="hc-add-sms"),
|
||||
path("add_whatsapp/", views.add_whatsapp, name="hc-add-whatsapp"),
|
||||
path("add_trello/", views.add_trello, name="hc-add-trello"),
|
||||
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
|
||||
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
|
||||
|
@ -232,6 +232,7 @@ def index(request):
|
||||
"enable_discord": settings.DISCORD_CLIENT_ID is not None,
|
||||
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
|
||||
"enable_sms": settings.TWILIO_AUTH is not None,
|
||||
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
|
||||
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
||||
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
||||
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
||||
@ -603,6 +604,7 @@ def channels(request):
|
||||
"enable_discord": settings.DISCORD_CLIENT_ID is not None,
|
||||
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
|
||||
"enable_sms": settings.TWILIO_AUTH is not None,
|
||||
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
|
||||
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
||||
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
||||
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
||||
@ -1222,6 +1224,39 @@ def add_sms(request):
|
||||
return render(request, "integrations/add_sms.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_whatsapp(request):
|
||||
if not settings.TWILIO_USE_WHATSAPP:
|
||||
raise Http404("whatsapp integration is not available")
|
||||
|
||||
if request.method == "POST":
|
||||
form = AddSmsForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel = Channel(project=request.project, kind="whatsapp")
|
||||
channel.name = form.cleaned_data["label"]
|
||||
channel.value = json.dumps(
|
||||
{
|
||||
"value": form.cleaned_data["value"],
|
||||
"up": form.cleaned_data["up"],
|
||||
"down": form.cleaned_data["down"],
|
||||
}
|
||||
)
|
||||
channel.save()
|
||||
|
||||
channel.assign_all_checks()
|
||||
return redirect("hc-channels")
|
||||
else:
|
||||
form = AddSmsForm()
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
"project": request.project,
|
||||
"form": form,
|
||||
"profile": request.project.owner_profile,
|
||||
}
|
||||
return render(request, "integrations/add_whatsapp.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_trello(request):
|
||||
if settings.TRELLO_APP_KEY is None:
|
||||
|
@ -187,10 +187,11 @@ PUSHBULLET_CLIENT_SECRET = os.getenv("PUSHBULLET_CLIENT_SECRET")
|
||||
TELEGRAM_BOT_NAME = os.getenv("TELEGRAM_BOT_NAME", "ExampleBot")
|
||||
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
||||
|
||||
# SMS (Twilio) integration
|
||||
# SMS and WhatsApp (Twilio) integration
|
||||
TWILIO_ACCOUNT = os.getenv("TWILIO_ACCOUNT")
|
||||
TWILIO_AUTH = os.getenv("TWILIO_AUTH")
|
||||
TWILIO_FROM = os.getenv("TWILIO_FROM")
|
||||
TWILIO_USE_WHATSAPP = envbool("TWILIO_USE_WHATSAPP", "False")
|
||||
|
||||
# PagerDuty
|
||||
PD_VENDOR_KEY = os.getenv("PD_VENDOR_KEY")
|
||||
|
BIN
static/img/integrations/whatsapp.png
Normal file
BIN
static/img/integrations/whatsapp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -15,8 +15,8 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="col-sm-12">
|
||||
<table class="table channels-table">
|
||||
{% if channels %}
|
||||
<table class="table channels-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="th-name">Name, Details</th>
|
||||
@ -90,6 +90,14 @@
|
||||
list <span>{{ ch.trello_board_list|last }}</span>
|
||||
{% elif ch.kind == "matrix" %}
|
||||
Matrix <span>{{ ch.value }}</span>
|
||||
{% elif ch.kind == "whatsapp" %}
|
||||
WhatsApp to <span>{{ ch.sms_number }}</span>
|
||||
{% if ch.whatsapp_notify_down and not ch.whatsapp_notify_up %}
|
||||
(down only)
|
||||
{% endif %}
|
||||
{% if ch.whatsapp_notify_up and not ch.whatsapp_notify_down %}
|
||||
(up only)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ch.kind }}
|
||||
{% endif %}
|
||||
@ -127,7 +135,7 @@
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
{% if ch.kind == "sms" %}
|
||||
{% if ch.kind == "sms" or ch.kind == "whatsapp" %}
|
||||
<p>Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
@ -156,8 +164,12 @@
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
The project <strong>{{ project }}</strong> has no integrations set up yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h1 class="ai-title">Add More</h1>
|
||||
@ -312,6 +324,17 @@
|
||||
<a href="{% url 'hc-add-matrix' %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if enable_whatsapp %}
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/whatsapp.png' %}"
|
||||
class="icon" alt="WhatsApp icon" />
|
||||
|
||||
<h2>WhatsApp {% if use_payments %}<small>(paid plans)</small>{% endif %}</h2>
|
||||
<p>Get a WhatsApp message when a check goes up or down.</p>
|
||||
|
||||
<a href="{% url 'hc-add-whatsapp' %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="link-to-github">
|
||||
<img src="{% static 'img/integrations/missing.png' %}"
|
||||
class="icon" alt="Suggest New Integration" />
|
||||
|
@ -423,6 +423,15 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if enable_whatsapp %}
|
||||
<div class="col-md-2 col-sm-4 col-xs-6">
|
||||
<div class="integration">
|
||||
<img src="{% static 'img/integrations/whatsapp.png' %}" class="icon" alt="WhatsApp icon" />
|
||||
<h3>WhatsApp<br><small>Chat</small></h3>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row tour-section">
|
||||
|
109
templates/integrations/add_whatsapp.html
Normal file
109
templates/integrations/add_whatsapp.html
Normal file
@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% load humanize static hc_extras %}
|
||||
|
||||
{% block title %}Notification Channels - {% site_name %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>WhatsApp</h1>
|
||||
|
||||
<p>
|
||||
Get a WhatsApp message when a check goes up or down.
|
||||
</p>
|
||||
|
||||
{% if show_pricing and profile.sms_limit == 0 %}
|
||||
<p class="alert alert-info">
|
||||
<strong>Paid plan required.</strong>
|
||||
WhatsApp messaging is not available on the free plan–sending the messages
|
||||
cost too much! Please upgrade to a
|
||||
<a href="{% url 'hc-billing' %}">paid plan</a> to enable WhatsApp messaging.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Integration Settings</h2>
|
||||
|
||||
<form method="post" class="form-horizontal" action="{% url 'hc-add-whatsapp' %}">
|
||||
{% 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 id="add-email-notify-group" class="form-group">
|
||||
<label class="col-sm-2 control-label">Notify When</label>
|
||||
<div class="col-sm-10">
|
||||
<label class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="down"
|
||||
value="true"
|
||||
{% if form.down.value %} checked {% endif %}>
|
||||
<span class="checkmark"></span>
|
||||
A check goes <strong>down</strong>
|
||||
</label>
|
||||
<label class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="up"
|
||||
value="true"
|
||||
{% if form.up.value %} checked {% endif %}>
|
||||
<span class="checkmark"></span>
|
||||
A check goes <strong>up</strong>
|
||||
</label>
|
||||
</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 %}
|
7
templates/integrations/whatsapp_message.html
Normal file
7
templates/integrations/whatsapp_message.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% load humanize %}{% spaceless %}
|
||||
{% if check.status == "down" %}
|
||||
The check "{{ check.name_then_code }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }}.
|
||||
{% else %}
|
||||
The check "{{ check.name_then_code }}" is now UP.
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
@ -110,7 +110,7 @@
|
||||
<li class="list-group-item">10 Team Members</li>
|
||||
<li class="list-group-item">1000 Log Entries per Check</li>
|
||||
<li class="list-group-item">API Access</li>
|
||||
<li class="list-group-item">50 SMS Alerts per Month</li>
|
||||
<li class="list-group-item">50 SMS & WhatsApp Alerts per Month</li>
|
||||
<li class="list-group-item">Email Support</li>
|
||||
</ul>
|
||||
{% if not request.user.is_authenticated %}
|
||||
@ -139,7 +139,7 @@
|
||||
<li class="list-group-item">Unlimited Team Members</li>
|
||||
<li class="list-group-item">1000 Log Entries per Check</li>
|
||||
<li class="list-group-item">API Access</li>
|
||||
<li class="list-group-item">500 SMS Alerts per Month</li>
|
||||
<li class="list-group-item">500 SMS & WhatsApp Alerts per Month</li>
|
||||
<li class="list-group-item">Priority Email Support</li>
|
||||
</ul>
|
||||
{% if not request.user.is_authenticated %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user