Skip to content

Commit

Permalink
Merge pull request #36 from iKonoTelecomunicaciones/add_interactive_i…
Browse files Browse the repository at this point in the history
…nput_buttons_and_buttons_list

Add interactive input buttons and buttons list
  • Loading branch information
bramenn committed Jul 31, 2023
2 parents 8ce696f + e158a3a commit b5a7934
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 3 deletions.
16 changes: 15 additions & 1 deletion menuflow/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@

from .flow_utils import FlowUtils
from .middlewares import HTTPMiddleware
from .nodes import CheckTime, Email, HTTPRequest, Input, Location, Media, Message, Switch
from .nodes import (
CheckTime,
Email,
HTTPRequest,
Input,
InteractiveInput,
Location,
Media,
Message,
Switch,
)
from .repository import Flow as FlowModel
from .room import Room

Expand Down Expand Up @@ -117,6 +127,10 @@ def node(
if node_data.get("middleware"):
middleware = self.middleware(node_data.get("middleware"), room)
node_initialized.middleware = middleware
elif node_data.get("type") == "interactive_input":
node_initialized = InteractiveInput(
interactive_input_data=node_data, room=room, default_variables=self.flow_variables
)
else:
return

Expand Down
4 changes: 2 additions & 2 deletions menuflow/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .config import Config
from .db.room import RoomState
from .flow import Flow
from .nodes import Base, Input
from .nodes import Base, Input, InteractiveInput
from .repository import Flow as FlowModel
from .repository import FlowUtils
from .room import Room
Expand Down Expand Up @@ -231,7 +231,7 @@ async def algorithm(self, room: Room, evt: Optional[MessageEvent] = None) -> Non

self.log.debug(f"The [room: {room.room_id}] [node: {node.id}] [state: {room.state}]")

if type(node) == Input:
if type(node) in (Input, InteractiveInput):
await node.run(evt=evt)
if room.state == RoomState.INPUT:
return
Expand Down
1 change: 1 addition & 0 deletions menuflow/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .email import Email
from .http_request import HTTPRequest
from .input import Input
from .interactive_input import InteractiveInput
from .location import Location
from .media import Media
from .message import Message
Expand Down
80 changes: 80 additions & 0 deletions menuflow/nodes/interactive_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Any, Dict, Optional

from mautrix.types import MessageEvent

from ..db.room import RoomState
from ..repository import InteractiveInput as InteractiveInputModel
from ..repository import InteractiveMessage
from ..room import Room
from ..utils import Util
from .input import Input


class InteractiveInput(Input):
def __init__(
self, interactive_input_data: InteractiveInputModel, room: Room, default_variables: Dict
) -> None:
Input.__init__(
self,
input_node_data=interactive_input_data,
room=room,
default_variables=default_variables,
)
self.content = interactive_input_data

@property
def interactive_message(self) -> Dict[str, Any]:
return self.render_data(self.content.get("interactive_message", {}))

@property
def interactive_message_type(self) -> str:
if self.interactive_message.get("type") == "list":
return "m.interactive.list_reply"
else:
return "m.interactive.quick_reply"

@property
def interactive_message_content(self) -> InteractiveMessage:
interactive_message = InteractiveMessage.from_dict(
msgtype=self.interactive_message_type,
interactive_message=self.interactive_message,
)
interactive_message.trim_reply_fallback()
return interactive_message

async def run(self, evt: Optional[MessageEvent]):
"""If the room is in input mode, then set the variable.
Otherwise, show the message and enter input mode
Parameters
----------
client : MatrixClient
The MatrixClient object.
evt : Optional[MessageEvent]
The event that triggered the node.
"""

if self.room.state == RoomState.INPUT:
if not evt or not self.variable:
self.log.warning("A problem occurred to trying save the variable")
return

await self.input_text(content=evt.content)

if self.inactivity_options:
await Util.cancel_task(task_name=self.room.room_id)
else:
# This is the case where the room is not in the input state
# and the node is an input node.
# In this case, the message is shown and the menu is updated to the node's id
# and the room state is set to input.
self.log.debug(f"Room {self.room.room_id} enters input node {self.id}")
await self.room.matrix_client.send_message_event(
room_id=self.room.room_id,
event_type="m.room.message",
content=self.interactive_message_content,
)
await self.room.update_menu(node_id=self.id, state=RoomState.INPUT)
if self.inactivity_options:
await self.inactivity_task()
2 changes: 2 additions & 0 deletions menuflow/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
HTTPRequest,
InactivityOptions,
Input,
InteractiveInput,
InteractiveMessage,
Location,
Media,
Message,
Expand Down
1 change: 1 addition & 0 deletions menuflow/repository/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .email import Email
from .http_request import HTTPRequest
from .input import InactivityOptions, Input
from .interactive_input import InteractiveInput, InteractiveMessage
from .location import Location
from .media import Media
from .message import Message
Expand Down
210 changes: 210 additions & 0 deletions menuflow/repository/nodes/interactive_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
from typing import Dict, List, Optional

from attr import dataclass, ib
from mautrix.types import BaseMessageEventContent, SerializableAttrs

from .input import Input


@dataclass
class ContentQuickReplay(SerializableAttrs):
type: str = ib(default=None, metadata={"json": "type"})
header: str = ib(default=None, metadata={"json": "header"})
text: str = ib(default=None, metadata={"json": "text"})
caption: str = ib(default=None, metadata={"json": "caption"})
filename: str = ib(default=None, metadata={"json": "2"})
url: str = ib(default=None, metadata={"json": "url"})


@dataclass
class InteractiveMessageOption(SerializableAttrs):
type: str = ib(default=None, metadata={"json": "type"})
title: str = ib(default=None, metadata={"json": "title"})
description: str = ib(default=None, metadata={"json": "description"})
postback_text: str = ib(default=None, metadata={"json": "postback_text"})


@dataclass
class ItemListReplay(SerializableAttrs):
title: str = ib(default=None, metadata={"json": "title"})
subtitle: str = ib(default=None, metadata={"json": "subtitle"})
options: List[InteractiveMessageOption] = ib(metadata={"json": "options"}, factory=list)

@classmethod
def from_dict(cls, data: dict):
return cls(
title=data.get("title"),
subtitle=data.get("subtitle"),
options=[InteractiveMessageOption(**option) for option in data.get("options", [])],
)


@dataclass
class GlobalButtonsListReplay(SerializableAttrs):
type: str = ib(default=None, metadata={"json": "type"})
title: str = ib(default=None, metadata={"json": "title"})


@dataclass
class InteractiveMessageContent(SerializableAttrs):
type: str = ib(default=None, metadata={"json": "type"})
content: ContentQuickReplay = ib(default=None, metadata={"json": "content"})
options: List[InteractiveMessageOption] = ib(metadata={"json": "options"}, factory=list)
title: str = ib(default=None, metadata={"json": "title"})
body: str = ib(default=None, metadata={"json": "body"})
msgid: str = ib(default=None, metadata={"json": "msgid"})
global_buttons: List[GlobalButtonsListReplay] = ib(
metadata={"json": "global_buttons"}, factory=list
)
items: List[ItemListReplay] = ib(metadata={"json": "items"}, factory=list)

@classmethod
def from_dict(cls, data: Dict):
if data["type"] == "quick_reply":
return cls(
type=data["type"],
content=ContentQuickReplay(**data["content"]),
options=[InteractiveMessageOption(**option) for option in data["options"]],
)
elif data["type"] == "list":
return cls(
type=data["type"],
title=data["title"],
body=data["body"],
global_buttons=[
GlobalButtonsListReplay(**item) for item in data["global_buttons"]
],
items=[ItemListReplay.from_dict(item) for item in data["items"]],
)


@dataclass
class InteractiveMessage(SerializableAttrs, BaseMessageEventContent):
msgtype: str = ib(default="none", metadata={"json": "msgtype"})
body: str = ib(default=None, metadata={"json": "body"})
interactive_message: InteractiveMessageContent = ib(
factory=InteractiveMessageContent, metadata={"json": "interactive_message"}
)

@classmethod
def from_dict(cls, msgtype: str, interactive_message: Dict, body: Optional[str] = ""):
return cls(
msgtype=msgtype,
body=body,
interactive_message=InteractiveMessageContent.from_dict(interactive_message),
)


@dataclass
class InteractiveInput(Input):
"""
## Interactive Input
An interactive input type node allows sending button and
button list messages to whatsapp using Gupshup Bridge.
Nota: This node is only available for whatsapp, matrix does not have support.
content:
```yaml
- id: i1
type: interactive_input
variable: opt
validation: '{{ opt }}'
validation_attempts: 3
inactivity_options:
chat_timeout: 20 #seconds
warning_message: "Message"
time_between_attempts: 10 #seconds
attempts: 3
interactive_message:
type: "quick_reply"
content:
type: "image | text | video | document"
# If type = image | video | document set url parameter
url: "https://images.unsplash.com/photo-1575936123452-b67c3203c357?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D&w=1000&q=80"
# If type = text set header parameter
header: "Gracias por comunicarte con iKono Telecomunicaciones."
text: "Para nosotros es un gusto poder ayudarte 😀"
caption: "Por favor selecciona una de las siguientes opciones:"
options:
- type: "text",
title: "Sales"
- type: "text",
title: "Support"
- type: "text",
title: "Development"
cases:
- id: "sales"
o_connection: "m1"
- id: "support"
o_connection: "m2"
- id: "development"
o_connection: "m3"
- id: "default"
o_connection: "m4"
- id: "timeout"
o_connection: "m5"
- id: "attempt_exceeded"
o_connection: "m6"
- id: i1
type: interactive_input
variable: opt
validation: '{{ opt }}'
validation_attempts: 3
inactivity_options:
chat_timeout: 20 #seconds
warning_message: "Message"
time_between_attempts: 10 #seconds
attempts: 3
interactive_message:
type: "list"
title: "title text"
body: "body text"
msgid: "list1"
global_buttons:
- type: "text",
title: "Global button"
items:
- title: "first Section"
subtitle: "first Subtitle"
options:
- type: "text"
title: "section 1 row 1"
description: "first row of first section description"
postback_text: "1"
- type: "text"
title: "section 1 row 2"
description: "second row of first section description"
postback_text: "2"
- title: "Second Section"
subtitle: "Second Subtitle"
options:
- type: "text"
title: "section 2 row 1"
description: "first row of first section description"
postback_text: "3"
- type: "text"
title: "section 2 row 2"
description: "second row of first section description"
postback_text: "4"
cases:
- id: "1"
o_connection: "m1"
- id: "2"
o_connection: "m2"
- id: "3"
o_connection: "m3"
- id: "4"
o_connection: "m4"
- id: "default"
o_connection: "m5"
- id: "timeout"
o_connection: "m6"
- id: "attempt_exceeded"
o_connection: "m7"
```
"""

interactive_message: InteractiveMessageContent = ib(factory=InteractiveMessageContent)

0 comments on commit b5a7934

Please sign in to comment.