Skip to content

Commit

Permalink
version with django
Browse files Browse the repository at this point in the history
  • Loading branch information
vaestvita committed Aug 6, 2024
1 parent 0ef5889 commit 97e22c7
Show file tree
Hide file tree
Showing 47 changed files with 1,345 additions and 0 deletions.
162 changes: 162 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
## Thoth: Bitrix24 Integration Hub

### Описание

Одна инсталляция Thoth позволяет создавать и обслуживать неограниченное количество локальных приложений Битрикс24 с OAuth 2.0 авторизацией.

## Видеоинструкции на Youtube

https://www.youtube.com/playlist?list=PLeniNJl73vVmmsG1XzTlimbZJf969LIpS


## Установка

Для тестового запуска использовался python 12

```
cd /opt
git clone https://github.com/vaestvita/thoth
cd thoth
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
cp env_example .env
nano .env
заменить HOME_URL, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS на свои значения
HOME_URL - домен по которму будет доступен thoth (example.com)
python manage.py runserver 0.0.0.0:8000
```

После запуска сервера в файле .env будет создан ADMIN_URL, который необходимо исопльзовать для входя в админку

## Подключение портала Битрикс24

+ В админке создайте токен

![alt text](docs/img/token.png)

+ В Битрикс24 создайте серверное локальное приложение без интерфейса (Приложения – Разработчикам – Другое – Локальное приложение) в Битрикс24 и заполните соответствующие поля (Путь вашего обработчика и Путь для первоначальной установки)
+ Необходимые права (Настройка прав): crm,imopenlines,contact_center,user,im,imconnector,disk
```
https://example.com/api/bitrix/?api-key=XXXXXXX
XXXXXXX - ваш токен
```

![alt text](docs/img/app.png)

+ В админке thoth перейдите в раздел Bitrix, там должен появиться ваш портал, откройте его и заполните значения полей Код приложения (client_id) и Ключ приложения (client_secret). Они были выданы Битриксом в предыдущем шаге при установке локального приложения

![alt text](docs/img/portal.png)

+ В битриксе в разделе "контакт-центр" должен появиться коннектор "THOTH WABA"

![alt text](docs/img/connector.png)

## Подключение WhatsApp (WABA)
+ Рекомендуется получить [Постоянный маркер](https://developers.facebook.com/docs/whatsapp/business-management-api/get-started), иначе придется перевыпускать токен каждый день
+ Создайте приложение на [портале разработчиков](https://developers.facebook.com/apps/)
+ В панели подключите продукты Webhooks, WhatsApp
+ В админке THOTH - WABA - Add waba
+ + name - имя вашего приложения
+ + Access token - Постоянный или временный маркер
+ + Bitrix - выберите портал к которму хотите привязать приложение WABA
+ + Verify token - скопируйте
![alt text](docs/img/waba.png)
+ На портале разработчиков - Quickstart > Configuration >
+ + Callback URL - https://example.com/api/waba/?api-key=XXXXXXX
+ + Verify token - Verify token из предыдущего шага
![alt text](docs/img/verify.png)
+ В админке thoth - waba - phones и добавляем номера (Phone - номер, Phone id - id из приложения фейсбук)
+ если все пройдет успешно, то в контакт центре коннектор станет зеленым и кнему будет прикрпелена линия THOTH_ваш_номер
Empty file added bitrix/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions bitrix/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Bitrix

@admin.register(Bitrix)
class BitrixAdmin(admin.ModelAdmin):
list_display = ('domain', 'owner', 'storage_id')
search_fields = ('domain',)
list_filter = ('domain',)
readonly_fields = ('domain', 'storage_id', 'client_endpoint', 'access_token', 'refresh_token', 'application_token')
fields = ('domain', 'owner', 'storage_id', 'client_endpoint', 'access_token', 'refresh_token', 'application_token', 'client_id', 'client_secret')
Empty file added bitrix/api/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions bitrix/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from rest_framework import serializers
from bitrix.models import Bitrix

class PortalSerializer(serializers.ModelSerializer):
class Meta:
model = Bitrix
fields = ['owner', 'domain', 'client_endpoint', 'access_token', 'refresh_token', 'application_token']

def create(self, validated_data):
return Bitrix.objects.create(**validated_data)
125 changes: 125 additions & 0 deletions bitrix/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from rest_framework.mixins import ListModelMixin, CreateModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework import status

from bitrix.models import Bitrix
from .serializers import PortalSerializer

from bitrix.crest import call_method
from bitrix.imconnector import register_connector
from waba.utils import send_message

import re


def get_personal_mobile(users):
personal_mobiles = []
for user_id, user_info in users.items():
if user_info.get('external_auth_id') == 'imconnector':
phones = user_info.get('phones', {})
personal_mobile = phones.get('personal_mobile')
if personal_mobile:
personal_mobiles.append(personal_mobile)
return personal_mobiles


class PortalViewSet(CreateModelMixin, GenericViewSet, ListModelMixin):
queryset = Bitrix.objects.all()
serializer_class = PortalSerializer

def create(self, request, *args, **kwargs):
try:
event = request.data.get('event', {})
domain = request.data.get('auth[domain]', {})
client_endpoint = request.data.get('auth[client_endpoint]', {})
access_token = request.data.get('auth[access_token]', {})
refresh_token = request.data.get('auth[refresh_token]', {})
application_token = request.data.get('auth[application_token]', {})
api_key = request.query_params.get('api-key', {})

if not domain:
return Response({"error": "Domain is required"}, status=status.HTTP_400_BAD_REQUEST)
if not access_token:
return Response({"error": "Access token is required"}, status=status.HTTP_400_BAD_REQUEST)

# Установка приложения
if event == 'ONAPPINSTALL':
data = {
"domain": domain,
"owner": request.user.id,
"client_endpoint": client_endpoint,
"access_token": access_token,
"refresh_token": refresh_token,
"application_token": application_token
}

# Debugging the input data for serializer
print("Serializer Input Data: ", data)

# Create the portal with the domain and access token
serializer = self.get_serializer(data=data)
if not serializer.is_valid():
print("Serializer Errors: ", serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

self.perform_create(serializer)

storage_id_data = call_method(domain, 'POST', 'disk.storage.getforapp', {})
storage_id = storage_id_data['result']['ID']

portal = Bitrix.objects.get(domain=domain)
portal.storage_id = storage_id
portal.save()

# imconnector register
register_connector(domain, api_key)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

# Обработка события ONIMCONNECTORMESSAGEADD
elif event == 'ONIMCONNECTORMESSAGEADD':
message = {}
line = request.data.get('data[LINE]', {})
chat_id = request.data.get('data[MESSAGES][0][im][chat_id]')
message_id = request.data.get('data[MESSAGES][0][im][message_id]')
message['biz_opaque_callback_data'] = f'{line}_{chat_id}_{message_id}'
file_type = request.data.get('data[MESSAGES][0][message][files][0][type]', None)
file_link = request.data.get('data[MESSAGES][0][message][files][0][link]', None)
if not file_type:
text = request.data.get('data[MESSAGES][0][message][text]')
text = re.sub(r'\[(?!(br|\n))[^\]]+\]', '', text)
if '@#template-' in text:
template_name = re.search(r'@#template-(\w+)', text).group(1)
message['type'] = 'template'
message['template'] = {'name': template_name, 'language': {'code': 'en_US'}}
else:
text = text.replace('[br]', '\n')
message['type'] = 'text'
message['text'] = {'body': text}

elif file_type in ['image']:
message['type'] = file_type
message[file_type] = {'link': file_link}

else:
message['type'] = 'document'
message['document'] = {'link': file_link}
message['document']['filename'] = request.data.get('data[MESSAGES][0][message][files][0][name]')

user_list = call_method(domain, 'POST', 'im.chat.user.list', {'CHAT_ID': chat_id})
if user_list:
users = call_method(domain, 'POST', 'im.user.list.get', {'ID': user_list['result']})
phones = get_personal_mobile(users['result'])

send_message(domain, message, line, phones)

return Response({"status": "ONIMCONNECTORMESSAGEADD event processed"}, status=status.HTTP_200_OK)

else:
return Response({"error": "Unsupported event"}, status=status.HTTP_400_BAD_REQUEST)

except Exception as e:
print(f"Error occurred: {str(e)}")
return Response({"error": "Internal server error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
6 changes: 6 additions & 0 deletions bitrix/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class BitrixConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bitrix'
Loading

0 comments on commit 97e22c7

Please sign in to comment.