Metadata-Version: 2.5
Name: yapper-bot
Version: 0.1.0
Summary: Python bot framework and typed client for the Yapper Bot API
Project-URL: Homepage, https://www.yapper.website/developers
Project-URL: Documentation, https://www.yapper.website/developers/api
Project-URL: Source, https://github.com/yapperpy/yapperpy
License-Expression: MIT
License-File: LICENSE
Keywords: api,bot,chat,discord,sdk,yapper
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# yapperpy

A Python package for the [Yapper](https://www.yapper.website) Bot API: a full bot framework with a
typed REST client, sync and async, and **no runtime dependencies**.

```bash
pip install yapper-bot
```

The distribution is `yapper-bot`; the import name is `yapperpy`.

Requires Python 3.10 or newer.

## A bot in ten lines

```python
import yapperpy

bot = yapperpy.Bot()  # token from YAPPER_BOT_TOKEN


@bot.command("ping", "Is the bot alive?")
def ping(msg):
    msg.reply("pong")


bot.run()
```

The same bot with asyncio:

```python
import asyncio
import yapperpy

bot = yapperpy.AsyncBot()


@bot.command("ping", "Is the bot alive?")
async def ping(msg):
    await msg.reply("pong")


asyncio.run(bot.run())
```

A callback may be a plain function or a coroutine function: the async runtime awaits whatever it is
gets, so one cog works in both.

## What a bot can do

| Call | What it does |
| --- | --- |
| `@bot.command(name, description)` | Registers a command. People type `/name` or `!name`. |
| `@bot.on(event)` | Listens for an event: `message`, `command`, `member_join`, `member_leave`, `reaction_add`, `reaction_remove`, `voice_join`, `voice_leave`, `interaction`, `test`, `ready`, `error`. |
| `@bot.button("vote:")` | Handles clicks on buttons whose `custom_id` starts with `vote:`. A regular expression works too. |
| `msg.reply(text)` | Answers the message, quoting it. `msg.say(text)` posts in the same channel without a quote. |
| `msg.args`, `msg.argv` | The rest of the command line, and the same split on spaces. |
| `i.reply()`, `i.ephemeral()`, `i.update()`, `i.ack()` | Answers a button click: a new message, one only the clicker sees, an edit of the clicked message, or nothing visible. |
| `bot.add_cog(Cog)` | Groups commands, listeners and buttons in one class. |
| `bot.load_extensions("cogs")` | Loads every `.py` file in a folder. `bot.unload_extension` and `bot.reload_extension` work while the bot runs. |
| `bot.send(channel_id, text)` | Posts a message. |

## Cogs

A cog is a class with commands, listeners and buttons in it:

```python
# cogs/fun.py
import yapperpy


class Fun(yapperpy.Cog):
    @yapperpy.command("joke", "Tell a joke")
    async def joke(self, msg):
        await msg.reply("I know a UDP joke, but you might not get it")

    @yapperpy.listener("member_join")
    async def hello(self, ev):
        print(ev.username, "joined")

    def check(self, msg):  # optional: return False to skip this cog
        return True
```

```python
# bot.py
import yapperpy

bot = yapperpy.AsyncBot()
bot.load_extensions("cogs")
asyncio.run(bot.run())
```

## The API client

The framework is built on a plain API client you can use on its own.

```python
import yapperpy

with yapperpy.Yapper("yb_...") as client:
    print(client.bot.get().username)
    for server in client.servers.list():
        print(server.name, server.member_count)
    for channel in client.channels.list(type="text"):
        print(channel.name)
    message = client.messages.send(channel.id, "hello", embeds=[yapperpy.Embed().title("Hi")])
    client.messages.react(channel.id, message.id, "👍")
```

Every call returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`, `Message`,
`Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`, `VoiceChannel`, `Image`,
`Embed`, `ActionRow`, `Button`, `Ok`.

Namespaces: `client.bot`, `client.events`, `client.servers`, `client.channels`, `client.messages`,
`client.interactions`, `client.voice`, `client.users`.

Anything the API does that this package does not cover yet is one call away:

```python
client.api("GET", "/servers")  # returns plain JSON
await async_client.api("GET", "/servers")
```

## Events

The bot long-polls `GET /events` and hands each event to the right listeners. The payload of a
message event is a `MessageContext`, which has every field of the message plus `reply`, `say` and
`author_mention`. Other events give you a typed payload: `MemberJoinPayload`, `MemberLeavePayload`,
`ReactionPayload`, `InteractionPayload`, `VoicePayload`, `TestPayload`.

The last event id is kept in `bot.last_event_id`, so a bot that restarts can pick up where it left
off. Events are kept by Yapper for one day.

## Buttons and embeds

```python
import yapperpy


@bot.command("vote", "Pizza or not")
def vote(msg):
    msg.reply(
        "Pizza tonight?",
        components=[
            yapperpy.row(
                yapperpy.btn("Yes", "vote:yes", "success", emoji="🍕"),
                yapperpy.btn("No", "vote:no", "danger"),
                yapperpy.link_button("Menu", "https://example.com/menu"),
            )
        ],
    )


@bot.button("vote:")
def on_vote(interaction):
    interaction.ephemeral("You voted " + interaction.custom_id[5:])
```

A Components V2 layout, where the components are the whole message:

```python
import yapperpy

bot.send_layout(
    channel_id,
    [
        yapperpy.container(
            "## Game night",
            yapperpy.section("**Minecraft**", accessory=yapperpy.btn("Vote", "poll:mc", "primary")),
            yapperpy.separator(),
            yapperpy.gallery("https://example.com/a.png"),
            accent="blue",
        )
    ],
)
```

## Errors

```
YapperError
  YapperValidationError      400
  YapperAuthError            401
  YapperPermissionError      403
  YapperNotFoundError        404
  YapperConflictError        409
  YapperRateLimitError       429   .retry_after
  YapperServiceError         503   .retry_after
  YapperServerError          other 5xx
  YapperConnectionError      the network failed
  YapperTimeoutError         the request timed out
```

A 429 or a 503 with a `Retry-After` header is waited out and retried automatically. A 429 without
one is raised right away, because waiting does not help. A failing command goes to the `error`
listeners, or is printed with a traceback when there are none.

## Configuration

| Setting | How |
| --- | --- |
| Token | `YAPPER_BOT_TOKEN`, or `Yapper("yb_...")` |
| Base URL | `Yapper(base_url=...)`, for a test server |
| Prefixes | `Bot(prefixes=("/", "!"))` |
| Bot messages | `Bot(ignore_bots=False)` to answer other bots too |
| Retries | `Yapper(retry=RetryPolicy(attempts=5))` |

## Development

```bash
python -m venv .venv
.venv\Scripts\python.exe -m pip install --editable ".[dev]"
.venv\Scripts\python.exe -m pytest
.venv\Scripts\python.exe -m mypy
.venv\Scripts\python.exe -m ruff check .
```

`start.bat` sets up the environment and runs `examples/echo_bot.py`.

## Publishing

`publish.bat` builds the wheel and the source archive, checks the metadata and uploads them.

```bat
publish.bat                  :: upload to PyPI
publish.bat test             :: upload to TestPyPI instead
publish.bat --dry-run        :: build and check everything, do not upload
```

The token comes from a file called `pypi.tkn` in this folder. One line, the whole token:

```
pypi-AgEIcHlwaS5vcmMFAKEtoken1234567890abcdef
```

`pypi.tkn` is in `.gitignore` and is never uploaded. If you would rather not keep a file around,
set the `PYPI_TOKEN` environment variable instead.

The token needs the `pypi` scope for the project `yapper-bot`. The script refuses to upload a
version that is already on the index, so bump `__version__` in `src/yapperpy/_version.py` first.

## Links

- API reference: https://www.yapper.website/developers/api
- OpenAPI file: https://www.yapper.website/openapi.json
- Developer guide: https://www.yapper.website/developers

## License

MIT
