deltabot-cli 1.0.0__tar.gz → 3.0.0__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.
- deltabot-cli-3.0.0/.github/workflows/python-ci.yml +56 -0
- deltabot-cli-3.0.0/.gitignore +13 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/PKG-INFO +8 -9
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/README.md +5 -6
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/__init__.py +1 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/cli.py +34 -39
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/client.py +22 -18
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/events.py +41 -39
- deltabot-cli-3.0.0/deltabot_cli/utils.py +17 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli.egg-info/PKG-INFO +8 -9
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli.egg-info/SOURCES.txt +7 -1
- deltabot-cli-3.0.0/examples/echobot.py +21 -0
- deltabot-cli-3.0.0/examples/echobot_advanced.py +46 -0
- deltabot-cli-3.0.0/pylama.ini +4 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/pyproject.toml +7 -4
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/LICENSE +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/_utils.py +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/const.py +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli/rpc.py +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli.egg-info/dependency_links.txt +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli.egg-info/requires.txt +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/deltabot_cli.egg-info/top_level.txt +0 -0
- {deltabot-cli-1.0.0 → deltabot-cli-3.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
tags:
|
|
7
|
+
- '*.*.*'
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [ master ]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ['3.9', '3.11']
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v2
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v2
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
python -m pip install '.[dev]'
|
|
27
|
+
- name: Check code with black
|
|
28
|
+
run: |
|
|
29
|
+
black --check .
|
|
30
|
+
- name: Lint code
|
|
31
|
+
run: |
|
|
32
|
+
pylama
|
|
33
|
+
- name: Test with pytest
|
|
34
|
+
run: |
|
|
35
|
+
#pytest
|
|
36
|
+
|
|
37
|
+
deploy:
|
|
38
|
+
needs: test
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v2
|
|
42
|
+
- uses: actions/setup-python@v2
|
|
43
|
+
with:
|
|
44
|
+
python-version: '3.x'
|
|
45
|
+
- id: check-tag
|
|
46
|
+
run: |
|
|
47
|
+
if [[ "${{ github.event.ref }}" =~ ^refs/tags/[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
48
|
+
echo ::set-output name=match::true
|
|
49
|
+
fi
|
|
50
|
+
- name: Create PyPI release
|
|
51
|
+
uses: casperdcl/deploy-pypi@v2
|
|
52
|
+
with:
|
|
53
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
54
|
+
build: true
|
|
55
|
+
# only upload if a tag is pushed (otherwise just build & check)
|
|
56
|
+
upload: ${{ github.event_name == 'push' && steps.check-tag.outputs.match == 'true' }}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deltabot-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Library to speedup Delta Chat bot development
|
|
5
|
-
Author-email: adbenitez <
|
|
5
|
+
Author-email: adbenitez <adb@merlinux.eu>
|
|
6
6
|
Keywords: deltachat,bot,deltabot-cli
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: deltachat-rpc-server>=1.133.2
|
|
@@ -44,19 +44,18 @@ pip install deltabot-cli
|
|
|
44
44
|
Example echo-bot written with deltabot-cli:
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
|
-
import logging
|
|
48
47
|
from deltabot_cli import BotCli, events
|
|
49
48
|
|
|
50
49
|
cli = BotCli("echobot")
|
|
51
50
|
|
|
52
51
|
@cli.on(events.RawEvent)
|
|
53
|
-
def log_event(event):
|
|
54
|
-
|
|
52
|
+
def log_event(bot, accid, event):
|
|
53
|
+
bot.logger.info(event)
|
|
55
54
|
|
|
56
55
|
@cli.on(events.NewMessage)
|
|
57
|
-
def echo(event):
|
|
56
|
+
def echo(bot, accid, event):
|
|
58
57
|
msg = event.msg
|
|
59
|
-
|
|
58
|
+
bot.rpc.misc_send_text_message(accid, msg.chat_id, msg.text)
|
|
60
59
|
|
|
61
60
|
if __name__ == "__main__":
|
|
62
61
|
cli.start()
|
|
@@ -65,4 +64,4 @@ if __name__ == "__main__":
|
|
|
65
64
|
If you run the above script you will have a bot CLI, that allows to configure and run a bot.
|
|
66
65
|
A progress bar is displayed while the bot is configuring, and logs are pretty-printed.
|
|
67
66
|
|
|
68
|
-
For more examples check the [examples](
|
|
67
|
+
For more examples check the [examples](./examples) folder.
|
|
@@ -20,19 +20,18 @@ pip install deltabot-cli
|
|
|
20
20
|
Example echo-bot written with deltabot-cli:
|
|
21
21
|
|
|
22
22
|
```python
|
|
23
|
-
import logging
|
|
24
23
|
from deltabot_cli import BotCli, events
|
|
25
24
|
|
|
26
25
|
cli = BotCli("echobot")
|
|
27
26
|
|
|
28
27
|
@cli.on(events.RawEvent)
|
|
29
|
-
def log_event(event):
|
|
30
|
-
|
|
28
|
+
def log_event(bot, accid, event):
|
|
29
|
+
bot.logger.info(event)
|
|
31
30
|
|
|
32
31
|
@cli.on(events.NewMessage)
|
|
33
|
-
def echo(event):
|
|
32
|
+
def echo(bot, accid, event):
|
|
34
33
|
msg = event.msg
|
|
35
|
-
|
|
34
|
+
bot.rpc.misc_send_text_message(accid, msg.chat_id, msg.text)
|
|
36
35
|
|
|
37
36
|
if __name__ == "__main__":
|
|
38
37
|
cli.start()
|
|
@@ -41,4 +40,4 @@ if __name__ == "__main__":
|
|
|
41
40
|
If you run the above script you will have a bot CLI, that allows to configure and run a bot.
|
|
42
41
|
A progress bar is displayed while the bot is configuring, and logs are pretty-printed.
|
|
43
42
|
|
|
44
|
-
For more examples check the [examples](
|
|
43
|
+
For more examples check the [examples](./examples) folder.
|
|
@@ -13,10 +13,13 @@ from rich.logging import RichHandler
|
|
|
13
13
|
|
|
14
14
|
from ._utils import AttrDict, ConfigProgressBar, parse_docstring
|
|
15
15
|
from .client import Bot
|
|
16
|
-
from .const import
|
|
17
|
-
from .events import EventFilter, HookCollection,
|
|
16
|
+
from .const import EventType
|
|
17
|
+
from .events import EventFilter, HookCollection, HookDecorator, RawEvent
|
|
18
18
|
from .rpc import JsonRpcError, Rpc
|
|
19
19
|
|
|
20
|
+
CliEventHook = Callable[[Bot, Namespace], None]
|
|
21
|
+
CmdCallback = Callable[["BotCli", Bot, Namespace], None]
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class BotCli:
|
|
22
25
|
"""Class implementing a bot CLI.
|
|
@@ -32,15 +35,15 @@ class BotCli:
|
|
|
32
35
|
self._parser = ArgumentParser(app_name)
|
|
33
36
|
self._subparsers = self._parser.add_subparsers(title="subcommands")
|
|
34
37
|
self._hooks = HookCollection()
|
|
35
|
-
self._init_hooks: Set[
|
|
36
|
-
self._start_hooks: Set[
|
|
38
|
+
self._init_hooks: Set[CliEventHook] = set()
|
|
39
|
+
self._start_hooks: Set[CliEventHook] = set()
|
|
37
40
|
self._bot: Bot
|
|
38
41
|
|
|
39
|
-
def on(self, event: Union[type, EventFilter]) ->
|
|
42
|
+
def on(self, event: Union[type, EventFilter]) -> HookDecorator: # noqa
|
|
40
43
|
"""Register decorated function as listener for the given event."""
|
|
41
44
|
return self._hooks.on(event)
|
|
42
45
|
|
|
43
|
-
def on_init(self, func:
|
|
46
|
+
def on_init(self, func: CliEventHook) -> CliEventHook:
|
|
44
47
|
"""Register function to be called before the bot starts serving requests.
|
|
45
48
|
|
|
46
49
|
The function will receive the bot instance and the CLI arguments received.
|
|
@@ -52,7 +55,7 @@ class BotCli:
|
|
|
52
55
|
for func in self._init_hooks:
|
|
53
56
|
func(bot, args)
|
|
54
57
|
|
|
55
|
-
def on_start(self, func:
|
|
58
|
+
def on_start(self, func: CliEventHook) -> CliEventHook:
|
|
56
59
|
"""Register function to be called when the bot is about to start serving requests.
|
|
57
60
|
|
|
58
61
|
The function will receive the bot instance.
|
|
@@ -64,14 +67,6 @@ class BotCli:
|
|
|
64
67
|
for func in self._start_hooks:
|
|
65
68
|
func(bot, args)
|
|
66
69
|
|
|
67
|
-
def is_not_known_command(self, event: AttrDict) -> bool:
|
|
68
|
-
if not event.command.startswith(COMMAND_PREFIX):
|
|
69
|
-
return True
|
|
70
|
-
for hook in self._bot._hooks.get(NewMessage, []): # pylint:disable=W0212
|
|
71
|
-
if event.command == hook[1].command:
|
|
72
|
-
return False
|
|
73
|
-
return True
|
|
74
|
-
|
|
75
70
|
def add_generic_option(self, *flags, **kwargs) -> None:
|
|
76
71
|
"""Add a generic argument option to the CLI."""
|
|
77
72
|
if not (flags and flags[0].startswith("-")):
|
|
@@ -80,7 +75,7 @@ class BotCli:
|
|
|
80
75
|
|
|
81
76
|
def add_subcommand(
|
|
82
77
|
self,
|
|
83
|
-
func:
|
|
78
|
+
func: CmdCallback,
|
|
84
79
|
**kwargs,
|
|
85
80
|
) -> ArgumentParser:
|
|
86
81
|
"""Add a subcommand to the CLI."""
|
|
@@ -189,9 +184,9 @@ def _serve_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
|
189
184
|
if rpc.is_configured(accid):
|
|
190
185
|
addrs.append(rpc.get_config(accid, "configured_addr"))
|
|
191
186
|
else:
|
|
192
|
-
|
|
187
|
+
bot.logger.error(f"account {accid} not configured")
|
|
193
188
|
if len(addrs) != 0:
|
|
194
|
-
|
|
189
|
+
bot.logger.info(f"Listening at: {', '.join(addrs)}")
|
|
195
190
|
cli._on_start(bot, args) # noqa
|
|
196
191
|
while True:
|
|
197
192
|
try:
|
|
@@ -199,10 +194,10 @@ def _serve_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
|
199
194
|
except KeyboardInterrupt:
|
|
200
195
|
return
|
|
201
196
|
except Exception as ex: # pylint:disable=W0703
|
|
202
|
-
|
|
197
|
+
bot.logger.exception(ex)
|
|
203
198
|
time.sleep(5)
|
|
204
199
|
else:
|
|
205
|
-
|
|
200
|
+
bot.logger.error("There are no configured accounts to serve")
|
|
206
201
|
|
|
207
202
|
|
|
208
203
|
def _init_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
@@ -210,18 +205,18 @@ def _init_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
|
210
205
|
|
|
211
206
|
def on_progress(event: AttrDict) -> None:
|
|
212
207
|
if event.comment:
|
|
213
|
-
|
|
208
|
+
bot.logger.info(event.comment)
|
|
214
209
|
pbar.set_progress(event.progress)
|
|
215
210
|
|
|
216
211
|
def configure() -> None:
|
|
217
212
|
try:
|
|
218
213
|
bot.configure(accid, email=args.addr, password=args.password)
|
|
219
214
|
except JsonRpcError as err:
|
|
220
|
-
|
|
215
|
+
bot.logger.error(err)
|
|
221
216
|
|
|
222
217
|
accid = cli.get_or_create_account(bot.rpc, args.addr)
|
|
223
218
|
|
|
224
|
-
|
|
219
|
+
bot.logger.info("Starting configuration process...")
|
|
225
220
|
pbar = ConfigProgressBar()
|
|
226
221
|
bot.add_hook(on_progress, RawEvent(EventType.CONFIGURE_PROGRESS))
|
|
227
222
|
task = Thread(target=configure)
|
|
@@ -230,9 +225,9 @@ def _init_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
|
230
225
|
task.join()
|
|
231
226
|
pbar.close()
|
|
232
227
|
if pbar.progress == -1:
|
|
233
|
-
|
|
228
|
+
bot.logger.error("Configuration failed.")
|
|
234
229
|
else:
|
|
235
|
-
|
|
230
|
+
bot.logger.info("Account configured successfully.")
|
|
236
231
|
|
|
237
232
|
|
|
238
233
|
def _config_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
@@ -241,26 +236,26 @@ def _config_cmd(cli: BotCli, bot: Bot, args: Namespace) -> None:
|
|
|
241
236
|
for accid in accounts:
|
|
242
237
|
addr = cli.get_address(bot.rpc, accid)
|
|
243
238
|
print(f"Account #{accid} ({addr}):")
|
|
244
|
-
_config_cmd_for_acc(bot
|
|
239
|
+
_config_cmd_for_acc(bot, accid, args)
|
|
245
240
|
print("")
|
|
246
241
|
if not accounts:
|
|
247
|
-
|
|
242
|
+
bot.logger.error("There are no accounts yet, add a new account using the init subcommand")
|
|
248
243
|
|
|
249
244
|
|
|
250
|
-
def _config_cmd_for_acc(
|
|
245
|
+
def _config_cmd_for_acc(bot: Bot, accid: int, args: Namespace) -> None:
|
|
251
246
|
if args.value:
|
|
252
|
-
rpc.set_config(accid, args.option, args.value)
|
|
247
|
+
bot.rpc.set_config(accid, args.option, args.value)
|
|
253
248
|
|
|
254
249
|
if args.option:
|
|
255
250
|
try:
|
|
256
|
-
value = rpc.get_config(accid, args.option)
|
|
251
|
+
value = bot.rpc.get_config(accid, args.option)
|
|
257
252
|
print(f"{args.option}={value!r}")
|
|
258
253
|
except JsonRpcError:
|
|
259
|
-
|
|
254
|
+
bot.logger.error("Unknown configuration option: %s", args.option)
|
|
260
255
|
else:
|
|
261
|
-
keys = rpc.get_config(accid, "sys.config_keys") or ""
|
|
256
|
+
keys = bot.rpc.get_config(accid, "sys.config_keys") or ""
|
|
262
257
|
for key in keys.split():
|
|
263
|
-
value = rpc.get_config(accid, key)
|
|
258
|
+
value = bot.rpc.get_config(accid, key)
|
|
264
259
|
print(f"{key}={value!r}")
|
|
265
260
|
|
|
266
261
|
|
|
@@ -270,19 +265,19 @@ def _qr_cmd(cli: BotCli, bot: Bot, _args: Namespace) -> None:
|
|
|
270
265
|
for accid in accounts:
|
|
271
266
|
addr = cli.get_address(bot.rpc, accid)
|
|
272
267
|
print(f"Account #{accid} ({addr}):")
|
|
273
|
-
_qr_cmd_for_acc(bot
|
|
268
|
+
_qr_cmd_for_acc(bot, accid)
|
|
274
269
|
print("")
|
|
275
270
|
if not accounts:
|
|
276
|
-
|
|
271
|
+
bot.logger.error("There are no accounts yet, add a new account using the init subcommand")
|
|
277
272
|
|
|
278
273
|
|
|
279
|
-
def _qr_cmd_for_acc(
|
|
274
|
+
def _qr_cmd_for_acc(bot: Bot, accid: int) -> None:
|
|
280
275
|
"""get bot's verification QR"""
|
|
281
|
-
if rpc.is_configured(accid):
|
|
282
|
-
qrdata, _ = rpc.get_chat_securejoin_qr_code_svg(accid, None)
|
|
276
|
+
if bot.rpc.is_configured(accid):
|
|
277
|
+
qrdata, _ = bot.rpc.get_chat_securejoin_qr_code_svg(accid, None)
|
|
283
278
|
code = qrcode.QRCode()
|
|
284
279
|
code.add_data(qrdata)
|
|
285
280
|
code.print_ascii(invert=True)
|
|
286
281
|
print(qrdata)
|
|
287
282
|
else:
|
|
288
|
-
|
|
283
|
+
bot.logger.error("account not configured")
|
|
@@ -14,6 +14,7 @@ from .events import (
|
|
|
14
14
|
EventFilter,
|
|
15
15
|
GroupImageChanged,
|
|
16
16
|
GroupNameChanged,
|
|
17
|
+
HookCallback,
|
|
17
18
|
MemberListChanged,
|
|
18
19
|
NewMessage,
|
|
19
20
|
RawEvent,
|
|
@@ -27,7 +28,7 @@ class Client:
|
|
|
27
28
|
def __init__(
|
|
28
29
|
self,
|
|
29
30
|
rpc: Rpc,
|
|
30
|
-
hooks: Optional[Iterable[Tuple[
|
|
31
|
+
hooks: Optional[Iterable[Tuple[HookCallback, Union[type, EventFilter]]]] = None,
|
|
31
32
|
logger: Optional[logging.Logger] = None,
|
|
32
33
|
) -> None:
|
|
33
34
|
self.rpc = rpc
|
|
@@ -36,11 +37,11 @@ class Client:
|
|
|
36
37
|
self._should_process_messages = 0
|
|
37
38
|
self.add_hooks(hooks or [])
|
|
38
39
|
|
|
39
|
-
def add_hooks(self, hooks: Iterable[Tuple[
|
|
40
|
+
def add_hooks(self, hooks: Iterable[Tuple[HookCallback, Union[type, EventFilter]]]) -> None:
|
|
40
41
|
for hook, event in hooks:
|
|
41
42
|
self.add_hook(hook, event)
|
|
42
43
|
|
|
43
|
-
def add_hook(self, hook:
|
|
44
|
+
def add_hook(self, hook: HookCallback, event: Union[type, EventFilter] = RawEvent) -> None:
|
|
44
45
|
"""Register hook for the given event filter."""
|
|
45
46
|
if isinstance(event, type):
|
|
46
47
|
event = event()
|
|
@@ -53,7 +54,7 @@ class Client:
|
|
|
53
54
|
)
|
|
54
55
|
self._hooks.setdefault(type(event), set()).add((hook, event))
|
|
55
56
|
|
|
56
|
-
def remove_hook(self, hook:
|
|
57
|
+
def remove_hook(self, hook: HookCallback, event: Union[type, EventFilter]) -> None:
|
|
57
58
|
"""Unregister hook from the given event filter."""
|
|
58
59
|
if isinstance(event, type):
|
|
59
60
|
event = event()
|
|
@@ -91,30 +92,33 @@ class Client:
|
|
|
91
92
|
self._process_messages(accid) # Process old messages.
|
|
92
93
|
while True:
|
|
93
94
|
raw_event = self.rpc.get_next_event()
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
accid = raw_event.context_id
|
|
96
|
+
event = raw_event.event
|
|
97
|
+
self._on_event(accid, event)
|
|
96
98
|
if event.kind == EventType.INCOMING_MSG:
|
|
97
|
-
self._process_messages(
|
|
99
|
+
self._process_messages(accid)
|
|
98
100
|
|
|
99
101
|
if func(event):
|
|
100
102
|
return event
|
|
101
103
|
|
|
102
|
-
def _on_event(
|
|
104
|
+
def _on_event(
|
|
105
|
+
self, accid: int, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
|
|
106
|
+
) -> None:
|
|
103
107
|
for hook, evfilter in self._hooks.get(filter_type, []):
|
|
104
108
|
if evfilter.filter(event):
|
|
105
109
|
try:
|
|
106
|
-
hook(event)
|
|
110
|
+
hook(self, accid, event)
|
|
107
111
|
except Exception as ex:
|
|
108
112
|
self.logger.exception(ex)
|
|
109
113
|
|
|
110
|
-
def _parse_command(self, event: AttrDict) -> None:
|
|
114
|
+
def _parse_command(self, accid: int, event: AttrDict) -> None:
|
|
111
115
|
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
|
112
116
|
parts = event.msg.text.split(maxsplit=1)
|
|
113
117
|
payload = parts[1] if len(parts) > 1 else ""
|
|
114
118
|
cmd = parts.pop(0)
|
|
115
119
|
|
|
116
120
|
if "@" in cmd:
|
|
117
|
-
suffix = "@" + self.rpc.get_contact(
|
|
121
|
+
suffix = "@" + self.rpc.get_contact(accid, SpecialContactId.SELF).address
|
|
118
122
|
if cmd.endswith(suffix):
|
|
119
123
|
cmd = cmd[: -len(suffix)]
|
|
120
124
|
else:
|
|
@@ -135,31 +139,31 @@ class Client:
|
|
|
135
139
|
event["command"], event["payload"] = cmd, payload
|
|
136
140
|
|
|
137
141
|
def _on_new_msg(self, accid: int, msg: AttrDict) -> None:
|
|
138
|
-
event = AttrDict(
|
|
142
|
+
event = AttrDict(command="", payload="", msg=msg)
|
|
139
143
|
if not msg.is_info and msg.text.startswith(COMMAND_PREFIX):
|
|
140
|
-
self._parse_command(event)
|
|
141
|
-
self._on_event(event, NewMessage)
|
|
144
|
+
self._parse_command(accid, event)
|
|
145
|
+
self._on_event(accid, event, NewMessage)
|
|
142
146
|
|
|
143
147
|
def _handle_info_msg(self, accid: int, snapshot: AttrDict) -> None:
|
|
144
|
-
event = AttrDict(
|
|
148
|
+
event = AttrDict(msg=snapshot)
|
|
145
149
|
|
|
146
150
|
img_changed = parse_system_image_changed(snapshot.text)
|
|
147
151
|
if img_changed:
|
|
148
152
|
_, event["image_deleted"] = img_changed
|
|
149
|
-
self._on_event(event, GroupImageChanged)
|
|
153
|
+
self._on_event(accid, event, GroupImageChanged)
|
|
150
154
|
return
|
|
151
155
|
|
|
152
156
|
title_changed = parse_system_title_changed(snapshot.text)
|
|
153
157
|
if title_changed:
|
|
154
158
|
_, event["old_name"] = title_changed
|
|
155
|
-
self._on_event(event, GroupNameChanged)
|
|
159
|
+
self._on_event(accid, event, GroupNameChanged)
|
|
156
160
|
return
|
|
157
161
|
|
|
158
162
|
members_changed = parse_system_add_remove(snapshot.text)
|
|
159
163
|
if members_changed:
|
|
160
164
|
action, event["member"], _ = members_changed
|
|
161
165
|
event["member_added"] = action == "added"
|
|
162
|
-
self._on_event(event, MemberListChanged)
|
|
166
|
+
self._on_event(accid, event, MemberListChanged)
|
|
163
167
|
return
|
|
164
168
|
|
|
165
169
|
self.logger.warning(
|
|
@@ -17,6 +17,10 @@ from .const import EventType
|
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from ._utils import AttrDict
|
|
20
|
+
from .client import Bot
|
|
21
|
+
FilterCallback = Callable[["Bot", "AttrDict"], bool]
|
|
22
|
+
HookCallback = Callable[["Bot", "AttrDict"], None]
|
|
23
|
+
HookDecorator = Callable[[HookCallback], HookCallback]
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
def _tuple_of(obj, type_: type) -> tuple:
|
|
@@ -33,12 +37,11 @@ def _tuple_of(obj, type_: type) -> tuple:
|
|
|
33
37
|
class EventFilter(ABC):
|
|
34
38
|
"""The base event filter.
|
|
35
39
|
|
|
36
|
-
:param func: A Callable
|
|
37
|
-
|
|
38
|
-
should be dispatched or not.
|
|
40
|
+
:param func: A Callable that should accept the bot and event as input parameters,
|
|
41
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
39
42
|
"""
|
|
40
43
|
|
|
41
|
-
def __init__(self, func: Optional[
|
|
44
|
+
def __init__(self, func: Optional[FilterCallback] = None):
|
|
42
45
|
self.func = func
|
|
43
46
|
|
|
44
47
|
@abstractmethod
|
|
@@ -52,13 +55,13 @@ class EventFilter(ABC):
|
|
|
52
55
|
def __ne__(self, other):
|
|
53
56
|
return not self == other
|
|
54
57
|
|
|
55
|
-
def _call_func(self, event) -> bool:
|
|
58
|
+
def _call_func(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
56
59
|
if not self.func:
|
|
57
60
|
return True
|
|
58
|
-
return self.func(event)
|
|
61
|
+
return self.func(bot, event)
|
|
59
62
|
|
|
60
63
|
@abstractmethod
|
|
61
|
-
def filter(self, event):
|
|
64
|
+
def filter(self, bot: "Bot", event: "AttrDict"):
|
|
62
65
|
"""Return True-like value if the event passed the filter and should be
|
|
63
66
|
used, or False-like value otherwise.
|
|
64
67
|
"""
|
|
@@ -68,13 +71,16 @@ class RawEvent(EventFilter):
|
|
|
68
71
|
"""Matches raw core events.
|
|
69
72
|
|
|
70
73
|
:param types: The types of event to match.
|
|
71
|
-
:param func: A Callable
|
|
72
|
-
|
|
73
|
-
should be dispatched or not.
|
|
74
|
+
:param func: A Callable that should accept the bot and event as input parameter,
|
|
75
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
74
76
|
"""
|
|
75
77
|
|
|
76
|
-
def __init__(
|
|
77
|
-
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
types: Union[None, EventType, Iterable[EventType]] = None,
|
|
81
|
+
func: Optional[FilterCallback] = None,
|
|
82
|
+
):
|
|
83
|
+
super().__init__(func=func)
|
|
78
84
|
try:
|
|
79
85
|
self.types = _tuple_of(types, EventType)
|
|
80
86
|
except TypeError as err:
|
|
@@ -88,10 +94,10 @@ class RawEvent(EventFilter):
|
|
|
88
94
|
return (self.types, self.func) == (other.types, other.func)
|
|
89
95
|
return False
|
|
90
96
|
|
|
91
|
-
def filter(self, event: "AttrDict") -> bool:
|
|
97
|
+
def filter(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
92
98
|
if self.types and event.kind not in self.types:
|
|
93
99
|
return False
|
|
94
|
-
return self._call_func(event)
|
|
100
|
+
return self._call_func(bot, event)
|
|
95
101
|
|
|
96
102
|
|
|
97
103
|
class NewMessage(EventFilter):
|
|
@@ -110,9 +116,8 @@ class NewMessage(EventFilter):
|
|
|
110
116
|
:param is_info: If set to True only match info/system messages, if set to False
|
|
111
117
|
only match messages that are not info/system messages. If omitted
|
|
112
118
|
info/system messages as well as normal messages will be matched.
|
|
113
|
-
:param func: A Callable
|
|
114
|
-
|
|
115
|
-
should be dispatched or not.
|
|
119
|
+
:param func: A Callable that should accept the bot and the event as input parameter,
|
|
120
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
116
121
|
"""
|
|
117
122
|
|
|
118
123
|
def __init__(
|
|
@@ -126,7 +131,7 @@ class NewMessage(EventFilter):
|
|
|
126
131
|
command: Optional[str] = None,
|
|
127
132
|
is_bot: Optional[bool] = False,
|
|
128
133
|
is_info: Optional[bool] = None,
|
|
129
|
-
func: Optional[
|
|
134
|
+
func: Optional[FilterCallback] = None,
|
|
130
135
|
) -> None:
|
|
131
136
|
super().__init__(func=func)
|
|
132
137
|
self.is_bot = is_bot
|
|
@@ -165,7 +170,7 @@ class NewMessage(EventFilter):
|
|
|
165
170
|
)
|
|
166
171
|
return False
|
|
167
172
|
|
|
168
|
-
def filter(self, event: "AttrDict") -> bool:
|
|
173
|
+
def filter(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
169
174
|
if self.is_bot is not None and self.is_bot != event.msg.is_bot:
|
|
170
175
|
return False
|
|
171
176
|
if self.is_info is not None and self.is_info != event.msg.is_info:
|
|
@@ -176,7 +181,7 @@ class NewMessage(EventFilter):
|
|
|
176
181
|
match = self.pattern(event.msg.text)
|
|
177
182
|
if not match:
|
|
178
183
|
return False
|
|
179
|
-
return super()._call_func(event)
|
|
184
|
+
return super()._call_func(bot, event)
|
|
180
185
|
|
|
181
186
|
|
|
182
187
|
class MemberListChanged(EventFilter):
|
|
@@ -188,9 +193,8 @@ class MemberListChanged(EventFilter):
|
|
|
188
193
|
:param added: If set to True only match if a member was added, if set to False
|
|
189
194
|
only match if a member was removed. If omitted both, member additions
|
|
190
195
|
and removals, will be matched.
|
|
191
|
-
:param func: A Callable
|
|
192
|
-
|
|
193
|
-
should be dispatched or not.
|
|
196
|
+
:param func: A Callable that should accept the bot and event as input parameter,
|
|
197
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
194
198
|
"""
|
|
195
199
|
|
|
196
200
|
def __init__(self, added: Optional[bool] = None, **kwargs):
|
|
@@ -205,10 +209,10 @@ class MemberListChanged(EventFilter):
|
|
|
205
209
|
return (self.added, self.func) == (other.added, other.func)
|
|
206
210
|
return False
|
|
207
211
|
|
|
208
|
-
def filter(self, event: "AttrDict") -> bool:
|
|
212
|
+
def filter(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
209
213
|
if self.added is not None and self.added != event.member_added:
|
|
210
214
|
return False
|
|
211
|
-
return self._call_func(event)
|
|
215
|
+
return self._call_func(bot, event)
|
|
212
216
|
|
|
213
217
|
|
|
214
218
|
class GroupImageChanged(EventFilter):
|
|
@@ -220,9 +224,8 @@ class GroupImageChanged(EventFilter):
|
|
|
220
224
|
:param deleted: If set to True only match if the image was deleted, if set to False
|
|
221
225
|
only match if a new image was set. If omitted both, image changes and
|
|
222
226
|
removals, will be matched.
|
|
223
|
-
:param func: A Callable
|
|
224
|
-
|
|
225
|
-
should be dispatched or not.
|
|
227
|
+
:param func: A Callable that should accept the bot and event as input parameter,
|
|
228
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
226
229
|
"""
|
|
227
230
|
|
|
228
231
|
def __init__(self, deleted: Optional[bool] = None, **kwargs):
|
|
@@ -237,10 +240,10 @@ class GroupImageChanged(EventFilter):
|
|
|
237
240
|
return (self.deleted, self.func) == (other.deleted, other.func)
|
|
238
241
|
return False
|
|
239
242
|
|
|
240
|
-
def filter(self, event: "AttrDict") -> bool:
|
|
243
|
+
def filter(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
241
244
|
if self.deleted is not None and self.deleted != event.image_deleted:
|
|
242
245
|
return False
|
|
243
|
-
return self._call_func(event)
|
|
246
|
+
return self._call_func(bot, event)
|
|
244
247
|
|
|
245
248
|
|
|
246
249
|
class GroupNameChanged(EventFilter):
|
|
@@ -249,9 +252,8 @@ class GroupNameChanged(EventFilter):
|
|
|
249
252
|
Warning: registering a handler for this event will cause the messages
|
|
250
253
|
to be marked as read. Its usage is mainly intended for bots.
|
|
251
254
|
|
|
252
|
-
:param func: A Callable
|
|
253
|
-
|
|
254
|
-
should be dispatched or not.
|
|
255
|
+
:param func: A Callable that should accept the bot and event as input parameter,
|
|
256
|
+
and return a bool value indicating whether the event should be dispatched or not.
|
|
255
257
|
"""
|
|
256
258
|
|
|
257
259
|
def __hash__(self) -> int:
|
|
@@ -262,8 +264,8 @@ class GroupNameChanged(EventFilter):
|
|
|
262
264
|
return self.func == other.func
|
|
263
265
|
return False
|
|
264
266
|
|
|
265
|
-
def filter(self, event: "AttrDict") -> bool:
|
|
266
|
-
return self._call_func(event)
|
|
267
|
+
def filter(self, bot: "Bot", event: "AttrDict") -> bool:
|
|
268
|
+
return self._call_func(bot, event)
|
|
267
269
|
|
|
268
270
|
|
|
269
271
|
class HookCollection:
|
|
@@ -272,18 +274,18 @@ class HookCollection:
|
|
|
272
274
|
"""
|
|
273
275
|
|
|
274
276
|
def __init__(self) -> None:
|
|
275
|
-
self._hooks: Set[Tuple[
|
|
277
|
+
self._hooks: Set[Tuple[HookCallback, Union[type, EventFilter]]] = set()
|
|
276
278
|
|
|
277
|
-
def __iter__(self) -> Iterator[Tuple[
|
|
279
|
+
def __iter__(self) -> Iterator[Tuple[HookCallback, Union[type, EventFilter]]]:
|
|
278
280
|
return iter(self._hooks)
|
|
279
281
|
|
|
280
|
-
def on(self, event: Union[type, EventFilter]) ->
|
|
282
|
+
def on(self, event: Union[type, EventFilter]) -> HookDecorator: # noqa
|
|
281
283
|
"""Register decorated function as listener for the given event."""
|
|
282
284
|
if isinstance(event, type):
|
|
283
285
|
event = event()
|
|
284
286
|
assert isinstance(event, EventFilter), "Invalid event filter"
|
|
285
287
|
|
|
286
|
-
def _decorator(func) ->
|
|
288
|
+
def _decorator(func: HookCallback) -> HookCallback:
|
|
287
289
|
self._hooks.add((func, event))
|
|
288
290
|
return func
|
|
289
291
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Utilities"""
|
|
2
|
+
|
|
3
|
+
from ._utils import AttrDict
|
|
4
|
+
from .client import Bot
|
|
5
|
+
from .const import COMMAND_PREFIX
|
|
6
|
+
from .events import NewMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_not_known_command(bot: Bot, event: AttrDict) -> bool:
|
|
10
|
+
"""Filter helper to be used by NewMessage filters.
|
|
11
|
+
Matches all NewMessage that don't match a previously registered command filter."""
|
|
12
|
+
if not event.command.startswith(COMMAND_PREFIX):
|
|
13
|
+
return True
|
|
14
|
+
for hook in bot._hooks.get(NewMessage, []): # pylint:disable=W0212
|
|
15
|
+
if event.command == hook[1].command:
|
|
16
|
+
return False
|
|
17
|
+
return True
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deltabot-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Library to speedup Delta Chat bot development
|
|
5
|
-
Author-email: adbenitez <
|
|
5
|
+
Author-email: adbenitez <adb@merlinux.eu>
|
|
6
6
|
Keywords: deltachat,bot,deltabot-cli
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: deltachat-rpc-server>=1.133.2
|
|
@@ -44,19 +44,18 @@ pip install deltabot-cli
|
|
|
44
44
|
Example echo-bot written with deltabot-cli:
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
|
-
import logging
|
|
48
47
|
from deltabot_cli import BotCli, events
|
|
49
48
|
|
|
50
49
|
cli = BotCli("echobot")
|
|
51
50
|
|
|
52
51
|
@cli.on(events.RawEvent)
|
|
53
|
-
def log_event(event):
|
|
54
|
-
|
|
52
|
+
def log_event(bot, accid, event):
|
|
53
|
+
bot.logger.info(event)
|
|
55
54
|
|
|
56
55
|
@cli.on(events.NewMessage)
|
|
57
|
-
def echo(event):
|
|
56
|
+
def echo(bot, accid, event):
|
|
58
57
|
msg = event.msg
|
|
59
|
-
|
|
58
|
+
bot.rpc.misc_send_text_message(accid, msg.chat_id, msg.text)
|
|
60
59
|
|
|
61
60
|
if __name__ == "__main__":
|
|
62
61
|
cli.start()
|
|
@@ -65,4 +64,4 @@ if __name__ == "__main__":
|
|
|
65
64
|
If you run the above script you will have a bot CLI, that allows to configure and run a bot.
|
|
66
65
|
A progress bar is displayed while the bot is configuring, and logs are pretty-printed.
|
|
67
66
|
|
|
68
|
-
For more examples check the [examples](
|
|
67
|
+
For more examples check the [examples](./examples) folder.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
.gitignore
|
|
1
2
|
LICENSE
|
|
2
3
|
README.md
|
|
4
|
+
pylama.ini
|
|
3
5
|
pyproject.toml
|
|
6
|
+
.github/workflows/python-ci.yml
|
|
4
7
|
deltabot_cli/__init__.py
|
|
5
8
|
deltabot_cli/_utils.py
|
|
6
9
|
deltabot_cli/cli.py
|
|
@@ -8,8 +11,11 @@ deltabot_cli/client.py
|
|
|
8
11
|
deltabot_cli/const.py
|
|
9
12
|
deltabot_cli/events.py
|
|
10
13
|
deltabot_cli/rpc.py
|
|
14
|
+
deltabot_cli/utils.py
|
|
11
15
|
deltabot_cli.egg-info/PKG-INFO
|
|
12
16
|
deltabot_cli.egg-info/SOURCES.txt
|
|
13
17
|
deltabot_cli.egg-info/dependency_links.txt
|
|
14
18
|
deltabot_cli.egg-info/requires.txt
|
|
15
|
-
deltabot_cli.egg-info/top_level.txt
|
|
19
|
+
deltabot_cli.egg-info/top_level.txt
|
|
20
|
+
examples/echobot.py
|
|
21
|
+
examples/echobot_advanced.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Minimal echo-bot example."""
|
|
3
|
+
|
|
4
|
+
from deltabot_cli import BotCli, events
|
|
5
|
+
|
|
6
|
+
cli = BotCli("echobot")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@cli.on(events.RawEvent)
|
|
10
|
+
def log_event(bot, _accid, event):
|
|
11
|
+
bot.logger.info(event)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.on(events.NewMessage)
|
|
15
|
+
def echo(bot, accid, event):
|
|
16
|
+
msg = event.msg
|
|
17
|
+
bot.rpc.misc_send_text_message(accid, msg.chat_id, msg.text)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
cli.start()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Advanced echo bot example."""
|
|
3
|
+
from deltabot_cli import BotCli, EventType, events
|
|
4
|
+
|
|
5
|
+
cli = BotCli("echobot")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@cli.on(events.RawEvent)
|
|
9
|
+
def log_event(bot, _accid, event):
|
|
10
|
+
if event.kind == EventType.INFO:
|
|
11
|
+
bot.logger.info(event.msg)
|
|
12
|
+
elif event.kind == EventType.WARNING:
|
|
13
|
+
bot.logger.warning(event.msg)
|
|
14
|
+
elif event.kind == EventType.ERROR:
|
|
15
|
+
bot.logger.error(event.msg)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.on(events.NewMessage)
|
|
19
|
+
def echo(bot, accid, event):
|
|
20
|
+
msg = event.msg
|
|
21
|
+
bot.rpc.misc_send_text_message(accid, msg.chat_id, msg.text)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.on_init
|
|
25
|
+
def on_init(bot, args):
|
|
26
|
+
bot.logger.info("Initializing bot with args: %s", args)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.on_start
|
|
30
|
+
def on_start(bot, _args):
|
|
31
|
+
bot.logger.info("Running bot...")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test(_cli, bot, args):
|
|
35
|
+
"""just some example subcommand"""
|
|
36
|
+
bot.logger.info("Hello %s, this is an example subcommand!", args.name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
subcmd = cli.add_subcommand(test)
|
|
41
|
+
subcmd.add_argument("name", help="your name")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
cli.start()
|
|
45
|
+
except KeyboardInterrupt:
|
|
46
|
+
pass
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools"]
|
|
2
|
+
requires = ["setuptools>=64", "setuptools_scm>=8"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
|
-
version = "1.0.0"
|
|
7
6
|
name = "deltabot-cli"
|
|
8
7
|
description = "Library to speedup Delta Chat bot development"
|
|
8
|
+
dynamic = ["version"]
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
11
|
license = {file = "LICENSE.txt"}
|
|
12
12
|
keywords = ["deltachat", "bot", "deltabot-cli"]
|
|
13
13
|
authors = [
|
|
14
|
-
{name = "adbenitez", email = "
|
|
14
|
+
{name = "adbenitez", email = "adb@merlinux.eu"},
|
|
15
15
|
]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
@@ -35,6 +35,9 @@ dev = [
|
|
|
35
35
|
"pytest",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
[tool.setuptools_scm]
|
|
39
|
+
# can be empty if no extra settings are needed, presence enables setuptools_scm
|
|
40
|
+
|
|
38
41
|
[tool.black]
|
|
39
42
|
line-length = 100
|
|
40
43
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|