Metadata-Version: 2.4
Name: chatxpy
Version: 0.0.2
Summary: A lightweight Python tool for sending messages to Webex, MS Teams, and Slack.
Keywords: cli,webex,microsoft-teams,slack,notification,chatops
Author: sig9
Author-email: sig9 <sig9@sig9.org>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
Maintainer: sig9
Maintainer-email: sig9 <sig9@sig9.org>
Requires-Python: >=3.13
Project-URL: Homepage, https://github.com/sig9org/chatxpy
Project-URL: Repository, https://github.com/sig9org/chatxpy
Project-URL: Issues, https://github.com/sig9org/chatxpy/issues
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/sig9org/chatxpy/main/assets/chatxgo.png" alt="chatxpy">
</p>

# chatxpy

A lightweight Python tool for sending a single Markdown-formatted notification (subject, body, mentions, attachments) to Cisco Webex, Microsoft Teams, and Slack at once.

This is a Python port of [chatxgo](https://github.com/sig9org/chatxgo), preserving the same CLI flags, config file format, and Markdown behavior per chat tool.

chatxpy can be used in two ways:

- **As a standalone CLI tool** — run the `chatxpy` binary (built with PyInstaller) directly from a shell script, CI job, cron task, etc.
- **As a Python library** (`chatxpy.notify`) — import the `notify` package into your own Python program and call it directly, without going through the CLI or a config file at all.

Every chat tool is optional and independent: giving a tool's destination (`*_DST`) enables it, leaving it empty disables it, and a single `chatxpy` invocation delivers the same message to every tool that is enabled at once. A delivery failure on one tool does not stop delivery to the others.

## Setup

Copy `config.ini.example` to `config.ini` and fill in the destinations/credentials for the chat tools you want to use. A tool is enabled simply by giving it a `*_DST` value; leave it empty to disable that tool.

```
WEBEX_TOKEN=...      # Webex bot/personal access token
WEBEX_DST=...        # destination roomId

MSTEAMS_DST=...      # Teams incoming webhook URL

SLACK_DST=...        # Slack incoming webhook URL
SLACK_TOKEN=...      # optional, needed only to upload local file attachments
SLACK_CHANNEL=...    # optional, channel for uploaded attachments
```

`config.ini` contains live credentials, so it is listed in `.gitignore` — never commit it. Commit `config.ini.example` instead.

### Config file location and priority

When run as the CLI, chatxpy resolves `config.ini` in this order:

1. A `config.ini` in the current directory takes priority.
2. Otherwise, a per-user location is used:
   - Linux/macOS: `~/.config/chatxpy/config.ini`
   - Windows: `%AppData%\chatxpy\config.ini`

Pass `-config /path/to/config.ini` to use a specific file instead of the resolved default. A missing config file is not an error — it simply leaves every chat tool disabled.

### Profiles

`config.ini` can hold several named profiles as INI sections, so you can keep, for example, a personal and a work destination side by side in one file:

```
[default]
SLACK_DST=https://hooks.slack.example/default

[work]
SLACK_DST=https://hooks.slack.example/work
```

Select which profile to use with `-profile`/`-p` (default: `default`). Settings written before any `[section]` header belong to the `default` profile. Requesting a profile that doesn't exist in the file is an error.

## CLI usage

```
chatxpy -subject "Deploy done" -body "**v1.2.3** shipped" -mention U0123456 -attach ./report.pdf
```

| Option | Shorthand | Description |
| --- | --- | --- |
| `-subject` | `-s` | Message subject/title |
| `-body` | `-b` | Message body, formatted as Markdown |
| `-mention` | `-m` | User to mention, as `id` or `id:label` (repeatable, or comma-separated). `id` is the native identifier for each tool: a Slack user ID, a Webex email/personId, or for Teams a Microsoft Entra object ID or user principal name/email. |
| `-attach` | `-a` | File path or URL to attach (repeatable, or comma-separated) |
| `-config` | | Path to the `config.ini` file (default: resolved as described in [Setup](#setup)) |
| `-profile` | `-p` | Profile (config.ini section) to use (default: `default`) |
| `-debug` | | Print verbose debug output |
| `-u` | `-update` | Self-update to the latest GitHub release |
| `-v` | `-version` | Show version information |
| `-h` | `-help` | Show usage information |

A mention can be given as `id:label`, separating the identifier and the display name with a colon:

```sh
./dist/chatxpy -profile "work" -subject "Deploy done" -body "**v1.2.3** shipped" -mention "jane.doe@example.com:Jane"
```

### Updating the CLI

Run `chatxpy -update` (or `chatxpy -u`) to check GitHub for a newer release of chatxpy and replace the currently running binary in place. This only replaces a PyInstaller-built binary from `task build`; it is a no-op concept when running from source via `python -m chatxpy`.

```sh
chatxpy -update
```

If the installed binary is already the latest version, chatxpy reports that and exits without changing anything.

### Notes on sending to Microsoft Teams

- Messages are sent to Teams as [Adaptive Cards](https://adaptivecards.microsoft.com/) rather than the legacy MessageCard format. An Adaptive Card `TextBlock` supports a subset of CommonMark Markdown (bold, italic, bullet lists, numbered lists, links); headings, tables, images, and code blocks are not supported.
- Incoming Webhooks that use Adaptive Cards officially support mentioning a user by either their Microsoft Entra object ID (a GUID) or their user principal name (UPN, typically their email address).

## Library usage

Build a `notify.Config` directly in code; a tool is enabled by setting its `dest`.

```python
from chatxpy import notify

cfg = notify.Config(slack=notify.SlackConfig(dest="https://hooks.slack.example/..."))
# or load a specific profile (section) from a config.ini file:
# cfg = notify.configfile.load_config_file("/path/to/config.ini", "work")
dispatcher = notify.Dispatcher(cfg)

results = dispatcher.send(
    notify.Message(
        subject="Deploy done",
        body="**v1.2.3** shipped",
        mentions=[notify.Mention(id="U0123456")],
        attachments=["./report.pdf"],
    )
)
```

`Dispatcher.send` returns one `notify.Result(tool, error)` per enabled tool; it raises `notify.NoRecipientsError` if no tool is enabled at all.

## Markdown support per tool

The `-body` text is passed through to each tool largely as-is, but every chat tool renders its own dialect of Markdown, and none of them support the full CommonMark syntax. Write `-body` for the tool(s) you actually send to, and check the table below before relying on a given syntax.

| Feature | Cisco Webex | Microsoft Teams (Adaptive Cards) | Slack (mrkdwn) |
| --- | --- | --- | --- |
| Bold | `**bold**` | `**bold**` | `*bold*` — CommonMark's `**bold**` is **not** supported and shows literally |
| Italic | `_italic_` | `_italic_` | `_italic_` |
| Strikethrough | Not supported | Not supported | `~strike~` |
| Headings (`#`, `##`, ...) | `#`/`##`/`###` (h1–h3 only) | Not supported | Not supported |
| Blockquote (`>`) | Supported | Not supported | Supported |
| Unordered list | `* item` | `- item` | No native list syntax — write each line manually (e.g. `• item`) |
| Ordered list | `1. item` | `1. item` | No native list syntax — write each line manually |
| Links | `[text](url)` | `[text](url)` | **Not** `[text](url)` — must be `<url` + `\|` + `text>`, or a bare URL |
| Inline code / code block | Supported | Not supported | Supported |
| Horizontal rule (`---`) | Supported | Not supported | Not supported |
| Tables | Not supported | Not supported | Not supported |
| Images | Not supported inline via Markdown | Not supported | Not supported inline (requires Slack Block Kit, not used by chatxpy) |

Notable pitfalls:

- **Slack does not speak CommonMark.** Slack's `mrkdwn` dialect uses single asterisks for bold and `<url|text>` for links, not double asterisks or `[text](url)`.
- **Tables are unsupported everywhere** — Webex, Teams, and Slack all lack table rendering in the message formats chatxpy uses.
- **Teams (Adaptive Cards) has the narrowest subset**: only bold, italic, lists, and links render; headings, blockquotes, code blocks, horizontal rules, and tables are all shown as plain/literal text or dropped.

## Installation

```sh
pip install chatxpy
# or, without installing it into your environment:
uvx chatxpy -subject "Deploy done" -body "**v1.2.3** shipped"
```

This installs the `chatxpy` console script plus the `chatxpy`/`chatxpy.notify` library. See [Releasing to PyPI](#releasing-to-pypi) below for how new versions are published.

A standalone PyInstaller binary (no Python interpreter required) is also available from [GitHub Releases](https://github.com/sig9org/chatxpy/releases); see `task build` below to build one yourself.

## Development

Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).

```
task sync    # install dependencies
task build   # build a standalone binary with PyInstaller for the current platform
task test    # run the test suite
task run     # run from source (pass flags with: task run -- -subject hi -body world)
task debug   # run from source with -debug
```

PyInstaller does not cross-compile: `task build` only produces a binary for the platform it runs on. Build on each target OS (e.g. via a CI matrix) to release for multiple platforms.

## Releasing to PyPI

1. Bump `version` in `pyproject.toml` (uv's build backend does not derive it from git tags) and commit.
2. Tag the release: `git tag vX.Y.Z && git push origin vX.Y.Z`.
3. `task dist` — builds the sdist and wheel into `dist/`.
4. `task dist:check` — validates the package metadata/long description with `twine check`.
5. `task publish:test` — uploads to [TestPyPI](https://test.pypi.org/) first, to dry-run the release.
6. `task publish` — uploads to PyPI. **This is irreversible**: a given version number can never be re-uploaded, even if deleted.

`task publish`/`task publish:test` read credentials from `UV_PUBLISH_TOKEN` (a PyPI/TestPyPI API token), or use PyPI's [trusted publishing](https://docs.pypi.org/trusted-publishers/) from CI with no token at all.

## License

[MIT](./LICENSE)
