ifa-nextcloud 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/PKG-INFO +1 -1
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/pyproject.toml +1 -1
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/async_bot.py +22 -1
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/bot_core.py +49 -16
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/async_.py +19 -20
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/sync.py +3 -2
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/LICENSE +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/README.md +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/reqirements.txt +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/bot.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/models.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/base.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/email/__init__.py +0 -0
- {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/email/client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ifa_nextcloud
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python библиотека для создания ботов в Nextcloud Talk с API, похожим на python-telegram-bot
|
|
5
5
|
Project-URL: Homepage, https://github.com/1FaKe110/nextcloud
|
|
6
6
|
Project-URL: Repository, https://github.com/1FaKe110/nextcloud.git
|
|
@@ -405,13 +405,14 @@ class AsyncBot(BotCore):
|
|
|
405
405
|
|
|
406
406
|
params = {'lookIntoFuture': 0, 'setReadMarker': 0}
|
|
407
407
|
|
|
408
|
+
# params теперь поддерживается
|
|
408
409
|
response = await self.http.post(endpoint, data=data, params=params)
|
|
410
|
+
|
|
409
411
|
if response.data is not None and response.data != {}:
|
|
410
412
|
logger.success(f"Отправка текстового сообщения в {chat_id}: {text[:50]}...")
|
|
411
413
|
return True
|
|
412
414
|
|
|
413
415
|
# Fallback: пробуем без параметров
|
|
414
|
-
logger.trace("Отправка с параметрами не удалась, пробуем без них...")
|
|
415
416
|
response = await self.http.post(endpoint, data=data)
|
|
416
417
|
if response.data is not None and response.data != {}:
|
|
417
418
|
logger.success("Сообщение успешно отправлено (без параметров)")
|
|
@@ -809,8 +810,12 @@ class AsyncBot(BotCore):
|
|
|
809
810
|
Args:
|
|
810
811
|
poll_interval: Интервал опроса для новых комнат
|
|
811
812
|
"""
|
|
813
|
+
# Убеждаемся, что сессия создана
|
|
814
|
+
await self._ensure_session()
|
|
815
|
+
|
|
812
816
|
try:
|
|
813
817
|
rooms = await self.get_rooms()
|
|
818
|
+
logger.info(f"Синхронизация: найдено {len(rooms)} комнат")
|
|
814
819
|
|
|
815
820
|
current_rooms = set()
|
|
816
821
|
for room in rooms:
|
|
@@ -838,6 +843,7 @@ class AsyncBot(BotCore):
|
|
|
838
843
|
|
|
839
844
|
except Exception as e:
|
|
840
845
|
logger.error(f"Ошибка при синхронизации комнат: {e}")
|
|
846
|
+
logger.error(traceback.format_exc())
|
|
841
847
|
|
|
842
848
|
async def _sync_rooms_loop(self, poll_interval: float = 2, sync_interval: int = 60):
|
|
843
849
|
"""
|
|
@@ -881,10 +887,25 @@ class AsyncBot(BotCore):
|
|
|
881
887
|
# Убеждаемся, что сессия создана
|
|
882
888
|
await self._ensure_session()
|
|
883
889
|
|
|
890
|
+
# Проверяем аутентификацию
|
|
891
|
+
status = await self.check_session_status()
|
|
892
|
+
if not status.get('authenticated'):
|
|
893
|
+
logger.error(f"Ошибка аутентификации: {status}")
|
|
894
|
+
logger.error("Проверьте логин и пароль/токен приложения")
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
logger.info(f"Аутентификация успешна как: {status.get('user')}")
|
|
898
|
+
|
|
884
899
|
# Получаем начальный список комнат
|
|
885
900
|
rooms = await self.get_rooms()
|
|
886
901
|
logger.info(f"Найдено {len(rooms)} доступных комнат")
|
|
887
902
|
|
|
903
|
+
if not rooms:
|
|
904
|
+
logger.warning("Не найдено ни одной комнаты. Проверьте:")
|
|
905
|
+
logger.warning("1. Что бот добавлен в комнату")
|
|
906
|
+
logger.warning("2. Правильность токена приложения")
|
|
907
|
+
logger.warning("3. Права доступа бота")
|
|
908
|
+
|
|
888
909
|
self.running = True
|
|
889
910
|
|
|
890
911
|
# Запускаем опрос для каждой комнаты
|
|
@@ -7,6 +7,7 @@ import time
|
|
|
7
7
|
import os
|
|
8
8
|
import mimetypes
|
|
9
9
|
import threading
|
|
10
|
+
import traceback
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from typing import Optional, Callable, Dict, Any, List, Union, Tuple
|
|
12
13
|
from loguru import logger
|
|
@@ -81,23 +82,45 @@ class BotCore:
|
|
|
81
82
|
# Способ 1: Проверяем поле messageParameters на наличие файлов
|
|
82
83
|
message_params = msg_data.get('messageParameters', {})
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
# Важно: messageParameters может быть либо словарём, либо списком
|
|
86
|
+
if isinstance(message_params, dict):
|
|
87
|
+
for key, param in message_params.items():
|
|
88
|
+
if param.get('type') == 'file':
|
|
89
|
+
file_info = {
|
|
90
|
+
'file_id': param.get('id', ''),
|
|
91
|
+
'file_name': param.get('name', 'unknown'),
|
|
92
|
+
'file_size': param.get('size', 0),
|
|
93
|
+
'mime_type': param.get('mimetype', 'application/octet-stream'),
|
|
94
|
+
'file_path': param.get('path', ''),
|
|
95
|
+
'download_url': param.get('link', ''),
|
|
96
|
+
'direct_link': param.get('directLink', '')
|
|
97
|
+
}
|
|
98
|
+
files.append(File(**file_info))
|
|
99
|
+
elif isinstance(message_params, list):
|
|
100
|
+
# Если это список - итерируем напрямую
|
|
101
|
+
for param in message_params:
|
|
102
|
+
if isinstance(param, dict) and param.get('type') == 'file':
|
|
103
|
+
file_info = {
|
|
104
|
+
'file_id': param.get('id', ''),
|
|
105
|
+
'file_name': param.get('name', 'unknown'),
|
|
106
|
+
'file_size': param.get('size', 0),
|
|
107
|
+
'mime_type': param.get('mimetype', 'application/octet-stream'),
|
|
108
|
+
'file_path': param.get('path', ''),
|
|
109
|
+
'download_url': param.get('link', ''),
|
|
110
|
+
'direct_link': param.get('directLink', '')
|
|
111
|
+
}
|
|
112
|
+
files.append(File(**file_info))
|
|
96
113
|
|
|
97
114
|
# Способ 2: Проверяем системные сообщения о расшаренных файлах
|
|
98
115
|
system_message = msg_data.get('systemMessage', '')
|
|
99
116
|
if system_message == 'file_shared':
|
|
100
|
-
|
|
117
|
+
# message_params может быть словарём или списком
|
|
118
|
+
if isinstance(message_params, dict):
|
|
119
|
+
file_param = message_params.get('file', {})
|
|
120
|
+
else:
|
|
121
|
+
# Если список, ищем элемент с type='file'
|
|
122
|
+
file_param = next((p for p in message_params if isinstance(p, dict) and p.get('type') == 'file'), {})
|
|
123
|
+
|
|
101
124
|
if file_param:
|
|
102
125
|
file_info = {
|
|
103
126
|
'file_id': file_param.get('id', ''),
|
|
@@ -129,9 +152,19 @@ class BotCore:
|
|
|
129
152
|
|
|
130
153
|
# Альтернативный способ через messageParameters
|
|
131
154
|
message_params = msg_data.get('messageParameters', {})
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
|
|
156
|
+
# messageParameters может быть списком или словарём
|
|
157
|
+
if isinstance(message_params, dict):
|
|
158
|
+
forward_param = message_params.get('forward')
|
|
159
|
+
is_forwarded = is_forwarded or (forward_param is not None)
|
|
160
|
+
if forward_param:
|
|
161
|
+
forward_origin = forward_param.get('name', 'unknown')
|
|
162
|
+
elif isinstance(message_params, list):
|
|
163
|
+
forward_param = next((p for p in message_params if isinstance(p, dict) and p.get('type') == 'forward'),
|
|
164
|
+
None)
|
|
165
|
+
if forward_param:
|
|
166
|
+
is_forwarded = True
|
|
167
|
+
forward_origin = forward_param.get('name', 'unknown')
|
|
135
168
|
|
|
136
169
|
return is_forwarded, forward_origin
|
|
137
170
|
|
|
@@ -72,18 +72,7 @@ class AsyncHTTPClient(BaseHTTPClient):
|
|
|
72
72
|
retry: bool = True,
|
|
73
73
|
**kwargs
|
|
74
74
|
) -> HttpResponse:
|
|
75
|
-
"""
|
|
76
|
-
Внутренний метод выполнения запроса с обработкой 401.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
method: HTTP метод
|
|
80
|
-
url: Полный URL
|
|
81
|
-
retry: Флаг повторной попытки
|
|
82
|
-
**kwargs: Параметры для aiohttp
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
HttpResponse объект
|
|
86
|
-
"""
|
|
75
|
+
"""Внутренний метод выполнения запроса с обработкой 401."""
|
|
87
76
|
await self._ensure_session()
|
|
88
77
|
|
|
89
78
|
# Добавляем OCS параметр format=json если его нет
|
|
@@ -95,7 +84,6 @@ class AsyncHTTPClient(BaseHTTPClient):
|
|
|
95
84
|
|
|
96
85
|
# Таймаут по умолчанию
|
|
97
86
|
if 'timeout' not in kwargs:
|
|
98
|
-
# aiohttp требует объект ClientTimeout
|
|
99
87
|
kwargs['timeout'] = aiohttp.ClientTimeout(total=30)
|
|
100
88
|
|
|
101
89
|
try:
|
|
@@ -105,19 +93,25 @@ class AsyncHTTPClient(BaseHTTPClient):
|
|
|
105
93
|
|
|
106
94
|
# При 401 пробуем переавторизоваться
|
|
107
95
|
if status_code == 401 and retry:
|
|
96
|
+
logger.warning(f"Получен 401 для {url}, пересоздаём сессию...")
|
|
108
97
|
await self._reinit_session()
|
|
109
98
|
return await self._make_request(method, url, retry=False, **kwargs)
|
|
110
99
|
|
|
111
100
|
# Парсим ответ
|
|
112
101
|
data = {}
|
|
113
|
-
if status_code in [200, 201]:
|
|
102
|
+
if status_code in [200, 201, 207]: # 207 - Multi-status для PROPFIND
|
|
114
103
|
try:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
# Для PROPFIND не парсим JSON
|
|
105
|
+
if 'xml' in response.headers.get('content-type', ''):
|
|
106
|
+
data = {} # XML обрабатывается отдельно
|
|
107
|
+
else:
|
|
108
|
+
json_response = json.loads(raw_text) if raw_text else {}
|
|
109
|
+
data = json_response.get('ocs', {}).get('data', {})
|
|
118
110
|
except json.JSONDecodeError:
|
|
119
|
-
|
|
111
|
+
if raw_text and not raw_text.startswith('<?xml'):
|
|
112
|
+
logger.error(f"Ошибка парсинга JSON: {raw_text[:200]}")
|
|
120
113
|
|
|
114
|
+
logger.debug(f"Request {method} {url} -> {status_code}")
|
|
121
115
|
return HttpResponse(
|
|
122
116
|
status_code=status_code,
|
|
123
117
|
data=data,
|
|
@@ -125,6 +119,9 @@ class AsyncHTTPClient(BaseHTTPClient):
|
|
|
125
119
|
headers=dict(response.headers)
|
|
126
120
|
)
|
|
127
121
|
|
|
122
|
+
except aiohttp.ClientResponseError as e:
|
|
123
|
+
logger.error(f"ClientResponseError: {e.status} {e.message}")
|
|
124
|
+
return HttpResponse(status_code=e.status, data={}, raw_text=str(e))
|
|
128
125
|
except asyncio.TimeoutError:
|
|
129
126
|
logger.error(f"Timeout при запросе к {url}")
|
|
130
127
|
return HttpResponse(status_code=408, data={}, raw_text="Timeout")
|
|
@@ -182,10 +179,12 @@ class AsyncHTTPClient(BaseHTTPClient):
|
|
|
182
179
|
endpoint: str,
|
|
183
180
|
data: Optional[Dict] = None,
|
|
184
181
|
json_data: Optional[Dict] = None,
|
|
185
|
-
files: Optional[Dict] = None
|
|
182
|
+
files: Optional[Dict] = None,
|
|
183
|
+
params: Optional[Dict] = None # Добавить этот параметр
|
|
186
184
|
) -> HttpResponse:
|
|
187
185
|
"""POST запрос"""
|
|
188
|
-
return await self.request('POST', endpoint, data=data, json_data=json_data, files=files)
|
|
186
|
+
return await self.request('POST', endpoint, data=data, json_data=json_data, files=files, params=params)
|
|
187
|
+
|
|
189
188
|
|
|
190
189
|
async def put(self, endpoint: str, data: Any, headers: Optional[Dict] = None) -> HttpResponse:
|
|
191
190
|
"""
|
|
@@ -153,10 +153,11 @@ class SyncHTTPClient(BaseHTTPClient):
|
|
|
153
153
|
endpoint: str,
|
|
154
154
|
data: Optional[Dict] = None,
|
|
155
155
|
json_data: Optional[Dict] = None,
|
|
156
|
-
files: Optional[Dict] = None
|
|
156
|
+
files: Optional[Dict] = None,
|
|
157
|
+
params: Optional[Dict] = None # Добавить
|
|
157
158
|
) -> HttpResponse:
|
|
158
159
|
"""POST запрос"""
|
|
159
|
-
return self.request('POST', endpoint, data=data, json_data=json_data, files=files)
|
|
160
|
+
return self.request('POST', endpoint, data=data, json_data=json_data, files=files, params=params)
|
|
160
161
|
|
|
161
162
|
def put(self, endpoint: str, data: Any, headers: Optional[Dict] = None) -> HttpResponse:
|
|
162
163
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|