From 3ee77103ee3c5b0af61ab940ac865fb91770a978 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 14 Apr 2023 17:39:42 -0300 Subject: [PATCH 1/5] WPPCONNECT: React on message deleted. Fixes #110 --- rocket_connect/plugins/wppconnect.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/rocket_connect/plugins/wppconnect.py b/rocket_connect/plugins/wppconnect.py index 32328bd..5e2b924 100644 --- a/rocket_connect/plugins/wppconnect.py +++ b/rocket_connect/plugins/wppconnect.py @@ -870,6 +870,23 @@ def incoming(self): ) and not self.message.get("id", {}).get("fromMe", False): self.logger_info(f"PROCESSED UNREAD MESSAGE. PAYLOAD {self.message}") + # message removed + if self.message.get("event") == "onrevokedmessage": + # get ref id + ref_id = self.message.get("refId") + if ref_id: + self.get_rocket_client() + msg = self.rocket.chat_get_message(msg_id=ref_id) + if msg: + new_message = ":warning: DELETED: ~{}~".format( + msg.json()["message"]["msg"] + ) + room = self.get_room() + a = self.rocket.chat_update( + room_id=room.room_id, msg_id=ref_id, text=new_message + ) + print(a.json()) + # webhook active chat integration if self.config.get("active_chat_webhook_integration_token"): if self.message.get("token") == self.config.get( @@ -929,7 +946,7 @@ def get_incoming_visitor_id(self): if self.message.get("id", {}).get("fromMe"): return self.message.get("id").get("remote") else: - if self.message.get("event") == "unreadmessages": + if self.message.get("event") in ["unreadmessages", "onrevokedmessage"]: return self.message.get("from") else: return self.message.get("chatId") From 8160311b2bb07a3dc290f9e86be0a6d1209333ed Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 14 Apr 2023 19:09:45 -0300 Subject: [PATCH 2/5] [WPPCONNECT] Add reactions to messages. Fixes #109 --- rocket_connect/plugins/wppconnect.py | 37 +++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/rocket_connect/plugins/wppconnect.py b/rocket_connect/plugins/wppconnect.py index 5e2b924..e00c13d 100644 --- a/rocket_connect/plugins/wppconnect.py +++ b/rocket_connect/plugins/wppconnect.py @@ -882,10 +882,31 @@ def incoming(self): msg.json()["message"]["msg"] ) room = self.get_room() - a = self.rocket.chat_update( + self.rocket.chat_update( room_id=room.room_id, msg_id=ref_id, text=new_message ) - print(a.json()) + + # reaction to a message + if self.message.get("event") == "onreactionmessage": + self.get_rocket_client() + room = self.get_room() + ref_id = self.message.get("msgId").get("_serialized") + msg = self.rocket.chat_get_message(msg_id=ref_id) + reaction = self.message.get("reactionText") + if msg.ok: + new_message = "{} {}".format(reaction, msg.json()["message"]["msg"]) + else: + # message may be from previous chats. + # lets get from wppconnect + message = self.get_message(message_id=ref_id) + new_message = "{} {}".format( + reaction, message.get("response").get("data").get("body") + ) + print(new_message) + room = self.get_room() + self.outcome_text( + room_id=room.room_id, text=new_message, message_id=self.get_message_id() + ).json() # webhook active chat integration if self.config.get("active_chat_webhook_integration_token"): @@ -931,7 +952,7 @@ def intake_unread_messages(self): def get_incoming_message_id(self): # unread messages has a different structure - if self.message.get("event") == "unreadmessages": + if self.message.get("event") in ["unreadmessages", "onreactionmessage"]: return self.message.get("id", {}).get("_serialized") if self.message.get("type") == "active_chat": return self.message.get("message_id") @@ -942,6 +963,8 @@ def get_incoming_message_id(self): def get_incoming_visitor_id(self): if self.message.get("event") == "incomingcall": return self.message.get("peerJid") + if self.message.get("event") == "onreactionmessage": + return self.message.get("id").get("remote") if self.message.get("event") == "onack": if self.message.get("id", {}).get("fromMe"): return self.message.get("id").get("remote") @@ -1272,6 +1295,14 @@ def handle_ack_fromme_message(self): message.ack = True message.save() + def get_message(self, message_id): + session = self.get_request_session() + endpoint = "{}/api/{}/message-by-id/{}".format( + self.config.get("endpoint"), self.config.get("instance_name"), message_id + ) + message = session.get(endpoint).json() + return message + class ConnectorConfigForm(BaseConnectorConfigForm): webhook = forms.CharField( From fccfb9f7c2b0358ac4956b8d30d163b3c68432d5 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Mon, 17 Apr 2023 10:02:37 -0300 Subject: [PATCH 3/5] - Add task and button to remove old, delievered messages - Initial Server Active Message Form --- rocket_connect/instance/forms.py | 20 +++++++- rocket_connect/instance/models.py | 25 +++++++++- rocket_connect/instance/tasks.py | 8 +++- rocket_connect/instance/urls.py | 6 +++ rocket_connect/instance/views.py | 47 +++++++++++++++++++ .../templates/instance/active_chat.html | 14 ++++++ .../instance/server_detail_view.html | 27 +++++++++++ 7 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 rocket_connect/templates/instance/active_chat.html diff --git a/rocket_connect/instance/forms.py b/rocket_connect/instance/forms.py index 7a5accf..e0789e3 100644 --- a/rocket_connect/instance/forms.py +++ b/rocket_connect/instance/forms.py @@ -1,4 +1,11 @@ -from django.forms import CharField, ChoiceField, ModelForm +from django.forms import ( + CharField, + ChoiceField, + Form, + ModelChoiceField, + ModelForm, + Textarea, +) from instance.models import Connector, Server @@ -16,6 +23,17 @@ class Meta: ] +class NewInboundForm(Form): + number = CharField(label="Number", max_length=100, help_text="eg. 553199851212") + destination = ChoiceField(choices=[]) + text = CharField( + label="Text", + max_length=100, + widget=Textarea(attrs={"rows": 4, "cols": 15}), + ) + connector = ModelChoiceField(queryset=Connector.objects.all()) + + class NewConnectorForm(ModelForm): def __init__(self, *args, **kwargs): server = kwargs.pop("server") diff --git a/rocket_connect/instance/models.py b/rocket_connect/instance/models.py index 47750c3..b5a4329 100644 --- a/rocket_connect/instance/models.py +++ b/rocket_connect/instance/models.py @@ -1,3 +1,4 @@ +import datetime import json import uuid @@ -5,7 +6,9 @@ from django.apps import apps from django.conf import settings from django.db import models +from django.utils import timezone from django_celery_beat.models import CrontabSchedule, PeriodicTask +from envelope.models import Message from rocketchat_API.APIExceptions.RocketExceptions import RocketAuthenticationException from rocketchat_API.rocketchat import RocketChat @@ -145,6 +148,18 @@ def room_sync(self, execute=False): response["executed"] = close_room_response return response + def delete_delivered_messages(self, age=None, execute=False): + if age and type(age) == int: + now = timezone.now() + target_date = now - datetime.timedelta(days=age) + messages = Message.objects.filter( + room__connector__server=self, delivered=True, created__lte=target_date + ) + if execute: + return messages.delete() + else: + return messages + def force_delivery(self): """ this method will force the intake of every undelivered message @@ -195,10 +210,16 @@ def install_server_tasks(self): crontab = CrontabSchedule.objects.first() task = PeriodicTask.objects.create( enabled=False, - name=f"General Maintenance for {self.name} (ID {self.id})", + name=f"General Maintenance for {self.name} (ID {self.id})." + + "Sync rooms and Remove delivered messages (Age in days).", crontab=crontab, task="instance.tasks.server_maintenance", - kwargs=json.dumps({"server_token": self.external_token}), + kwargs=json.dumps( + { + "server_token": self.external_token, + "delete_delivered_messages_age": 15, + } + ), ) self.tasks.add(task) added_tasks.append(task) diff --git a/rocket_connect/instance/tasks.py b/rocket_connect/instance/tasks.py index b3422bc..08f7504 100644 --- a/rocket_connect/instance/tasks.py +++ b/rocket_connect/instance/tasks.py @@ -28,13 +28,19 @@ def intake_unread_messages(connector_id): retry_kwargs={"max_retries": 7, "countdown": 5}, autoretry_for=(requests.ConnectionError,), ) -def server_maintenance(server_token): +def server_maintenance(server_token, delete_delivered_messages_age=None): """do all sorts of server maintenance""" server = Server.objects.get(external_token=server_token) response = {} # sync room response["room_sync"] = server.room_sync(execute=True) + # delete delivered messages + if delete_delivered_messages_age: + response["delete_delivered_messages"] = server.delete_delivered_messages( + age=delete_delivered_messages_age, execute=True + ) # return results + return response diff --git a/rocket_connect/instance/urls.py b/rocket_connect/instance/urls.py index ff0c04e..6146c4c 100644 --- a/rocket_connect/instance/urls.py +++ b/rocket_connect/instance/urls.py @@ -1,6 +1,7 @@ from django.urls import re_path from rocket_connect.instance.views import ( + active_chat, connector_analyze, new_connector, new_server, @@ -18,6 +19,11 @@ re_path( r"^server/(?P\w+)/?$", view=server_detail_view, name="server_detail" ), + re_path( + r"^server/(?P\w+)/active-chat/?$", + view=active_chat, + name="active_chat", + ), re_path( r"^server/(?P\w+)/analyze/(?P\w+)/?$", view=connector_analyze, diff --git a/rocket_connect/instance/views.py b/rocket_connect/instance/views.py index a3a043f..8930b1c 100644 --- a/rocket_connect/instance/views.py +++ b/rocket_connect/instance/views.py @@ -15,6 +15,8 @@ from instance.forms import NewConnectorForm, NewServerForm from instance.models import Connector, Server +from .forms import NewInboundForm + @csrf_exempt def connector_endpoint(request, connector_id): @@ -136,11 +138,44 @@ def server_messages_endpoint(request, server_id): return JsonResponse(list(messages), safe=False) +@login_required(login_url="/accounts/login/") +@must_be_yours +def active_chat(request, server_id): + server = get_object_or_404(Server.objects, external_token=server_id) + form = NewInboundForm(request.POST or None) + # get online agents and departments + rocket = server.get_rocket_client() + departments_raw = rocket.call_api_get("livechat/department").json() + departments_choice = [ + ("@" + d["name"], "Department: " + d["name"]) + for d in departments_raw["departments"] + ] + destinations = departments_choice + # now get online agents + agents = rocket.livechat_get_users(user_type="agent").json() + available_agents = [ + agent["username"] + for agent in agents["users"] + if agent["status"] == "online" and agent["statusLivechat"] == "available" + ] + print(available_agents) + for agent in available_agents: + destinations.append(("@" + agent, "Agent: " + agent)) + connectors = server.connectors.filter(enabled=True) + form.fields["connector"].queryset = connectors + form.fields["destination"].choices = destinations + if form.is_valid(): + pass + context = {"server": server, "form": form} + return render(request, "instance/active_chat.html", context) + + @login_required(login_url="/accounts/login/") @must_be_yours def server_detail_view(request, server_id): server = get_object_or_404(Server.objects, external_token=server_id) room_sync = None + delivered_messages_to_delete = None # get server status status = server.status() if request.GET.get("force_connector_delivery"): @@ -176,6 +211,17 @@ def server_detail_view(request, server_id): messages.success(request, "Sync Executed!") room_sync = server.room_sync() + if request.GET.get("delete-delivered-messages"): + if request.GET.get("do-delete-delivered-messages"): + delivered_messages_to_delete = server.delete_delivered_messages( + age=10, execute=True + ) + messages.success( + request, f"Delivered messages deleted: {delivered_messages_to_delete}" + ) + else: + delivered_messages_to_delete = server.delete_delivered_messages(age=10) + if request.GET.get("install-default-tasks"): added_tasks = server.install_server_tasks() for added_task in added_tasks: @@ -221,6 +267,7 @@ def server_detail_view(request, server_id): "connectors": connectors, "status": status, "room_sync": room_sync, + "delivered_messages_to_delete": delivered_messages_to_delete, "tasks": tasks, } return render(request, "instance/server_detail_view.html", context) diff --git a/rocket_connect/templates/instance/active_chat.html b/rocket_connect/templates/instance/active_chat.html new file mode 100644 index 0000000..e5558d9 --- /dev/null +++ b/rocket_connect/templates/instance/active_chat.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} {% load parse_date crispy_forms_tags %} + + +{% block content %} + + + +{{form|crispy}} + +{% endblock content %} diff --git a/rocket_connect/templates/instance/server_detail_view.html b/rocket_connect/templates/instance/server_detail_view.html index 8a37ed7..c53e17d 100644 --- a/rocket_connect/templates/instance/server_detail_view.html +++ b/rocket_connect/templates/instance/server_detail_view.html @@ -26,6 +26,9 @@

sync + + remove delivered messages + {% endif %} {{server}} {{ server.enabled|yesno:"Active,Inactive" }}

@@ -66,6 +69,30 @@

{% endif %} + + + {% if delivered_messages_to_delete %} + + + + + {% endif %} + From 309e0af76983a4ade418efbf4e8a05828af510f6 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Mon, 17 Apr 2023 10:03:03 -0300 Subject: [PATCH 4/5] Server Dev Version to 1.1.21 --- rocket_connect/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocket_connect/__init__.py b/rocket_connect/__init__.py index e706c7c..49ec801 100644 --- a/rocket_connect/__init__.py +++ b/rocket_connect/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.20" +__version__ = "1.1.21" __version_info__ = tuple( int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".") From dad8a128d30d6fddf3ba616dc3c5a01f79202fbc Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 21 Apr 2023 10:27:59 -0300 Subject: [PATCH 5/5] Added Task 7: Manage Abandoned Calls Added button to easily add/update default tasks. --- local.yml | 37 ++++----- rocket_connect/instance/models.py | 31 +++++++ rocket_connect/instance/tasks.py | 83 +++++++++++++++++++ .../instance/server_detail_view.html | 6 +- 4 files changed, 137 insertions(+), 20 deletions(-) diff --git a/local.yml b/local.yml index c0b51b2..5d2baa6 100644 --- a/local.yml +++ b/local.yml @@ -120,7 +120,7 @@ services: rocketchat: - image: registry.rocket.chat/rocketchat/rocket.chat:${RELEASE:-6.1.0} + image: registry.rocket.chat/rocketchat/rocket.chat:${RELEASE:-develop} restart: on-failure environment: MONGO_URL: "${MONGO_URL:-\ @@ -197,24 +197,23 @@ services: ports: - "21465:21465" - quepasa: - image: sufficit/quepasa - mem_limit: 4096M - ports: - - 31000:31000 - extra_hosts: - - "host.docker.internal:host-gateway" - restart: always - stdin_open: true - tty: true - environment: - - WEBSOCKETSSL=false - - WEBAPIPORT=31000 - - APP_ENV=production - - MIGRATIONS=/opt/quepasa/migrations - - DEBUGJSONMESSAGES=false - - HTTPLOGS=false - + # quepasa: + # image: sufficit/quepasa + # mem_limit: 4096M + # ports: + # - 31000:31000 + # extra_hosts: + # - "host.docker.internal:host-gateway" + # restart: always + # stdin_open: true + # tty: true + # environment: + # - WEBSOCKETSSL=false + # - WEBAPIPORT=31000 + # - APP_ENV=production + # - MIGRATIONS=/opt/quepasa/migrations + # - DEBUGJSONMESSAGES=false + # - HTTPLOGS=false apache: image: 'php:apache' diff --git a/rocket_connect/instance/models.py b/rocket_connect/instance/models.py index b5a4329..47112bf 100644 --- a/rocket_connect/instance/models.py +++ b/rocket_connect/instance/models.py @@ -354,6 +354,37 @@ def install_server_tasks(self): ) self.tasks.add(task) added_tasks.append(task) + # + # T7 manage_abandoned_chats + # + task = PeriodicTask.objects.filter( + task="instance.tasks.manage_abandoned_chats", + kwargs__contains=self.external_token, + ) + if not task.exists(): + crontab = CrontabSchedule.objects.first() + task = PeriodicTask.objects.create( + enabled=False, + name=f"Manage Abandoned Calls for {self.name} (ID {self.id})", + description="""This task can transfer to department or agent""" + + """, close or just alert the user on abandoned chats""", + crontab=crontab, + task="instance.tasks.manage_abandoned_chats", + kwargs=json.dumps( + { + "server_token": self.external_token, + "excluded_departments": [], + "message_template": "This chat is abandoned", + "last_message_seconds": 10, + "last_message_users": "*", + "action": "transfer|close|alert", + "target_department_id": None, + "target_agent_user_id": None, + } + ), + ) + self.tasks.add(task) + added_tasks.append(task) # return added tasks return added_tasks diff --git a/rocket_connect/instance/tasks.py b/rocket_connect/instance/tasks.py index 08f7504..9d7e530 100644 --- a/rocket_connect/instance/tasks.py +++ b/rocket_connect/instance/tasks.py @@ -254,3 +254,86 @@ def alert_undelivered_messages( responses = {"targets": targets, "sent_messages": sent_messages} return responses + + +# T7 +@celery_app.task(retry_kwargs={"max_retries": 7, "countdown": 5}) +def manage_abandoned_chats( + server_token, + excluded_departments, + message_template, + last_message_seconds, + last_message_users, + action="close", + target_department_id=None, + target_agent_user_id=None, +): + # get server + server = Server.objects.get(external_token=server_token) + # get rocket + rocket = server.get_rocket_client() + # list all open messages + open_rooms = server.get_open_rooms() + # create returns + output = {"action": action, "rooms": []} + + if open_rooms: + for room in open_rooms.get("rooms"): + # do not close for configured rooms + if room.get("departmentId") not in excluded_departments: + if room.get("lastMessage", False): + # get last message + last_message = room["lastMessage"] + # define last message users or all + if ( + last_message["u"]["username"] in last_message_users + or last_message_users == "*" + ): + ts = dateutil.parser.parse(last_message["ts"]) + now = timezone.now() + delta = now - ts + if delta.total_seconds() >= last_message_seconds: + if message_template: + rocket.chat_post_message( + room_id=room["_id"], text=message_template + ).json() + if action == "close": + # close messages on this situation + room_close_options = { + "rid": room["_id"], + "token": room["v"]["token"], + } + close = rocket.call_api_post( + "livechat/room.close", **room_close_options + ) + output["rooms"].append(close.json()) + elif action == "transfer": + if target_department_id and not target_agent_user_id: + # transfer messages on this situation to department + room_transfer_options = { + "rid": room["_id"], + "token": room["v"]["token"], + "department": target_department_id, + } + transfer = rocket.call_api_post( + "livechat/room.transfer", + **room_transfer_options + ) + output["rooms"].append(transfer.json()) + if target_agent_user_id and not target_department_id: + # forward messages on this situation to agent + room_transfer_options = { + "roomId": room["_id"], + "userId": target_agent_user_id, + } + transfer = rocket.call_api_post( + "livechat/room.forward", **room_transfer_options + ) + output["rooms"].append( + { + "room_id": room["_id"], + "response": transfer.json(), + } + ) + + return output diff --git a/rocket_connect/templates/instance/server_detail_view.html b/rocket_connect/templates/instance/server_detail_view.html index c53e17d..cbca104 100644 --- a/rocket_connect/templates/instance/server_detail_view.html +++ b/rocket_connect/templates/instance/server_detail_view.html @@ -70,7 +70,6 @@

{% endif %} - {% if delivered_messages_to_delete %}
+ + + {% for task in tasks.all %}