-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
47 changed files
with
1,345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
Oops, something went wrong.