roboherd 0.1.4__tar.gz → 0.1.6__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.
Potentially problematic release.
This version of roboherd might be problematic. Click here for more details.
- {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/test.yml +3 -1
- {roboherd-0.1.4 → roboherd-0.1.6}/CHANGES.md +14 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/PKG-INFO +4 -1
- roboherd-0.1.6/docs/assets/bull-horns.png +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/index.md +71 -11
- {roboherd-0.1.4 → roboherd-0.1.6}/mkdocs.yml +2 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/pyproject.toml +6 -2
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/annotations/__init__.py +2 -15
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/annotations/bovine.py +28 -2
- roboherd-0.1.6/roboherd/annotations/common.py +43 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/__init__.py +48 -58
- roboherd-0.1.6/roboherd/cow/const.py +6 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/profile.py +4 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_init.py +4 -3
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_profile.py +35 -3
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/types.py +7 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/moocow.py +4 -2
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/scarecrow.py +7 -1
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/__init__.py +4 -4
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/__init__.py +1 -1
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/config.py +14 -7
- roboherd-0.1.6/roboherd/herd/manager/load.py +15 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/test_config.py +16 -0
- roboherd-0.1.6/roboherd/herd/manager/test_load.py +19 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/processor.py +2 -2
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/scheduler.py +1 -1
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/util.py +2 -51
- {roboherd-0.1.4 → roboherd-0.1.6}/uv.lock +217 -237
- roboherd-0.1.4/roboherd/annotations/common.py +0 -14
- roboherd-0.1.4/roboherd/test_util.py +0 -24
- {roboherd-0.1.4 → roboherd-0.1.6}/.gitignore +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/create_release.yml +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/publish_docker.yml +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/publish_pypi.yml +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/website.yml +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/README.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/annotations.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/assets/mastodon.png +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/cli.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/cow.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/herd.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/docs/util.md +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/resources/docker/Dockerfile +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/resources/docker/build.sh +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/__init__.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/__main__.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/handlers.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_handlers.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_util.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/util.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/__init__.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/dev_null.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/json_echo.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/meta.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/number.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/rooster.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/builder.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/test_manager.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/test_herd.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/test_scheduler.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/types.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/register.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/test_validators.py +0 -0
- {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/validators.py +0 -0
|
@@ -13,8 +13,10 @@ steps:
|
|
|
13
13
|
test_format:
|
|
14
14
|
image: ghcr.io/astral-sh/uv:python3.11-alpine
|
|
15
15
|
commands:
|
|
16
|
-
- uv run ruff format .
|
|
16
|
+
- uv run ruff format --check .
|
|
17
17
|
- uv run ruff check .
|
|
18
|
+
when:
|
|
19
|
+
- matrix: { extras: "--all-extras" }
|
|
18
20
|
test_pytest:
|
|
19
21
|
image: ghcr.io/astral-sh/uv:python3.11-alpine
|
|
20
22
|
commands:
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changes to roboherd
|
|
2
2
|
|
|
3
|
+
## 0.1.6 ([Milestone](https://codeberg.org/bovine/roboherd/milestone/10694))
|
|
4
|
+
|
|
5
|
+
- Comment on python path [roboherd#34](https://codeberg.org/bovine/roboherd/issues/34)
|
|
6
|
+
- Enable direct posting of markdown [roboherd#35](https://codeberg.org/bovine/roboherd/issues/35)
|
|
7
|
+
|
|
8
|
+
## 0.1.5 ([Milestone](https://codeberg.org/bovine/roboherd/milestone/10581))
|
|
9
|
+
|
|
10
|
+
- Allow overriding the base_url [roboherd#12](https://codeberg.org/bovine/roboherd/issues/12)
|
|
11
|
+
- Separate out the internal state of a RoboCow
|
|
12
|
+
- If no preferredUsername is set and handle is updated, send Update Service [roboherd#33](https://codeberg.org/bovine/roboherd/issues/33)
|
|
13
|
+
- Add an icon property [roboherd#10](https://codeberg.org/bovine/roboherd/issues/10)
|
|
14
|
+
- Added a logo [roboherd#32](https://codeberg.org/bovine/roboherd/issues/32)
|
|
15
|
+
- Add PublishActivity annotation [roboherd#30](https://codeberg.org/bovine/roboherd/issues/30)
|
|
16
|
+
|
|
3
17
|
## 0.1.4 ([Milestone](https://codeberg.org/bovine/roboherd/milestone/10442))
|
|
4
18
|
|
|
5
19
|
- Type check the project
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roboherd
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A Fediverse bot framework
|
|
5
|
+
Project-URL: Documentation, https://bovine.codeberg.page/roboherd/
|
|
6
|
+
Project-URL: Repository, https://codeberg.org/bovine/roboherd
|
|
5
7
|
Requires-Python: >=3.11
|
|
6
8
|
Requires-Dist: aiohttp>=3.11.12
|
|
7
9
|
Requires-Dist: almabtrieb[mqtt]>=0.1.0a1
|
|
@@ -14,6 +16,7 @@ Requires-Dist: tomli-w>=1.1.0
|
|
|
14
16
|
Requires-Dist: watchfiles>=1.0.4
|
|
15
17
|
Provides-Extra: bovine
|
|
16
18
|
Requires-Dist: bovine>=0.5.15; extra == 'bovine'
|
|
19
|
+
Requires-Dist: markdown>=3.7; extra == 'bovine'
|
|
17
20
|
Description-Content-Type: text/markdown
|
|
18
21
|
|
|
19
22
|
# Roboherd
|
|
Binary file
|
|
@@ -11,6 +11,19 @@ We will now tour how to write bots with Roboherd. We start with
|
|
|
11
11
|
a basic bot that does nothing except have a profile. Then we will
|
|
12
12
|
start by adding features.
|
|
13
13
|
|
|
14
|
+
First, you will need to install roboherd, see [Installation](#installation).
|
|
15
|
+
For our purposes, you can start by running the docker container via
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
docker run --rm -ti -v .:/app helgekr/roboherd
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then you can run roboherd with local files by running
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -mroboherd run
|
|
25
|
+
```
|
|
26
|
+
|
|
14
27
|
### Basic bot aka how to configure the profile
|
|
15
28
|
|
|
16
29
|
The configuration file defines the actor by a name and the used python file,
|
|
@@ -93,23 +106,55 @@ Setting me as the author and the source code is contained in [meta_information][
|
|
|
93
106
|
--8<-- "roboherd/examples/meta.py"
|
|
94
107
|
```
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
By setting to `icon` property of [roboherd.cow.types.Information][], one can
|
|
110
|
+
change the profile picture.
|
|
111
|
+
|
|
112
|
+
### Config overrides
|
|
113
|
+
|
|
114
|
+
By running
|
|
115
|
+
|
|
116
|
+
```toml title="roboherd.toml"
|
|
117
|
+
base_url = "https://dev.bovine.social"
|
|
118
|
+
|
|
119
|
+
[cow.devnull]
|
|
120
|
+
bot = "roboherd.examples.dev_null:bot"
|
|
121
|
+
handle = "nothingness"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
one would instead create a bot with the handle `acct:nothingness@dev.bovine.social`.
|
|
125
|
+
Similarly, one can override the name and the `base_url` to use.
|
|
98
126
|
|
|
99
127
|
### Posting on startup
|
|
100
128
|
|
|
129
|
+
The simplest way to post is to use
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
@bot.startup
|
|
133
|
+
async def startup(poster: MarkdownPoster):
|
|
134
|
+
await poster("__Booo!__ 🐦")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
here [MarkdownPoster][roboherd.annotations.bovine.MarkdownPoster] is an annotation.
|
|
138
|
+
Annotations are injected through [FastDepends](https://lancetnik.github.io/FastDepends/). The markdown poster abstracts away the following steps
|
|
139
|
+
|
|
140
|
+
- Convert markdown to HTML
|
|
141
|
+
- Create a note based with the HTML as content (done below with the object factory)
|
|
142
|
+
- Send the note to the server (done below with publish object)
|
|
143
|
+
|
|
144
|
+
### Manually posting
|
|
145
|
+
|
|
101
146
|
By adding
|
|
102
147
|
|
|
103
148
|
```python
|
|
104
149
|
@bot.startup
|
|
105
150
|
async def startup(publish_object: PublishObject, object_factory: ObjectFactory):
|
|
106
|
-
|
|
151
|
+
note = object_factory.note(content="Booo! 🐦").as_public().build()
|
|
152
|
+
await publish_object(note)
|
|
107
153
|
```
|
|
108
154
|
|
|
109
155
|
one can post on startup. Here [PublishObject][roboherd.annotations.PublishObject]
|
|
110
156
|
and [ObjectFactory][roboherd.annotations.bovine.ObjectFactory]
|
|
111
|
-
are annotations
|
|
112
|
-
through [FastDepends](https://lancetnik.github.io/FastDepends/).
|
|
157
|
+
are annotations.
|
|
113
158
|
|
|
114
159
|
PublishObject depends on the [simple storage extension of cattle_grid](https://bovine.codeberg.page/cattle_grid/extensions/simple_storage/), whereas
|
|
115
160
|
ObjectFactory injects an [ObjectFactory][bovine.activitystreams.object_factory.ObjectFactory] provided by the [bovine](https://bovine.readthedocs.io/en/latest/) library.
|
|
@@ -119,6 +164,11 @@ ObjectFactory injects an [ObjectFactory][bovine.activitystreams.object_factory.O
|
|
|
119
164
|
--8<-- "roboherd/examples/scarecrow.py"
|
|
120
165
|
```
|
|
121
166
|
|
|
167
|
+
!!! hint
|
|
168
|
+
Similarly to PublishObject, one can use
|
|
169
|
+
[PublishActivity][roboherd.annotations.PublishActivity]
|
|
170
|
+
to post activities.
|
|
171
|
+
|
|
122
172
|
### Replying to messages
|
|
123
173
|
|
|
124
174
|
By adding a block such as
|
|
@@ -193,14 +243,14 @@ and the appropriate bots, one can run these bots
|
|
|
193
243
|
by running
|
|
194
244
|
|
|
195
245
|
```bash
|
|
196
|
-
|
|
246
|
+
python -mroboherd run
|
|
197
247
|
```
|
|
198
248
|
|
|
199
249
|
inside the container. Alternatively, this can be done directly
|
|
200
250
|
by running
|
|
201
251
|
|
|
202
252
|
```bash
|
|
203
|
-
docker run --rm -v .:/app helgekr/roboherd
|
|
253
|
+
docker run --rm -v .:/app helgekr/roboherd python -mroboherd run
|
|
204
254
|
```
|
|
205
255
|
|
|
206
256
|
### Installing roboherd as a python package
|
|
@@ -211,18 +261,24 @@ To install roboherd with [bovine](https://bovine.readthedocs.io/en/latest/) supp
|
|
|
211
261
|
pip install roboherd[bovine]
|
|
212
262
|
```
|
|
213
263
|
|
|
214
|
-
You are then able to run the roboherd command
|
|
264
|
+
You are then able to run the roboherd command
|
|
215
265
|
|
|
216
266
|
```bash
|
|
217
|
-
|
|
267
|
+
python -mroboherd run
|
|
218
268
|
```
|
|
219
269
|
|
|
220
|
-
|
|
270
|
+
### The roboherd command
|
|
271
|
+
|
|
272
|
+
You can also run roboherd by running
|
|
221
273
|
|
|
222
274
|
```bash
|
|
223
|
-
|
|
275
|
+
roboherd run
|
|
224
276
|
```
|
|
225
277
|
|
|
278
|
+
The difference to `python -mroboherd run` is that with the former,
|
|
279
|
+
the current directory is not added to the python path. It will still
|
|
280
|
+
be used to find the `roboherd.toml` configuration file.
|
|
281
|
+
|
|
226
282
|
## Configuration
|
|
227
283
|
|
|
228
284
|
roboherd has three global configuration variables, e.g.
|
|
@@ -322,3 +378,7 @@ version, have a [milestone](https://codeberg.org/bovine/roboherd/milestones)
|
|
|
322
378
|
matching the new version, and appropriate entries in
|
|
323
379
|
`CHANGES.md`. Then once a pull request with this is merged,
|
|
324
380
|
the rest happens automatically.
|
|
381
|
+
|
|
382
|
+
## Acknowledgements
|
|
383
|
+
|
|
384
|
+
Logo via [Lorc](https://game-icons.net/1x1/lorc/bull-horns.html).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "roboherd"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.6"
|
|
4
4
|
description = "A Fediverse bot framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -16,6 +16,10 @@ dependencies = [
|
|
|
16
16
|
"watchfiles>=1.0.4",
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
+
[project.urls]
|
|
20
|
+
Documentation = "https://bovine.codeberg.page/roboherd/"
|
|
21
|
+
Repository = "https://codeberg.org/bovine/roboherd"
|
|
22
|
+
|
|
19
23
|
[build-system]
|
|
20
24
|
requires = ["hatchling"]
|
|
21
25
|
build-backend = "hatchling.build"
|
|
@@ -26,7 +30,7 @@ roboherd = "roboherd.__main__:main"
|
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
[project.optional-dependencies]
|
|
29
|
-
bovine = ["bovine>=0.5.15"]
|
|
33
|
+
bovine = ["bovine>=0.5.15", "markdown>=3.7"]
|
|
30
34
|
|
|
31
35
|
[tool.uv]
|
|
32
36
|
dev-dependencies = [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from fast_depends import Depends
|
|
2
|
-
from typing import Annotated
|
|
2
|
+
from typing import Annotated
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from .common import PublishObject, PublishActivity # noqa
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def get_raw(data: dict) -> dict:
|
|
@@ -41,16 +41,3 @@ Activity = Annotated[dict, Depends(get_activity)]
|
|
|
41
41
|
|
|
42
42
|
EmbeddedObject = Annotated[dict, Depends(get_embedded_object)]
|
|
43
43
|
"""The embedded object in the activity as parsed by muck_out"""
|
|
44
|
-
|
|
45
|
-
Publisher = Callable[[dict], Awaitable[None]]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def construct_publish_object(connection: Almabtrieb, actor_id: str) -> Publisher:
|
|
49
|
-
async def publish(data: dict):
|
|
50
|
-
await connection.trigger("publish_object", {"actor": actor_id, "data": data})
|
|
51
|
-
|
|
52
|
-
return publish
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
PublishObject = Annotated[Publisher, Depends(construct_publish_object)]
|
|
56
|
-
"""Allows one to publish an object as the actor. Assumes cattle_grid has the extension `simple_object_storage` or equivalent"""
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Test documentation"""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated, Awaitable, Callable
|
|
4
4
|
from fast_depends import Depends
|
|
5
|
-
from .common import Profile
|
|
5
|
+
from .common import Profile, PublishObject
|
|
6
6
|
|
|
7
7
|
try:
|
|
8
8
|
from bovine.activitystreams import factories_for_actor_object
|
|
@@ -39,3 +39,29 @@ ActivityFactory = Annotated[BovineActivityFactory, Depends(get_activity_factory)
|
|
|
39
39
|
|
|
40
40
|
ObjectFactory = Annotated[BovineObjectFactory, Depends(get_object_factory)] # type: ignore
|
|
41
41
|
"""The object factory of type [bovine.activitystreams.object_factory.ObjectFactory][]"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import markdown
|
|
46
|
+
|
|
47
|
+
def get_markdown_poster( # type: ignore
|
|
48
|
+
object_factory: ObjectFactory, publish_object: PublishObject
|
|
49
|
+
):
|
|
50
|
+
async def publish_markdown(message: str):
|
|
51
|
+
content = markdown.markdown(message)
|
|
52
|
+
note = object_factory.note(content=content).as_public().build() # type: ignore
|
|
53
|
+
await publish_object(note)
|
|
54
|
+
|
|
55
|
+
return publish_markdown
|
|
56
|
+
|
|
57
|
+
except ImportError:
|
|
58
|
+
|
|
59
|
+
def get_markdown_poster() -> None:
|
|
60
|
+
raise ImportError("bovine not installed")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
MarkdownPublisher = Callable[[str], Awaitable[None]]
|
|
64
|
+
"""Type of the markdown publisher"""
|
|
65
|
+
|
|
66
|
+
MarkdownPoster = Annotated[MarkdownPublisher, Depends(get_markdown_poster)] # type: ignore
|
|
67
|
+
"""A function that takes a markdown string and posts it as the content of a note"""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from fast_depends import Depends
|
|
2
|
+
from typing import Annotated, Callable, Awaitable
|
|
3
|
+
|
|
4
|
+
from almabtrieb import Almabtrieb
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from roboherd.cow import RoboCow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_profile(cow: RoboCow) -> dict:
|
|
11
|
+
if cow.internals.profile is None:
|
|
12
|
+
raise ValueError("Cow has no profile")
|
|
13
|
+
return cow.internals.profile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Profile = Annotated[dict, Depends(get_profile)]
|
|
17
|
+
"""The profile of the cow"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Publisher = Callable[[dict], Awaitable[None]]
|
|
21
|
+
"""Type returned by the publishing functions"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def construct_publish_object(connection: Almabtrieb, actor_id: str) -> Publisher:
|
|
25
|
+
async def publish(data: dict):
|
|
26
|
+
await connection.trigger("publish_object", {"actor": actor_id, "data": data})
|
|
27
|
+
|
|
28
|
+
return publish
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def construct_publish_activity(connection: Almabtrieb, actor_id: str) -> Publisher:
|
|
32
|
+
async def publish(data: dict):
|
|
33
|
+
await connection.trigger("publish_activity", {"actor": actor_id, "data": data})
|
|
34
|
+
|
|
35
|
+
return publish
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PublishObject = Annotated[Publisher, Depends(construct_publish_object)]
|
|
39
|
+
"""Allows one to publish an object as the actor. Assumes cattle_grid has the extension `simple_object_storage` or equivalent"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
PublishActivity = Annotated[Publisher, Depends(construct_publish_activity)]
|
|
43
|
+
"""Allows one to publish an activity as the actor. Assumes cattle_grid has the extension `simple_object_storage` or equivalent"""
|
|
@@ -25,17 +25,8 @@ class CronEntry:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
|
28
|
-
class
|
|
29
|
-
|
|
30
|
-
metadata=dict(description="Information about the cow")
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
auto_follow: bool = field(
|
|
34
|
-
default=True,
|
|
35
|
-
metadata=dict(
|
|
36
|
-
description="""Whether to automatically accept follow requests"""
|
|
37
|
-
),
|
|
38
|
-
)
|
|
28
|
+
class RoboCowInternals:
|
|
29
|
+
"""Internal data for the cow"""
|
|
39
30
|
|
|
40
31
|
profile: dict | None = field(
|
|
41
32
|
default=None,
|
|
@@ -69,6 +60,27 @@ class RoboCow:
|
|
|
69
60
|
|
|
70
61
|
startup_routine: Callable | None = None
|
|
71
62
|
|
|
63
|
+
base_url: str | None = field(default=None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class RoboCow:
|
|
68
|
+
information: Information = field(
|
|
69
|
+
metadata=dict(description="Information about the cow")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
auto_follow: bool = field(
|
|
73
|
+
default=True,
|
|
74
|
+
metadata=dict(
|
|
75
|
+
description="""Whether to automatically accept follow requests"""
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
internals: RoboCowInternals = field(
|
|
80
|
+
default_factory=RoboCowInternals,
|
|
81
|
+
metadata=dict(description="Internal data for the cow"),
|
|
82
|
+
)
|
|
83
|
+
|
|
72
84
|
def action(self, action: str = "*", activity_type: str = "*"):
|
|
73
85
|
"""Adds a handler for an event. Use "*" as a wildcard.
|
|
74
86
|
|
|
@@ -90,15 +102,15 @@ class RoboCow:
|
|
|
90
102
|
|
|
91
103
|
def inner(func):
|
|
92
104
|
config.func = func
|
|
93
|
-
self.handlers.add_handler(config, func)
|
|
94
|
-
self.handler_configuration.append(config)
|
|
105
|
+
self.internals.handlers.add_handler(config, func)
|
|
106
|
+
self.internals.handler_configuration.append(config)
|
|
95
107
|
return func
|
|
96
108
|
|
|
97
109
|
return inner
|
|
98
110
|
|
|
99
111
|
def cron(self, crontab):
|
|
100
112
|
def inner(func):
|
|
101
|
-
self.cron_entries.append(CronEntry(crontab, func))
|
|
113
|
+
self.internals.cron_entries.append(CronEntry(crontab, func))
|
|
102
114
|
|
|
103
115
|
return func
|
|
104
116
|
|
|
@@ -119,7 +131,7 @@ class RoboCow:
|
|
|
119
131
|
action="incoming",
|
|
120
132
|
activity_type="*",
|
|
121
133
|
)
|
|
122
|
-
self.handlers.add_handler(config, func)
|
|
134
|
+
self.internals.handlers.add_handler(config, func)
|
|
123
135
|
return func
|
|
124
136
|
|
|
125
137
|
def incoming_create(self, func):
|
|
@@ -137,79 +149,57 @@ class RoboCow:
|
|
|
137
149
|
config = HandlerConfiguration(
|
|
138
150
|
action="incoming", activity_type="Create", func=func
|
|
139
151
|
)
|
|
140
|
-
self.handler_configuration.append(config)
|
|
141
|
-
self.handlers.add_handler(config, func)
|
|
152
|
+
self.internals.handler_configuration.append(config)
|
|
153
|
+
self.internals.handlers.add_handler(config, func)
|
|
142
154
|
return func
|
|
143
155
|
|
|
144
156
|
def startup(self, func):
|
|
145
157
|
"""Adds a startup routine to be run when the cow is started."""
|
|
146
158
|
|
|
147
|
-
self.startup_routine = func
|
|
159
|
+
self.internals.startup_routine = func
|
|
148
160
|
|
|
149
161
|
def needs_update(self):
|
|
150
162
|
"""Checks if the cow needs to be updated"""
|
|
151
|
-
if self.profile is None:
|
|
163
|
+
if self.internals.profile is None:
|
|
152
164
|
return True
|
|
153
165
|
|
|
154
|
-
if self.information.name != self.profile.get("name"):
|
|
166
|
+
if self.information.name != self.internals.profile.get("name"):
|
|
155
167
|
return True
|
|
156
168
|
|
|
157
|
-
if self.information.description != self.profile.get("summary"):
|
|
169
|
+
if self.information.description != self.internals.profile.get("summary"):
|
|
158
170
|
return True
|
|
159
171
|
|
|
160
172
|
return False
|
|
161
173
|
|
|
162
|
-
def update_data(self):
|
|
163
|
-
"""
|
|
164
|
-
Returns the update_actor message to send to cattle_grid
|
|
165
|
-
|
|
166
|
-
```pycon
|
|
167
|
-
>>> info = Information(handle="moocow", name="name", description="description")
|
|
168
|
-
>>> cow = RoboCow(information=info, actor_id="http://host.example/actor/1")
|
|
169
|
-
>>> cow.update_data()
|
|
170
|
-
{'actor': 'http://host.example/actor/1',
|
|
171
|
-
'profile': {'name': 'name',
|
|
172
|
-
'summary': 'description'},
|
|
173
|
-
'automaticallyUpdateFollowers': True}
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
"""
|
|
177
|
-
return {
|
|
178
|
-
"actor": self.actor_id,
|
|
179
|
-
"profile": {
|
|
180
|
-
"name": self.information.name,
|
|
181
|
-
"summary": self.information.description,
|
|
182
|
-
},
|
|
183
|
-
"automaticallyUpdateFollowers": self.auto_follow,
|
|
184
|
-
}
|
|
185
|
-
|
|
186
174
|
async def run_startup(self, connection: Almabtrieb):
|
|
187
175
|
"""Runs when the cow is birthed"""
|
|
188
176
|
|
|
189
|
-
if self.profile is None:
|
|
190
|
-
if not self.actor_id:
|
|
177
|
+
if self.internals.profile is None:
|
|
178
|
+
if not self.internals.actor_id:
|
|
191
179
|
raise ValueError("Actor ID is not set")
|
|
192
|
-
result = await connection.fetch(
|
|
180
|
+
result = await connection.fetch(
|
|
181
|
+
self.internals.actor_id, self.internals.actor_id
|
|
182
|
+
)
|
|
193
183
|
if not result.data:
|
|
194
184
|
raise ValueError("Could not retrieve profile")
|
|
195
|
-
self.profile = result.data
|
|
185
|
+
self.internals.profile = result.data
|
|
196
186
|
|
|
197
|
-
if self.cron_entries:
|
|
187
|
+
if self.internals.cron_entries:
|
|
198
188
|
frequency = ", ".join(
|
|
199
|
-
get_description(entry.crontab) for entry in self.cron_entries
|
|
189
|
+
get_description(entry.crontab) for entry in self.internals.cron_entries
|
|
200
190
|
)
|
|
201
191
|
self.information.frequency = frequency
|
|
202
192
|
|
|
203
|
-
update = determine_profile_update(self.information, self.profile)
|
|
193
|
+
update = determine_profile_update(self.information, self.internals.profile)
|
|
204
194
|
|
|
205
195
|
if update:
|
|
206
196
|
logger.info("Updating profile for %s", self.information.handle)
|
|
207
197
|
|
|
208
198
|
await connection.trigger("update_actor", update)
|
|
209
199
|
|
|
210
|
-
if self.startup_routine:
|
|
211
|
-
await inject(self.startup_routine)(
|
|
212
|
-
cow=self,
|
|
213
|
-
connection=connection,
|
|
214
|
-
actor_id=self.actor_id,
|
|
215
|
-
)
|
|
200
|
+
if self.internals.startup_routine:
|
|
201
|
+
await inject(self.internals.startup_routine)(
|
|
202
|
+
cow=self, # type:ignore
|
|
203
|
+
connection=connection, # type:ignore
|
|
204
|
+
actor_id=self.internals.actor_id, # type:ignore
|
|
205
|
+
) # type:ignore
|
|
@@ -24,6 +24,9 @@ def profile_part_needs_update(information: Information, profile: dict) -> bool:
|
|
|
24
24
|
if information.type != profile.get("type"):
|
|
25
25
|
return True
|
|
26
26
|
|
|
27
|
+
if information.icon != profile.get("icon"):
|
|
28
|
+
return True
|
|
29
|
+
|
|
27
30
|
return False
|
|
28
31
|
|
|
29
32
|
|
|
@@ -110,6 +113,7 @@ def determine_profile_update(information: Information, profile: dict) -> dict |
|
|
|
110
113
|
"type": information.type,
|
|
111
114
|
"name": information.name,
|
|
112
115
|
"summary": information.description,
|
|
116
|
+
"icon": information.icon,
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
actions = determine_actions(information, profile)
|
|
@@ -27,7 +27,8 @@ def test_needs_update(name, summary, profile, expected):
|
|
|
27
27
|
name=name,
|
|
28
28
|
description=summary,
|
|
29
29
|
)
|
|
30
|
-
cow = RoboCow(information=info
|
|
30
|
+
cow = RoboCow(information=info)
|
|
31
|
+
cow.internals.profile = profile
|
|
31
32
|
|
|
32
33
|
assert cow.needs_update() == expected
|
|
33
34
|
|
|
@@ -40,13 +41,13 @@ def test_cron():
|
|
|
40
41
|
async def test_func():
|
|
41
42
|
pass
|
|
42
43
|
|
|
43
|
-
assert len(cow.cron_entries) == 1
|
|
44
|
+
assert len(cow.internals.cron_entries) == 1
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
async def test_startup():
|
|
47
48
|
info = Information(handle="testcow")
|
|
48
49
|
cow = RoboCow(information=info)
|
|
49
|
-
cow.profile = {"id": "http://host.test/actor/cow"}
|
|
50
|
+
cow.internals.profile = {"id": "http://host.test/actor/cow"}
|
|
50
51
|
mock = AsyncMock()
|
|
51
52
|
|
|
52
53
|
cow.startup(mock)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from .types import Information, MetaInformation
|
|
4
|
-
|
|
4
|
+
from .const import default_icon
|
|
5
5
|
from .profile import determine_profile_update
|
|
6
6
|
|
|
7
7
|
|
|
@@ -14,6 +14,7 @@ from .profile import determine_profile_update
|
|
|
14
14
|
],
|
|
15
15
|
)
|
|
16
16
|
def test_determine_profile_update_no_update(info_params, profile):
|
|
17
|
+
profile["icon"] = default_icon
|
|
17
18
|
info = Information(**info_params)
|
|
18
19
|
|
|
19
20
|
assert determine_profile_update(info, profile) is None
|
|
@@ -27,13 +28,22 @@ def test_determine_profile_update():
|
|
|
27
28
|
|
|
28
29
|
assert result == {
|
|
29
30
|
"actor": "http://host.test/actor/1",
|
|
30
|
-
"profile": {
|
|
31
|
+
"profile": {
|
|
32
|
+
"type": "Service",
|
|
33
|
+
"name": "name",
|
|
34
|
+
"summary": "description",
|
|
35
|
+
"icon": default_icon,
|
|
36
|
+
},
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
|
|
34
40
|
def test_determine_profile_update_author():
|
|
35
41
|
info = Information(meta_information=MetaInformation(author="acct:author@host.test"))
|
|
36
|
-
profile = {
|
|
42
|
+
profile = {
|
|
43
|
+
"id": "http://host.test/actor/1",
|
|
44
|
+
"type": "Service",
|
|
45
|
+
"icon": default_icon,
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
result = determine_profile_update(info, profile)
|
|
39
49
|
|
|
@@ -47,3 +57,25 @@ def test_determine_profile_update_author():
|
|
|
47
57
|
}
|
|
48
58
|
],
|
|
49
59
|
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_profile_update_for_new_preferredUsername():
|
|
63
|
+
info = Information(handle="handle")
|
|
64
|
+
profile = {
|
|
65
|
+
"id": "http://host.test/actor/1",
|
|
66
|
+
"type": "Service",
|
|
67
|
+
"icon": default_icon,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
result = determine_profile_update(info, profile)
|
|
71
|
+
|
|
72
|
+
assert result == {
|
|
73
|
+
"actor": "http://host.test/actor/1",
|
|
74
|
+
"actions": [
|
|
75
|
+
{
|
|
76
|
+
"action": "add_identifier",
|
|
77
|
+
"identifier": "acct:handle@host.test",
|
|
78
|
+
"primary": True,
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
2
|
|
|
3
|
+
from .const import default_icon
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class MetaInformation(BaseModel):
|
|
5
7
|
"""Meta Information about the bot. This includes
|
|
@@ -48,6 +50,11 @@ class Information(BaseModel):
|
|
|
48
50
|
description="The description of the cow, used as summary of the actor",
|
|
49
51
|
)
|
|
50
52
|
|
|
53
|
+
icon: dict = Field(
|
|
54
|
+
default=default_icon,
|
|
55
|
+
description="The profile image",
|
|
56
|
+
)
|
|
57
|
+
|
|
51
58
|
frequency: str | None = Field(
|
|
52
59
|
default=None,
|
|
53
60
|
examples=["daily"],
|