diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a40e121..ecd3100 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,7 +7,7 @@ on: - 'dev' tags: - 'v*' - + pull_request: branches: - 'master' @@ -39,4 +39,4 @@ jobs: file: ./compose/production/django/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c64d1a..2bea8e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,39 @@ -exclude: 'docs|node_modules|migrations|.git|.tox|emojipy|.dot' +exclude: '^docs/|/migrations/|node_modules|migrations|.git|.tox|emojipy|.dot|emojipy' default_stages: [commit] -fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + - repo: https://github.com/asottile/pyupgrade + rev: v2.32.1 + hooks: + - id: pyupgrade + args: [--py39-plus] + - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black - - repo: https://github.com/timothycrosley/isort - rev: 5.7.0 + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 hooks: - id: isort - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 - args: ['--config=setup.cfg'] + args: ["--config=setup.cfg"] additional_dependencies: [flake8-isort] + +# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile index 2a0a66c..59ae6ea 100644 --- a/compose/local/django/Dockerfile +++ b/compose/local/django/Dockerfile @@ -1,26 +1,63 @@ -FROM python:3.8-slim-buster +ARG PYTHON_VERSION=3.9-slim-bullseye -ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} as python + +# Python build stage +FROM python as python-build-stage -RUN apt-get update \ +ARG BUILD_ENVIRONMENT=local + +# Install apt packages +RUN apt-get update && apt-get install --no-install-recommends -y \ # dependencies for building Python packages - && apt-get install -y build-essential python3-dev \ + build-essential \ # psycopg2 dependencies - && apt-get install -y libpq-dev \ - # Translations dependencies - && apt-get install -y gettext \ + libpq-dev \ # QR TOOLS - && apt-get install -y libzbar0 libzbar-dev \ + && apt install -y libzbar0 libzbar-dev python3-zbar \ # misc dependencies - && apt-get install -y curl \ + && apt-get install -y curl + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements . + +# Create Python Dependency and Sub-Dependency Wheels. +RUN pip wheel --wheel-dir /usr/src/app/wheels \ + -r ${BUILD_ENVIRONMENT}.txt + + +# Python 'run' stage +FROM python as python-run-stage + +ARG BUILD_ENVIRONMENT=local +ARG APP_HOME=/app + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV BUILD_ENV ${BUILD_ENVIRONMENT} + +WORKDIR ${APP_HOME} + +# Install required system dependencies +RUN apt-get update && apt-get install --no-install-recommends -y \ + # psycopg2 dependencies + libpq-dev \ + # Translations dependencies + # qrcode deps \ + python3-zbar \ + gettext \ # cleaning up unused files && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* -# Requirements are installed here to ensure they will be cached. -COPY ./requirements /requirements -RUN pip install -r /requirements/local.txt +# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels/ + +# use wheels to install python dependencies +RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ + && rm -rf /wheels/ COPY ./compose/production/django/entrypoint /entrypoint RUN sed -i 's/\r$//g' /entrypoint @@ -42,6 +79,7 @@ COPY ./compose/local/django/celery/flower/start /start-flower RUN sed -i 's/\r$//g' /start-flower RUN chmod +x /start-flower -WORKDIR /app +# copy application code to WORKDIR +COPY . ${APP_HOME} ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/docs/Dockerfile b/compose/local/docs/Dockerfile index c931e92..b4a9a21 100644 --- a/compose/local/docs/Dockerfile +++ b/compose/local/docs/Dockerfile @@ -1,33 +1,69 @@ -FROM python:3.8-slim-buster +ARG PYTHON_VERSION=3.9-slim-bullseye + +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} as python + + +# Python build stage +FROM python as python-build-stage -ENV PYTHONUNBUFFERED 1 ENV PYTHONDONTWRITEBYTECODE 1 -RUN apt-get update \ - # dependencies for building Python packages - && apt-get install -y build-essential python3-dev \ - # psycopg2 dependencies - && apt-get install -y libpq-dev \ - # QR TOOLS - && apt-get install -y libzbar0 libzbar-dev \ - # Translations dependencies - && apt-get install -y gettext \ - # Uncomment below lines to enable Sphinx output to latex and pdf - # && apt-get install -y texlive-latex-recommended \ - # && apt-get install -y texlive-fonts-recommended \ - # && apt-get install -y texlive-latex-extra \ - # && apt-get install -y latexmk \ - # cleaning up unused files - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install --no-install-recommends -y \ + # dependencies for building Python packages + build-essential \ + # psycopg2 dependencies + libpq-dev \ + # QR TOOLS + && apt install -y libzbar0 libzbar-dev python3-zbar \ + # misc dependencies + && apt-get install -y curl \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* # Requirements are installed here to ensure they will be cached. COPY ./requirements /requirements -# All imports needed for autodoc. -RUN pip install -r /requirements/local.txt -r /requirements/production.txt + +# create python dependency wheels +RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels \ + -r /requirements/local.txt -r /requirements/production.txt \ + && rm -rf /requirements + + +# Python 'run' stage +FROM python as python-run-stage + +ARG BUILD_ENVIRONMENT +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +RUN apt-get update && apt-get install --no-install-recommends -y \ + # To run the Makefile + make \ + # psycopg2 dependencies + libpq-dev \ + # Translations dependencies + gettext \ + # Uncomment below lines to enable Sphinx output to latex and pdf + texlive-latex-recommended \ + texlive-fonts-recommended \ + texlive-latex-extra \ + latexmk \ + python3-zbar \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels + +# use wheels to install python dependencies +RUN pip install --no-cache /wheels/* \ + && rm -rf /wheels COPY ./compose/local/docs/start /start-docs RUN sed -i 's/\r$//g' /start-docs RUN chmod +x /start-docs -WORKDIR /docs \ No newline at end of file +WORKDIR /docs diff --git a/config/settings/base.py b/config/settings/base.py index 397f0eb..fa4ba11 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -76,6 +76,7 @@ "rest_framework", "rest_framework.authtoken", "corsheaders", + "fontawesomefree", ] LOCAL_APPS = [ diff --git a/config/urls.py b/config/urls.py index 74afa73..45fa17d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -25,6 +25,9 @@ path("accounts/", include("allauth.urls")), path("instance/", include("rocket_connect.instance.urls", namespace="instance")), # Your stuff: custom urls includes go here + re_path( + r"^connector/(?P\w+)/inbound/?$", views.connector_inbound_endpoint + ), re_path(r"^connector/(?P\w+)/?$", views.connector_endpoint), re_path(r"^server/(?P\w+)/?$", views.server_endpoint), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/local.yml b/local.yml index af0e690..c950e65 100644 --- a/local.yml +++ b/local.yml @@ -1,12 +1,9 @@ -version: "3.4" +version: "3.7" volumes: local_postgres_data: {} local_postgres_data_backups: {} - local_rocket_uploads: {} - local_rocket_db: {} - local_rocket_db_config: {} - local_rocket_db_dump: {} + local_rocket_mongodb_data: { driver: local } local_waautomate_data: {} local_wppconnect_data: {} local_wppconnect_tokens: {} @@ -25,6 +22,8 @@ services: depends_on: - postgres - mailhog + - redis + - celeryworker volumes: - .:/app:z env_file: @@ -72,8 +71,7 @@ services: command: /start-docs mailhog: - image: mailhog/mailhog:v1.0.0 - container_name: mailhog + image: mailhog/mailhog:v1.0.1 ports: - "8025:8025" @@ -120,45 +118,55 @@ services: retries: 3 command: /start-flower + rocketchat: - image: registry.rocket.chat/rocketchat/rocket.chat:latest - command: > - bash -c - "for i in `seq 1 30`; do - node main.js && - s=$$? && break || s=$$?; - echo \"Tried $$i times. Waiting 5 secs...\"; - sleep 5; - done; (exit $$s)" + image: registry.rocket.chat/rocketchat/rocket.chat:${RELEASE:-latest} restart: on-failure - volumes: - - local_rocket_uploads:/app/uploads environment: - - ADMIN_USERNAME=admin - - ADMIN_PASS=admin - - ADMIN_EMAIL=admin@example.com - - PORT=3000 - #- ROOT_URL=http://localhost:3000 - - MONGO_URL=mongodb://mongo:27017/rocketchat - - MONGO_OPLOG_URL=mongodb://mongo:27017/local - - OVERWRITE_SETTING_Accounts_TwoFactorAuthentication_Enforce_Password_Fallback=false - - OVERWRITE_SETTING_SMTP_Host=mailhog - - OVERWRITE_SETTING_SMTP_Port=1025 - - OVERWRITE_SETTING_From_Email=from@email.com - - OVERWRITE_SETTING_API_Enable_Rate_Limiter=false - - OVERWRITE_SETTING_Livechat_validate_offline_email=false - - CREATE_TOKENS_FOR_USERS=true - - OVERWRITE_SETTING_Accounts_SystemBlockedUsernameList=administrator,system,user + MONGO_URL: "${MONGO_URL:-\ + mongodb://${MONGODB_ADVERTISED_HOSTNAME:-mongodb}:${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017}/\ + ${MONGODB_DATABASE:-rocketchat}?replicaSet=${MONGODB_REPLICA_SET_NAME:-rs0}}" + MONGO_OPLOG_URL: "${MONGO_OPLOG_URL:\ + -mongodb://${MONGODB_ADVERTISED_HOSTNAME:-mongodb}:${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017}/\ + local?replicaSet=${MONGODB_REPLICA_SET_NAME:-rs0}}" + ROOT_URL: ${ROOT_URL:-http://localhost:${HOST_PORT:-3000}} + PORT: ${PORT:-3000} + ADMIN_USERNAME: admin + ADMIN_PASS: admin + ADMIN_EMAIL: admin@example.com + OVERWRITE_SETTING_Accounts_TwoFactorAuthentication_Enforce_Password_Fallback: "false" + OVERWRITE_SETTING_SMTP_Host: mailhog + OVERWRITE_SETTING_SMTP_Port: 1025 + OVERWRITE_SETTING_From_Email: from@email.com + OVERWRITE_SETTING_API_Enable_Rate_Limiter: "false" + OVERWRITE_SETTING_Livechat_validate_offline_email: "false" + CREATE_TOKENS_FOR_USERS: "true" + OVERWRITE_SETTING_Accounts_SystemBlockedUsernameList: administrator,system,user depends_on: - - mongo + - mongodb + expose: + - ${PORT:-3000} ports: - - 3000:3000 - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:3000/api/info" ] - interval: 1m30s - timeout: 10s - retries: 3 - start_period: 40s + - host_ip: ${BIND_IP:-0.0.0.0} + target: ${PORT:-3000} + published: ${HOST_PORT:-3000} + protocol: tcp + mode: host + + mongodb: + image: docker.io/bitnami/mongodb:${MONGODB_VERSION:-4.4} + restart: on-failure + volumes: + - local_rocket_mongodb_data:/bitnami/mongodb + environment: + MONGODB_REPLICA_SET_MODE: primary + MONGODB_REPLICA_SET_NAME: ${MONGODB_REPLICA_SET_NAME:-rs0} + MONGODB_PORT_NUMBER: ${MONGODB_PORT_NUMBER:-27017} + MONGODB_INITIAL_PRIMARY_HOST: ${MONGODB_INITIAL_PRIMARY_HOST:-mongodb} + MONGODB_INITIAL_PRIMARY_PORT_NUMBER: ${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017} + MONGODB_ADVERTISED_HOSTNAME: ${MONGODB_ADVERTISED_HOSTNAME:-mongodb} + MONGODB_ENABLE_JOURNAL: ${MONGODB_ENABLE_JOURNAL:-true} + ALLOW_EMPTY_PASSWORD: ${ALLOW_EMPTY_PASSWORD:-yes} webdav: image: bytemark/webdav @@ -172,27 +180,6 @@ services: ports: - 8011:80 - mongo: - image: mongo:4.4 - restart: on-failure - ports: - - 27017:27017 - volumes: - - local_rocket_db:/data/db - - local_rocket_db_config:/data/configdb - - local_rocket_db_dump:/dump - command: mongod --oplogSize 128 --replSet rs0 --storageEngine=wiredTiger - labels: - - "traefik.enable=false" - - mongo-init-replica: - image: mongo:4.4 - command: 'bash -c "for i in `seq 1 30`; do mongo mongo/rocketchat --eval - \"rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: - ''localhost:27017'' } ]})\" && s=$$? && break || s=$$?; echo \"Tried $$i - times. Waiting 5 secs...\"; sleep 5; done; (exit $$s)"' - depends_on: - - mongo browser: image: browserless/chrome:1.48.0-chrome-stable diff --git a/merge_production_dotenvs_in_dotenv.py b/merge_production_dotenvs_in_dotenv.py index d1170ef..09fedbb 100644 --- a/merge_production_dotenvs_in_dotenv.py +++ b/merge_production_dotenvs_in_dotenv.py @@ -18,7 +18,7 @@ def merge( ) -> None: with open(output_file_path, "w") as output_file: for merged_file_path in merged_file_paths: - with open(merged_file_path, "r") as merged_file: + with open(merged_file_path) as merged_file: merged_file_content = merged_file.read() output_file.write(merged_file_content) if append_linesep: @@ -41,7 +41,7 @@ def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool): for i in range(merged_file_count): merged_file_ord = i + 1 - merged_filename = ".service{}".format(merged_file_ord) + merged_filename = f".service{merged_file_ord}" merged_file_path = tmp_dir_path / merged_filename merged_file_content = merged_filename * merged_file_ord @@ -57,7 +57,7 @@ def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool): merge(output_file_path, merged_file_paths, append_linesep) - with open(output_file_path, "r") as output_file: + with open(output_file_path) as output_file: actual_output_file_content = output_file.read() assert actual_output_file_content == expected_output_file_content diff --git a/requirements/base.txt b/requirements/base.txt index 88a50ce..0fe006d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,33 +1,34 @@ pytz # https://github.com/stub42/pytz -python-slugify==5.0.2 # https://github.com/un33k/python-slugify -Pillow==8.4.0 # https://github.com/python-pillow/Pillow -argon2-cffi==21.1.0 # https://github.com/hynek/argon2_cffi -whitenoise==5.3.0 # https://github.com/evansd/whitenoise -redis==3.5.3 # https://github.com/andymccurdy/redis-py +python-slugify==6.1.2 # https://github.com/un33k/python-slugify +Pillow==9.1.1 # https://github.com/python-pillow/Pillow +argon2-cffi==21.3.0 # https://github.com/hynek/argon2_cffi +whitenoise==6.1.0 # https://github.com/evansd/whitenoise +redis==4.3.1 # https://github.com/andymccurdy/redis-py hiredis==2.0.0 # https://github.com/redis/hiredis-py -celery==5.2.0 # pyup: < 5.0,!=4.4.7 # https://github.com/celery/celery +celery==5.2.6 # pyup: < 5.0,!=4.4.7 # https://github.com/celery/celery django-celery-beat==2.2.1 # https://github.com/celery/django-celery-beat flower==1.0.0 # https://github.com/mher/flower -uvicorn[standard]==0.15.0 # https://github.com/encode/uvicorn +uvicorn[standard]==0.17.6 # https://github.com/encode/uvicorn # Django # ------------------------------------------------------------------------------ -django==3.2.9 # pyup: < 3.2 # https://www.djangoproject.com/ +django==3.2.13 # pyup: < 3.2 # https://www.djangoproject.com/ django-environ==0.8.1 # https://github.com/joke2k/django-environ django-model-utils==4.2.0 # https://github.com/jazzband/django-model-utils -django-allauth==0.45.0 # https://github.com/pennersr/django-allauth -django-crispy-forms==1.13.0 # https://github.com/django-crispy-forms/django-crispy-forms -django-redis==5.0.0 # https://github.com/jazzband/django-redis +django-allauth==0.50.0 # https://github.com/pennersr/django-allauth +django-crispy-forms==1.14.0 # https://github.com/django-crispy-forms/django-crispy-forms +django-redis==5.2.0 # https://github.com/jazzband/django-redis # Django REST Framework -djangorestframework==3.12.4 # https://github.com/encode/django-rest-framework -django-cors-headers==3.10.0 # https://github.com/adamchainz/django-cors-headers +djangorestframework==3.13.1 # https://github.com/encode/django-rest-framework +django-cors-headers==3.12.0 # https://github.com/adamchainz/django-cors-headers # RocketChat # ------------------------------------------------------------------------------ -rocketchat-API==1.20.0 # https://github.com/jadolg/rocketchat_API/ +rocketchat-API==1.25.0 # https://github.com/jadolg/rocketchat_API/ # QR # ------------------------------------------------------------------------------ +#zbar zbarlight qrcode[pil] @@ -35,6 +36,7 @@ qrcode[pil] # HELPERS # requests-toolbelt==0.9.1 +fontawesomefree==5.15.4 # ASTERISK CONNECTOR PLUGIN diff --git a/requirements/local.txt b/requirements/local.txt index 00602c3..f5c7f92 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,37 +1,37 @@ -r base.txt -Werkzeug==2.0.2 # https://github.com/pallets/werkzeug +Werkzeug==2.1.2 # https://github.com/pallets/werkzeug ipdb==0.13.9 # https://github.com/gotcha/ipdb -psycopg2==2.9.2 # https://github.com/psycopg/psycopg2 -watchgod==0.7 # https://github.com/samuelcolvin/watchgod +psycopg2==2.9.3 # https://github.com/psycopg/psycopg2 +watchgod==0.8.2 # https://github.com/samuelcolvin/watchgod # Testing # ------------------------------------------------------------------------------ -mypy==0.910 # https://github.com/python/mypy -django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs -pytest==6.2.5 # https://github.com/pytest-dev/pytest +mypy==0.950 # https://github.com/python/mypy +django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs +pytest==7.1.2 # https://github.com/pytest-dev/pytest pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar # Documentation # ------------------------------------------------------------------------------ -sphinx==4.3.0 # https://github.com/sphinx-doc/sphinx +sphinx==4.5.0 # https://github.com/sphinx-doc/sphinx sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild # Code quality # ------------------------------------------------------------------------------ flake8==4.0.1 # https://github.com/PyCQA/flake8 flake8-isort==4.1.1 # https://github.com/gforcada/flake8-isort -coverage==6.1.2 # https://github.com/nedbat/coveragepy -black==20.8b1 # https://github.com/ambv/black -pylint-django==2.4.4 # https://github.com/PyCQA/pylint-django +coverage==6.3.3 # https://github.com/nedbat/coveragepy +black==22.3.0 # https://github.com/ambv/black +pylint-django==2.5.3 # https://github.com/PyCQA/pylint-django pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery -pre-commit==2.15.0 # https://github.com/pre-commit/pre-commit +pre-commit==2.19.0 # https://github.com/pre-commit/pre-commit # Django # ------------------------------------------------------------------------------ factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy -django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar +django-debug-toolbar==3.4.0 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions -django-coverage-plugin==2.0.2 # https://github.com/nedbat/django_coverage_plugin -pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django +django-coverage-plugin==2.0.3 # https://github.com/nedbat/django_coverage_plugin +pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django diff --git a/requirements/production.txt b/requirements/production.txt index ac3b295..3c6a79b 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -3,8 +3,8 @@ -r base.txt gunicorn==20.1.0 # https://github.com/benoitc/gunicorn -psycopg2==2.9.2 # https://github.com/psycopg/psycopg2 +psycopg2==2.9.3 # https://github.com/psycopg/psycopg2 # Django # ------------------------------------------------------------------------------ -django-anymail==8.4 # https://github.com/anymail/django-anymail +django-anymail==8.6 # https://github.com/anymail/django-anymail diff --git a/rocket_connect/__init__.py b/rocket_connect/__init__.py index 2a236f8..58c2d40 100644 --- a/rocket_connect/__init__.py +++ b/rocket_connect/__init__.py @@ -1,7 +1,5 @@ -__version__ = "0.4.0" +__version__ = "1.0.0" __version_info__ = tuple( - [ - int(num) if num.isdigit() else num - for num in __version__.replace("-", ".", 1).split(".") - ] + int(num) if num.isdigit() else num + for num in __version__.replace("-", ".", 1).split(".") ) diff --git a/rocket_connect/asterisk/admin.py b/rocket_connect/asterisk/admin.py index 1f62a3a..143b007 100644 --- a/rocket_connect/asterisk/admin.py +++ b/rocket_connect/asterisk/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib import admin from .models import Call, CallMessages diff --git a/rocket_connect/envelope/admin.py b/rocket_connect/envelope/admin.py index 131894b..a8179ec 100644 --- a/rocket_connect/envelope/admin.py +++ b/rocket_connect/envelope/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib import admin from .models import LiveChatRoom, Message diff --git a/rocket_connect/envelope/migrations/0007_alter_message_type.py b/rocket_connect/envelope/migrations/0007_alter_message_type.py new file mode 100644 index 0000000..1a205a7 --- /dev/null +++ b/rocket_connect/envelope/migrations/0007_alter_message_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-05-18 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('envelope', '0006_auto_20210420_1735'), + ] + + operations = [ + migrations.AlterField( + model_name='message', + name='type', + field=models.CharField(choices=[['incoming', 'Incoming Message'], ['ingoing', 'Ingoing Message'], ['active_chat', 'Active Chat']], default='incoming', max_length=50), + ), + ] diff --git a/rocket_connect/envelope/migrations/0008_message_ack.py b/rocket_connect/envelope/migrations/0008_message_ack.py new file mode 100644 index 0000000..ee8146a --- /dev/null +++ b/rocket_connect/envelope/migrations/0008_message_ack.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-05-19 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('envelope', '0007_alter_message_type'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='ack', + field=models.BooleanField(default=False), + ), + ] diff --git a/rocket_connect/envelope/models.py b/rocket_connect/envelope/models.py index 7b07f6b..0c572c4 100644 --- a/rocket_connect/envelope/models.py +++ b/rocket_connect/envelope/models.py @@ -10,7 +10,7 @@ class Meta: verbose_name_plural = "Live Chat Rooms" def __str__(self): - return "{0} at {1}".format(self.token, self.room_id) + return f"{self.token} at {self.room_id}" uuid = models.UUIDField(default=uuid.uuid4, editable=False) connector = models.ForeignKey( @@ -79,6 +79,7 @@ def force_delivery(self): ) response = models.JSONField(blank=True, null=True, default=dict) delivered = models.BooleanField(default=False) + ack = models.BooleanField(default=False) # meta created = models.DateTimeField( blank=True, auto_now_add=True, verbose_name="Created" diff --git a/rocket_connect/instance/admin.py b/rocket_connect/instance/admin.py index 3c9b158..0cf54cf 100644 --- a/rocket_connect/instance/admin.py +++ b/rocket_connect/instance/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib import admin from .models import Connector, Server diff --git a/rocket_connect/instance/forms.py b/rocket_connect/instance/forms.py index 3179008..96d62f7 100644 --- a/rocket_connect/instance/forms.py +++ b/rocket_connect/instance/forms.py @@ -8,6 +8,7 @@ class Meta: fields = [ "name", "url", + "external_url", "secret_token", "admin_user_id", "admin_user_token", @@ -18,8 +19,12 @@ class Meta: class NewConnectorForm(ModelForm): def __init__(self, *args, **kwargs): server = kwargs.pop("server") - super(NewConnectorForm, self).__init__(*args, **kwargs) - connector_choices = [("wppconnect", "WPPConnect"), ("facebook", "Facebook")] + super().__init__(*args, **kwargs) + connector_choices = [ + ("wppconnect", "WPPConnect"), + ("facebook", "Facebook"), + ("metacloudapi_whatsapp", "Meta Cloud WhatsApp"), + ] # get departments rocket = server.get_rocket_client() departments_raw = rocket.call_api_get("livechat/department").json() diff --git a/rocket_connect/instance/management/commands/dev_settings.py b/rocket_connect/instance/management/commands/dev_settings.py index a97cff7..d8e9845 100644 --- a/rocket_connect/instance/management/commands/dev_settings.py +++ b/rocket_connect/instance/management/commands/dev_settings.py @@ -31,6 +31,7 @@ def handle_django(self): else: print("SERVER UPDATED") server.url = "http://rocketchat:3000" + server.external_url = "http://localhost:3000" server.admin_user = "admin" server.admin_password = "admin" server.bot_user = "bot" @@ -39,7 +40,7 @@ def handle_django(self): server.external_token = "SERVER_EXTERNAL_TOKEN" server.owners.add(admin) server.save() - # crete default 2 WA-automate connectors + # crete default WA-automate connector connectors2create = [ { "external_token": "CONNECTOR_EXTERNAL_TOKEN1", @@ -118,13 +119,25 @@ def handle_django(self): "secret_key": "THISISMYSECURETOKEN", "instance_name": "test", "include_connector_status": True, + "enable_ack_receipt": True, "outcome_attachment_description_as_new_message": True, "active_chat_webhook_integration_token": "WPP_ZAPIT_TOKEN", "session_management_token": "session_management_secret_token", "department_triage_payload": { - "title": "Title for Button goes here", - "footer": "This is the footer for the message. Its optional to send", - "message": "Test Sending Buttons. Let me know what you think about this function in wppconnect?", + "message": "Message for your buttons", + "options": { + "title": "Title text", + "footer": "Footer text", + "useTemplateButtons": "true", + "buttons": [ + {"id": "2", "phoneNumber": "5531999999999", "text": "Call Us"}, + { + "id": "3", + "url": "https://wppconnect-team.github.io/", + "text": "Long Life WPPCONNECT", + }, + ], + }, }, "no_agent_online_alert_admin": "No agent online!. **Message**: {{body}} **From**: {{from}}", "session_taken_alert_template": "You are now talking with {{agent.name}}" @@ -158,6 +171,25 @@ def handle_django(self): else: print("CONNECTOR UPDATED: ", connector) + # create default 1 meta cloud connector + connector, connector_created = server.connectors.get_or_create( + external_token="META_CLOUD_API_WHATSAPP" + ) + connector.config = { + "verify_token": "verify_token_here", + "bearer_token": "generate this at facebook for developers", + "endpoint": "https://graph.facebook.com/v13.0/111042638282794/", + } + connector.name = "META CLOUD API WHATSAPP" + connector.connector_type = "metacloudapi_whatsapp" + connector.managers = "" + connector.department = "METACLOUD-DEPARTMENT" + connector.save() + if connector_created: + print("CONNECTOR CREATED: ", connector) + else: + print("CONNECTOR UPDATED: ", connector) + def handle_rocketchat(self): server = Server.objects.first() rocket = server.get_rocket_client() @@ -247,6 +279,29 @@ def handle_rocketchat(self): ], } rocket.call_api_post("livechat/department", **new_department) + # + # ADD META CLOUD API DEPARTMENT + # + new_department = { + "department": { + "_id": "metacloud_api_department", + "enabled": True, + "showOnRegistration": True, + "showOnOfflineForm": True, + "email": "metacloud@email.com", + "name": "METACLOUD-DEPARTMENT", + "description": """meta cloud department created by dev_settings""", + }, + "agents": [ + { + "agentId": aa[user].json()["user"]["_id"], + "username": aa[user].json()["user"]["username"], + "count": 0, + "order": 0, + } + ], + } + rocket.call_api_post("livechat/department", **new_department) for user in ["manager1", "manager2"]: data = { @@ -315,53 +370,67 @@ def handle_rocketchat(self): rocket.settings_update(config[0], config[1]) # create if dont exist: - r = rocket.call_api_get( - "integrations.get", integrationId="wppconnect-integration" - ) - if not r.ok: + integrations = rocket.call_api_get("integrations.list").json() + existing_integrations_name = [a["name"] for a in integrations["integrations"]] + if "WPPCONNECT ACTIVE CHAT INTEGRATION" not in existing_integrations_name: print("CREATING WEBHOOK FOR ZAPIT OUTGOING") payload = { - "_id": "wppconnect-integration", "type": "webhook-outgoing", - "name": "WPPCONNECT ACTIVE CHAT INTEGRATION", - "event": "sendMessage", "enabled": True, - "username": "rocket.cat", + "impersonateUser": True, + "event": "sendMessage", "urls": ["http://django:8000/connector/WPP_EXTERNAL_TOKEN/"], - "scriptEnabled": False, + "triggerWords": ["zapit"], + "targetRoom": "", "channel": "#manager_channel", - "triggerWords": [ - "zapit", - ], + "username": "rocket.cat", + "name": "WPPCONNECT ACTIVE CHAT INTEGRATION", + "alias": "", + "avatar": "", + "emoji": "", + "scriptEnabled": False, + "script": "", + "retryFailedCalls": True, + "retryCount": 6, + "retryDelay": "powers-of-ten", + "triggerWordAnywhere": False, + "runOnEdits": False, "token": "WPP_ZAPIT_TOKEN", } - rocket.call_api_post("integrations.create", **payload) + + c = rocket.call_api_post("integrations.create", **payload) + print(c.json()) else: print("WEBHOOK FOR ZAPIT OUTGOING ALREADY EXISTS") # create webhook wppconnect manager: - r = rocket.call_api_get( - "integrations.get", integrationId="wppconnect-manager-integration" - ) - if not r.ok: + if "WPPCONNECT MANAGER INTEGRATION" not in existing_integrations_name: print("CREATING WEBHOOK FOR WPPCONNECT MANAGER OUTGOING") payload = { - "_id": "wppconnect-manager-integration", "type": "webhook-outgoing", - "name": "WPPCONNECT MANAGER INTEGRATION", - "event": "sendMessage", "enabled": True, - "username": "rocket.cat", + "impersonateUser": True, + "event": "sendMessage", "urls": ["http://django:8000/connector/WPP_EXTERNAL_TOKEN/"], + "triggerWords": ["rc"], + "targetRoom": "", + "channel": "#manager_channel", + "username": "rocket.cat", + "name": "WPPCONNECT MANAGER INTEGRATION", + "alias": "", + "avatar": "", + "emoji": "", "scriptEnabled": True, "script": wpp_admin_script, - "channel": "#manager_channel", - "triggerWords": [ - "rc", - ], + "retryFailedCalls": True, + "retryCount": 6, + "retryDelay": "powers-of-ten", + "triggerWordAnywhere": False, + "runOnEdits": False, "token": "session_management_secret_token", } - rocket.call_api_post("integrations.create", **payload) + c = rocket.call_api_post("integrations.create", **payload) + print(c.json()) else: print("WEBHOOK FOR ZAPIT OUTGOING ALREADY EXISTS") diff --git a/rocket_connect/instance/migrations/0011_auto_20220518_1824.py b/rocket_connect/instance/migrations/0011_auto_20220518_1824.py new file mode 100644 index 0000000..3a186f4 --- /dev/null +++ b/rocket_connect/instance/migrations/0011_auto_20220518_1824.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.13 on 2022-05-18 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('instance', '0010_connector_secondary_connectors'), + ] + + operations = [ + migrations.AddField( + model_name='server', + name='external_url', + field=models.CharField(blank=True, max_length=150), + ), + migrations.AlterField( + model_name='connector', + name='managers', + field=models.CharField(blank=True, help_text='separate users or channels with comma, eg: user1,user2,user3,#channel1,#channel2', max_length=50, null=True), + ), + migrations.AlterField( + model_name='server', + name='admin_user_id', + field=models.CharField(blank=True, help_text='Admin User Personal Access Token', max_length=50), + ), + migrations.AlterField( + model_name='server', + name='bot_user_id', + field=models.CharField(blank=True, help_text='Bot User Personal Access Token', max_length=50), + ), + migrations.AlterField( + model_name='server', + name='managers', + field=models.CharField(help_text='separate users or channels with comma, eg: user1,user2,user3,#channel1,#channel2', max_length=50), + ), + migrations.AlterField( + model_name='server', + name='secret_token', + field=models.CharField(blank=True, help_text='same secret_token used at Rocket.Chat Omnichannel Webhook Config', max_length=50, null=True), + ), + ] diff --git a/rocket_connect/instance/migrations/0012_alter_server_external_token.py b/rocket_connect/instance/migrations/0012_alter_server_external_token.py new file mode 100644 index 0000000..5df5232 --- /dev/null +++ b/rocket_connect/instance/migrations/0012_alter_server_external_token.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-05-18 21:26 + +from django.db import migrations, models +import instance.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('instance', '0011_auto_20220518_1824'), + ] + + operations = [ + migrations.AlterField( + model_name='server', + name='external_token', + field=models.CharField(default=instance.models.random_string, help_text='This field is used to link the actual server', max_length=50, unique=True), + ), + ] diff --git a/rocket_connect/instance/migrations/0013_auto_20220518_1833.py b/rocket_connect/instance/migrations/0013_auto_20220518_1833.py new file mode 100644 index 0000000..08bcbdc --- /dev/null +++ b/rocket_connect/instance/migrations/0013_auto_20220518_1833.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2022-05-18 21:33 + +from django.db import migrations, models +import instance.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('instance', '0012_alter_server_external_token'), + ] + + operations = [ + migrations.AlterField( + model_name='server', + name='external_token', + field=models.CharField(default=instance.models.random_string, max_length=50, unique=True), + ), + migrations.AlterField( + model_name='server', + name='external_url', + field=models.CharField(blank=True, help_text='This field is used to link to actual server', max_length=150), + ), + ] diff --git a/rocket_connect/instance/migrations/0014_alter_server_external_url.py b/rocket_connect/instance/migrations/0014_alter_server_external_url.py new file mode 100644 index 0000000..4cdb4fc --- /dev/null +++ b/rocket_connect/instance/migrations/0014_alter_server_external_url.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-05-18 21:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('instance', '0013_auto_20220518_1833'), + ] + + operations = [ + migrations.AlterField( + model_name='server', + name='external_url', + field=models.CharField(blank=True, help_text='This field is used to link to actual server. If blank, url is used.', max_length=150), + ), + ] diff --git a/rocket_connect/instance/models.py b/rocket_connect/instance/models.py index 1b074e9..09aa639 100644 --- a/rocket_connect/instance/models.py +++ b/rocket_connect/instance/models.py @@ -100,36 +100,27 @@ def get_managers_channel(self, as_string=True): def get_open_rooms(self): rocket = self.get_rocket_client() rooms = rocket.livechat_rooms(open="true") - if rooms.ok: + if rooms.ok and rooms.json().get("rooms"): return rooms.json()["rooms"] else: return [] - def sync_open_rooms(self, default_connector=None, filter_token=None): - """This method will get the open rooms, filter by a word in token - and recreate the rooms binded to the default connector. - The idea is to help on a migration where the actual open rooms has no - reference at Rocket Connect + def room_sync(self, execute=False): """ - rooms = self.get_open_rooms() - for room in rooms: - if filter_token: - # pass if do not match filter - if filter_token not in room.get("v", {}).get("token"): - pass - else: - LiveChatRoom = apps.get_model("envelope.LiveChatRoom") - room_item, created = LiveChatRoom.objects.get_or_create( - connector=default_connector, - token=room.get("v", {}).get("token"), - room_id=room.get("_id", {}), - ) - room_item.open = True - room_item.save() - if created: - print("ROOM CREATED:", room["v"]["token"]) - else: - print("ROOM UPDATED:", room["v"]["token"]) + Close all open rooms not open in Rocket.Chat + """ + open_rooms = self.get_open_rooms() + open_rooms_id = [r["_id"] for r in open_rooms] + # get all open rooms in connector, except the actually open ones + LiveChatRoom = apps.get_model(app_label="envelope", model_name="LiveChatRoom") + close_rooms = LiveChatRoom.objects.filter( + connector__server=self, open=True + ).exclude(room_id__in=open_rooms_id) + total = close_rooms.count() + if execute: + close_rooms.update(open=False) + response = {"total": total} + return response def force_delivery(self): """ @@ -138,6 +129,26 @@ def force_delivery(self): for connector in self.connectors.all(): connector.force_delivery() + def multiple_connector_admin_message(self, text): + """ + this method will send an admin message to all active connectors + """ + output = [] + for connector in self.connectors.filter(enabled=True): + Connector = connector.get_connector_class() + conncetor_cls = Connector(connector, message={}, type="outgoing") + text = f"{connector.name} > {text}" + message_sent = conncetor_cls.outcome_admin_message(text=text) + output.append(message_sent) + if output and all(output): + return True + return False + + def get_external_url(self): + if not self.external_url: + return self.url + return self.external_url + uuid = models.UUIDField(default=uuid.uuid4, editable=False) owners = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name="servers", blank=True @@ -152,6 +163,11 @@ def force_delivery(self): name = models.CharField(max_length=50) enabled = models.BooleanField(default=True) url = models.CharField(max_length=150) + external_url = models.CharField( + max_length=150, + blank=True, + help_text="This field is used to link to actual server. If blank, url is used.", + ) admin_user = models.CharField(max_length=50) admin_password = models.CharField(max_length=50) admin_user_id = models.CharField( @@ -187,7 +203,7 @@ def __str__(self): def get_connector_class(self): connector_type = self.connector_type # import the connector plugin - plugin_string = "rocket_connect.plugins.{0}".format(connector_type) + plugin_string = f"rocket_connect.plugins.{connector_type}" try: plugin = __import__(plugin_string, fromlist=["Connector"]) # no connector plugin, going base @@ -204,7 +220,7 @@ def get_connector_class(self): def get_connector_config_form(self): connector_type = self.connector_type # import the connector plugin - plugin_string = "rocket_connect.plugins.{0}".format(connector_type) + plugin_string = f"rocket_connect.plugins.{connector_type}" try: plugin = __import__(plugin_string, fromlist=["ConnectorConfigForm"]) # return form or false @@ -274,7 +290,7 @@ def intake(self, request): ) # log it connector.logger_info( - "RUNING SECONDARY CONNECTOR *{0}* WITH BODY {1}:".format( + "RUNING SECONDARY CONNECTOR *{}* WITH BODY {}:".format( sconnector.connector, request.body ) ) @@ -290,6 +306,14 @@ def outtake(self, message): # income message connector.outgoing() + def inbound_intake(self, request): + # get connector + Connector = self.get_connector_class() + # initiate with raw message + connector = Connector(self, {}, "inbound") + # handle inbound requests + return connector.handle_inbound(request) + def get_managers(self, as_string=True): """ this method will return the managers both from server and @@ -346,6 +370,21 @@ def connector_status(self): total_visitors=models.Count("room__token", distinct=True), ) + def room_sync(self, execute=False): + """ + Close all open rooms not open in Rocket.Chat + """ + rocket = self.server.get_rocket_client() + open_rooms = rocket.livechat_rooms(open="true").json() + open_rooms_id = [r["_id"] for r in open_rooms["rooms"]] + # get all open rooms in connector, except the actually open ones + close_rooms = self.rooms.filter(open=True).exclude(room_id__in=open_rooms_id) + total = close_rooms.count() + if execute: + close_rooms.update(open=False) + response = {"total": total} + return response + uuid = models.UUIDField(default=uuid.uuid4, editable=False) external_token = models.CharField(max_length=50, default=random_string, unique=True) name = models.CharField( diff --git a/rocket_connect/instance/views.py b/rocket_connect/instance/views.py index f376350..c9ce671 100644 --- a/rocket_connect/instance/views.py +++ b/rocket_connect/instance/views.py @@ -22,6 +22,22 @@ def connector_endpoint(request, connector_id): return return_response +@csrf_exempt +def connector_inbound_endpoint(request, connector_id): + connector = get_object_or_404( + Connector, external_token=connector_id, enabled=True, server__enabled=True + ) + return_response = connector.inbound_intake(request) + if not return_response: + return HttpResponse("No inbound return.", status=404) + # it can request a redirect + if return_response.get("redirect"): + return redirect(return_response["redirect"]) + if return_response.get("notfound"): + return HttpResponse(return_response.get("notfound"), status=404) + return JsonResponse(return_response) + + # Custom decorator def must_be_yours(func): def check_and_call(request, *args, **kwargs): @@ -55,7 +71,15 @@ def server_endpoint(request, server_id): # roketchat test message # if raw_message.get("_id") == "fasd6f5a4sd6f8a4sdf": - return JsonResponse({}) + message_sent = server.multiple_connector_admin_message( + "Rocket.Chat Omnichannel Connection Test was Received. This is the response." + ) + if message_sent: + return JsonResponse({}) + else: + return HttpResponse( + "Unauthorized. No X-Rocketchat-Livechat-Token provided.", status=401 + ) else: # process ingoing message try: @@ -82,7 +106,7 @@ def server_endpoint(request, server_id): secondary_connector, request.body, "ingoing", request ) connector.logger_info( - "RUNING SECONDARY CONNECTOR *{0}* WITH BODY {1}:".format( + "RUNING SECONDARY CONNECTOR *{}* WITH BODY {}:".format( sconnector.connector, request.body ) ) @@ -95,6 +119,7 @@ def server_endpoint(request, server_id): @must_be_yours def server_detail_view(request, server_id): server = get_object_or_404(Server.objects, external_token=server_id) + room_sync = None # get server status status = server.status() if request.GET.get("force_connector_delivery"): @@ -109,20 +134,27 @@ def server_detail_view(request, server_id): if message.delivered: messages.success( request, - "Sucess! Message #{0} was delivered at connector {1}".format( + "Sucess! Message #{} was delivered at connector {}".format( message.id, message.connector.name ), ) else: messages.error( request, - "Error! Could not deliver Message #{0} at connector {1}".format( + "Error! Could not deliver Message #{} at connector {}".format( message.id, message.connector.name ), ) return redirect(reverse("instance:server_detail", args=[server.external_token])) + if request.GET.get("check-room-sync"): + room_sync = server.room_sync() + if request.GET.get("do-check-room-sync"): + room_sync = server.room_sync(execute=True) + messages.success(request, "Sync Executed!") + room_sync = connector.room_sync() + connectors = ( server.connectors.distinct() .annotate( @@ -146,6 +178,7 @@ def server_detail_view(request, server_id): "server": server, "connectors": connectors, "status": status, + "room_sync": room_sync, } return render(request, "instance/server_detail_view.html", context) @@ -164,6 +197,7 @@ def connector_analyze(request, server_id, connector_id): connector_action_response["status_session"] = connector.status_session() undelivered_messages = None date = None + room_sync = None if request.GET.get("connector_action") == "status_session": connector_action_response["status_session"] = connector.status_session() @@ -193,23 +227,21 @@ def connector_analyze(request, server_id, connector_id): if delivery_happened: messages.success( request, - "Success! Message #{0} was delivered at connector {1}".format( + "Success! Message #{} was delivered at connector {}".format( message.id, connector.name ), ) else: messages.error( request, - "Error! Could not deliver Message #{0} at connector {1}".format( + "Error! Could not deliver Message #{} at connector {}".format( message.id, connector.name ), ) if request.GET.get("action") == "mark_as_delivered": undelivered_messages.update(delivered=True) for um in undelivered_messages: - messages.success( - request, "Message #{0} marked as delivered".format(um.id) - ) + messages.success(request, f"Message #{um.id} marked as delivered") if request.GET.get("action") == "show": # we want to show the messages, so just pass # as the other actions will redirect @@ -222,6 +254,13 @@ def connector_analyze(request, server_id, connector_id): ) ) + if request.GET.get("check-room-sync"): + room_sync = connector.room_sync() + if request.GET.get("do-check-room-sync"): + room_sync = connector.room_sync(execute=True) + messages.success(request, "Sync Executed!") + room_sync = connector.room_sync() + messages_undelivered_by_date = ( connector.messages.filter(delivered=False) .annotate(date=TruncDay("created")) @@ -239,9 +278,7 @@ def connector_analyze(request, server_id, connector_id): if config_form.is_valid(): # TODO: better save here config_form.save() - messages.success( - request, "Configurations changed for {0}".format(connector.name) - ) + messages.success(request, f"Configurations changed for {connector.name}") else: if config_form: config_form = config_form(connector=connector) @@ -251,6 +288,7 @@ def connector_analyze(request, server_id, connector_id): "messages_undelivered_by_date": messages_undelivered_by_date, "undelivered_messages": undelivered_messages, "date": date, + "room_sync": room_sync, "connector_action_response": connector_action_response, "config_form": config_form, "base_uri": base_uri, @@ -277,9 +315,7 @@ def new_connector(request, server_id): form = NewConnectorForm(instance=new_connector, server=server) if form.is_valid(): new_connector = form.save() - messages.success( - request, "New connector {0} created.".format(new_connector.name) - ) + messages.success(request, f"New connector {new_connector.name} created.") return redirect( reverse( "instance:connector_analyze", @@ -311,7 +347,7 @@ def new_server(request): reverse("instance:server_detail", args=[server.external_token]) ) else: - messages.error(request, "Error {0}".format(status)) + messages.error(request, f"Error {status}") context = {"form": form} return render(request, "instance/new_server.html", context) diff --git a/rocket_connect/plugins/asterisk.py b/rocket_connect/plugins/asterisk.py index 5d4008e..fb56a24 100644 --- a/rocket_connect/plugins/asterisk.py +++ b/rocket_connect/plugins/asterisk.py @@ -158,7 +158,7 @@ def hook_queue_caller_leave(self): if notify[0] == "#": if settings.DEBUG: print( - "NOTIFYING CHANNEL {0} CALLER LEFT QUEUE: ".format(notify), + f"NOTIFYING CHANNEL {notify} CALLER LEFT QUEUE: ", self.message, ) # notify a channel @@ -174,7 +174,7 @@ def hook_queue_caller_leave(self): else: if settings.DEBUG: print( - "NOTIFYING USER {0} CALLER LEFT QUEUE: ".format(notify), + f"NOTIFYING USER {notify} CALLER LEFT QUEUE: ", self.message, ) room = self.rocket.im_create(username=notify) diff --git a/rocket_connect/plugins/base.py b/rocket_connect/plugins/base.py index 56594c1..ee8ada2 100644 --- a/rocket_connect/plugins/base.py +++ b/rocket_connect/plugins/base.py @@ -22,7 +22,7 @@ from emojipy import emojipy -class Connector(object): +class Connector: def __init__(self, connector, message, type, request=None): self.connector = connector self.type = type @@ -49,23 +49,21 @@ def close_session(self): return True def logger_info(self, message): - output = "{0} > {1} > {2}".format(self.connector, self.type.upper(), message) + output = f"{self.connector} > {self.type.upper()} > {message}" if self.message: if self.get_message_id(): - output = "MESSAGE ID {0} > ".format(self.get_message_id()) + output + output = f"MESSAGE ID {self.get_message_id()} > " + output self.logger.info(output) def logger_error(self, message): - self.logger.error( - "{0} > {1} > {2}".format(self.connector, self.type.upper(), message) - ) + self.logger.error(f"{self.connector} > {self.type.upper()} > {message}") def incoming(self): """ this method will process the incoming messages and ajust what necessary, to output to rocketchat """ - self.logger_info("INCOMING MESSAGE: {0}".format(self.message)) + self.logger_info(f"INCOMING MESSAGE: {self.message}") return JsonResponse( { "connector": self.connector.name, @@ -97,7 +95,7 @@ def outcome_qrbase64(self, qrbase64): rocket.rooms_upload( rid=im_room_created["room"]["rid"], file=tmp.name, - msg=":rocket: Connect > *Connector Name*: {0}".format( + msg=":rocket: Connect > *Connector Name*: {}".format( self.connector.name ), description="Scan this QR Code at your Whatsapp Phone:", @@ -113,19 +111,19 @@ def outcome_qrbase64(self, qrbase64): send_qr_code = rocket.rooms_upload( rid=rid, file=tmp.name, - msg=":rocket: Connect > *Connector Name*: {0}".format( + msg=":rocket: Connect > *Connector Name*: {}".format( self.connector.name ), description="Scan this QR Code at your Whatsapp Phone:", ) self.logger_info( - "SENDING QRCODE TO ROOM... {0}: {1}".format( + "SENDING QRCODE TO ROOM... {}: {}".format( channel, send_qr_code.json() ) ) else: self.logger_error( - "FAILED TO SEND QRCODE TO ROOM... {0}: {1}".format( + "FAILED TO SEND QRCODE TO ROOM... {}: {}".format( channel, room_infos.json() ) ) @@ -151,11 +149,11 @@ def outcome_file(self, base64_data, room_id, mime, filename=None, description=No data = {} if description: data["description"] = description - url = "{0}/api/v1/livechat/upload/{1}".format( + url = "{}/api/v1/livechat/upload/{}".format( self.connector.server.url, room_id ) deliver = requests.post(url, headers=headers, files=files, data=data) - self.logger_info("RESPONSE OF FILE OUTCOME: {0}".format(deliver.json())) + self.logger_info(f"RESPONSE OF FILE OUTCOME: {deliver.json()}") timestamp = int(time.time()) if self.message_object: self.message_object.payload[timestamp] = { @@ -187,13 +185,11 @@ def outcome_text(self, room_id, text, message_id=None): self.message_object.payload[timestamp] = json.loads(deliver.request.body) self.message_object.response[timestamp] = deliver.json() if settings.DEBUG: - self.logger_info("DELIVERING... {0}".format(deliver.request.body)) - self.logger_info("RESPONSE... {0}".format(deliver.json())) + self.logger_info(f"DELIVERING... {deliver.request.body}") + self.logger_info(f"RESPONSE... {deliver.json()}") if deliver.ok: if settings.DEBUG: - self.logger_info( - "MESSAGE DELIVERED... {0}".format(deliver.request.body) - ) + self.logger_info(f"MESSAGE DELIVERED... {deliver.request.body}") if self.message_object: self.message_object.delivered = True self.message_object.room = self.room @@ -239,6 +235,7 @@ def generate_qrcode(self, code): return img_str def outcome_admin_message(self, text): + output = [] managers = self.connector.get_managers() managers_channel = self.connector.get_managers_channel(as_string=False) if settings.DEBUG: @@ -250,32 +247,38 @@ def outcome_admin_message(self, text): response = im_room.json() if settings.DEBUG: print("CREATE ADMIN ROOM TO OUTCOME", im_room.json()) - text_message = ":rocket: CONNECT {0}".format(text) + text_message = f":rocket: CONNECT {text}" if response.get("success"): if settings.DEBUG: print("SENDING ADMIN MESSAGE") - self.rocket.chat_post_message( + direct_message = self.rocket.chat_post_message( alias=self.connector.name, text=text_message, room_id=response["room"]["rid"], ) + output.append(direct_message.ok) # send to managers channel for manager_channel in managers_channel: manager_channel_message = self.rocket.chat_post_message( text=text_message, channel=manager_channel.replace("#", "") ) + output.append(manager_channel_message.ok) if manager_channel_message.ok: self.logger_info( - "OK! manager_channel_message payload received: {0}".format( + "OK! manager_channel_message payload received: {}".format( manager_channel_message.json() ) ) else: self.logger_info( - "ERROR! manager_channel_message: {0}".format( + "ERROR! manager_channel_message: {}".format( manager_channel_message.json() ) ) + if output and all(output): + return True + # return false + return False def get_visitor_name(self): try: @@ -286,7 +289,7 @@ def get_visitor_name(self): def get_visitor_username(self): try: - visitor_username = "whatsapp:{0}".format( + visitor_username = "whatsapp:{}".format( # works for wa-automate self.message.get("data", {}).get("from") ) @@ -370,12 +373,20 @@ def get_visitor_token(self): try: # this works for wa-automate EASYAPI visitor_id = self.get_visitor_id() - visitor_id = "whatsapp:{0}".format(visitor_id) + visitor_id = f"whatsapp:{visitor_id}" return visitor_id except IndexError: return "channel:visitor-id" - def get_room(self, department=None, create=True, allow_welcome_message=True): + def get_room( + self, + department=None, + create=True, + allow_welcome_message=True, + check_if_open=False, + force_transfer=None, + ): + open_rooms = None room = None room_created = False connector_token = self.get_visitor_token() @@ -383,7 +394,20 @@ def get_room(self, department=None, create=True, allow_welcome_message=True): room = LiveChatRoom.objects.get( connector=self.connector, token=connector_token, open=True ) - print("get_room, got: ", room) + self.logger_info(f"get_room, got {room}") + if check_if_open: + self.logger_info("checking if room is open") + open_rooms = self.rocket.livechat_rooms(open="true").json() + open_rooms_id = [r["_id"] for r in open_rooms["rooms"]] + if room.room_id not in open_rooms_id: + self.logger_info( + "room was open in Rocket.Connect, but not in Rocket.Chat" + ) + # close room + room.open = False + room.save() + raise LiveChatRoom.DoesNotExist + except LiveChatRoom.MultipleObjectsReturned: # this should not happen. Mitigation for issue #12 # TODO: replicate error at development @@ -396,7 +420,7 @@ def get_room(self, department=None, create=True, allow_welcome_message=True): ) except LiveChatRoom.DoesNotExist: if create: - print("get_room, didnt get for: ", connector_token) + self.logger_info("get_room, didn't got room") if self.config.get("open_room", True): # room not available, let's create one. # get the visitor json @@ -448,9 +472,25 @@ def get_room(self, department=None, create=True, allow_welcome_message=True): if settings.DEBUG: print("Erro! No Agents Online") self.room = room + # optionally force transfer to department + if force_transfer: + payload = { + "rid": self.room.room_id, + "token": self.room.token, + "department": force_transfer, + } + force_transfer_response = self.rocket.call_api_post( + "livechat/room.transfer", **payload + ) + if force_transfer_response.ok: + self.logger_info(f"Force Transfer Response: {force_transfer_response}") + else: + self.logger_error(f"Force Transfer ERROR: {force_transfer_response}") + # optionally allow welcome message if allow_welcome_message: if self.config.get("welcome_message"): + # only send welcome message when # 1 - open_room is False and there is a welcome_message # 2 - open_room is True, room_created is True and there is a welcome_message @@ -462,20 +502,22 @@ def get_room(self, department=None, create=True, allow_welcome_message=True): and room_created and self.config.get("welcome_message") ): - message = {"msg": self.config.get("welcome_message")} - self.outgo_text_message(message) - # if room was created - if room and self.config.get( - "alert_agent_of_automated_message_sent", False - ): - # let the agent know - self.outcome_text( - room.room_id, - "MESSAGE SENT: {0}".format( - self.config.get("welcome_message") - ), - message_id=self.get_message_id() + "WELCOME", + # if we have room, send it using the room + if room_created: + payload = { + "rid": self.room.room_id, + "msg": self.config.get("welcome_message"), + } + a = self.outgo_message_from_rocketchat(payload) + print("AQUI! ", a) + self.logger_info( + "OUTWENT welcome message from Rocket.Chat " + str(payload) ) + # no room, send directly + else: + message = {"msg": self.config.get("welcome_message")} + self.outgo_text_message(message) + if self.config.get("welcome_vcard") != {}: # only send welcome vcard when # @@ -498,7 +540,7 @@ def get_room(self, department=None, create=True, allow_welcome_message=True): # let the agent know self.outcome_text( room_id=room.room_id, - text="VCARD SENT: {0}".format( + text="VCARD SENT: {}".format( self.config.get("welcome_vcard") ), message_id=self.get_message_id() + "VCARD", @@ -521,7 +563,7 @@ def room_close_and_reintake(self, room): def room_send_text(self, room_id, text, message_id=None): if settings.DEBUG: - print("SENDING MESSAGE TO ROOM ID {0}: {1}".format(room_id, text)) + print(f"SENDING MESSAGE TO ROOM ID {room_id}: {text}") if not message_id: message_id = self.get_message_id() rocket = self.get_rocket_client() @@ -532,11 +574,11 @@ def room_send_text(self, room_id, text, message_id=None): _id=message_id, ) if settings.DEBUG: - self.logger_info("MESSAGE SENT. RESPONSE: {0}".format(response.json())) + self.logger_info(f"MESSAGE SENT. RESPONSE: {response.json()}") return response def register_message(self, type=None): - self.logger_info("REGISTERING MESSAGE: {0}".format(self.message)) + self.logger_info(f"REGISTERING MESSAGE: {self.message}") try: if not type: type = self.type @@ -548,17 +590,15 @@ def register_message(self, type=None): self.message_object.room = self.room self.message_object.save() if created: - self.logger_info( - "NEW MESSAGE REGISTERED: {0}".format(self.message_object.id) - ) + self.logger_info(f"NEW MESSAGE REGISTERED: {self.message_object.id}") else: self.logger_info( - "EXISTING MESSAGE REGISTERED: {0}".format(self.message_object.id) + f"EXISTING MESSAGE REGISTERED: {self.message_object.id}" ) return self.message_object, created except IntegrityError: self.logger_info( - "CANNOT CREATE THIS MESSAGE AGAIN: {0}".format(self.get_message_id()) + f"CANNOT CREATE THIS MESSAGE AGAIN: {self.get_message_id()}" ) return "", False @@ -592,15 +632,15 @@ def get_message_body(self): # this works for wa-automate EASYAPI message_body = self.message.get("data", {}).get("body") except IndexError: - message_body = "New Message: {0}".format( + message_body = "New Message: {}".format( "".join(random.choice(string.ascii_letters) for i in range(10)) ) return message_body - def get_rocket_client(self, bot=False): + def get_rocket_client(self, bot=False, force=False): # this will prevent multiple client initiation at the same # Classe initiation - if not self.rocket: + if not self.rocket or force: try: self.rocket = self.connector.server.get_rocket_client(bot=bot) except requests.exceptions.ConnectionError: @@ -609,6 +649,10 @@ def get_rocket_client(self, bot=False): self.rocket = False return self.rocket + def outgo_message_from_rocketchat(self, payload): + self.get_rocket_client(bot=True, force=True) + return self.rocket.chat_send_message(payload) + def rocket_down(self): if settings.DEBUG: print("DO SOMETHING FOR WHEN ROCKETCHAT SERVER IS DOWN") @@ -620,7 +664,7 @@ def joypixel_to_unicode(self, content): def decrypt_media(self, message_id=None): if not message_id: message_id = self.get_message_id() - url_decrypt = "{0}/decryptMedia".format(self.config["endpoint"]) + url_decrypt = "{}/decryptMedia".format(self.config["endpoint"]) payload = {"args": {"message": message_id}} s = self.get_request_session() decrypted_data_request = s.post(url_decrypt, json=payload) @@ -636,10 +680,8 @@ def decrypt_media(self, message_id=None): def close_room(self): if self.room: - if settings.DEBUG: - print("Closing Message...") - self.room.open = False - self.room.save() + # close all room from connector with same room_id + self.connector.rooms.filter(room_id=self.room.room_id).update(open=False) self.post_close_room() def post_close_room(self): @@ -654,7 +696,7 @@ def ingoing(self): this method will process the outcoming messages comming from Rocketchat, and deliver to the connector """ - self.logger_info("RECEIVED: {0}".format(self.message)) + self.logger_info(f"RECEIVED: {self.message}") # Session start if self.message.get("type") == "LivechatSessionStart": if settings.DEBUG: @@ -718,7 +760,7 @@ def ingoing(self): else: self.outgo_text_message(message, agent_name=agent_name) else: - self.logger_info("MESSAGE ALREADY SEND. IGNORING.") + self.logger_info("MESSAGE ALREADY SENT. IGNORING.") def get_agent_name(self, message): agent_name = message.get("u", {}).get("name", {}) @@ -732,20 +774,18 @@ def outgo_text_message(self, message, agent_name=None): this method should be overwritten to send the message back to the client """ if agent_name: - self.logger_info( - "OUTGOING MESSAGE {0} FROM AGENT {1}".format(message, agent_name) - ) + self.logger_info(f"OUTGOING MESSAGE {message} FROM AGENT {agent_name}") else: - self.logger_info("OUTGOING MESSAGE {0}".format(message)) + self.logger_info(f"OUTGOING MESSAGE {message}") return True def outgo_vcard(self, vcard_json): - self.logger_info("OUTGOING VCARD {0}".format(vcard_json)) + self.logger_info(f"OUTGOING VCARD {vcard_json}") def handle_incoming_call(self): if self.config.get("auto_answer_incoming_call"): self.logger_info( - "auto_answer_incoming_call: {0}".format( + "auto_answer_incoming_call: {}".format( self.config.get("auto_answer_incoming_call") ) ) @@ -757,11 +797,21 @@ def handle_incoming_call(self): self.room.room_id, text=self.config.get("convert_incoming_call_to_text"), ) + # mark incoming call message as delivered + m = self.message_object + m.delivered = True + m.save() + self.message_object = m + self.logger_info( + "handle_incoming_call marked message {} as read".format( + self.message_object.id + ) + ) def handle_ptt(self): if self.config.get("auto_answer_on_audio_message"): self.logger_info( - "auto_answer_on_audio_message: {0}".format( + "auto_answer_on_audio_message: {}".format( self.config.get("auto_answer_on_audio_message") ) ) @@ -792,21 +842,21 @@ def handle_livechat_session_taken(self): ignore_departments = [i for i in departments_list] if transferred_department in ignore_departments: self.logger_info( - "IGNORING LIVECHATSESSION Alert for DEPARTMENT {0}".format( + "IGNORING LIVECHATSESSION Alert for DEPARTMENT {}".format( self.message.get("department") ) ) # ignore this message return { "success": False, - "message": "Ignoring department {0}".format( + "message": "Ignoring department {}".format( self.message.get("department") ), } self.get_rocket_client() # enrich context with department data department = self.rocket.call_api_get( - "livechat/department/{0}".format(self.message.get("departmentId")) + "livechat/department/{}".format(self.message.get("departmentId")) ).json() self.message["department"] = department["department"] template = Template(self.config.get("session_taken_alert_template")) @@ -820,15 +870,25 @@ def handle_livechat_session_taken(self): # let the agent know self.outcome_text( self.room.room_id, - "MESSAGE SENT: {0}".format(message), + f"MESSAGE SENT: {message}", message_id=self.get_message_id() + "SESSION_TAKEN", ) outgo_text_obj = self.outgo_text_message(message_payload) - self.logger_info( - "HANDLING LIVECHATSESSION TAKEN {0}".format(outgo_text_obj) - ) + self.logger_info(f"HANDLING LIVECHATSESSION TAKEN {outgo_text_obj}") return outgo_text_obj + def handle_inbound(self, request): + """ + this method will handle inbound payloads + you can return + + {"success": True, "redirect":"http://rocket.chat"} + + for redirecting to a new page. + """ + self.logger_info("HANDLING INBOUND, returning default") + return {"success": True, "redirect": "http://rocket.chat"} + class BaseConnectorConfigForm(forms.Form): def __init__(self, *args, **kwargs): diff --git a/rocket_connect/plugins/facebook.py b/rocket_connect/plugins/facebook.py index b7b619a..17c0047 100644 --- a/rocket_connect/plugins/facebook.py +++ b/rocket_connect/plugins/facebook.py @@ -78,10 +78,10 @@ def incoming(self): if attachment.get("type") == "location": lat = attachment["payload"]["coordinates"]["lat"] lng = attachment["payload"]["coordinates"]["long"] - link = "https://www.google.com/maps/search/?api=1&query={0}+{1}".format( + link = "https://www.google.com/maps/search/?api=1&query={}+{}".format( lat, lng ) - text = "Lat:{0}, Long:{1}: Link: {2}".format( + text = "Lat:{}, Long:{}: Link: {}".format( lat, lng, link ) deliver = self.outcome_text(room.room_id, text) @@ -116,11 +116,11 @@ def get_incoming_visitor_id(self): def get_visitor_token(self): visitor_id = self.get_visitor_id() - token = "facebook:{0}".format(visitor_id) + token = f"facebook:{visitor_id}" return token def get_visitor_username(self): - return "facebook:{0}".format(self.get_visitor_id()) + return f"facebook:{self.get_visitor_id()}" def get_visitor_json(self): # cal api to get more infos @@ -131,7 +131,7 @@ def get_visitor_json(self): print("GETTING FACEBOOK CONTACT: ", url) print("GOT: ", data.json()) if data.ok: - visitor_name = "{0} {1}".format( + visitor_name = "{} {}".format( data.json()["first_name"], data.json()["last_name"] ) else: @@ -177,7 +177,7 @@ def outgo_text_message(self, message, agent_name=None): content = message["msg"] # replace emojis content = self.joypixel_to_unicode(content) - url = "https://graph.facebook.com/v2.6/me/messages?access_token={0}".format( + url = "https://graph.facebook.com/v2.6/me/messages?access_token={}".format( self.connector.config["access_token"] ) payload = {"recipient": {"id": visitor_id}, "message": {"text": content}} diff --git a/rocket_connect/plugins/metacloudapi_whatsapp.py b/rocket_connect/plugins/metacloudapi_whatsapp.py new file mode 100644 index 0000000..618c1c6 --- /dev/null +++ b/rocket_connect/plugins/metacloudapi_whatsapp.py @@ -0,0 +1,249 @@ +import base64 +import time + +import requests +from django import forms +from django.http import HttpResponse, HttpResponseForbidden, JsonResponse + +from .base import BaseConnectorConfigForm +from .base import Connector as ConnectorBase + + +class Connector(ConnectorBase): + + # main incoming hub + def incoming(self): + self.logger_info(f"INCOMING MESSAGE: {self.message}") + # no rocket client, abort + # get rocket client + self.get_rocket_client() + if not self.rocket: + return HttpResponse("Rocket Down!", status=503) + + # handle challenge + if self.request and self.request.GET.get("hub.mode") == "subscribe": + return self.handle_challenge() + # handle regular messages + self.raw_message = self.message + if self.message.get("object") == "whatsapp_business_account": + # it can be a forced delivery + if not self.message.get("entry"): + # here it can be a status message + self.handle_message() + else: + for entry in self.message.get("entry"): + for change in entry["changes"]: + self.change = change + # it has messages + if change["value"].get("messages"): + for message in change["value"]["messages"]: + self.message = message + # enrich message + self.message["metadata"] = change["value"]["metadata"] + self.message["contacts"] = change["value"]["contacts"] + self.message["object"] = self.raw_message["object"] + # handle text message + self.handle_message() + # it has a status receipt + if change["value"].get("statuses"): + # TODO: handle read receipt + pass + + return JsonResponse({}) + + def handle_challenge(self): + self.logger_info( + "VERIFYING META CLOUD ENDPOINT against with path: " + + str(self.request.get_full_path) + ) + verify_token = self.request.GET.get("hub.verify_token") + if verify_token == self.connector.config.get("verify_token"): + self.logger_info( + "VERIFYING META CLOUD ENDPOINT against: " + + str(self.request.GET.get("hub.challenge")) + ) + challenge = self.request.GET.get("hub.challenge") + text = "Connector: {}. Status: {}".format( + self.connector.name, + """:white_check_mark: :white_check_mark: :white_check_mark: :satellite:""" + + "Endpoint Sucessfuly verified by Meta Cloud!", + ) + self.outcome_admin_message(text) + return HttpResponse(challenge) + else: + self.logger_info("ERROR VERIFYING META CLOUD ENDPOINT") + text = "Connector: {}. Status: {}".format( + self.connector.name, + """:warning: :warning: :warning: :satellite: *endpoint NOT VERIFIED* by Meta Cloud!""", + ) + self.outcome_admin_message(text) + return HttpResponseForbidden() + + def handle_message(self): + message, created = self.register_message() + room = self.get_room() + if self.message.get("type") == "text": + # outcome text message + # + self.outcome_text(room.room_id, self.message["text"]["body"]) + allowed_media_types = self.config.get( + "allowed_media_types", "audio,image,video,documento,sticker" + ).split(",") + if self.message.get("type") in allowed_media_types: + # outcome text message + # + self.handle_media() + else: + # outcome text message, alerting this is not allowed + # TODO, improve this to outcome and outgo customizable messages + payload = { + "rid": self.room.room_id, + "msg": "This media type is not allowed", + } + self.outgo_message_from_rocketchat(payload) + message.delivered = True + message.save() + + # no room was generated + # + if not room: + return JsonResponse({"message": "no room generated"}) + + def handle_media(self): + # register message + message, created = self.register_message() + room = self.get_room() + # get media id + media_type = self.message["type"] + media_id = self.message[media_type]["id"] + # get media url + url = "https://graph.facebook.com/v13.0/" + media_id + session = self.get_request_session() + media_info = session.get( + url, + ) + mime = media_info.json().get("mime_type") + description = None + if self.message.get("image", {}).get("caption"): + description = self.message.get("image", {}).get("caption") + + # get media base64 + media_url = media_info.json().get("url") + base64_data = base64.b64encode(session.get(media_url).content) + self.outcome_file(base64_data, room.room_id, mime, description) + + def get_incoming_message_id(self): + return self.message.get("id") + + def get_visitor_phone(self): + return self.message["from"] + + def get_visitor_name(self): + return self.message["contacts"][0]["profile"]["name"] + + def get_visitor_token(self): + return "whatsapp:" + self.message["from"] + "@c.us" + + def get_request_session(self): + s = requests.Session() + s.headers = {"content-type": "application/json"} + token = self.connector.config.get("bearer_token") + if token: + s.headers.update({"Authorization": f"Bearer {token}"}) + return s + + def outgo_text_message(self, message, agent_name=None): + sent = False + if type(message) == str: + content = message + else: + content = message["msg"] + content = self.joypixel_to_unicode(content) + # message may not have an agent + if agent_name: + content = "*[" + agent_name + "]*\n" + content + session = self.get_request_session() + url = self.connector.config["endpoint"] + "messages" + # get number + number = self.message["visitor"]["token"].split("@")[0].split(":")[1] + payload = { + "messaging_product": "whatsapp", + "preview_url": False, + "recipient_type": "individual", + "to": number, + "type": "text", + "text": {"body": self.message["messages"][0]["msg"]}, + } + self.logger_info(f"OUTGOING TEXT MESSAGE. URL: {url}. PAYLOAD {payload}") + # try with regular number + sent = session.post(url, json=payload) + # this is due to BRazil's WhatsApp API not handling 55319[9] correctly + if not sent.ok: + if payload["to"].startswith("55"): + # 5531XXXXXXXX - > 55319XXXXXXXX + payload["to"] = payload["to"].replace( + payload["to"][:4], payload["to"][:4] + "9" + ) + # retry with different number + sent = session.post(url, json=payload) + # we got the message sent! + if sent.ok: + timestamp = int(time.time()) + if self.message_object: + self.message_object.delivered = sent.ok + self.message_object.response[timestamp] = sent.json() + if sent.ok: + if not self.message_object.response.get("id"): + self.message_object.response["id"] = [ + sent.json()["messages"][0]["id"] + ] + else: + self.message_object.response["id"].append( + sent.json()["messages"][0]["id"] + ) + self.message_object.save() + # message not sent + else: + self.logger_info(f"ERROR SENDING MESSAGE {sent.json()}") + if self.message_object: + self.message_object.delivered = False + self.logger_info(f"CONNECTOR DOWN: {self.connector}") + + return sent + + def status_session(self): + # generate token + status = {} + if self.config.get("endpoint"): + endpoint = self.config.get("endpoint") + session = self.get_request_session() + status_req = session.get(endpoint) + if status_req.ok: + status = status_req.json() + return status + + +class ConnectorConfigForm(BaseConnectorConfigForm): + + endpoint = forms.CharField( + help_text="Where to connect to your meta cloud accont", + required=True, + initial="", + ) + verify_token = forms.CharField( + help_text="The verify token for the Challenge", + required=True, + ) + + bearer_token = forms.CharField( + required=True, + help_text="The bearer token for the Meta Cloud account", + ) + + allowed_media_types = forms.CharField( + help_text="Allowed Media Types", + required=True, + initial="audio,image,video,documento,sticker", + ) + + field_order = ["endpoint", "verify_token", "bearer_token", "allowed_media_types"] diff --git a/rocket_connect/plugins/venom_simple_api.py b/rocket_connect/plugins/venom_simple_api.py index fdc1c39..0305289 100644 --- a/rocket_connect/plugins/venom_simple_api.py +++ b/rocket_connect/plugins/venom_simple_api.py @@ -65,7 +65,7 @@ def incoming(self): return HttpResponse("Rocket Down!", status=503) self.outcome_qrbase64(self.message["data"]["base64Qrimg"]) self.outcome_admin_message( - "Attempt: {0}".format(self.message["data"]["attempts"]) + "Attempt: {}".format(self.message["data"]["attempts"]) ) if self.message.get("event") == "onStateChanged": diff --git a/rocket_connect/plugins/waautomate.py b/rocket_connect/plugins/waautomate.py index 7ef3194..41dff70 100644 --- a/rocket_connect/plugins/waautomate.py +++ b/rocket_connect/plugins/waautomate.py @@ -143,10 +143,10 @@ def incoming(self): ): lat = self.message.get("data", {}).get("lat") lng = self.message.get("data", {}).get("lng") - link = "https://www.google.com/maps/search/?api=1&query={0}+{1}".format( + link = "https://www.google.com/maps/search/?api=1&query={}+{}".format( lat, lng ) - text = "Lat:{0}, Long:{1}: Link: {2}".format(lat, lng, link) + text = f"Lat:{lat}, Long:{lng}: Link: {link}" self.outcome_text(room.room_id, text) # # @@ -178,7 +178,7 @@ def incoming(self): else: message = self.get_message_body() elif quote_type in ["document", "image", "ptt"]: - message = "DOCUMENT RESENT:\n {0}".format( + message = "DOCUMENT RESENT:\n {}".format( self.get_message_body() ) quoted_id = self.message.get("data", {}).get( @@ -210,7 +210,7 @@ def incoming(self): return HttpResponse("Rocket Down!", status=503) # this prevent some bogus request from wa after logout on this event if self.message.get("data") and int(self.message.get("data")): - text_message = ":battery:\n:satellite: Battery level: {0}%".format( + text_message = ":battery:\n:satellite: Battery level: {}%".format( self.message.get("data") ) self.outcome_admin_message(text_message) @@ -251,12 +251,10 @@ def incoming(self): if not self.rocket: return HttpResponse("Rocket Down!", status=503) - text_message = ( - ":information_source:\n:satellite: {0} > {1}: {2} ".format( - self.message.get("sessionId"), - self.message.get("event"), - self.message.get("data"), - ) + text_message = ":information_source:\n:satellite: {} > {}: {} ".format( + self.message.get("sessionId"), + self.message.get("event"), + self.message.get("data"), ) self.outcome_admin_message(text_message) @@ -319,7 +317,7 @@ def incoming(self): base64_fixed_code = self.generate_qrcode(code) self.outcome_qrbase64(base64_fixed_code) else: - text_message = ":information_source:\n:satellite: {0} > {1}: {2} ".format( + text_message = ":information_source:\n:satellite: {} > {}: {} ".format( message.get("sessionId"), message.get("namespace"), message.get("data") diff --git a/rocket_connect/plugins/wppconnect.py b/rocket_connect/plugins/wppconnect.py index aaf769d..57d986c 100644 --- a/rocket_connect/plugins/wppconnect.py +++ b/rocket_connect/plugins/wppconnect.py @@ -38,7 +38,7 @@ def populate_config(self): def generate_token(self): # generate token - endpoint = "{0}/api/{1}/{2}/generate-token".format( + endpoint = "{}/api/{}/{}/generate-token".format( self.config.get("endpoint"), self.config.get("instance_name"), self.config.get("secret_key"), @@ -55,7 +55,7 @@ def status_session(self): # generate token status = {} if self.config.get("endpoint"): - endpoint = "{0}/api/{1}/status-session".format( + endpoint = "{}/api/{}/status-session".format( self.config.get("endpoint"), self.config.get("instance_name"), ) @@ -66,7 +66,7 @@ def status_session(self): # if connected, get battery and host device if status.get("status") == "CONNECTED": # host device - endpoint = "{0}/api/{1}/host-device".format( + endpoint = "{}/api/{}/host-device".format( self.config.get("endpoint"), self.config.get("instance_name"), ) @@ -79,7 +79,7 @@ def status_session(self): def close_session(self): # generate token - endpoint = "{0}/api/{1}/close-session".format( + endpoint = "{}/api/{}/close-session".format( self.config.get("endpoint"), self.config.get("instance_name"), ) @@ -142,7 +142,7 @@ def livechat_manager(self, payload): pytz.timezone(self.timezone) ) messages.append( - "Closing rooms created before: {0}".format( + "Closing rooms created before: {}".format( str(local_time), ) ) @@ -152,7 +152,7 @@ def livechat_manager(self, payload): if agent != "*": kwargs["agents"] = [agent] serving_agent = agent if agent else "ALL" - msg = "Rooms with agent serving: {0}".format(serving_agent) + msg = f"Rooms with agent serving: {serving_agent}" messages.append(msg) except IndexError: # the close action may not have agent @@ -168,7 +168,7 @@ def livechat_manager(self, payload): if agent != "*": kwargs["agents"] = [agent] serving_agent = agent if agent else "ALL" - msg = "Rooms with agent serving: {0}".format(serving_agent) + msg = f"Rooms with agent serving: {serving_agent}" messages.append(msg) except IndexError: messages.append("ERROR! No Agent Provided.") @@ -185,25 +185,25 @@ def livechat_manager(self, payload): close = self.rocket.call_api_post( "livechat/room.close", rid=room_id, token=room["v"]["token"] ) - room_url = "{0}/omnichannel/current/{1}/room-info".format( - self.connector.server.url, room_id + room_url = "{}/omnichannel/current/{}/room-info".format( + self.connector.server.external_url, room_id ) if close.ok: messages.append( - ":heavy_check_mark: [Room closed: {0}]({1})".format( + ":heavy_check_mark: [Room closed: {}]({})".format( room_id, room_url ) ) else: messages.append( - ":stop_sign: (ERROR CLOSING ROOM: {0}]({1})".format( + ":stop_sign: (ERROR CLOSING ROOM: {}]({})".format( room_id, room_url ) ) return {"success": True, "message": "\n".join(messages)} def check_number_status(self, number): - endpoint = "{0}/api/{1}/check-number-status/{2}".format( + endpoint = "{}/api/{}/check-number-status/{}".format( self.config.get("endpoint"), self.config.get("instance_name"), number ) @@ -217,9 +217,7 @@ def check_number_status(self, number): data = {"webhook": self.config.get("webhook")} try: start_session_req = requests.get(endpoint, headers=headers, json=data) - self.logger.info( - "CHECKING NUMBER: {0}: {1}".format(number, start_session_req.json()) - ) + self.logger.info(f"CHECKING NUMBER: {number}: {start_session_req.json()}") return start_session_req.json() except requests.ConnectionError: return {"success": False, "message": "Could not connect to wppconnect"} @@ -229,7 +227,7 @@ def check_number_info(self, number, augment_message=False): this method will get infos from the contact api and insert into self message """ - endpoint = "{0}/api/{1}/contact/{2}".format( + endpoint = "{}/api/{}/contact/{}".format( self.config.get("endpoint"), self.config.get("instance_name"), number ) @@ -240,12 +238,9 @@ def check_number_info(self, number, augment_message=False): token = self.config.get("token", {}).get("token") headers = {"Authorization": "Bearer " + token} - data = {"webhook": self.config.get("webhook")} - number_info_req = requests.get(endpoint, headers=headers, json=data) + number_info_req = requests.get(endpoint, headers=headers) number_info = number_info_req.json() - self.logger.info( - "CHECKING CONTACT INFO FOR NUMBER {0}: {1}".format(number, number_info) - ) + self.logger.info(f"CHECKING CONTACT INFO FOR NUMBER {number}: {number_info}") if augment_message: if not self.message.get("sender"): self.message["sender"] = {} @@ -276,7 +271,7 @@ def active_chat(self): self.type = "active_chat" self.message["type"] = self.type department = False - transfer = False + department_id = None # get client self.get_rocket_client() now_str = datetime.datetime.now().replace(microsecond=0).isoformat() @@ -291,17 +286,13 @@ def active_chat(self): self.message["visitor"] = {"token": "whatsapp:" + number} check_number = self.check_number_status(number) # could not get number validation - if ( - not check_number.get("response") - and check_number.get("status") == "Disconnected" - ) or not check_number.get("success", True): - alert = "CONNECTOR *{0}* IS DISCONNECTED".format(self.connector.name) + if not check_number.get("response", {}).get("numberExists", False): + alert = f"COULD NOT SEND ACTIVE MESSAGE TO *{self.connector.name}*" self.logger_info(alert) self.rocket.chat_update( room_id=room_id, msg_id=msg_id, - text=self.message.get("text") - + "\n:warning: {0} {1}".format(now_str, alert), + text=self.message.get("text") + f"\n:warning: {now_str} {alert}", ) # return nothing return {"success": False, "message": "NO MESSAGE TO SEND"} @@ -313,7 +304,7 @@ def active_chat(self): room_id=room_id, msg_id=msg_id, text=self.message.get("text") - + "\n:warning: {0} NO MESSAGE TO SEND. *SYNTAX: {1} {2} *".format( + + "\n:warning: {} NO MESSAGE TO SEND. *SYNTAX: {} {} *".format( now_str, self.message.get("trigger_word"), reference ), ) @@ -349,7 +340,7 @@ def active_chat(self): and agent["statusLivechat"] == "available" ] self.logger_info( - "NO DEPARTMENT FOUND. LOOKING INTO ONLINE AGENTS: {0}".format( + "NO DEPARTMENT FOUND. LOOKING INTO ONLINE AGENTS: {}".format( available_agents ) ) @@ -363,20 +354,20 @@ def active_chat(self): room_id=room_id, msg_id=msg_id, text=self.message.get("text") - + "\n:warning: AGENT {0} NOT ONLINE".format(department), + + f"\n:warning: AGENT {department} NOT ONLINE", ) return { "success": False, - "message": "AGENT {0} NOT ONLINE".format(department), + "message": f"AGENT {department} NOT ONLINE", "available_agents": available_agents, } # > 1 departments found if len(departments) > 1: - alert_message = "\n:warning: {0} More than one department found. Try one of the below:".format( + alert_message = "\n:warning: {} More than one department found. Try one of the below:".format( now_str ) for dpto in departments: - alert_message = alert_message + "\n*{0}*".format( + alert_message = alert_message + "\n*{}*".format( self.message.get("text").replace( "@" + department, "@" + dpto["name"] ), @@ -401,6 +392,7 @@ def active_chat(self): agent_id = departments[0].split(":")[1] else: department = departments[0]["name"] + department_id = departments[0]["_id"] # define message type self.type = "active_chat" @@ -436,12 +428,19 @@ def active_chat(self): check_number["response"]["id"]["user"], augment_message=True ) self.logger_info( - "ACTIVE MESSAGE PAYLOAD GENERATED: {0}".format(self.message) + f"ACTIVE MESSAGE PAYLOAD GENERATED: {self.message}" ) + # if force transfer for active chat, for it. + # register room - room = self.get_room(department, allow_welcome_message=False) + room = self.get_room( + department, + allow_welcome_message=False, + check_if_open=True, + force_transfer=department_id, + ) if room: - self.logger_info("ACTIVE CHAT GOT A ROOM {0}".format(room)) + self.logger_info(f"ACTIVE CHAT GOT A ROOM {room}") # send the message to the room, in order to be delivered to the # webhook and go the flow # send message_raw to the room @@ -522,13 +521,12 @@ def active_chat(self): self.rocket.chat_update( room_id=room_id, msg_id=msg_id, - text=self.message.get("text") - + "\n:warning: {0} INVALID NUMER".format(now_str), + text=self.message.get("text") + f"\n:warning: {now_str} INVALID NUMER", ) return {"success": True, "message": "INVALID NUMBER"} def start_session(self): - endpoint = "{0}/api/{1}/start-session".format( + endpoint = "{}/api/{}/start-session".format( self.config.get("endpoint"), self.config.get("instance_name"), ) @@ -566,7 +564,7 @@ def incoming(self): this method will process the incoming messages and ajust what necessary, to output to rocketchat """ - self.logger_info("INCOMING MESSAGE: {0}".format(self.message)) + self.logger_info(f"INCOMING MESSAGE: {self.message}") # qr code if self.message.get("action"): @@ -591,7 +589,7 @@ def incoming(self): # return status output = {**output, **response} - self.logger_info("RETURN OF ACTION MESSAGE: {0}".format(output)) + self.logger_info(f"RETURN OF ACTION MESSAGE: {output}") return JsonResponse(output) if self.message.get("event") == "qrcode": @@ -600,7 +598,7 @@ def incoming(self): # admin message if self.message.get("event") == "status-find": - text = "Session: {0}. Status: {1}".format( + text = "Session: {}. Status: {}".format( self.message.get("session"), self.message.get("status") ) if self.message.get("status") in ["isLogged", "inChat", "qrReadSuccess"]: @@ -625,11 +623,10 @@ def incoming(self): if self.message.get("event") in ["onmessage", "unreadmessages"]: department = None if self.message.get("event") == "unreadmessages": - self.logger_info( - "PROCESSING UNREAD MESSAGE. PAYLOAD {0}".format(self.message) - ) + self.logger_info(f"PROCESSING UNREAD MESSAGE. PAYLOAD {self.message}") # if it's a message from Me, ignore: if self.message.get("id", {}).get("fromMe"): + self.handle_ack_fromme_message() return JsonResponse({}) # adapt unread messages to intake like a regular message pass @@ -667,31 +664,36 @@ def incoming(self): not in department_triage_to_ignore ): button = { - "buttonId": department.get("_id"), - "buttonText": { - "displayText": department.get("name") - }, - "type": 1, + "id": department.get("_id"), + "text": department.get("name"), } buttons.append(button) # the message is a button reply. we now register the room # with the choosen department and return - if self.message.get("quotedMsg", {}).get( - "isDynamicReplyButtonsMsg", False - ): + if self.message.get("type") == "template_button_reply": # the department text is body choosen_department = self.message.get("body") department_map = {} for b in buttons: - department_map[b["buttonText"]["displayText"]] = b[ - "buttonId" - ] + department_map[b["text"]] = b["buttonId"] department = department_map[choosen_department] else: # add destination phone payload = self.config.get("department_triage_payload") + if not payload.get("options"): + payload["options"] = {"buttons": []} + if not payload.get("options").get("buttons"): + payload["options"]["buttons"] = [] payload["phone"] = self.get_visitor_id() - payload["buttons"] = buttons + payload_buttons = payload["options"]["buttons"] + # limit to 3 department buttons, otherwise will not work + payload["options"]["buttons"] = ( + buttons[:3] + payload_buttons + ) + payload["options"]["buttons"] = payload["options"][ + "buttons" + ][:5] + # payload["options"]["buttons"] = buttons # outcome buttons message = {"msg": json.dumps(payload)} self.outgo_text_message(message) @@ -746,7 +748,7 @@ def incoming(self): ) # type of message is others elif quote_type in ["document", "image", "ptt"]: - message = "DOCUMENT RESENT:\n {0}".format( + message = "DOCUMENT RESENT:\n {}".format( self.get_message_body() ) mime = self.message.get("quotedMsg").get("mimetype") @@ -786,16 +788,16 @@ def incoming(self): deliver = self.outcome_text(room.room_id, message) if settings.DEBUG: self.logger_info( - "DELIVER OF TEXT MESSAGE: {0}".format(deliver.ok) + f"DELIVER OF TEXT MESSAGE: {deliver.ok}" ) # location type elif self.message.get("type") == "location": lat = self.message.get("lat") lng = self.message.get("lng") - link = "https://www.google.com/maps/search/?api=1&query={0}+{1}".format( + link = "https://www.google.com/maps/search/?api=1&query={}+{}".format( lat, lng ) - text = "Lat:{0}, Long:{1}: Link: {2}".format( + text = "Lat:{}, Long:{}: Link: {}".format( lat, lng, link, @@ -821,19 +823,21 @@ def incoming(self): self.message_object.save() else: self.logger_info( - "Message Object {0} Already delivered. Ignoring".format( + "Message Object {} Already delivered. Ignoring".format( message.id ) ) + # handle ack fromme + if self.message.get("event") == "onack": + self.handle_ack_fromme_message() + # unread messages - just logging if self.message.get("event") == "unreadmessages": if "status@broadcast" not in self.message.get( "from" ) and not self.message.get("id", {}).get("fromMe", False): - self.logger_info( - "PROCESSED UNREAD MESSAGE. PAYLOAD {0}".format(self.message) - ) + self.logger_info(f"PROCESSED UNREAD MESSAGE. PAYLOAD {self.message}") # webhook active chat integration if self.config.get("active_chat_webhook_integration_token"): @@ -851,7 +855,7 @@ def intake_unread_messages(self): """ intake unread messages """ - endpoint = "{0}/api/{1}/unread-messages".format( + endpoint = "{}/api/{}/unread-messages".format( self.config.get("endpoint"), self.config.get("instance_name"), ) @@ -859,7 +863,7 @@ def intake_unread_messages(self): unread_contacts = session.get(endpoint) if unread_contacts.ok: self.logger_error( - "PROCESSING UNREAD {0} CONTACTS ON START".format( + "PROCESSING UNREAD {} CONTACTS ON START".format( len(unread_contacts.get("response")) ) ) @@ -868,7 +872,7 @@ def intake_unread_messages(self): for message in contact["messages"]: message["event"] = "onmessage" message["chatId"] = message["from"] - self.logger_error("PROCESSING UNREAD MESSAGE {0}".format(message)) + self.logger_error(f"PROCESSING UNREAD MESSAGE {message}") self.message = message self.type = "incoming" self.incoming() @@ -883,11 +887,16 @@ def get_incoming_message_id(self): return self.message.get("id", {}).get("_serialized") if self.message.get("type") == "active_chat": return self.message.get("message_id") + if self.message.get("event") == "onack": + return self.message.get("id", {}).get("id") return self.message.get("id") def get_incoming_visitor_id(self): if self.message.get("event") == "incomingcall": return self.message.get("peerJid") + if self.message.get("event") == "onack": + if self.message.get("id", {}).get("fromMe"): + return self.message.get("id").get("remote") else: if self.message.get("event") == "unreadmessages": return self.message.get("from") @@ -918,12 +927,12 @@ def get_visitor_phone(self): def get_visitor_username(self): if self.message.get("event") == "incomingcall": - visitor_username = "whatsapp:{0}".format( + visitor_username = "whatsapp:{}".format( # works for wa-automate self.message.get("peerJid") ) else: - visitor_username = "whatsapp:{0}".format(self.message.get("from")) + visitor_username = "whatsapp:{}".format(self.message.get("from")) return visitor_username def get_message_body(self): @@ -934,13 +943,16 @@ def get_request_session(self): s.headers = {"content-type": "application/json"} token = self.connector.config.get("token", {}).get("token") if token: - s.headers.update({"Authorization": "Bearer {0}".format(token)}) + s.headers.update({"Authorization": f"Bearer {token}"}) return s def outgo_text_message(self, message, agent_name=None): sent = False - content = message["msg"] - url = self.connector.config["endpoint"] + "/api/{0}/send-message".format( + if type(message) == str: + content = message + else: + content = message["msg"] + url = self.connector.config["endpoint"] + "/api/{}/send-message".format( self.connector.config["instance_name"] ) try: @@ -952,15 +964,11 @@ def outgo_text_message(self, message, agent_name=None): if payload.get("buttons"): if not payload.get("phone"): payload["phone"] = self.get_visitor_id() - url = self.connector.config[ - "endpoint" - ] + "/api/{0}/send-buttons".format( + url = self.connector.config["endpoint"] + "/api/{}/send-buttons".format( self.connector.config["instance_name"] ) self.logger_info( - "OUTGOING BUTTON MESSAGE. URL: {0}. PAYLOAD {1}".format( - url, payload - ) + f"OUTGOING BUTTON MESSAGE. URL: {url}. PAYLOAD {payload}" ) except (ValueError, TypeError): content = self.joypixel_to_unicode(content) @@ -973,9 +981,7 @@ def outgo_text_message(self, message, agent_name=None): "message": content, "isGroup": False, } - self.logger_info( - "OUTGOING TEXT MESSAGE. URL: {0}. PAYLOAD {1}".format(url, payload) - ) + self.logger_info(f"OUTGOING TEXT MESSAGE. URL: {url}. PAYLOAD {payload}") # SEND MESSAGE session = self.get_request_session() # TODO: Simulate typing @@ -983,14 +989,29 @@ def outgo_text_message(self, message, agent_name=None): timestamp = int(time.time()) try: + self.logger_info(f"OUTGOING TEXT MESSAGE: URL and PAYLOAD {url} {payload}") sent = session.post(url, json=payload) - if self.message_object: + if self.message_object and sent.ok: self.message_object.delivered = sent.ok self.message_object.response[timestamp] = sent.json() + if not self.message_object.response.get("id"): + self.message_object.response["id"] = [ + sent.json()["response"][0]["id"] + ] + else: + self.message_object.response["id"].append( + sent.json()["response"][0]["id"] + ) + + if sent.ok: + self.logger_info(f"OUTGOING TEXT MESSAGE SUCCESS: {sent.json()}") + else: + self.logger_info(f"OUTGOING TEXT MESSAGE ERROR: {sent.json()}") + except requests.ConnectionError: if self.message_object: self.message_object.delivered = False - self.logger_info("CONNECTOR DOWN: {0}".format(self.connector)) + self.logger_info(f"CONNECTOR DOWN: {self.connector}") # save message object if self.message_object: self.message_object.payload[timestamp] = payload @@ -1018,13 +1039,13 @@ def outgo_file_message(self, message, agent_name=None): mime = self.message["messages"][0]["fileUpload"]["type"] payload = { "phone": self.get_visitor_id(), - "base64": "data:{0};base64,{1}".format(mime, content), + "base64": f"data:{mime};base64,{content}", "isGroup": False, } if settings.DEBUG: print("PAYLOAD OUTGOING FILE: ", payload) session = self.get_request_session() - url = self.connector.config["endpoint"] + "/api/{0}/send-file-base64".format( + url = self.connector.config["endpoint"] + "/api/{}/send-file-base64".format( self.connector.config["instance_name"] ) sent = session.post(url, json=payload) @@ -1040,10 +1061,10 @@ def outgo_file_message(self, message, agent_name=None): def outgo_vcard(self, payload): session = self.get_request_session() - url = self.connector.config["endpoint"] + "/api/{0}/contact-vcard".format( + url = self.connector.config["endpoint"] + "/api/{}/contact-vcard".format( self.connector.config["instance_name"] ) - self.logger_info("OUTGOING VCARD. URL: {0}. PAYLOAD {1}".format(url, payload)) + self.logger_info(f"OUTGOING VCARD. URL: {url}. PAYLOAD {payload}") timestamp = int(time.time()) try: # replace destination phone @@ -1053,12 +1074,151 @@ def outgo_vcard(self, payload): self.message_object.response[timestamp] = sent.json() except requests.ConnectionError: self.message_object.delivered = False - self.logger_info("CONNECTOR DOWN: {0}".format(self.connector)) + self.logger_info(f"CONNECTOR DOWN: {self.connector}") # save message object if self.message_object: self.message_object.payload[timestamp] = payload self.message_object.save() + def handle_inbound(self, request): + if request.GET.get("phone"): + check = self.check_number_status(request.GET.get("phone")) + if check["response"]["numberExists"]: + serialized_id = check.get("response").get("id").get("_serialized") + # get proper number + proper_number = check["response"]["id"]["user"] + + department = request.GET.get("department", None) + if not department: + department = self.config.get("default_inbound_department", None) + + self.message = { + "from": serialized_id, + "chatId": serialized_id, + "id": self.message.get("message_id"), + "visitor": {"token": "whatsapp:" + serialized_id}, + } + self.check_number_info(proper_number, augment_message=True) + self.message["visitor"] = {"token": "whatsapp:" + serialized_id} + self.get_rocket_client() + room = self.get_room(department, allow_welcome_message=False) + if room: + # outcome message + if request.GET.get("text"): + # send message to channel + self.rocket.chat_post_message( + text=request.GET.get("text"), room_id=room.room_id + ) + base_url = self.connector.server.external_url + external_url = f"{base_url}/omnichannel/current/{room.room_id}" + return {"success": True, "redirect": external_url} + else: + return { + "success": False, + "notfound": f"{request.GET.get('phone')} was not found", + } + + self.logger_info(f"INBOUND MESSAGE. {request.GET}") + + trigger_word = self.config.get("default_fromme_ack_department_trigger") + if trigger_word: + # here we will return the last message that has the trigger word + # for a triggered phone + if request.GET.get("trigger_id"): + trigger_id = request.GET.get("trigger_id") + phone = trigger_id.split("@")[0] + if "whatsapp" in phone: + phone = phone.replace("whatsapp:", "") + + # get the last triggered message + session = self.get_request_session() + url = self.connector.config[ + "endpoint" + ] + "/api/{}/all-messages-in-chat/{}".format( + self.connector.config["instance_name"], phone + ) + last_messages_req = session.get(url).json() + last_messages = last_messages_req["response"] + if not last_messages_req["response"]: + output = { + "success": False, + "notfound": f"{trigger_id} trigger id was not found", + } + return output + # as the message may be recent, this can save some processing + last_messages.reverse() + # find the trigger message + trigger_message = None + for message in last_messages: + if trigger_word in message["body"]: + trigger_message = message + break + # enhance trigger_message with external_url + trigger_message["external_url"] = self.connector.server.external_url + return trigger_message + + if request.GET.get("check-phone"): + return self.check_number_status(request.GET.get("check-phone")) + + def handle_ack_fromme_message(self): + # activate this if default_fromme_ack_department is set + if self.config.get("default_fromme_ack_department") and self.config.get( + "default_fromme_ack_department_trigger" + ): + if self.config.get( + "default_fromme_ack_department_trigger" + ) in self.message.get("body"): + self.get_rocket_client() + # lets force it to transfer if room is open + if self.config.get("fromme_ack_department_force_transfer"): + force_transfer = self.config.get("default_fromme_ack_department") + else: + # no forcing, leave it at the department + force_transfer = None + # get the room + room_response = self.get_room( + department=self.config.get("default_fromme_ack_department"), + allow_welcome_message=False, + check_if_open=True, + force_transfer=force_transfer, + ) + self.logger_info( + f"HANDLING ACK FROMME MESSAGE TRIGGER. PAYLOAD {self.message}, room response: {room_response}" + ) + # ack receipt + if self.config.get("enable_ack_receipt"): + # get the sent message + self.get_rocket_client() + message_id = self.message.get("id", {}).get("_serialized") + self.logger_info(f"enable_ack_receipt for {message_id}") + for message in self.connector.messages.filter( + response__id__contains=message_id + ): + # TODO: if ack == 2, it must have both ack and seen + # what is happening, due to race condition, + # it can leave only with the green one. + # Solution: replace the ballot_box_with_check if present, + # or add only the white check + original_message = self.rocket.chat_get_message( + msg_id=message.envelope_id + ) + body = original_message.json()["message"]["msg"] + # remove previous markers + body = body.replace(":ballot_box_with_check:", "") + body = body.replace(":white_check_mark:", "") + if self.message["ack"] == 1: + mark = ":ballot_box_with_check:" + else: + mark = ":white_check_mark:" + + self.rocket.chat_update( + room_id=message.room.room_id, + msg_id=message.envelope_id, + text=f"{mark} {body}", + ) + message.ack = True + message.save() + class ConnectorConfigForm(BaseConnectorConfigForm): @@ -1085,6 +1245,12 @@ class ConnectorConfigForm(BaseConnectorConfigForm): validators=[validators.validate_slug], ) + active_chat_force_department_transfer = forms.BooleanField( + help_text="If the Chat is already open, force the transfer to this department", + required=False, + initial=False, + ) + name_extraction_order = forms.CharField( required=False, help_text="The prefered order to extract a visitor name", @@ -1103,12 +1269,39 @@ class ConnectorConfigForm(BaseConnectorConfigForm): session_management_token = forms.CharField(required=False) + default_fromme_ack_department = forms.CharField( + required=False, + help_text="This is a deparment where should be created a message sent from the mobile", + ) + + fromme_ack_department_force_transfer = forms.BooleanField( + help_text="Force the transfer if chat is already open with visitor", + initial=True, + required=False, + ) + + default_fromme_ack_department_trigger = forms.CharField( + required=False, + help_text="This is trigger word a message must have in order to trigger the ack from me feature", + ) + + enable_ack_receipt = forms.BooleanField( + required=False, + help_text="This will update the ingoing message to show it was delivered and received", + ) + + default_inbound_department = forms.CharField( + required=False, + help_text="This is the deparment that will be opened inbound active messages to by default", + ) + field_order = [ "webhook", "endpoint", "secret_key", "instance_name", "active_chat_webhook_integration_token", + "active_chat_force_department_transfer", "session_management_token", "name_extraction_order", "process_unread_messages_on_start", diff --git a/rocket_connect/templates/base.html b/rocket_connect/templates/base.html index 7922c44..3795a53 100644 --- a/rocket_connect/templates/base.html +++ b/rocket_connect/templates/base.html @@ -7,7 +7,7 @@ - +