Skip to content

Commit 3cacc59

Browse files
authored
TalkAPI: added send_file; receive_messages can return TalkFileMessage (#135)
Added simple APIs that will make life much easier for chats: sending a file/directory and converting a text message containing the file/directory directly into FsNode Later will write and examples and add them to documentation, but these API are really simple. Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 734f4fa commit 3cacc59

File tree

9 files changed

+233
-131
lines changed

9 files changed

+233
-131
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file.
66

77
### Added
88

9+
- TalkAPI:
10+
* `send_file` to easy send `FsNode` to Talk chat.
11+
* `receive_messages` can return the `TalkFileMessage` subclass of usual `TalkMessage` with additional functionality.
912
- NextcloudApp: The `ex_app.verify_version` function to simply check whether the application has been updated.
1013

1114
### Changed

docs/reference/ExApp.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.. py:currentmodule:: nc_py_api.ex_app
22
3-
AppAPI Application
4-
==================
3+
External Application
4+
====================
55

66
Constants
77
---------

docs/reference/Talk.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Talk API
99
:members:
1010
:inherited-members:
1111

12+
.. autoclass:: nc_py_api.talk.TalkFileMessage
13+
:members:
14+
:inherited-members:
15+
1216
.. autoclass:: nc_py_api._talk_api._TalkAPI
1317
:members:
1418

nc_py_api/_talk_api.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require_capabilities,
1111
)
1212
from ._session import NcSessionBasic
13+
from .files import FsNode, Share, ShareType
1314
from .talk import (
1415
BotInfo,
1516
BotInfoBasic,
@@ -18,6 +19,7 @@
1819
MessageReactions,
1920
NotificationLevel,
2021
Poll,
22+
TalkFileMessage,
2123
TalkMessage,
2224
)
2325

@@ -191,7 +193,7 @@ def delete_conversation(self, conversation: typing.Union[Conversation, str]) ->
191193
"""Deletes a conversation.
192194
193195
.. note:: Deleting a conversation that is the parent of breakout rooms, will also delete them.
194-
``ONE_TO_ONE`` conversations can not be deleted for them
196+
``ONE_TO_ONE`` conversations cannot be deleted for them
195197
:py:class:`~nc_py_api._talk_api._TalkAPI.leave_conversation` should be used.
196198
197199
:param conversation: conversation token or :py:class:`~nc_py_api.talk.Conversation`.
@@ -244,6 +246,23 @@ def send_message(
244246
r = self._session.ocs("POST", self._ep_base + f"/api/v1/chat/{token}", json=params)
245247
return TalkMessage(r)
246248

249+
def send_file(
250+
self,
251+
path: typing.Union[str, FsNode],
252+
conversation: typing.Union[Conversation, str] = "",
253+
) -> tuple[Share, str]:
254+
require_capabilities("files_sharing.api_enabled", self._session.capabilities)
255+
token = conversation.token if isinstance(conversation, Conversation) else conversation
256+
reference_id = hashlib.sha256(random_string(32).encode("UTF-8")).hexdigest()
257+
params = {
258+
"shareType": ShareType.TYPE_ROOM,
259+
"shareWith": token,
260+
"path": path.user_path if isinstance(path, FsNode) else path,
261+
"referenceId": reference_id,
262+
}
263+
r = self._session.ocs("POST", "/ocs/v1.php/apps/files_sharing/api/v1/shares", json=params)
264+
return Share(r), reference_id
265+
247266
def receive_messages(
248267
self,
249268
conversation: typing.Union[Conversation, str],
@@ -268,7 +287,7 @@ def receive_messages(
268287
"noStatusUpdate": int(no_status_update),
269288
}
270289
result = self._session.ocs("GET", self._ep_base + f"/api/v1/chat/{token}", params=params)
271-
return [TalkMessage(i) for i in result]
290+
return [TalkFileMessage(i, self._session.user) if i["message"] == "{file}" else TalkMessage(i) for i in result]
272291

273292
def delete_message(
274293
self, message: typing.Union[TalkMessage, str], conversation: typing.Union[Conversation, str] = ""

nc_py_api/ex_app/ui/files.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ..._exceptions import NextcloudExceptionNotFound
99
from ..._misc import require_capabilities
1010
from ..._session import NcSessionApp
11-
from ...files import FilePermissions, FsNode
11+
from ...files import FsNode, permissions_to_str
1212

1313

1414
class UiActionFileInfo(BaseModel):
@@ -51,16 +51,7 @@ def to_fs_node(self) -> FsNode:
5151
file_id = str(self.fileId).rjust(8, "0")
5252

5353
permissions = "S" if self.shareOwnerId else ""
54-
if self.permissions & FilePermissions.PERMISSION_SHARE:
55-
permissions += "R"
56-
if self.permissions & FilePermissions.PERMISSION_READ:
57-
permissions += "G"
58-
if self.permissions & FilePermissions.PERMISSION_DELETE:
59-
permissions += "D"
60-
if self.permissions & FilePermissions.PERMISSION_UPDATE:
61-
permissions += "NV" if is_dir else "NVW"
62-
if is_dir and self.permissions & FilePermissions.PERMISSION_CREATE:
63-
permissions += "CK"
54+
permissions += permissions_to_str(self.permissions, is_dir)
6455
return FsNode(
6556
full_path,
6657
etag=self.etag,

nc_py_api/files/__init__.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import enum
77
import typing
88

9+
from .. import _misc
10+
911

1012
@dataclasses.dataclass
1113
class FsNodeInfo:
@@ -216,6 +218,26 @@ class FilePermissions(enum.IntFlag):
216218
"""Access to re-share object(s)"""
217219

218220

221+
def permissions_to_str(permissions: int, is_dir: bool = False) -> str:
222+
"""Converts integer permissions to string permissions.
223+
224+
:param permissions: concatenation of ``FilePermissions`` integer flags.
225+
:param is_dir: Flag indicating is permissions related to the directory object or not.
226+
"""
227+
r = ""
228+
if permissions & FilePermissions.PERMISSION_SHARE:
229+
r += "R"
230+
if permissions & FilePermissions.PERMISSION_READ:
231+
r += "G"
232+
if permissions & FilePermissions.PERMISSION_DELETE:
233+
r += "D"
234+
if permissions & FilePermissions.PERMISSION_UPDATE:
235+
r += "NV" if is_dir else "NVW"
236+
if is_dir and permissions & FilePermissions.PERMISSION_CREATE:
237+
r += "CK"
238+
return r
239+
240+
219241
@dataclasses.dataclass
220242
class SystemTag:
221243
"""Nextcloud System Tag."""
@@ -242,3 +264,113 @@ def user_visible(self) -> bool:
242264
def user_assignable(self) -> bool:
243265
"""Flag indicating if User can assign this Tag."""
244266
return bool(self._raw_data.get("oc:user-assignable", "false").lower() == "true")
267+
268+
269+
class ShareType(enum.IntEnum):
270+
"""Type of the object that will receive share."""
271+
272+
TYPE_USER = 0
273+
"""Share to the user"""
274+
TYPE_GROUP = 1
275+
"""Share to the group"""
276+
TYPE_LINK = 3
277+
"""Share by link"""
278+
TYPE_EMAIL = 4
279+
"""Share by the email"""
280+
TYPE_REMOTE = 6
281+
"""Share to the Federation"""
282+
TYPE_CIRCLE = 7
283+
"""Share to the Nextcloud Circle"""
284+
TYPE_GUEST = 8
285+
"""Share to `Guest`"""
286+
TYPE_REMOTE_GROUP = 9
287+
"""Share to the Federation group"""
288+
TYPE_ROOM = 10
289+
"""Share to the Talk room"""
290+
TYPE_DECK = 11
291+
"""Share to the Nextcloud Deck"""
292+
TYPE_SCIENCE_MESH = 15
293+
"""Share to the Reva instance(Science Mesh)"""
294+
295+
296+
class Share:
297+
"""Information about Share."""
298+
299+
def __init__(self, raw_data: dict):
300+
self.raw_data = raw_data
301+
302+
@property
303+
def share_id(self) -> int:
304+
"""Unique ID of the share."""
305+
return int(self.raw_data["id"])
306+
307+
@property
308+
def share_type(self) -> ShareType:
309+
"""Type of the share."""
310+
return ShareType(int(self.raw_data["share_type"]))
311+
312+
@property
313+
def share_with(self) -> str:
314+
"""To whom Share was created."""
315+
return self.raw_data["share_with"]
316+
317+
@property
318+
def permissions(self) -> FilePermissions:
319+
"""Recipient permissions."""
320+
return FilePermissions(int(self.raw_data["permissions"]))
321+
322+
@property
323+
def url(self) -> str:
324+
"""URL at which Share is avalaible."""
325+
return self.raw_data.get("url", "")
326+
327+
@property
328+
def path(self) -> str:
329+
"""Share path relative to the user's root directory."""
330+
return self.raw_data.get("path", "").lstrip("/")
331+
332+
@property
333+
def label(self) -> str:
334+
"""Label for the Shared object."""
335+
return self.raw_data.get("label", "")
336+
337+
@property
338+
def note(self) -> str:
339+
"""Note for the Shared object."""
340+
return self.raw_data.get("note", "")
341+
342+
@property
343+
def mimetype(self) -> str:
344+
"""Mimetype of the Shared object."""
345+
return self.raw_data.get("mimetype", "")
346+
347+
@property
348+
def share_owner(self) -> str:
349+
"""Share's creator ID."""
350+
return self.raw_data.get("uid_owner", "")
351+
352+
@property
353+
def file_owner(self) -> str:
354+
"""File/directory owner ID."""
355+
return self.raw_data.get("uid_file_owner", "")
356+
357+
@property
358+
def password(self) -> str:
359+
"""Password to access share."""
360+
return self.raw_data.get("password", "")
361+
362+
@property
363+
def send_password_by_talk(self) -> bool:
364+
"""Flag indicating was password send by Talk."""
365+
return self.raw_data.get("send_password_by_talk", False)
366+
367+
@property
368+
def expire_date(self) -> datetime.datetime:
369+
"""Share expiration time."""
370+
return _misc.nc_iso_time_to_datetime(self.raw_data.get("expiration", ""))
371+
372+
def __str__(self):
373+
return (
374+
f"{self.share_type.name}: `{self.path}` with id={self.share_id}"
375+
f" from {self.share_owner} to {self.share_with}"
376+
)

nc_py_api/files/sharing.py

Lines changed: 3 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,9 @@
11
"""Nextcloud API for working with the files shares."""
2-
import datetime
3-
import enum
2+
43
import typing
54

65
from .. import _misc, _session
7-
from . import FilePermissions, FsNode
8-
9-
10-
class ShareType(enum.IntEnum):
11-
"""Type of the object that will receive share."""
12-
13-
TYPE_USER = 0
14-
"""Share to the user"""
15-
TYPE_GROUP = 1
16-
"""Share to the group"""
17-
TYPE_LINK = 3
18-
"""Share by link"""
19-
TYPE_EMAIL = 4
20-
"""Share by the email"""
21-
TYPE_REMOTE = 6
22-
"""Share to the Federation"""
23-
TYPE_CIRCLE = 7
24-
"""Share to the Nextcloud Circle"""
25-
TYPE_GUEST = 8
26-
"""Share to `Guest`"""
27-
TYPE_REMOTE_GROUP = 9
28-
"""Share to the Federation group"""
29-
TYPE_ROOM = 10
30-
"""Share to the Talk room"""
31-
TYPE_DECK = 11
32-
"""Share to the Nextcloud Deck"""
33-
TYPE_SCIENCE_MESH = 15
34-
"""Share to the Reva instance(Science Mesh)"""
35-
36-
37-
class Share:
38-
"""Information about Share."""
39-
40-
def __init__(self, raw_data: dict):
41-
self.raw_data = raw_data
42-
43-
@property
44-
def share_id(self) -> int:
45-
"""Unique ID of the share."""
46-
return int(self.raw_data["id"])
47-
48-
@property
49-
def share_type(self) -> ShareType:
50-
"""Type of the share."""
51-
return ShareType(int(self.raw_data["share_type"]))
52-
53-
@property
54-
def share_with(self) -> str:
55-
"""To whom Share was created."""
56-
return self.raw_data["share_with"]
57-
58-
@property
59-
def permissions(self) -> FilePermissions:
60-
"""Recipient permissions."""
61-
return FilePermissions(int(self.raw_data["permissions"]))
62-
63-
@property
64-
def url(self) -> str:
65-
"""URL at which Share is avalaible."""
66-
return self.raw_data.get("url", "")
67-
68-
@property
69-
def path(self) -> str:
70-
"""Share path relative to the user's root directory."""
71-
return self.raw_data.get("path", "").lstrip("/")
72-
73-
@property
74-
def label(self) -> str:
75-
"""Label for the Shared object."""
76-
return self.raw_data.get("label", "")
77-
78-
@property
79-
def note(self) -> str:
80-
"""Note for the Shared object."""
81-
return self.raw_data.get("note", "")
82-
83-
@property
84-
def mimetype(self) -> str:
85-
"""Mimetype of the Shared object."""
86-
return self.raw_data.get("mimetype", "")
87-
88-
@property
89-
def share_owner(self) -> str:
90-
"""Share's creator ID."""
91-
return self.raw_data.get("uid_owner", "")
92-
93-
@property
94-
def file_owner(self) -> str:
95-
"""File/directory owner ID."""
96-
return self.raw_data.get("uid_file_owner", "")
97-
98-
@property
99-
def password(self) -> str:
100-
"""Password to access share."""
101-
return self.raw_data.get("password", "")
102-
103-
@property
104-
def send_password_by_talk(self) -> bool:
105-
"""Flag indicating was password send by Talk."""
106-
return self.raw_data.get("send_password_by_talk", False)
107-
108-
@property
109-
def expire_date(self) -> datetime.datetime:
110-
"""Share expiration time."""
111-
return _misc.nc_iso_time_to_datetime(self.raw_data.get("expiration", ""))
112-
113-
def __str__(self):
114-
return (
115-
f"{self.share_type.name}: `{self.path}` with id={self.share_id}"
116-
f" from {self.share_owner} to {self.share_with}"
117-
)
6+
from . import FilePermissions, FsNode, Share, ShareType
1187

1198

1209
class _FilesSharingAPI:
@@ -193,9 +82,8 @@ def create(
19382
* ``label`` - string with label, if any. default = ``""``
19483
"""
19584
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
196-
path = path.user_path if isinstance(path, FsNode) else path
19785
params = {
198-
"path": path,
86+
"path": path.user_path if isinstance(path, FsNode) else path,
19987
"shareType": int(share_type),
20088
}
20189
if permissions is not None:

0 commit comments

Comments
 (0)