-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
aka Cranky Uncle adaptation to IoGT/Rapidpro Co-authored-by: Md Eamin Hossain <[email protected]> Co-authored-by: alviriseup <[email protected]>
- Loading branch information
1 parent
d637f2c
commit 1c9986c
Showing
31 changed files
with
1,402 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
Users can communicate with RapidPro by creating an Interactive chatbots in IoGT. | ||
|
||
## Create a service account in IoGT | ||
|
||
RapidPro needs a service account within IoGT to be able to authenticate itself to IoGT and establish communication. The service account is set up within IoGT as an unprivileged user account within a specific user group. Any Wagtail Admin should be able to set up the service account manually. | ||
|
||
- Create a new group (_Settings_ > _Groups_, _Add a group_) | ||
- The name must be exactly 'rapidpro_chatbot' | ||
- No permissions are required | ||
- Create the service account (_Settings_ > _Users_, _Add a user_) | ||
- The username just has to be unique | ||
- Use a long and varied password (no need to remember it) | ||
- It is recommended to give the user a descriptive first and last name, e.g. 'RapidPro Bot' | ||
- Add the user to the 'rapidpro_chatbot' group (in the _Roles_ section) | ||
- Verify everything is set up correctly, by checking that the authorization header to be used by RapidPro when authenticating to IoGT now appears in _Interactive_ > _Interactive RapidPro Channels_. | ||
|
||
You can now set up a channel in RapidPro, and add this channel to IoGT so you can start communicating with it. | ||
|
||
## Setting up an Interactive Chatbot channel | ||
1. Find the _Chatbot Authentication Header_ for the chatbot in the IoGT admin panel at _Interactive_ > _Interactive RapidPro Channels_ - displayed at the top of the page. | ||
2. In your workspace in RapidPro, go to _Settings_ > _Add Channel_. | ||
3. Select _External Api_. | ||
4. Fill in the following form fields | ||
1. **URN Type**: External identifier | ||
2. **Address**: Enter a name identifying your RapidPro server, e.g. my_rapidpro_server | ||
3. **Method**: HTTP POST | ||
4. **Encoding**: Default Encoding (TODO: Confirm this) | ||
5. **Content type**: JSON - application/json | ||
6. **Max length**: 6400 (see notes: [1](#note-1), [2](#note-2)) | ||
7. **Authorization Header Value**: Enter the _Chatbot Authentication Header_ from the IoGT admin panel _Interactive_ > _Interactive RapidPro Channels_, mentioned earlier (include the word 'Bearer' as well as the code) | ||
8. **Send URL**: `https://[URL of the IoGT site]/api/interactive/rapidpro-webhook/ ` where `[URL of the IoGT site]` is the URL of your IoGT site, e.g. `rw.goodinternet.org`. | ||
9. **Request Body**: `{"id":{{id}}, "text":{{text}}, "to":{{to}}, "to_no_plus":{{to_no_plus}}, "from":{{from}}, "from_no_plus":{{from_no_plus}}, "channel":{{channel}}, "quick_replies":{{quick_replies}}}` (TODO: Check whether we can omit the `to_no_plus` and `from_no_plus`) | ||
10. **MT Response check**: ok | ||
5. Click _Submit_ | ||
6. You will land on the _External API Configuration_ page | ||
7. Copy the _Received URL_ that is displayed on the page. This URL should look roughly like this: `https://[your RapidPro server]/c/ex/some-uuid-here-7afd839d7123-a95d/receive` | ||
8. Go back to the IoGT admin panel, _Interactive_ > _Interactive RapidPro Channels_, _Add interactive channel_. | ||
1. **DISPLAY NAME**: This is the name that users will see when interacting with the interactive bot. | ||
2. **REQUEST URL**: Enter the _Received URL_ from step 7, immediately above. | ||
|
||
### Notes | ||
|
||
<a id="note-1">**1**</a> On older RapidPro installations the upper limit might only be up to 640. | ||
|
||
<a id="note-2">**2**</a> Messages longer than the limit will be split up into multiple parts and will have to be re-joined on the IoGT side, which can cause spaces between words to be lost. Thus, larger limits are better, so we can avoid messages being split as much as possible. | ||
|
||
## Allowing users to interact with a Chatbot | ||
As part of an Interactive page content, you can now add an _Interactive RapidPro Chatbot button_. It has the following form fields: | ||
|
||
- **Title**: The title identifying the conversation with the chatbot in the user's inbox. | ||
- **Trigger string**: The initial message that will be sent to the chatbot, starting the conversation | ||
- **Channel**: Select the channel you just created. | ||
|
||
Upon clicking the page title, the user will be directed to the interactive page (showing the first message from RapidPro). | ||
|
||
## Create a service account in IoGT with the management command | ||
|
||
This is only an option for those who are operating an instance of IoGT (system administrator, ops team). The management commmand performs the manual steps for creating a service account in IoGT (detailed above) and is provided to automate the process (e.g. when an IoGT site is first created). | ||
|
||
The setup command. | ||
``` | ||
python manage.py sync_rapidpro_bot_user | ||
``` | ||
|
||
There is another command that will print the authorization header to be used by RapidPro when authenticating to IoGT. | ||
``` | ||
python manage.py get_rapidpro_authentication_header_value | ||
``` | ||
|
||
This is the same value that appears in the IoGT admin panel under _Chatbot_ > _Chatbot channels_. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.contrib import admin | ||
|
||
from .models import InteractiveChannel | ||
|
||
|
||
@admin.register(InteractiveChannel) | ||
class InteractiveChannelAdmin(admin.ModelAdmin): | ||
list_display = ("id", "display_name", "request_url") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from django.conf import settings | ||
from rest_framework import permissions | ||
|
||
|
||
class IsRapidProGroupUser(permissions.BasePermission): | ||
message = "User is not allowed to access the webhook" | ||
|
||
def has_permission(self, request, view): | ||
return request.user.groups.filter( | ||
name=settings.RAPIDPRO_BOT_GROUP_NAME | ||
).exists() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from rest_framework import serializers | ||
|
||
|
||
class RapidProMessageSerializer(serializers.Serializer): | ||
channel = serializers.UUIDField() | ||
from_ = serializers.CharField(required=False) | ||
id = serializers.CharField() | ||
quick_replies = serializers.JSONField() | ||
text = serializers.CharField() | ||
to = serializers.UUIDField() | ||
|
||
def get_fields(self): | ||
fields = super().get_fields() | ||
fields["from"] = fields.pop("from_") | ||
return fields |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from django.urls import path | ||
|
||
from .views import RapidProWebhook | ||
|
||
app_name = "interactive_api" | ||
|
||
urlpatterns = [ | ||
path( | ||
"rapidpro-webhook/", RapidProWebhook.as_view(), name="rapidpro_message_webhook" | ||
) | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from django.utils.timezone import now | ||
from rest_framework import status | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
|
||
from interactive.models import Message | ||
|
||
from .permissions import IsRapidProGroupUser | ||
from .serializers import RapidProMessageSerializer | ||
|
||
|
||
class RapidProWebhook(APIView): | ||
permission_classes = [IsAuthenticated, IsRapidProGroupUser] | ||
|
||
def post(self, request): | ||
serializer = RapidProMessageSerializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
rapidpro_message_id = serializer.validated_data.get("id") | ||
to = serializer.validated_data.get("to") | ||
text = serializer.validated_data.get("text") | ||
quick_replies = serializer.validated_data.get("quick_replies") | ||
from_field = serializer.validated_data.get("from") | ||
channel = serializer.validated_data.get("channel") | ||
|
||
# Get the latest message for the 'to' recipient | ||
prev_msg = ( | ||
Message.objects.filter(to=to, channel=channel) | ||
.order_by("-created_at") | ||
.first() | ||
) | ||
|
||
# Update or create a new message | ||
if prev_msg: | ||
prev_msg_text = prev_msg.text.strip() | ||
|
||
if prev_msg_text.endswith("[CONTINUE]"): | ||
text = prev_msg_text + text | ||
else: | ||
text = text | ||
|
||
fields_to_update = { | ||
"rapidpro_message_id": rapidpro_message_id, | ||
"text": text, | ||
"quick_replies": quick_replies, | ||
"updated_at": now(), | ||
} | ||
Message.objects.filter( | ||
rapidpro_message_id=prev_msg.rapidpro_message_id | ||
).update(**fields_to_update) | ||
else: | ||
# Create a new message | ||
message_data = { | ||
"rapidpro_message_id": rapidpro_message_id, | ||
"text": text, | ||
"quick_replies": quick_replies, | ||
"to": to, | ||
"from_field": from_field, | ||
"channel": channel, | ||
"created_at": now(), | ||
"updated_at": now(), | ||
} | ||
Message.objects.create(**message_data) | ||
|
||
return Response(data="ok", status=status.HTTP_200_OK) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class InteractiveConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "interactive" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django import forms | ||
|
||
|
||
class MessageSendForm(forms.Form): | ||
text = forms.CharField( | ||
widget=forms.TextInput(attrs={"class": "btn btn-outline-secondary"}), | ||
min_length=1, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 3.2.24 on 2024-03-21 08:46 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='InteractiveChannel', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('display_name', models.CharField(help_text='Name for the interactive bot that the user will seen when interacting with it', max_length=80)), | ||
('request_url', models.URLField(help_text='To set up a interactive bot channel on your RapidPro server and get a request URL, follow the steps outline in the Section "Setting up a Chatbot channel" here: https://github.com/unicef/iogt/blob/develop/messaging/README.md')), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Generated by Django 3.2.24 on 2024-03-23 10:04 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('interactive', '0001_initial'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Message', | ||
fields=[ | ||
('rapidpro_message_id', models.AutoField(primary_key=True, serialize=False)), | ||
('text', models.TextField()), | ||
('quick_replies', models.JSONField(blank=True, null=True)), | ||
('to', models.CharField(max_length=255)), | ||
('from_field', models.CharField(max_length=255)), | ||
('channel', models.CharField(max_length=255)), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('updated_at', models.DateTimeField(auto_now=True)), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Generated by Django 3.2.24 on 2024-03-23 12:52 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import home.mixins | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('wagtailcore', '0066_collection_management_permissions'), | ||
('interactive', '0002_message'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='InteractivePage', | ||
fields=[ | ||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), | ||
('button_text', models.CharField(blank=True, max_length=255, null=True)), | ||
('trigger_string', models.CharField(blank=True, help_text='Language short code will postfix after trigger string e.g string_en', max_length=255, null=True)), | ||
('channel', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='interactive.interactivechannel')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
bases=('wagtailcore.page', home.mixins.PageUtilsMixin, home.mixins.TitleIconMixin), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Generated by Django 3.2.24 on 2024-03-28 07:00 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('interactive', '0003_interactivepage'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='interactivepage', | ||
name='button_text', | ||
field=models.CharField(max_length=255), | ||
), | ||
migrations.AlterField( | ||
model_name='interactivepage', | ||
name='channel', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='interactive.interactivechannel'), | ||
), | ||
migrations.AlterField( | ||
model_name='interactivepage', | ||
name='trigger_string', | ||
field=models.CharField(help_text='Language short code will postfix after trigger string e.g string_en', max_length=255), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 3.2.24 on 2024-05-06 11:28 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('interactive', '0004_auto_20240328_0700'), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveField( | ||
model_name='interactivepage', | ||
name='button_text', | ||
), | ||
migrations.AlterField( | ||
model_name='interactivepage', | ||
name='trigger_string', | ||
field=models.CharField(max_length=255), | ||
), | ||
] |
Empty file.
Oops, something went wrong.