Skip to content

Commit 2f4e225

Browse files
authored
added Activities API (#112)
Also added `nc_iso_time_to_datetime` internal method to parse time for shares, notifications and activities. This leads to fix of a bug when `NotificationInfo.time` was always incorrectly parsed and equal to `datetime(1970,1,1)` --------- Signed-off-by: Alexander Piskun <[email protected]>
1 parent 870b6af commit 2f4e225

File tree

19 files changed

+335
-17
lines changed

19 files changed

+335
-17
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,13 @@ jobs:
653653
ref: "master"
654654
path: apps/notifications
655655

656+
- name: Checkout Activity
657+
uses: actions/checkout@v3
658+
with:
659+
repository: nextcloud/activity
660+
ref: "master"
661+
path: apps/activity
662+
656663
- name: Set up & run Nextcloud
657664
env:
658665
DB_PORT: 4444
@@ -664,6 +671,7 @@ jobs:
664671
./occ config:system:set loglevel --value=0 --type=integer
665672
./occ config:system:set debug --value=true --type=boolean
666673
./occ app:enable notifications
674+
./occ app:enable activity
667675
php -S localhost:8080 &
668676
669677
- name: Checkout NcPyApi
@@ -761,6 +769,13 @@ jobs:
761769
ref: ${{ matrix.nextcloud }}
762770
path: apps/notifications
763771

772+
- name: Checkout Activity
773+
uses: actions/checkout@v3
774+
with:
775+
repository: nextcloud/activity
776+
ref: ${{ matrix.nextcloud }}
777+
path: apps/activity
778+
764779
- name: Set up & run Nextcloud
765780
env:
766781
DB_PORT: 4444
@@ -772,6 +787,7 @@ jobs:
772787
./occ config:system:set loglevel --value=0 --type=integer
773788
./occ config:system:set debug --value=true --type=boolean
774789
./occ app:enable notifications
790+
./occ app:enable activity
775791
php -S localhost:8080 &
776792
777793
- name: Checkout NcPyApi

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.0.44 - 2023-09-0x]
6+
7+
### Added
8+
9+
- Activity API: `get_filters` and `get_activities`.
10+
11+
### Fixed
12+
13+
- `NotificationInfo.time` - was always incorrectly parsed and equal to `datetime(1970,1,1)`
14+
515
## [0.0.43 - 2023-09-02]
616

717
### Added

docs/reference/ActivityApp.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Activity App
2+
------------
3+
4+
.. autoclass:: nc_py_api.activity.ActivityFilter
5+
:members:
6+
7+
.. autoclass:: nc_py_api.activity.Activity
8+
:members:
9+
10+
.. autoclass:: nc_py_api.activity._ActivityAPI
11+
:members:

docs/reference/Exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Avalaible as `nc_py_api.{exception_name}`
88
.. autoclass:: NextcloudException
99
:members:
1010

11+
.. autoclass:: NextcloudExceptionNotModified
12+
:members:
13+
1114
.. autoclass:: NextcloudExceptionNotFound
1215
:members:
1316

docs/reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Reference
66

77
Nextcloud
88
Apps
9+
ActivityApp
910
Files/index.rst
1011
Users/index.rst
1112
ExApp

nc_py_api/_exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ def __str__(self):
2020
return f"[{self.status_code}]{reason}{info}"
2121

2222

23+
class NextcloudExceptionNotModified(NextcloudException):
24+
"""The exception indicates that there is no need to retransmit the requested resources."""
25+
26+
def __init__(self, reason="Not modified", info: str = ""):
27+
super().__init__(304, reason=reason, info=info)
28+
29+
2330
class NextcloudExceptionNotFound(NextcloudException):
2431
"""The exception that is thrown during operations when the object is not found."""
2532

nc_py_api/_misc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
For internal use, prototypes can change between versions.
44
"""
5-
5+
import datetime
66
from random import choice
77
from string import ascii_lowercase, ascii_uppercase, digits
88
from typing import Callable, Union
@@ -66,3 +66,11 @@ def random_string(size: int) -> str:
6666
"""Generates a random ASCII string of the given size."""
6767
letters = ascii_lowercase + ascii_uppercase + digits
6868
return "".join(choice(letters) for _ in range(size))
69+
70+
71+
def nc_iso_time_to_datetime(iso8601_time: str) -> datetime.datetime:
72+
"""Returns parsed ``datetime`` or datetime(1970, 1, 1) in case of error."""
73+
try:
74+
return datetime.datetime.fromisoformat(iso8601_time)
75+
except (ValueError, TypeError):
76+
return datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)

nc_py_api/_session.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727

2828

2929
from . import options
30-
from ._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
30+
from ._exceptions import (
31+
NextcloudException,
32+
NextcloudExceptionNotFound,
33+
NextcloudExceptionNotModified,
34+
check_error,
35+
)
3136

3237

3338
class OCSRespond(IntEnum):
@@ -219,6 +224,8 @@ def _ocs(self, method: str, path_params: str, headers: dict, data: Optional[byte
219224
return self._ocs(method, path_params, headers, data, **kwargs, nested_req=True)
220225
if ocs_meta["statuscode"] in (404, OCSRespond.RESPOND_NOT_FOUND):
221226
raise NextcloudExceptionNotFound(reason=ocs_meta["message"], info=info)
227+
if ocs_meta["statuscode"] == 304:
228+
raise NextcloudExceptionNotModified(reason=ocs_meta["message"], info=info)
222229
raise NextcloudException(status_code=ocs_meta["statuscode"], reason=ocs_meta["message"], info=info)
223230
return response_data["ocs"]["data"]
224231

nc_py_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version of nc_py_api."""
22

3-
__version__ = "0.0.43"
3+
__version__ = "0.0.44.dev0"

nc_py_api/activity.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""API for working with Activity App."""
2+
3+
import dataclasses
4+
import datetime
5+
import typing
6+
7+
from ._exceptions import NextcloudExceptionNotModified
8+
from ._misc import check_capabilities, nc_iso_time_to_datetime
9+
from ._session import NcSessionBasic
10+
11+
12+
@dataclasses.dataclass
13+
class ActivityFilter:
14+
"""Activity filter description."""
15+
16+
def __init__(self, raw_data: dict):
17+
self._raw_data = raw_data
18+
19+
@property
20+
def icon(self) -> str:
21+
"""Icon for filter."""
22+
return self._raw_data["icon"]
23+
24+
@property
25+
def filter_id(self) -> str:
26+
"""Filter ID."""
27+
return self._raw_data["id"]
28+
29+
@property
30+
def name(self) -> str:
31+
"""Filter name."""
32+
return self._raw_data["name"]
33+
34+
@property
35+
def priority(self) -> int:
36+
"""Arrangement priority in ascending order. Values from 0 to 99."""
37+
return self._raw_data["priority"]
38+
39+
40+
@dataclasses.dataclass
41+
class Activity:
42+
"""Description of one activity."""
43+
44+
def __init__(self, raw_data: dict):
45+
self._raw_data = raw_data
46+
47+
@property
48+
def activity_id(self) -> int:
49+
"""Unique for one Nextcloud instance activity ID."""
50+
return self._raw_data["activity_id"]
51+
52+
@property
53+
def app(self) -> str:
54+
"""App that created the activity (e.g. 'files', 'files_sharing', etc.)."""
55+
return self._raw_data["app"]
56+
57+
@property
58+
def activity_type(self) -> str:
59+
"""String describing the activity type, depends on the **app** field."""
60+
return self._raw_data["type"]
61+
62+
@property
63+
def actor_id(self) -> str:
64+
"""User ID of the user that triggered/created this activity.
65+
66+
.. note:: Can be empty in case of public link/remote share action.
67+
"""
68+
return self._raw_data["user"]
69+
70+
@property
71+
def subject(self) -> str:
72+
"""Translated simple subject without markup, ready for use (e.g. 'You created hello.jpg')."""
73+
return self._raw_data["subject"]
74+
75+
@property
76+
def subject_rich(self) -> list:
77+
"""`0` is the string subject including placeholders, `1` is an array with the placeholders."""
78+
return self._raw_data["subject_rich"]
79+
80+
@property
81+
def message(self) -> str:
82+
"""Translated message without markup, ready for use (longer text, unused by core apps)."""
83+
return self._raw_data["message"]
84+
85+
@property
86+
def message_rich(self) -> list:
87+
"""See description of **subject_rich**."""
88+
return self._raw_data["message_rich"]
89+
90+
@property
91+
def object_type(self) -> str:
92+
"""The Type of the object this activity is about (e.g. 'files' is used for files and folders)."""
93+
return self._raw_data["object_type"]
94+
95+
@property
96+
def object_id(self) -> int:
97+
"""ID of the object this activity is about (e.g., ID in the file cache is used for files and folders)."""
98+
return self._raw_data["object_id"]
99+
100+
@property
101+
def object_name(self) -> str:
102+
"""The name of the object this activity is about (e.g., for files it's the relative path to the user's root)."""
103+
return self._raw_data["object_name"]
104+
105+
@property
106+
def objects(self) -> dict:
107+
"""Contains the objects involved in multi-object activities, like editing multiple files in a folder.
108+
109+
.. note:: They are stored in objects as key-value pairs of the object_id and the object_name:
110+
{ object_id: object_name}
111+
"""
112+
return self._raw_data["objects"]
113+
114+
@property
115+
def link(self) -> str:
116+
"""A full URL pointing to a suitable location (e.g. 'http://localhost/apps/files/?dir=%2Ffolder' for folder)."""
117+
return self._raw_data["link"]
118+
119+
@property
120+
def icon(self) -> str:
121+
"""URL of the icon."""
122+
return self._raw_data["icon"]
123+
124+
@property
125+
def activity_time(self) -> datetime.datetime:
126+
"""Time when the activity occurred."""
127+
return nc_iso_time_to_datetime(self._raw_data["datetime"])
128+
129+
130+
class _ActivityAPI:
131+
"""The class provides the Activity Application API."""
132+
133+
_ep_base: str = "/ocs/v1.php/apps/activity"
134+
last_given: int
135+
"""Used by ``get_activities``, when **since** param is ``True``."""
136+
137+
def __init__(self, session: NcSessionBasic):
138+
self._session = session
139+
self.last_given = 0
140+
141+
@property
142+
def available(self) -> bool:
143+
"""Returns True if the Nextcloud instance supports this feature, False otherwise."""
144+
return not check_capabilities("activity.apiv2", self._session.capabilities)
145+
146+
def get_activities(
147+
self,
148+
filter_id: typing.Union[ActivityFilter, str] = "",
149+
since: typing.Union[int, bool] = 0,
150+
limit: int = 50,
151+
object_type: str = "",
152+
object_id: int = 0,
153+
sort: str = "desc",
154+
) -> list[Activity]:
155+
"""Returns activities for the current user.
156+
157+
:param filter_id: Filter to apply, if needed.
158+
:param since: Last activity ID you have seen. When specified, only activities after provided are returned.
159+
Can be set to ``True`` to automatically use last ``last_given`` from previous calls. Default = **0**.
160+
:param limit: Max number of activities to be returned.
161+
:param object_type: Filter the activities to a given object.
162+
:param object_id: Filter the activities to a given object.
163+
:param sort: Sort activities ascending or descending. Default is ``desc``.
164+
165+
.. note:: ``object_type`` and ``object_id`` should only appear together with ``filter_id`` unset.
166+
"""
167+
if bool(object_id) != bool(object_type):
168+
raise ValueError("Either specify both `object_type` and `object_id`, or don't specify any at all.")
169+
if since is True:
170+
since = self.last_given
171+
filter_id = filter_id.filter_id if isinstance(filter_id, ActivityFilter) else filter_id
172+
params = {
173+
"since": since,
174+
"limit": limit,
175+
"object_type": object_type,
176+
"object_id": object_id,
177+
"sort": sort,
178+
}
179+
url = (
180+
f"/api/v2/activity/{filter_id}"
181+
if filter_id
182+
else "/api/v2/activity/filter" if object_id else "/api/v2/activity"
183+
)
184+
try:
185+
result = self._session.ocs("GET", self._ep_base + url, params=params)
186+
except NextcloudExceptionNotModified:
187+
return []
188+
self.last_given = int(self._session.response_headers["X-Activity-Last-Given"])
189+
return [Activity(i) for i in result]
190+
191+
def get_filters(self) -> list[ActivityFilter]:
192+
"""Returns avalaible activity filters."""
193+
return [ActivityFilter(i) for i in self._session.ocs("GET", self._ep_base + "/api/v2/activity/filters")]

0 commit comments

Comments
 (0)