# Signal Bot Framework
Python package to build your own Signal bots.
## Getting Started
Install it with `pip install signalbot`.
Below you can find a minimal example on how to use the package.
Save it as `bot.py`.
There is also a bigger example in the [example folder](https://github.com/filipre/signalbot/tree/master/example).
```python
import os
import logging
from signalbot import SignalBot, Command, Context, triggered, enable_console_logging
class PingCommand(Command):
@triggered("Ping")
async def handle(self, c: Context) -> None:
await c.send("Pong")
if __name__ == "__main__":
enable_console_logging(logging.INFO)
bot = SignalBot({
"signal_service": os.environ["SIGNAL_SERVICE"],
"phone_number": os.environ["PHONE_NUMBER"]
})
bot.register(PingCommand()) # Run the command for all contacts and groups
bot.start()
```
Please check out https://github.com/bbernhard/signal-cli-rest-api#getting-started to learn about [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) and [signal-cli](https://github.com/AsamK/signal-cli). A good first step is to make the example above work.
1. Run signal-cli-rest-api in `normal` mode first.
```bash
docker run -p 8080:8080 \
-v $(pwd)/signal-cli-config:/home/.local/share/signal-cli \
-e 'MODE=normal' bbernhard/signal-cli-rest-api:latest
```
2. Open http://127.0.0.1:8080/v1/qrcodelink?device_name=local to link your account with the signal-cli-rest-api server
3. In your Signal app, open settings and scan the QR code. The server can now receive and send messages. The access key will be stored in `$(PWD)/signal-cli-config`.
4. Restart the server in `json-rpc` mode.
```bash
docker run -p 8080:8080 \
-v $(pwd)/signal-cli-config:/home/.local/share/signal-cli \
-e 'MODE=json-rpc' bbernhard/signal-cli-rest-api:latest
```
5. The logs should show something like this. You can also confirm that the server is running in the correct mode by visiting http://127.0.0.1:8080/v1/about.
```
...
time="2022-03-07T13:02:22Z" level=info msg="Found number +491234567890 and added it to jsonrpc2.yml"
...
time="2022-03-07T13:02:24Z" level=info msg="Started Signal Messenger REST API"
```
6. Install `signalbot` and start your python script. You need to pass following environment variables to make the example run:
- `SIGNAL_SERVICE`: Address of the signal service without protocol, e.g. `127.0.0.1:8080`
- `PHONE_NUMBER`: Phone number of the bot, e.g. `+49123456789`
```bash
export SIGNAL_SERVICE="127.0.0.1"
export PHONE_NUMBER="+49123456789"
pip install signalbot
python bot.py
```
7. The logs should indicate that one "producer" and three "consumers" have started. The producer checks for new messages sent to the linked account using a web socket connection. It creates a task for every registered command and the consumers work off the tasks. In case you are working with many blocking function calls, you may need to adjust the number of consumers such that the bot stays reactive.
```
<date> signalbot [WARNING] - __init__ - [Bot] Could not initialize Redis and no SQLite DB name was given. In-memory storage will be used. Restarting will delete the storage! Add storage: {'type': 'in-memory'} to the config to silence this error.
<date> signalbot [INFO] - _detect_groups - [Bot] 3 groups detected
<date> signalbot [INFO] - _produce - [Bot] Producer #1 started
<date> signalbot [INFO] - _consume - [Bot] Consumer #1 started
<date> signalbot [INFO] - _consume - [Bot] Consumer #2 started
<date> signalbot [INFO] - _consume - [Bot] Consumer #3 started
```
8. Send the message `Ping` (case sensitive) to the number that the bot is listening to. The bot (i.e. the linked account) should respond with a `Pong`. Confirm that the bot received a raw message, that the consumer worked on the message and that a new message has been sent.
```
<date> signalbot [INFO] - _produce - [Raw Message] {"envelope": <raw message dictionary>}
<date> signalbot [INFO] - _consume_new_item - [Bot] Consumer #2 got new job in 0.00046 seconds
<date> signalbot [INFO] - _produce - [Raw Message] {"envelope": <raw message dictionary>}
<date> signalbot [INFO] - send - [Bot] New message 1760797696983 sent:
Pong
```
## Classes and API
*Documentation work in progress. Feel free to open an issue for questions.*
The package provides methods to easily listen for incoming messages and responding or reacting on them. It also provides a class to develop new commands which then can be registered within the bot.
### Signalbot
- `bot.register(command, contacts=True, groups=True)`: Register a new command, listen in all contacts and groups, same as `bot.register(command)`
- `bot.register(command, contacts=False, groups=["Hello World"])`: Only listen in the "Hello World" group
- `bot.register(command, contacts=["+49123456789"], groups=False)`: Only respond to one contact
- `bot.start()`: Start the bot
- `bot.send(receiver, text)`: Send a new message
- `bot.react(message, emoji)`: React to a message
- `bot.start_typing(receiver)`: Start typing
- `bot.stop_typing(receiver)`: Stop typing
- `bot.edit(receiver, text, timestamp)`: Edit a previously sent message
- `bot.remote_delete(receiver, timestamp)`: Delete a previously sent message
- `bot.receipt(message, receipt_type)`: Mark a message as read
- `bot.update_group(group_id, avatar, description, expiration, name)`: Change group settings
- `bot.delete_attachment(attachment_filename)`: Delete the local copy of an attachment
- `bot.scheduler`: APScheduler > AsyncIOScheduler, see [here](https://apscheduler.readthedocs.io/en/3.x/modules/schedulers/asyncio.html?highlight=AsyncIOScheduler#apscheduler.schedulers.asyncio.AsyncIOScheduler)
### Persistent storage
By default the `bot.storage` is in-memory.
Any changes are lost when the bot is stopped or reseted.
For persistent storage to disk, check the SQLite or Redis storage in `storage.py`.
### Command
To implement your own commands, you need to inherent `Command` and overwrite following methods:
- `setup(self)`: Start any task that requires to send messages already, optional
- `describe(self)`: String to describe your command, optional
- `handle(self, c: Context)`: Handle an incoming message. By default, any command will read any incoming message. `Context` can be used to easily send (`c.send(text)`), reply (`c.reply(text)`), react (`c.react(emoji)`) and to type in a group (`c.start_typing()` and `c.stop_typing()`). You can use the `@triggered` decorator to listen for specific commands, the `@regex_triggered` decorator to listen for regular expressions, or you can inspect `c.message.text`.
### Logging
The logger name for the library is `"signalbot"`.
It does not have any handlers attached, for convenience the `enable_console_logging(level)` function is provided.
### Unit Testing
*Note: deprecated, the plan is to switch to pytest eventually*
The tests can be executed with
```bash
poetry run python -m unittest discover --start-directory ./tests
```
In many cases, we can mock receiving and sending messages to speed up development time. To do so, you can use `signalbot.utils.ChatTestCase` which sets up a "skeleton" bot. Then, you can send messages using the `@chat` decorator in `signalbot.utils` like this:
```python
class PingChatTest(ChatTestCase):
def setUp(self):
# initialize self.singal_bot
super().setUp()
# all that is left to do is to register the commands that you want to test
self.signal_bot.register(PingCommand())
@chat("ping", "ping")
async def test_ping(self, query, replies, reactions):
self.assertEqual(replies.call_count, 2)
for recipient, message in replies.results():
self.assertEqual(recipient, ChatTestCase.group_secret)
self.assertEqual(message, "pong")
```
In `signalbot.utils`, check out `ReceiveMessagesMock`, `SendMessagesMock` and `ReactMessageMock` to learn more about their API.
## Troubleshooting
- Check that you linked your account successfully
- Is the API server running in `json-rpc` mode?
- Can you receive messages using `wscat` (websockets) and send messages using `curl` (http)?
- Do you see incoming messages in the API logs?
- Do you see the "raw" messages in the bot's logs?
- Do you see "consumers" picking up jobs and handling incoming messages?
- Do you see the response in the bot's logs?
## Local development
```bash
poetry install
poetry run pre-commit install
```
## Real world bot examples
There are many real world examples of bot implementations using this library.
Check the whole list at https://github.com/filipre/signalbot/network/dependents
## Other Projects
There are a few other projects similar to this one. You may want to check them out and see if it fits your needs.
|Project|Description|Language|
|-------|-----------|--------|
|https://codeberg.org/lazlo/semaphore|signald Library / Bot Framework|Python|
|https://git.sr.ht/~nicoco/aiosignald|signald Library / Bot Framework|Python|
|https://gitlab.com/stavros/pysignald/|signald Library / Bot Framework|Python|
|https://gitlab.com/signald/signald-go|signald Library|Go|
|https://github.com/signal-bot/signal-bot|Bot Framework using Signal CLI|Python|
|https://github.com/bbernhard/signal-cli-rest-api|REST API Wrapper for Signal CLI|Go|
|https://github.com/bbernhard/pysignalclirestapi|Python Wrapper for REST API|Python|
|https://github.com/AsamK/signal-cli|A CLI and D-Bus interface for Signal|Java|
|https://github.com/signalapp/libsignal-service-java|Signal Library|Java|
|https://github.com/aaronetz/signal-bot|Bot Framework|Java|
Raw data
{
"_id": null,
"home_page": "https://github.com/filipre/signalbot",
"name": "signalbot",
"maintainer": "Ren\u00e9 Filip",
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "Signal, Bot, Framework, Home Automation",
"author": "Ren\u00e9 Filip",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/b8/d4/6fe610242da93fda888ee58a00503e7c73568d8f6164f3e16454e7151675/signalbot-0.20.0.tar.gz",
"platform": null,
"description": "# Signal Bot Framework\n\nPython package to build your own Signal bots.\n\n## Getting Started\n\nInstall it with `pip install signalbot`.\n\nBelow you can find a minimal example on how to use the package.\nSave it as `bot.py`.\nThere is also a bigger example in the [example folder](https://github.com/filipre/signalbot/tree/master/example).\n\n```python\nimport os\nimport logging\nfrom signalbot import SignalBot, Command, Context, triggered, enable_console_logging\n\n\nclass PingCommand(Command):\n @triggered(\"Ping\")\n async def handle(self, c: Context) -> None:\n await c.send(\"Pong\")\n\n\nif __name__ == \"__main__\":\n enable_console_logging(logging.INFO)\n\n bot = SignalBot({\n \"signal_service\": os.environ[\"SIGNAL_SERVICE\"],\n \"phone_number\": os.environ[\"PHONE_NUMBER\"]\n })\n bot.register(PingCommand()) # Run the command for all contacts and groups\n bot.start()\n```\n\nPlease check out https://github.com/bbernhard/signal-cli-rest-api#getting-started to learn about [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) and [signal-cli](https://github.com/AsamK/signal-cli). A good first step is to make the example above work.\n\n1. Run signal-cli-rest-api in `normal` mode first.\n```bash\ndocker run -p 8080:8080 \\\n -v $(pwd)/signal-cli-config:/home/.local/share/signal-cli \\\n -e 'MODE=normal' bbernhard/signal-cli-rest-api:latest\n```\n\n2. Open http://127.0.0.1:8080/v1/qrcodelink?device_name=local to link your account with the signal-cli-rest-api server\n\n3. In your Signal app, open settings and scan the QR code. The server can now receive and send messages. The access key will be stored in `$(PWD)/signal-cli-config`.\n\n4. Restart the server in `json-rpc` mode.\n```bash\ndocker run -p 8080:8080 \\\n -v $(pwd)/signal-cli-config:/home/.local/share/signal-cli \\\n -e 'MODE=json-rpc' bbernhard/signal-cli-rest-api:latest\n```\n\n5. The logs should show something like this. You can also confirm that the server is running in the correct mode by visiting http://127.0.0.1:8080/v1/about.\n```\n...\ntime=\"2022-03-07T13:02:22Z\" level=info msg=\"Found number +491234567890 and added it to jsonrpc2.yml\"\n...\ntime=\"2022-03-07T13:02:24Z\" level=info msg=\"Started Signal Messenger REST API\"\n```\n\n6. Install `signalbot` and start your python script. You need to pass following environment variables to make the example run:\n- `SIGNAL_SERVICE`: Address of the signal service without protocol, e.g. `127.0.0.1:8080`\n- `PHONE_NUMBER`: Phone number of the bot, e.g. `+49123456789`\n\n```bash\nexport SIGNAL_SERVICE=\"127.0.0.1\"\nexport PHONE_NUMBER=\"+49123456789\"\npip install signalbot\npython bot.py\n```\n\n7. The logs should indicate that one \"producer\" and three \"consumers\" have started. The producer checks for new messages sent to the linked account using a web socket connection. It creates a task for every registered command and the consumers work off the tasks. In case you are working with many blocking function calls, you may need to adjust the number of consumers such that the bot stays reactive.\n```\n<date> signalbot [WARNING] - __init__ - [Bot] Could not initialize Redis and no SQLite DB name was given. In-memory storage will be used. Restarting will delete the storage! Add storage: {'type': 'in-memory'} to the config to silence this error.\n<date> signalbot [INFO] - _detect_groups - [Bot] 3 groups detected\n<date> signalbot [INFO] - _produce - [Bot] Producer #1 started\n<date> signalbot [INFO] - _consume - [Bot] Consumer #1 started\n<date> signalbot [INFO] - _consume - [Bot] Consumer #2 started\n<date> signalbot [INFO] - _consume - [Bot] Consumer #3 started\n```\n\n8. Send the message `Ping` (case sensitive) to the number that the bot is listening to. The bot (i.e. the linked account) should respond with a `Pong`. Confirm that the bot received a raw message, that the consumer worked on the message and that a new message has been sent.\n```\n<date> signalbot [INFO] - _produce - [Raw Message] {\"envelope\": <raw message dictionary>}\n<date> signalbot [INFO] - _consume_new_item - [Bot] Consumer #2 got new job in 0.00046 seconds\n<date> signalbot [INFO] - _produce - [Raw Message] {\"envelope\": <raw message dictionary>}\n<date> signalbot [INFO] - send - [Bot] New message 1760797696983 sent:\nPong\n```\n\n## Classes and API\n\n*Documentation work in progress. Feel free to open an issue for questions.*\n\nThe package provides methods to easily listen for incoming messages and responding or reacting on them. It also provides a class to develop new commands which then can be registered within the bot.\n\n### Signalbot\n\n- `bot.register(command, contacts=True, groups=True)`: Register a new command, listen in all contacts and groups, same as `bot.register(command)`\n- `bot.register(command, contacts=False, groups=[\"Hello World\"])`: Only listen in the \"Hello World\" group\n- `bot.register(command, contacts=[\"+49123456789\"], groups=False)`: Only respond to one contact\n- `bot.start()`: Start the bot\n- `bot.send(receiver, text)`: Send a new message\n- `bot.react(message, emoji)`: React to a message\n- `bot.start_typing(receiver)`: Start typing\n- `bot.stop_typing(receiver)`: Stop typing\n- `bot.edit(receiver, text, timestamp)`: Edit a previously sent message\n- `bot.remote_delete(receiver, timestamp)`: Delete a previously sent message\n- `bot.receipt(message, receipt_type)`: Mark a message as read\n- `bot.update_group(group_id, avatar, description, expiration, name)`: Change group settings\n- `bot.delete_attachment(attachment_filename)`: Delete the local copy of an attachment\n- `bot.scheduler`: APScheduler > AsyncIOScheduler, see [here](https://apscheduler.readthedocs.io/en/3.x/modules/schedulers/asyncio.html?highlight=AsyncIOScheduler#apscheduler.schedulers.asyncio.AsyncIOScheduler)\n\n### Persistent storage\n\nBy default the `bot.storage` is in-memory.\nAny changes are lost when the bot is stopped or reseted.\nFor persistent storage to disk, check the SQLite or Redis storage in `storage.py`.\n\n### Command\n\nTo implement your own commands, you need to inherent `Command` and overwrite following methods:\n\n- `setup(self)`: Start any task that requires to send messages already, optional\n- `describe(self)`: String to describe your command, optional\n- `handle(self, c: Context)`: Handle an incoming message. By default, any command will read any incoming message. `Context` can be used to easily send (`c.send(text)`), reply (`c.reply(text)`), react (`c.react(emoji)`) and to type in a group (`c.start_typing()` and `c.stop_typing()`). You can use the `@triggered` decorator to listen for specific commands, the `@regex_triggered` decorator to listen for regular expressions, or you can inspect `c.message.text`.\n\n### Logging\n\nThe logger name for the library is `\"signalbot\"`.\nIt does not have any handlers attached, for convenience the `enable_console_logging(level)` function is provided.\n\n### Unit Testing\n\n*Note: deprecated, the plan is to switch to pytest eventually*\n\nThe tests can be executed with\n\n```bash\npoetry run python -m unittest discover --start-directory ./tests\n```\n\nIn many cases, we can mock receiving and sending messages to speed up development time. To do so, you can use `signalbot.utils.ChatTestCase` which sets up a \"skeleton\" bot. Then, you can send messages using the `@chat` decorator in `signalbot.utils` like this:\n```python\nclass PingChatTest(ChatTestCase):\n def setUp(self):\n # initialize self.singal_bot\n super().setUp()\n # all that is left to do is to register the commands that you want to test\n self.signal_bot.register(PingCommand())\n\n @chat(\"ping\", \"ping\")\n async def test_ping(self, query, replies, reactions):\n self.assertEqual(replies.call_count, 2)\n for recipient, message in replies.results():\n self.assertEqual(recipient, ChatTestCase.group_secret)\n self.assertEqual(message, \"pong\")\n```\nIn `signalbot.utils`, check out `ReceiveMessagesMock`, `SendMessagesMock` and `ReactMessageMock` to learn more about their API.\n\n## Troubleshooting\n\n- Check that you linked your account successfully\n- Is the API server running in `json-rpc` mode?\n- Can you receive messages using `wscat` (websockets) and send messages using `curl` (http)?\n- Do you see incoming messages in the API logs?\n- Do you see the \"raw\" messages in the bot's logs?\n- Do you see \"consumers\" picking up jobs and handling incoming messages?\n- Do you see the response in the bot's logs?\n\n## Local development\n\n```bash\npoetry install\npoetry run pre-commit install\n```\n\n## Real world bot examples\n\nThere are many real world examples of bot implementations using this library.\nCheck the whole list at https://github.com/filipre/signalbot/network/dependents\n\n## Other Projects\n\nThere are a few other projects similar to this one. You may want to check them out and see if it fits your needs.\n\n|Project|Description|Language|\n|-------|-----------|--------|\n|https://codeberg.org/lazlo/semaphore|signald Library / Bot Framework|Python|\n|https://git.sr.ht/~nicoco/aiosignald|signald Library / Bot Framework|Python|\n|https://gitlab.com/stavros/pysignald/|signald Library / Bot Framework|Python|\n|https://gitlab.com/signald/signald-go|signald Library|Go|\n|https://github.com/signal-bot/signal-bot|Bot Framework using Signal CLI|Python|\n|https://github.com/bbernhard/signal-cli-rest-api|REST API Wrapper for Signal CLI|Go|\n|https://github.com/bbernhard/pysignalclirestapi|Python Wrapper for REST API|Python|\n|https://github.com/AsamK/signal-cli|A CLI and D-Bus interface for Signal|Java|\n|https://github.com/signalapp/libsignal-service-java|Signal Library|Java|\n|https://github.com/aaronetz/signal-bot|Bot Framework|Java|\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Framework to create your own Signal bots",
"version": "0.20.0",
"project_urls": {
"Homepage": "https://github.com/filipre/signalbot",
"Repository": "https://github.com/filipre/signalbot"
},
"split_keywords": [
"signal",
" bot",
" framework",
" home automation"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "114fb64c06b3aa26f5bf91b7d25f4620e374c8f02e1d3d7d18ab86ad707bc370",
"md5": "befc454333606c173bfe659949051fac",
"sha256": "2f25107fd9322c72923e96487fa0e0d4216acbc39852e2d06ee7342e3dc979c7"
},
"downloads": -1,
"filename": "signalbot-0.20.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "befc454333606c173bfe659949051fac",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 21495,
"upload_time": "2025-10-18T17:23:41",
"upload_time_iso_8601": "2025-10-18T17:23:41.352901Z",
"url": "https://files.pythonhosted.org/packages/11/4f/b64c06b3aa26f5bf91b7d25f4620e374c8f02e1d3d7d18ab86ad707bc370/signalbot-0.20.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "b8d46fe610242da93fda888ee58a00503e7c73568d8f6164f3e16454e7151675",
"md5": "8e1d4aadfb0d5b3817996fc1f25f56db",
"sha256": "5825841fc7f160b1e6549db6cd396efef814e8d453a8a12626184ea50ba7790d"
},
"downloads": -1,
"filename": "signalbot-0.20.0.tar.gz",
"has_sig": false,
"md5_digest": "8e1d4aadfb0d5b3817996fc1f25f56db",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 21133,
"upload_time": "2025-10-18T17:23:42",
"upload_time_iso_8601": "2025-10-18T17:23:42.242118Z",
"url": "https://files.pythonhosted.org/packages/b8/d4/6fe610242da93fda888ee58a00503e7c73568d8f6164f3e16454e7151675/signalbot-0.20.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-18 17:23:42",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "filipre",
"github_project": "signalbot",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "signalbot"
}