Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI

on:
push:
branches: [ main ]
tags-ignore: [ '**' ]
pull_request:
branches: [ main ]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# The tests create accounts on the ci-chatmail.testrun.org relay.
py-checks:
uses: chatmail/workflows/.github/workflows/py-checks.yml@main
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Release to PyPI

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
# Checks and build via the shared reusable workflow, which uploads
# the built distributions as a "dist" artifact.
py-checks:
uses: chatmail/workflows/.github/workflows/py-checks.yml@main

# The publish job must stay here in the repo's own workflow: PyPI
# trusted publishing rejects publishes from reusable workflows.
# See https://github.com/pypi/warehouse/issues/11096
publish:
needs: py-checks
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/

- uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
26 changes: 19 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@

# cmsend changelog
# cmsend changelog

## unreleased

- adopt shared CI from chatmail/workflows (ruff lint+format, build)
and release via PyPI trusted publishing on v* tags

- fix "cmsend -l" crashing with AttributeError when listing chat
members

- `main()` accepts an optional argv list so tests can invoke it

- fix pyproject description (was copied from cmping)

## 0.4.2

- improve output on "cmsend -l" to show members of each chat
- improve output on "cmsend -l" to show members of each chat

## 0.4.1
## 0.4.1

- add warning
- add warning

## 0.4.0 tagged chats

- added "-t" tagged chats option, so that "--join" can be accompanied by "-t"

- added "cmsend -l" to list all tagged chats
- added "cmsend -l" to list all tagged chats


## 0.3.2
Expand All @@ -26,8 +38,8 @@

## 0.3.0

- added more options
- added more options

## 0.1.0

- initial release
- initial release
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

To install use:

pip install cmsend
uv tool install cmsend

To send and receive from a single chatmail relay:

Expand All @@ -31,21 +31,22 @@ To show help:
cmsend -h


## Example outputs


## Developing / Releasing cmsend

1. clone the git repository at https://github.com/chatmail/cmsend

2. install 'cmsend" in editing mode: `pip install -e .`
2. install 'cmsend' in editing mode: `uv pip install -e .`

3. edit cmsend.py and test, finally commit your changes

4. set a new git-tag
[chatmail/workflows](https://github.com/chatmail/workflows)
defines py-checks for this repository.
Run checks locally with `uvx ruff check .` and `uvx ruff format --check .`

5. install build/release tools: `pip install build twine`
To release, update CHANGELOG.md, then create and push a version tag:

6. run the following command:
git tag -a v0.5.0 -m "Release v0.5.0"
git push origin main v0.5.0

rm -rf dist && python -m build && twine upload -r pypi dist/cmsend*
The release.yml workflow then builds and publishes to PyPI via
trusted publishing (OIDC); no local twine or PyPI token is involved.
19 changes: 9 additions & 10 deletions cmsend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from xdg_base_dirs import xdg_config_home


def main():
def main(argv=None):
"""Send end-to-end encrypted messages to groups/contacts."""

parser = argparse.ArgumentParser(description=main.__doc__)
Expand All @@ -34,10 +34,7 @@ def main():
help="use the specified tag for joining a chat or sending a message (default: GENESIS)",
)
parser.add_argument(
"-l",
dest="listtags",
action="store_true",
help="list existing tagged chats"
"-l", dest="listtags", action="store_true", help="list existing tagged chats"
)
parser.add_argument(
"-m",
Expand All @@ -52,7 +49,7 @@ def main():
parser.add_argument(
"-a", dest="filename", type=str, default=None, help="add file attachment"
)
args = parser.parse_args()
args = parser.parse_args(argv)

try:
return perform_main(args)
Expand Down Expand Up @@ -161,8 +158,8 @@ def perform_listtags(self):
chat = self.get_tagged_chat(tag)
snap = chat.get_full_snapshot()
print(f"{tag}: chat_id={chat.id} name={snap.name}")
for contact in snap.contacts:
print(f" - {contact.name_and_addr}")
for contact in chat.get_contacts():
print(f" - {contact.get_snapshot().name_and_addr}")

def perform_send(self, tag, text, filename=None):
self._account.start_io()
Expand All @@ -179,8 +176,10 @@ def perform_send(self, tag, text, filename=None):
def get_tagged_chat(self, tag):
chat_id = self._account.get_config(f"{self.UI_CONFIG_TAGGED_CHATS}.{tag}")
if not chat_id:
print(f"No chat tagged with tag={tag} found for sending on {self!r}, "
f"use -t {tag} --join 'https://i.delta.chat/...'")
print(
f"No chat tagged with tag={tag} found for sending on {self!r}, "
f"use -t {tag} --join 'https://i.delta.chat/...'"
)
raise SystemExit(5)

return self._account.get_chat_by_id(int(chat_id))
Expand Down
18 changes: 12 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ build-backend = "setuptools.build_meta"
[project]
name = "cmsend"
readme = "README.md"
description = "ping messages between chatmail relays"
description = "send end-to-end encrypted messages to chats on chatmail relays"
requires-python = ">=3.11"
dependencies = [
"deltachat-rpc-server>=2.24.0",
"deltachat-rpc-client>=2.24.0",
Expand All @@ -23,17 +24,22 @@ py-modules = ["cmsend"]

[project.urls]
Repository = "https://github.com/chatmail/cmsend"
Changelog = "https://github.com/chatmail/cmsend/blob/master/CHANGELOG.md"
Changelog = "https://github.com/chatmail/cmsend/blob/main/CHANGELOG.md"

[project.scripts]
cmsend = "cmsend:main"

[dependency-groups]
test = [
"pytest",
"pytest-timeout",
"execnet", # imported by the deltachat_rpc_client pytest plugin
]

[tool.pytest.ini_options]
addopts = "-v -ra --strict-markers"

[tool.setuptools.packages.find]
where = ["."]

# cmsend waits for core events without a deadline of its own.
timeout = 180

[tool.ruff]
lint.select = [
Expand Down
50 changes: 50 additions & 0 deletions test_cmsend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest

import cmsend

pytest_plugins = ("deltachat_rpc_client.pytestplugin",)
ci_chatmail_domain = "ci-chatmail.testrun.org"


@pytest.fixture(autouse=True)
def _inject_xdg_config_home(tmp_path, monkeypatch):
xdg_config = tmp_path.joinpath("xdg-config")
monkeypatch.setattr(cmsend, "xdg_config_home", lambda: xdg_config)
monkeypatch.setenv("CHATMAIL_DOMAIN", ci_chatmail_domain)


@pytest.fixture
def invoke_main(capsys):
def invoke(*args):
with capsys.disabled():
print(f"$ cmsend {' '.join(args)}")
ret = cmsend.main(args)
out, err = capsys.readouterr()
with capsys.disabled():
if out:
print(out)
return ret, out, err

return invoke


def test_init_join_and_send(acfactory, invoke_main):
(ac,) = acfactory.get_online_accounts(1)

invoke_main("--init", ci_chatmail_domain)

_ret, out, _err = invoke_main("-l")
assert "LOG" not in out

# "--join" expects a Join-Group QR code, not a Setup-Contact one.
group = ac.create_group("cmsend log")
invoke_main("-t", "LOG", "--join", group.get_qr_code())

_ret, out, _err = invoke_main("-l")
assert "LOG" in out
assert "cmsend log" in out

invoke_main("-t", "LOG", "-m", "hello from cmsend")
event = ac.wait_for_incoming_msg_event()
snapshot = ac.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "hello from cmsend"