Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive Game Developed Version. #1705

Merged
merged 36 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5406ec0
initialize interactive app
eamin-dev Mar 21, 2024
8d838c3
interactive channel added
eamin-dev Mar 21, 2024
d71959a
interactive api created for storing rapidpro message
eamin-dev Mar 23, 2024
6e6a344
authentication header token added in interactive chennel
eamin-dev Mar 23, 2024
e5caa69
interactive page module added to admin panel
eamin-dev Mar 23, 2024
931016f
interactive api serializer class updated
eamin-dev Mar 24, 2024
5fc1d3b
new column (interactive_uuid) added to iogt_users model
eamin-dev Mar 24, 2024
0426c95
interactive page show in iogt homepage
eamin-dev Mar 24, 2024
101d28f
interactive css file updated
eamin-dev Mar 27, 2024
61d614e
interactive game css file updated..
eamin-dev Mar 28, 2024
58f2750
Changed Interactive class to make button_text, trigger_string, and ch…
alviriseup Mar 28, 2024
1cc5bd8
interactive get url validation.
eamin-dev Mar 28, 2024
278de66
Merge branch 'interactive' of https://github.com/mizanriseup/iogt int…
eamin-dev Mar 28, 2024
2d679f6
interactive - get_user_identifier updated
eamin-dev Mar 28, 2024
3f1faa0
shortcode feature added in interactive
eamin-dev Apr 3, 2024
74b543d
interactive views.py file optimized
eamin-dev Apr 4, 2024
9ee2d97
trick button shortcode added in interactive game
eamin-dev Apr 15, 2024
2347bfb
Merge branch 'develop' into interactive
alviriseup Apr 16, 2024
f3ee059
Added changes according to new Wagtail 3.0.3 update
alviriseup Apr 16, 2024
53a2df1
ui updated for interactive game
eamin-dev Apr 22, 2024
d9673f3
removed uppercase style from interactive-btn
eamin-dev Apr 22, 2024
02d3973
image shortcode updated
eamin-dev Apr 23, 2024
9f37ca1
image and trick button shortcode updated
eamin-dev Apr 28, 2024
d0bbbb7
Added unit test for Interactive module
alviriseup May 5, 2024
62fc6c6
interactive models updated with wagtail serve method
eamin-dev May 6, 2024
06b2b82
conflict fixed
eamin-dev May 6, 2024
b829391
interactive css updated
eamin-dev May 6, 2024
6512e4d
Added unit test for Interactive module +
alviriseup May 13, 2024
37e0665
interactive game css file updated
eamin-dev May 23, 2024
0f9b960
Added README for setting up Interactive RapidPro Channel
alviriseup Jun 3, 2024
1eb3968
Added README for setting up Interactive RapidPro Channel +
alviriseup Jun 3, 2024
41aa4a7
Added README for setting up Interactive RapidPro Channel ++
alviriseup Jun 3, 2024
4afd3ce
trick button shortcode updated
eamin-dev Jun 26, 2024
a37c1a4
Merge branch 'interactive' of https://github.com/mizanriseup/iogt int…
eamin-dev Jun 26, 2024
8fa7292
Merge branch 'unicef:develop' into interactive
mizanriseup Jul 10, 2024
43c04df
Clean up redundant comments, commented code, and redundant imports
eamin-dev Jul 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions home/templates/home/tags/child_pages.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
{% render_article_card child is_first_content %}
{% elif child.get_type == 'poll' or child.get_type == 'survey' or child.get_type == 'quiz' %}
{% render_questionnaire_wrapper child child.direct_display %}
{% elif child.get_type == 'interactivepage' %}
{% render_interactive_card child %}
{% endif %}
{% endwith %}
{% endfor %}
Expand Down
11 changes: 11 additions & 0 deletions home/templatetags/generic_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ def render_questionnaire_card(context, page, background_color=None, font_color=N
return context


@register.inclusion_tag('generic_components/interactive_card.html', takes_context=True)
def render_interactive_card(context, page=None, background_color=None, font_color=None):

context.update({
'interactive': page,
'background_color': background_color,
'font_color': font_color,
})
return context


@register.simple_tag
def language_picker_style():
theme_settings = globals_.theme_settings
Expand Down
70 changes: 70 additions & 0 deletions interactive/README.md
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 added interactive/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions interactive/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin

from django.contrib import admin

from .models import InteractiveChannel


@admin.register(InteractiveChannel)
class InteractiveChannelAdmin(admin.ModelAdmin):
list_display = ('id', 'display_name', 'request_url')
9 changes: 9 additions & 0 deletions interactive/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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()
15 changes: 15 additions & 0 deletions interactive/api/serializers.py
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
9 changes: 9 additions & 0 deletions interactive/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path

from .views import RapidProWebhook

app_name = 'interactive_api'

urlpatterns = [
path('rapidpro-webhook/', RapidProWebhook.as_view(), name='rapidpro_message_webhook')
]
59 changes: 59 additions & 0 deletions interactive/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.utils.timezone import now
from rest_framework.response import Response
from rest_framework.views import APIView
from interactive.models import Message
from rest_framework import status
from .serializers import RapidProMessageSerializer
from rest_framework.permissions import IsAuthenticated
from .permissions import IsRapidProGroupUser


class RapidProWebhook(APIView):
permission_classes = [IsAuthenticated, IsRapidProGroupUser]

def post(self, request):
serializer = RapidProMessageSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

# Extract validated data from serializer
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)
6 changes: 6 additions & 0 deletions interactive/apps.py
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'
5 changes: 5 additions & 0 deletions interactive/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django import forms


class MessageSendForm(forms.Form):
text = forms.CharField(widget=forms.TextInput(attrs={'class': 'btn btn-outline-secondary'}), min_length=1)
22 changes: 22 additions & 0 deletions interactive/migrations/0001_initial.py
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')),
],
),
]
26 changes: 26 additions & 0 deletions interactive/migrations/0002_message.py
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)),
],
),
]
29 changes: 29 additions & 0 deletions interactive/migrations/0003_interactivepage.py
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),
),
]
29 changes: 29 additions & 0 deletions interactive/migrations/0004_auto_20240328_0700.py
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),
),
]
22 changes: 22 additions & 0 deletions interactive/migrations/0005_auto_20240506_1128.py
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.
Loading