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.

Files changed (64) hide show
  1. {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/test.yml +3 -1
  2. {roboherd-0.1.4 → roboherd-0.1.6}/CHANGES.md +14 -0
  3. {roboherd-0.1.4 → roboherd-0.1.6}/PKG-INFO +4 -1
  4. roboherd-0.1.6/docs/assets/bull-horns.png +0 -0
  5. {roboherd-0.1.4 → roboherd-0.1.6}/docs/index.md +71 -11
  6. {roboherd-0.1.4 → roboherd-0.1.6}/mkdocs.yml +2 -0
  7. {roboherd-0.1.4 → roboherd-0.1.6}/pyproject.toml +6 -2
  8. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/annotations/__init__.py +2 -15
  9. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/annotations/bovine.py +28 -2
  10. roboherd-0.1.6/roboherd/annotations/common.py +43 -0
  11. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/__init__.py +48 -58
  12. roboherd-0.1.6/roboherd/cow/const.py +6 -0
  13. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/profile.py +4 -0
  14. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_init.py +4 -3
  15. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_profile.py +35 -3
  16. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/types.py +7 -0
  17. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/moocow.py +4 -2
  18. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/scarecrow.py +7 -1
  19. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/__init__.py +4 -4
  20. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/__init__.py +1 -1
  21. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/config.py +14 -7
  22. roboherd-0.1.6/roboherd/herd/manager/load.py +15 -0
  23. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/test_config.py +16 -0
  24. roboherd-0.1.6/roboherd/herd/manager/test_load.py +19 -0
  25. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/processor.py +2 -2
  26. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/scheduler.py +1 -1
  27. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/util.py +2 -51
  28. {roboherd-0.1.4 → roboherd-0.1.6}/uv.lock +217 -237
  29. roboherd-0.1.4/roboherd/annotations/common.py +0 -14
  30. roboherd-0.1.4/roboherd/test_util.py +0 -24
  31. {roboherd-0.1.4 → roboherd-0.1.6}/.gitignore +0 -0
  32. {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/create_release.yml +0 -0
  33. {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/publish_docker.yml +0 -0
  34. {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/publish_pypi.yml +0 -0
  35. {roboherd-0.1.4 → roboherd-0.1.6}/.woodpecker/website.yml +0 -0
  36. {roboherd-0.1.4 → roboherd-0.1.6}/README.md +0 -0
  37. {roboherd-0.1.4 → roboherd-0.1.6}/docs/annotations.md +0 -0
  38. {roboherd-0.1.4 → roboherd-0.1.6}/docs/assets/mastodon.png +0 -0
  39. {roboherd-0.1.4 → roboherd-0.1.6}/docs/cli.md +0 -0
  40. {roboherd-0.1.4 → roboherd-0.1.6}/docs/cow.md +0 -0
  41. {roboherd-0.1.4 → roboherd-0.1.6}/docs/herd.md +0 -0
  42. {roboherd-0.1.4 → roboherd-0.1.6}/docs/util.md +0 -0
  43. {roboherd-0.1.4 → roboherd-0.1.6}/resources/docker/Dockerfile +0 -0
  44. {roboherd-0.1.4 → roboherd-0.1.6}/resources/docker/build.sh +0 -0
  45. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/__init__.py +0 -0
  46. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/__main__.py +0 -0
  47. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/handlers.py +0 -0
  48. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_handlers.py +0 -0
  49. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/test_util.py +0 -0
  50. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/cow/util.py +0 -0
  51. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/__init__.py +0 -0
  52. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/dev_null.py +0 -0
  53. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/json_echo.py +0 -0
  54. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/meta.py +0 -0
  55. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/number.py +0 -0
  56. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/examples/rooster.py +0 -0
  57. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/builder.py +0 -0
  58. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/manager/test_manager.py +0 -0
  59. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/test_herd.py +0 -0
  60. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/test_scheduler.py +0 -0
  61. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/herd/types.py +0 -0
  62. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/register.py +0 -0
  63. {roboherd-0.1.4 → roboherd-0.1.6}/roboherd/test_validators.py +0 -0
  64. {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.4
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
@@ -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
- !!! todo
97
- How to configure a profile picture [roboherd#10](https://codeberg.org/bovine/roboherd/issues/10)
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
- await publish_object(object_factory.note(content="Booo! 🐦").as_public().build())
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 that are injected
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
- roboherd run
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 roboherd run
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 either by
264
+ You are then able to run the roboherd command
215
265
 
216
266
  ```bash
217
- roboherd run
267
+ python -mroboherd run
218
268
  ```
219
269
 
220
- or
270
+ ### The roboherd command
271
+
272
+ You can also run roboherd by running
221
273
 
222
274
  ```bash
223
- python -mroboherd run
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).
@@ -5,6 +5,8 @@ repo_url: https://codeberg.org/bovine/roboherd/
5
5
  repo_name: bovine/roboherd
6
6
  theme:
7
7
  name: material
8
+ logo: assets/bull-horns.png
9
+ favicon: assets/bull-horns.png
8
10
  icon:
9
11
  repo: fontawesome/brands/git-alt
10
12
  palette:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "roboherd"
3
- version = "0.1.4"
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, Callable, Awaitable
2
+ from typing import Annotated
3
3
 
4
- from almabtrieb import Almabtrieb
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 RoboCow:
29
- information: Information = field(
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(self.actor_id, self.actor_id)
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
@@ -0,0 +1,6 @@
1
+ default_icon = {
2
+ "mediaType": "image/png",
3
+ "type": "Image",
4
+ "url": "https://dev.bovine.social/assets/bull-horns.png",
5
+ }
6
+ """The default icon to be used"""
@@ -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, profile=profile)
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": {"type": "Service", "name": "name", "summary": "description"},
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 = {"id": "http://host.test/actor/1", "type": "Service"}
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"],