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.
Files changed (20) hide show
  1. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/PKG-INFO +1 -1
  2. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/pyproject.toml +1 -1
  3. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/async_bot.py +22 -1
  4. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/bot_core.py +49 -16
  5. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/async_.py +19 -20
  6. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/sync.py +3 -2
  7. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/.github/workflows/publish-to-pypi.yml +0 -0
  8. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/LICENSE +0 -0
  9. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/README.md +0 -0
  10. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/reqirements.txt +0 -0
  11. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/__init__.py +0 -0
  12. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/__init__.py +0 -0
  13. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/__init__.py +0 -0
  14. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/bot.py +0 -0
  15. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/__init__.py +0 -0
  16. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/core/models.py +0 -0
  17. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/__init__.py +0 -0
  18. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/bot/http/base.py +0 -0
  19. {ifa_nextcloud-0.2.2 → ifa_nextcloud-0.2.3}/src/nextcloud/email/__init__.py +0 -0
  20. {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.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ifa_nextcloud"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  authors = [
9
9
  {name = "ifake", email = "your@email.com"},
10
10
  ]
@@ -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
- for key, param in message_params.items():
85
- if param.get('type') == 'file':
86
- file_info = {
87
- 'file_id': param.get('id', ''),
88
- 'file_name': param.get('name', 'unknown'),
89
- 'file_size': param.get('size', 0),
90
- 'mime_type': param.get('mimetype', 'application/octet-stream'),
91
- 'file_path': param.get('path', ''),
92
- 'download_url': param.get('link', ''),
93
- 'direct_link': param.get('directLink', '')
94
- }
95
- files.append(File(**file_info))
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
- file_param = message_params.get('file', {})
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
- if message_params.get('forward'):
133
- is_forwarded = True
134
- forward_origin = message_params.get('forward', {}).get('name', 'unknown')
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
- json_response = json.loads(raw_text) if raw_text else {}
116
- # Извлекаем ocs.data как в оригинальном боте
117
- data = json_response.get('ocs', {}).get('data', {})
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
- logger.error(f"Ошибка парсинга JSON: {raw_text[:200]}")
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