Skip to content

TalkAPI: added send_file; receive_messages can return TalkFileMessage #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file.

### Added

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

### Changed
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/ExApp.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. py:currentmodule:: nc_py_api.ex_app

AppAPI Application
==================
External Application
====================

Constants
---------
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/Talk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Talk API
:members:
:inherited-members:

.. autoclass:: nc_py_api.talk.TalkFileMessage
:members:
:inherited-members:

.. autoclass:: nc_py_api._talk_api._TalkAPI
:members:

Expand Down
23 changes: 21 additions & 2 deletions nc_py_api/_talk_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require_capabilities,
)
from ._session import NcSessionBasic
from .files import FsNode, Share, ShareType
from .talk import (
BotInfo,
BotInfoBasic,
Expand All @@ -18,6 +19,7 @@
MessageReactions,
NotificationLevel,
Poll,
TalkFileMessage,
TalkMessage,
)

Expand Down Expand Up @@ -191,7 +193,7 @@ def delete_conversation(self, conversation: typing.Union[Conversation, str]) ->
"""Deletes a conversation.

.. note:: Deleting a conversation that is the parent of breakout rooms, will also delete them.
``ONE_TO_ONE`` conversations can not be deleted for them
``ONE_TO_ONE`` conversations cannot be deleted for them
:py:class:`~nc_py_api._talk_api._TalkAPI.leave_conversation` should be used.

:param conversation: conversation token or :py:class:`~nc_py_api.talk.Conversation`.
Expand Down Expand Up @@ -244,6 +246,23 @@ def send_message(
r = self._session.ocs("POST", self._ep_base + f"/api/v1/chat/{token}", json=params)
return TalkMessage(r)

def send_file(
self,
path: typing.Union[str, FsNode],
conversation: typing.Union[Conversation, str] = "",
) -> tuple[Share, str]:
require_capabilities("files_sharing.api_enabled", self._session.capabilities)
token = conversation.token if isinstance(conversation, Conversation) else conversation
reference_id = hashlib.sha256(random_string(32).encode("UTF-8")).hexdigest()
params = {
"shareType": ShareType.TYPE_ROOM,
"shareWith": token,
"path": path.user_path if isinstance(path, FsNode) else path,
"referenceId": reference_id,
}
r = self._session.ocs("POST", "/ocs/v1.php/apps/files_sharing/api/v1/shares", json=params)
return Share(r), reference_id

def receive_messages(
self,
conversation: typing.Union[Conversation, str],
Expand All @@ -268,7 +287,7 @@ def receive_messages(
"noStatusUpdate": int(no_status_update),
}
result = self._session.ocs("GET", self._ep_base + f"/api/v1/chat/{token}", params=params)
return [TalkMessage(i) for i in result]
return [TalkFileMessage(i, self._session.user) if i["message"] == "{file}" else TalkMessage(i) for i in result]

def delete_message(
self, message: typing.Union[TalkMessage, str], conversation: typing.Union[Conversation, str] = ""
Expand Down
13 changes: 2 additions & 11 deletions nc_py_api/ex_app/ui/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ..._exceptions import NextcloudExceptionNotFound
from ..._misc import require_capabilities
from ..._session import NcSessionApp
from ...files import FilePermissions, FsNode
from ...files import FsNode, permissions_to_str


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

permissions = "S" if self.shareOwnerId else ""
if self.permissions & FilePermissions.PERMISSION_SHARE:
permissions += "R"
if self.permissions & FilePermissions.PERMISSION_READ:
permissions += "G"
if self.permissions & FilePermissions.PERMISSION_DELETE:
permissions += "D"
if self.permissions & FilePermissions.PERMISSION_UPDATE:
permissions += "NV" if is_dir else "NVW"
if is_dir and self.permissions & FilePermissions.PERMISSION_CREATE:
permissions += "CK"
permissions += permissions_to_str(self.permissions, is_dir)
return FsNode(
full_path,
etag=self.etag,
Expand Down
132 changes: 132 additions & 0 deletions nc_py_api/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import enum
import typing

from .. import _misc


@dataclasses.dataclass
class FsNodeInfo:
Expand Down Expand Up @@ -216,6 +218,26 @@ class FilePermissions(enum.IntFlag):
"""Access to re-share object(s)"""


def permissions_to_str(permissions: int, is_dir: bool = False) -> str:
"""Converts integer permissions to string permissions.

:param permissions: concatenation of ``FilePermissions`` integer flags.
:param is_dir: Flag indicating is permissions related to the directory object or not.
"""
r = ""
if permissions & FilePermissions.PERMISSION_SHARE:
r += "R"
if permissions & FilePermissions.PERMISSION_READ:
r += "G"
if permissions & FilePermissions.PERMISSION_DELETE:
r += "D"
if permissions & FilePermissions.PERMISSION_UPDATE:
r += "NV" if is_dir else "NVW"
if is_dir and permissions & FilePermissions.PERMISSION_CREATE:
r += "CK"
return r


@dataclasses.dataclass
class SystemTag:
"""Nextcloud System Tag."""
Expand All @@ -242,3 +264,113 @@ def user_visible(self) -> bool:
def user_assignable(self) -> bool:
"""Flag indicating if User can assign this Tag."""
return bool(self._raw_data.get("oc:user-assignable", "false").lower() == "true")


class ShareType(enum.IntEnum):
"""Type of the object that will receive share."""

TYPE_USER = 0
"""Share to the user"""
TYPE_GROUP = 1
"""Share to the group"""
TYPE_LINK = 3
"""Share by link"""
TYPE_EMAIL = 4
"""Share by the email"""
TYPE_REMOTE = 6
"""Share to the Federation"""
TYPE_CIRCLE = 7
"""Share to the Nextcloud Circle"""
TYPE_GUEST = 8
"""Share to `Guest`"""
TYPE_REMOTE_GROUP = 9
"""Share to the Federation group"""
TYPE_ROOM = 10
"""Share to the Talk room"""
TYPE_DECK = 11
"""Share to the Nextcloud Deck"""
TYPE_SCIENCE_MESH = 15
"""Share to the Reva instance(Science Mesh)"""


class Share:
"""Information about Share."""

def __init__(self, raw_data: dict):
self.raw_data = raw_data

@property
def share_id(self) -> int:
"""Unique ID of the share."""
return int(self.raw_data["id"])

@property
def share_type(self) -> ShareType:
"""Type of the share."""
return ShareType(int(self.raw_data["share_type"]))

@property
def share_with(self) -> str:
"""To whom Share was created."""
return self.raw_data["share_with"]

@property
def permissions(self) -> FilePermissions:
"""Recipient permissions."""
return FilePermissions(int(self.raw_data["permissions"]))

@property
def url(self) -> str:
"""URL at which Share is avalaible."""
return self.raw_data.get("url", "")

@property
def path(self) -> str:
"""Share path relative to the user's root directory."""
return self.raw_data.get("path", "").lstrip("/")

@property
def label(self) -> str:
"""Label for the Shared object."""
return self.raw_data.get("label", "")

@property
def note(self) -> str:
"""Note for the Shared object."""
return self.raw_data.get("note", "")

@property
def mimetype(self) -> str:
"""Mimetype of the Shared object."""
return self.raw_data.get("mimetype", "")

@property
def share_owner(self) -> str:
"""Share's creator ID."""
return self.raw_data.get("uid_owner", "")

@property
def file_owner(self) -> str:
"""File/directory owner ID."""
return self.raw_data.get("uid_file_owner", "")

@property
def password(self) -> str:
"""Password to access share."""
return self.raw_data.get("password", "")

@property
def send_password_by_talk(self) -> bool:
"""Flag indicating was password send by Talk."""
return self.raw_data.get("send_password_by_talk", False)

@property
def expire_date(self) -> datetime.datetime:
"""Share expiration time."""
return _misc.nc_iso_time_to_datetime(self.raw_data.get("expiration", ""))

def __str__(self):
return (
f"{self.share_type.name}: `{self.path}` with id={self.share_id}"
f" from {self.share_owner} to {self.share_with}"
)
118 changes: 3 additions & 115 deletions nc_py_api/files/sharing.py
Original file line number Diff line number Diff line change
@@ -1,120 +1,9 @@
"""Nextcloud API for working with the files shares."""
import datetime
import enum

import typing

from .. import _misc, _session
from . import FilePermissions, FsNode


class ShareType(enum.IntEnum):
"""Type of the object that will receive share."""

TYPE_USER = 0
"""Share to the user"""
TYPE_GROUP = 1
"""Share to the group"""
TYPE_LINK = 3
"""Share by link"""
TYPE_EMAIL = 4
"""Share by the email"""
TYPE_REMOTE = 6
"""Share to the Federation"""
TYPE_CIRCLE = 7
"""Share to the Nextcloud Circle"""
TYPE_GUEST = 8
"""Share to `Guest`"""
TYPE_REMOTE_GROUP = 9
"""Share to the Federation group"""
TYPE_ROOM = 10
"""Share to the Talk room"""
TYPE_DECK = 11
"""Share to the Nextcloud Deck"""
TYPE_SCIENCE_MESH = 15
"""Share to the Reva instance(Science Mesh)"""


class Share:
"""Information about Share."""

def __init__(self, raw_data: dict):
self.raw_data = raw_data

@property
def share_id(self) -> int:
"""Unique ID of the share."""
return int(self.raw_data["id"])

@property
def share_type(self) -> ShareType:
"""Type of the share."""
return ShareType(int(self.raw_data["share_type"]))

@property
def share_with(self) -> str:
"""To whom Share was created."""
return self.raw_data["share_with"]

@property
def permissions(self) -> FilePermissions:
"""Recipient permissions."""
return FilePermissions(int(self.raw_data["permissions"]))

@property
def url(self) -> str:
"""URL at which Share is avalaible."""
return self.raw_data.get("url", "")

@property
def path(self) -> str:
"""Share path relative to the user's root directory."""
return self.raw_data.get("path", "").lstrip("/")

@property
def label(self) -> str:
"""Label for the Shared object."""
return self.raw_data.get("label", "")

@property
def note(self) -> str:
"""Note for the Shared object."""
return self.raw_data.get("note", "")

@property
def mimetype(self) -> str:
"""Mimetype of the Shared object."""
return self.raw_data.get("mimetype", "")

@property
def share_owner(self) -> str:
"""Share's creator ID."""
return self.raw_data.get("uid_owner", "")

@property
def file_owner(self) -> str:
"""File/directory owner ID."""
return self.raw_data.get("uid_file_owner", "")

@property
def password(self) -> str:
"""Password to access share."""
return self.raw_data.get("password", "")

@property
def send_password_by_talk(self) -> bool:
"""Flag indicating was password send by Talk."""
return self.raw_data.get("send_password_by_talk", False)

@property
def expire_date(self) -> datetime.datetime:
"""Share expiration time."""
return _misc.nc_iso_time_to_datetime(self.raw_data.get("expiration", ""))

def __str__(self):
return (
f"{self.share_type.name}: `{self.path}` with id={self.share_id}"
f" from {self.share_owner} to {self.share_with}"
)
from . import FilePermissions, FsNode, Share, ShareType


class _FilesSharingAPI:
Expand Down Expand Up @@ -193,9 +82,8 @@ def create(
* ``label`` - string with label, if any. default = ``""``
"""
_misc.require_capabilities("files_sharing.api_enabled", self._session.capabilities)
path = path.user_path if isinstance(path, FsNode) else path
params = {
"path": path,
"path": path.user_path if isinstance(path, FsNode) else path,
"shareType": int(share_type),
}
if permissions is not None:
Expand Down
Loading