56 Commits

Author SHA1 Message Date
renovate[bot]
51b221770b Update mcr.microsoft.com/devcontainers/python Docker tag to v3 2025-11-27 16:13:09 +00:00
renovate[bot]
8a70e2c2b4 Lock file maintenance (#89)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4xNi4xIiwidXBkYXRlZEluVmVyIjoiNDIuMTYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:39:33 +00:00
renovate[bot]
9be6c49447 Lock file maintenance (#87)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 06:53:29 +00:00
eb3d81e857 Update mcr.microsoft.com/devcontainers/python Docker tag to v2 (#73)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| mcr.microsoft.com/devcontainers/python | image | major |
`1-3.13-bookworm` -> `2-3.13-bookworm` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuMTMxLjkiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->
2025-11-15 17:30:19 +00:00
d9823bac66 Update actions/setup-python action to v6 (#74)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/setup-python](https://redirect.github.com/actions/setup-python)
| action | major | `v5` -> `v6` |

---

### Release Notes

<details>
<summary>actions/setup-python (actions/setup-python)</summary>

###
[`v6`](https://redirect.github.com/actions/setup-python/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/setup-python/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuMTMxLjkiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->
2025-11-15 17:30:08 +00:00
bd0b1186de Update python Docker tag to v3.14 (#80)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| python |  | minor | `3.13` -> `3.14` |
| python | final | minor | `3.13-slim-bookworm` -> `3.14-slim-bookworm`
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xMzEuOSIsInVwZGF0ZWRJblZlciI6IjQxLjEzMS45IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-15 17:29:56 +00:00
cf33eed589 Update astral-sh/setup-uv action to v7 (#81)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | major | `v6` -> `v7` |

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v7`](https://redirect.github.com/astral-sh/setup-uv/compare/v6...v7)

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6...v7)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xMzEuOSIsInVwZGF0ZWRJblZlciI6IjQxLjEzMS45IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-15 17:29:41 +00:00
renovate[bot]
484b802115 Update astral-sh/setup-uv action to v7 2025-11-10 18:39:07 +00:00
renovate[bot]
5e0f8a61f1 Update actions/setup-python action to v6 2025-11-10 18:39:05 +00:00
renovate[bot]
e14ad0041f Lock file maintenance (#86)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 05:03:00 +00:00
renovate[bot]
e2ead09c2a Lock file maintenance (#85)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 06:48:25 +00:00
renovate[bot]
06a07a727c Lock file maintenance (#84)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 05:45:02 +00:00
renovate[bot]
7129632b4e Update python Docker tag to v3.14 2025-10-20 15:33:13 +00:00
renovate[bot]
44b5010432 Lock file maintenance (#83)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNDMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE0My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 06:06:42 +00:00
renovate[bot]
933d2299fa Lock file maintenance (#82)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNDMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE0My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 05:56:23 +00:00
renovate[bot]
6b9349e3c1 Lock file maintenance (#79)
> [!NOTE]
> Mend has cancelled [the proposed
renaming](https://redirect.github.com/renovatebot/renovate/discussions/37842)
of the Renovate GitHub app being renamed to `mend[bot]`.
> 
> This notice will be removed on 2025-10-07.

<hr>

This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xMzEuOSIsInVwZGF0ZWRJblZlciI6IjQxLjEzMS45IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 05:12:29 +00:00
renovate[bot]
3c3533ce71 Lock file maintenance (#78)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xMzEuOSIsInVwZGF0ZWRJblZlciI6IjQxLjEzMS45IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 06:32:34 +00:00
renovate[bot]
afe2d8d966 Lock file maintenance (#77)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 05:30:17 +00:00
renovate[bot]
c39e06bd83 Lock file maintenance (#76)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:10:23 +00:00
renovate[bot]
0c44f14cc8 Lock file maintenance (#75)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 05:26:15 +00:00
renovate[bot]
db3d9f9422 Update mcr.microsoft.com/devcontainers/python Docker tag to v2 2025-09-03 22:13:18 +00:00
renovate[bot]
e6201f09cc Lock file maintenance (#72)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuODIuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 05:11:20 +00:00
renovate[bot]
a85ee22d01 Lock file maintenance (#71)
This PR contains the following updates:

| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |

🔧 This Pull Request updates lock files to use the latest dependency
versions.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nikdoof/smsbot).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS43MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNzEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 04:36:54 +00:00
dc445bcc66 Ignore invalid environment variables 2025-08-17 15:36:07 +01:00
d00a4048cd Version 0.2.2 2025-08-17 15:28:55 +01:00
bb5472f092 Add a basic config file into the container 2025-08-17 15:28:34 +01:00
6ce86042fb Build ARM64 container 2025-08-17 15:26:32 +01:00
0a2970c38f Add workdir to Dockerfile 2025-08-17 15:25:42 +01:00
126713c84a Fix environment variable config overriding 2025-08-17 15:24:23 +01:00
bace0200ab Add devcontainer configuration 2025-08-17 12:51:32 +01:00
e98b8e6b8c Version 0.2.1 2025-08-17 12:38:05 +01:00
876b0363c0 Merge pull request #69 from nikdoof/renovate/actions-checkout-5.x
Update actions/checkout action to v5
2025-08-17 12:37:05 +01:00
d0c18a1d00 Add default task 2025-08-17 12:35:25 +01:00
b056d6328d Add missing var in the example configuration 2025-08-17 12:33:39 +01:00
40a263686d Add example deployment for Flux 2025-08-17 12:31:51 +01:00
5f1e5508f0 Correct default port for Docker 2025-08-17 12:28:22 +01:00
renovate[bot]
6f36caf6e1 Update actions/checkout action to v5 2025-08-17 11:25:44 +00:00
40e23c32a8 Fix Docker build 2025-08-17 12:21:29 +01:00
51f4a62738 Cleanup lint issues 2025-08-17 12:18:56 +01:00
b0afb9b15d Version 0.2.0 2025-08-17 12:17:07 +01:00
1e28526be7 Use '.python-version' for builds 2025-08-17 12:16:55 +01:00
594f4ba8ef Support sending SMS 2025-08-17 11:59:11 +01:00
b4e833f440 Update workflows to test on the current Python version 2025-08-17 08:21:58 +01:00
4de940be7c Add logfile support 2025-08-17 08:21:47 +01:00
837b959b9b Update README 2025-08-17 07:30:45 +01:00
8ba995bc5f Cleanup types 2025-08-17 07:30:34 +01:00
70282e3596 Add some comments 2025-08-17 00:02:17 +01:00
facb9c4991 Cleanup imports 2025-08-16 23:37:57 +01:00
fb5e1bffee Fix escaping error 2025-08-16 23:37:16 +01:00
34ba83ffb8 Map help to start command on Telegram 2025-08-16 23:32:00 +01:00
23984da65a Pass auth token to the Twilio handler 2025-08-16 23:31:39 +01:00
4206bb63f4 Add config file support 2025-08-16 23:30:59 +01:00
f52d8ca81e Bump to Python 3.13 2025-08-16 23:28:10 +01:00
1872a97088 Version 0.1.1 2025-08-16 17:39:22 +01:00
51d37a3e61 Handle call webhook types correctly 2025-08-16 17:38:54 +01:00
8ff16ba9d3 Fix release workflow 2025-08-16 17:08:22 +01:00
23 changed files with 1011 additions and 525 deletions

View File

@@ -0,0 +1,20 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Development",
"image": "mcr.microsoft.com/devcontainers/python:3-3.13-bookworm",
"features": {
"ghcr.io/eitsupi/devcontainer-features/go-task:1": {},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}
},
"forwardPorts": [
5000
],
"portsAttributes": {
"5000": {
"label": "Application",
"protocol": "http",
"public": true
}
}
}

View File

@@ -22,11 +22,18 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v5
- name: Set build args
run: |
echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
push: true push: true
platforms: linux/amd64,linux/arm64
build-args: |
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:latest

View File

@@ -11,19 +11,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.9"] python-version: ["3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Install Task - name: Install Task
uses: arduino/setup-task@v2 uses: arduino/setup-task@v2
with: with:
version: 3.x version: 3.x
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v7
- name: Run Lint - name: Run Lint
run: | run: |
task python:lint task python:lint

View File

@@ -2,8 +2,6 @@ name: Release
"on": "on":
push: push:
branches:
- main
tags: tags:
- "[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+"
@@ -12,15 +10,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with:
python-version: "3.9"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v7
- name: Build Release - name: Build Release
run: uv build run: uv build

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.9"] python-version: ["3.13"]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Install Task - name: Install Task
@@ -19,11 +19,11 @@ jobs:
with: with:
version: 3.x version: 3.x
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v7
- name: Run Tests - name: Run Tests
run: | run: |
task python:tests task python:tests

3
.gitignore vendored
View File

@@ -212,3 +212,6 @@ __marimo__/
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
# smsbot config file
config.ini

View File

@@ -1 +1 @@
3.9 3.14

View File

@@ -1,4 +1,6 @@
FROM ghcr.io/astral-sh/uv:python3.9-bookworm-slim AS builder ARG PYTHON_VERSION="3.13"
FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
ENV UV_PYTHON_DOWNLOADS=0 ENV UV_PYTHON_DOWNLOADS=0
WORKDIR /app WORKDIR /app
@@ -11,8 +13,10 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev uv sync --locked --no-dev
FROM python:3.9-slim-bookworm FROM python:${PYTHON_VERSION}-slim-bookworm
COPY --from=builder --chown=app:app /app /app COPY --from=builder --chown=app:app /app /app
COPY ./docs/examples/config-basic.ini /app/config.ini
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 80/tcp EXPOSE 5000/tcp
WORKDIR /app
CMD ["smsbot"] CMD ["smsbot"]

View File

@@ -12,27 +12,55 @@ The bot is designed to run within a Kubernetes environment, but can be operated
## Configuration ## Configuration
All configuration is provided via environment variables SMSBot can be configured using either a configuration file or environment variables. Environment variables will override any values set in the configuration file.
| Variable | Required? | Description | ### Configuration File
| -------------------------- | --------- | --------------------------------------------------------------------------- |
| SMSBOT_DEFAULT_SUBSCRIBERS | No | A list of IDs, seperated by commas, to add to the subscribers list on start | Create a configuration file (e.g., `config.ini`) based on the provided `config-example.ini`:
| SMSBOT_LISTEN_HOST | No | The host for the webhooks to listen on, defaults to `0.0.0.0` |
| SMSBOT_LISTEN_PORT | No | The port to listen to, defaults to `80` | ```ini
| SMSBOT_OWNER_ID | No | ID of the owner of this bot | [logging]
| SMSBOT_TELEGRAM_BOT_TOKEN | Yes | Your Bot Token for Telegram | level = INFO
| SMSBOT_TWILIO_AUTH_TOKEN | No | Twilio auth token, used to validate any incoming webhook calls |
[webhook]
host = 127.0.0.1
port = 80
[telegram]
owner_id = OWNER_USER_ID
bot_token = BOT_TOKEN
[twilio]
account_sid = TWILIO_ACCOUNT_SID
auth_token = TWILIO_AUTH_TOKEN
```
### Environment Variables
All configuration options can be overridden using environment variables:
| Environment Variable | Config Section | Config Key | Required? | Description |
| --------------------------- | -------------- | ----------- | --------- | --------------------------------------------------------------------------- |
| SMSBOT_LOGGING_LEVEL | logging | level | No | The log level to output to the console, defaults to `INFO` |
| SMSBOT_TELEGRAM_BOT_TOKEN | telegram | bot_token | Yes | Your Bot Token for Telegram |
| SMSBOT_TELEGRAM_OWNER_ID | telegram | owner_id | No | ID of the owner of this bot |
| SMSBOT_TELEGRAM_SUBSCRIBERS | telegram | subscribers | No | A list of IDs, separated by commas, to add to the subscribers list on start |
| SMSBOT_TWILIO_ACCOUNT_SID | twilio | account_sid | No | Twilio account SID |
| SMSBOT_TWILIO_AUTH_TOKEN | twilio | auth_token | No | Twilio auth token, used to validate any incoming webhook calls |
| SMSBOT_WEBHOOK_HOST | webhook | host | No | The host for the webhooks to listen on, defaults to `127.0.0.1` |
| SMSBOT_WEBHOOK_PORT | webhook | port | No | The port to listen to, defaults to `80` |
## Setup ## Setup
To configure SMSBot, you'll need a Twilio account, either paid or trial is fine. To configure SMSBot, you'll need a Twilio account, either paid or trial is fine.
* Setup a number in the location you want. 1. Copy `config-example.ini` to `config.ini` and update the values, or set the appropriate environment variables.
* Under Phone Numbers -> Manage -> Active Numbers, click the number you want to setup. 2. Setup a number in the location you want.
* In the "Voice & Fax" section, update the "A Call Comes In" to the URL of your SMSBot instance, with the endpoint being `/call`, e.g. `http://mymachine.test.com/call` 3. Under Phone Numbers -> Manage -> Active Numbers, click the number you want to setup.
* In the "Messaging" section, update the "A Message Comes In" to the URL of your SMSBot instance, with the endpoint being `/message`, e.g. `http://mymachine.test.com/message` 4. In the "Voice & Fax" section, update the "A Call Comes In" to the URL of your SMSBot instance, with the endpoint being `/call`, e.g. `http://mymachine.test.com/call`
5. In the "Messaging" section, update the "A Message Comes In" to the URL of your SMSBot instance, with the endpoint being `/message`, e.g. `http://mymachine.test.com/message`
Your bot should now receive messages, on Telegram you need to start a chat or invite it into any channels you want, then update the `SMSBOT_DEFAULT_SUBSCRIBERS` values with their IDs. Your bot should now receive messages, on Telegram you need to start a chat or invite it into any channels you want, then update the `SMSBOT_TELEGRAM_SUBSCRIBERS` values with their IDs.
**Note**: You cannot send test messages from your Twilio account to your Twilio numbers, they'll be silently dropped or fail with an "Invalid Number" error. **Note**: You cannot send test messages from your Twilio account to your Twilio numbers, they'll be silently dropped or fail with an "Invalid Number" error.

View File

@@ -1,5 +1,10 @@
version: 3 version: 3
tasks: tasks:
default:
deps:
- python:tests
- python:lint
python:tests: python:tests:
desc: Run Python tests desc: Run Python tests
cmds: cmds:
@@ -14,7 +19,7 @@ tasks:
docker:build: docker:build:
desc: Build the container using Docker desc: Build the container using Docker
cmds: cmds:
- docker build . -t smsbot:latest - docker build . --build-arg PYTHON_VERSION=$(cat .python-version) -t smsbot:latest
smsbot:run: smsbot:run:
desc: Run the SMSBot desc: Run the SMSBot

5
docs/examples/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Example deployments
Examples of how to deploy SMSBot.
* [Flux HelmRelease](flux-helmrelease.yaml) - An example Flux `HelmRelease` using a common chart for basic deployment.

View File

@@ -0,0 +1,2 @@
[logging]
level = INFO

View File

@@ -0,0 +1,16 @@
[logging]
level = INFO
[webhook]
host = 127.0.0.1
port = 80
[telegram]
owner_id = OWNER_USER_ID
subscribers = 1111,2222,3333
bot_token = BOT_TOKEN
[twilio]
account_sid = TWILIO_ACCOUNT_SID
auth_token = TWILIO_AUTH_TOKEN
from_number = +12345678901

View File

@@ -0,0 +1,55 @@
---
# yaml-language-server: $schema=https://nikdoof.github.io/flux-gitops/schemas/source.toolkit.fluxcd.io/helmrepository_v1.json
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: nikdoof
namespace: flux-system
spec:
interval: 4h
url: https://nikdoof.github.io/helm-charts/
---
# yaml-language-server: $schema=https://nikdoof.github.io/flux-gitops/schemas/helm.toolkit.fluxcd.io/helmrelease_v2.json
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: smsbot
spec:
interval: 12h
chart:
spec:
chart: common-chart
version: 1.2.3
sourceRef:
kind: HelmRepository
name: nikdoof
namespace: flux-system
interval: 12h
values:
global:
nameOverride: smsbot
image:
repository: ghcr.io/nikdoof/smsbot
tag: 0.2.0
imagePullPolicy: IfNotPresent
controller:
strategy: Recreate
annotations:
secret.reloader.stakater.com/reload: "smsbot-secrets"
envFrom:
- secretRef:
name: smsbot-secrets
service:
main:
ports:
http:
port: 5000
ingress:
main:
enabled: true
hosts:
- host: smsbot-webhooks.example.com
paths:
- path: /
pathType: Prefix

View File

@@ -1,11 +1,11 @@
[project] [project]
name = "smsbot" name = "smsbot"
version = "0.1.0" version = "0.2.2"
description = "A simple Telegram bot to receive SMS messages." description = "A simple Telegram bot to receive SMS messages."
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }] authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
license = { text = "MIT" } license = { text = "MIT" }
readme = "README.md" readme = "README.md"
requires-python = ">=3.9,<3.10" requires-python = ">=3.13"
dependencies = [ dependencies = [
"flask[async]>=3.1.1", "flask[async]>=3.1.1",
"prometheus-async>=25.1.0", "prometheus-async>=25.1.0",
@@ -30,3 +30,6 @@ dev = [
"ruff>=0.12.9", "ruff>=0.12.9",
] ]
github = ["pytest-github-actions-annotate-failures>=0.3.0"] github = ["pytest-github-actions-annotate-failures>=0.3.0"]
[tool.ruff]
line-length = 120

View File

@@ -2,66 +2,122 @@ import argparse
import asyncio import asyncio
import logging import logging
import os import os
import sys
from configparser import ConfigParser
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from twilio.rest import Client
import uvicorn import uvicorn
from asgiref.wsgi import WsgiToAsgi from asgiref.wsgi import WsgiToAsgi
from smsbot.telegram import TelegramSmsBot from smsbot.telegram import TelegramSmsBot
from smsbot.utils import get_smsbot_version from smsbot.utils import get_smsbot_version
from smsbot.webhook_handler import TwilioWebhookHandler from smsbot.webhook import TwilioWebhookHandler
# Prefix of the environment variables to override config values
ENVIRONMENT_PREFIX = "SMSBOT_"
def main(): def main():
parser = argparse.ArgumentParser("smsbot") parser = argparse.ArgumentParser("smsbot")
parser.add_argument( parser.add_argument(
"--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0" "-c",
"--config",
default=os.environ.get("SMSBOT_CONFIG_FILE", "config.ini"),
type=argparse.FileType("r"),
help="Path to the config file",
) )
parser.add_argument( parser.add_argument("--debug", action="store_true", help="Enable debug mode")
"--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or 80, type=int parser.add_argument("--log-file", type=argparse.FileType("a"), help="Path to the log file", default=sys.stdout)
)
parser.add_argument(
"--telegram-bot-token", default=os.environ.get("SMSBOT_TELEGRAM_BOT_TOKEN")
)
parser.add_argument("--owner-id", default=os.environ.get("SMSBOT_OWNER_ID"))
parser.add_argument(
"--default-subscribers", default=os.environ.get("SMSBOT_DEFAULT_SUBSCRIBERS")
)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args() args = parser.parse_args()
if args.debug: if args.debug:
level = logging.DEBUG level = logging.DEBUG
else: else:
level = logging.INFO level = logging.INFO
logging.basicConfig(level=level) logging.basicConfig(level=level, stream=args.log_file)
logging.info("smsbot v%s", get_smsbot_version()) logging.info("smsbot v%s", get_smsbot_version())
logging.debug("Arguments: %s", args) logging.debug("Arguments: %s", args)
# Load configuration ini file if provided
config = ConfigParser()
if args.config:
logging.info("Loading configuration from %s", args.config.name)
config.read_file(args.config)
# Override with environment variables, named SMSBOT_<SECTION>_<VALUE>
for key, value in os.environ.items():
if key.startswith(ENVIRONMENT_PREFIX):
try:
section, option = key[7:].lower().split("_", 1)
except ValueError:
logging.debug("Invalid environment variable format: %s", key)
continue
logging.debug("Overriding config %s/%s = %s", section, option, value)
if not config.has_section(section):
config.add_section(section)
config[section][option] = value
# Validate configuration
if not config.has_section("telegram") or not config.get("telegram", "bot_token"):
logging.error(
"Telegram bot token is required, define a token either in the config file or as an environment variable."
)
return
if config.has_section("twilio") and not (config.get("twilio", "account_sid") and config.get("twilio", "auth_token") and config.get("twilio", "from_number")):
logging.error(
"Twilio account SID, auth token, and from number are required for outbound SMS functionality, define them in the config file or as environment variables."
)
return
# Now the config is loaded, set the logger level
level = getattr(logging, config.get("logging", "level", fallback="INFO").upper(), logging.INFO)
logging.getLogger().setLevel(level)
# Configure Twilio client if we have credentials
if config.has_section("twilio") and config.get("twilio", "account_sid") and config.get("twilio", "auth_token"):
twilio_client = Client(
config.get("twilio", "account_sid"),
config.get("twilio", "auth_token"),
)
else:
twilio_client = None
logging.warning("No Twilio credentials found, outbound SMS functionality will be disabled.")
# Start bot # Start bot
telegram_bot = TelegramSmsBot(token=args.telegram_bot_token) telegram_bot = TelegramSmsBot(
token=config.get("telegram", "bot_token"),
twilio_client=twilio_client,
twilio_from_number=config.get("twilio", "from_number", fallback=None),
)
# Set the owner ID if configured # Set the owner ID if configured
if args.owner_id: if config.has_option("telegram", "owner_id"):
telegram_bot.owners = [int(args.owner_id)] telegram_bot.owners = [config.getint("telegram", "owner_id")]
else: else:
logging.warning("No Owner ID is set, which is not a good idea...") logging.warning("No Owner ID is set, which is not a good idea...")
# Add default subscribers # Add default subscribers
if args.default_subscribers: if config.has_option("telegram", "subscribers"):
for chat_id in args.default_subscribers.split(","): for chat_id in config.get("telegram", "subscribers").split(","):
telegram_bot.subscribers.append(int(chat_id.strip())) telegram_bot.subscribers.append(int(chat_id.strip()))
webhooks = TwilioWebhookHandler() # Init the webhook handler
webhooks.set_bot(telegram_bot) webhooks = TwilioWebhookHandler(
account_sid=config.get("twilio", "account_sid", fallback=None),
auth_token=config.get("twilio", "auth_token", fallback=None),
)
webhooks.set_telegram_application(telegram_bot)
# Build a uvicorn ASGI server # Build a uvicorn ASGI server
flask_app = uvicorn.Server( flask_app = uvicorn.Server(
config=uvicorn.Config( config=uvicorn.Config(
app=WsgiToAsgi(webhooks.app), app=WsgiToAsgi(webhooks.app),
port=args.listen_port, port=config.getint("webhook", "port", fallback=5000),
use_colors=False, use_colors=False,
host=args.listen_host, host=config.get("webhook", "host", fallback="127.0.0.1"),
) )
) )

View File

@@ -1,54 +1,61 @@
import logging import logging
from smsbot.utils import get_smsbot_version
from prometheus_client import Counter, Summary from prometheus_client import Counter, Summary
from telegram import Update from telegram import Update
from telegram.ext import ( from telegram.ext import (
Application, Application,
ApplicationHandlerStop, ApplicationHandlerStop,
CommandHandler,
ContextTypes, ContextTypes,
TypeHandler, TypeHandler,
CommandHandler,
) )
from twilio.rest import Client
REQUEST_TIME = Summary( from smsbot.utils import get_smsbot_version
"telegram_request_processing_seconds", "Time spent processing request"
) REQUEST_TIME = Summary("telegram_request_processing_seconds", "Time spent processing request")
COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands processed") COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands processed")
class TelegramSmsBot: class TelegramSmsBot:
def __init__(self, token: str, owners: list[int] = [], subscribers: list[int] = []): def __init__(
self,
token: str,
twilio_client: Client | None = None,
twilio_from_number: str | None = None,
owners: list[int] = [],
subscribers: list[int] = [],
):
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.app = Application.builder().token(token).build() self.app = Application.builder().token(token).build()
self.owners = owners self.owners = owners
self.subscribers = subscribers self.subscribers = subscribers
self.twilio_client = twilio_client
self.twilio_from_number = twilio_from_number
self.init_handlers() self.init_handlers()
def init_handlers(self): def init_handlers(self):
self.app.add_handler(TypeHandler(Update, self.callback), -1) self.app.add_handler(TypeHandler(Update, self.callback), -1)
self.app.add_handler(CommandHandler("help", self.handler_help)) self.app.add_handler(CommandHandler(["help", "start"], self.handler_help))
self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe)) self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe))
self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe)) self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe))
self.app.add_handler(CommandHandler("sms", self.handler_sms))
async def callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle the update""" """Handle the update"""
if update.effective_user.id in self.owners: if update.effective_user and update.message:
self.logger.info( if update.effective_user.id in self.owners:
f"{update.effective_user.username} sent {update.message.text}" self.logger.info(f"{update.effective_user.username} sent {update.message.text}")
) COMMAND_COUNT.inc()
COMMAND_COUNT.inc() else:
else: self.logger.debug(f"Ignoring message from user {update.effective_user.username}")
self.logger.debug(f"Ignoring message from user {update.effective_user.id}") raise ApplicationHandlerStop
raise ApplicationHandlerStop
async def send_message(self, chat_id: int, text: str): async def send_message(self, chat_id: int, text: str):
"""Send a message to a specific chat""" """Send a message to a specific chat"""
self.logger.info(f"Sending message to chat {chat_id}: {text}") self.logger.info(f"Sending message to chat {chat_id}: {text}")
await self.app.bot.send_message( await self.app.bot.send_message(chat_id=chat_id, text=text, parse_mode="MarkdownV2")
chat_id=chat_id, text=text, parse_mode="MarkdownV2"
)
async def send_subscribers(self, text: str): async def send_subscribers(self, text: str):
"""Send a message to all subscribers""" """Send a message to all subscribers"""
@@ -63,48 +70,60 @@ class TelegramSmsBot:
await self.send_message(owner, text) await self.send_message(owner, text)
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_help(self, update, context): async def handler_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a message when the command /help is issued.""" """Send a message when the command /help is issued."""
self.logger.info("/help command received in chat: %s", update.message.chat) if update.message:
self.logger.info("/help command received in chat: %s", update.message.chat)
commands = [] commands = []
for command in self.app.handlers[0]: for command in self.app.handlers[0]:
if isinstance(command, CommandHandler): if isinstance(command, CommandHandler):
commands.extend(["/{0}".format(cmd) for cmd in command.commands]) commands.extend(["/{0}".format(cmd) for cmd in command.commands])
await update.message.reply_markdown( await update.message.reply_markdown("Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands)))
"Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands)) COMMAND_COUNT.inc()
)
COMMAND_COUNT.inc()
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_subscribe( async def handler_subscribe(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
self, update: Update, context: ContextTypes.DEFAULT_TYPE
):
"""Handle subscription requests""" """Handle subscription requests"""
user_id = update.effective_user.id if update.effective_user and update.message:
if user_id not in self.subscribers: user_id = update.effective_user.id
self.subscribers.append(user_id) if user_id not in self.subscribers:
self.logger.info(f"User {user_id} subscribed.") self.subscribers.append(user_id)
self.logger.info(f"Current subscribers: {self.subscribers}") self.logger.info(f"User {user_id} subscribed.")
await update.message.reply_markdown( self.logger.info(f"Current subscribers: {self.subscribers}")
"You have successfully subscribed to updates." await update.message.reply_markdown("You have successfully subscribed to updates.")
) else:
else: self.logger.info(f"User {user_id} is already subscribed.")
self.logger.info(f"User {user_id} is already subscribed.")
@REQUEST_TIME.time() @REQUEST_TIME.time()
async def handler_unsubscribe( async def handler_unsubscribe(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
self, update: Update, context: ContextTypes.DEFAULT_TYPE
):
"""Handle unsubscription requests""" """Handle unsubscription requests"""
user_id = update.effective_user.id if update.effective_user and update.message:
if user_id in self.subscribers: user_id = update.effective_user.id
self.subscribers.remove(user_id) if user_id in self.subscribers:
self.logger.info(f"User {user_id} unsubscribed.") self.subscribers.remove(user_id)
self.logger.info(f"Current subscribers: {self.subscribers}") self.logger.info(f"User {user_id} unsubscribed.")
await update.message.reply_markdown( self.logger.info(f"Current subscribers: {self.subscribers}")
"You have successfully unsubscribed from updates." await update.message.reply_markdown("You have successfully unsubscribed from updates.")
) else:
else: self.logger.info(f"User {user_id} is not subscribed.")
self.logger.info(f"User {user_id} is not subscribed.")
@REQUEST_TIME.time()
async def handler_sms(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle sending SMS requests"""
if update.effective_user and update.message:
user_id = update.effective_user.id
if self.twilio_client and self.twilio_from_number:
to = context.args[0] if context.args else "No recipient provided"
message = context.args[1] if context.args else "No message provided"
self.logger.info(f"Sending SMS from user {user_id} -> {to}: {message}")
try:
self.twilio_client.messages.create(body=message, to=to, from_=self.twilio_from_number)
except Exception:
self.logger.exception("Failed to send SMS due to exception")
await update.message.reply_markdown("Failed to send SMS")
pass
else:
await update.message.reply_markdown("Twilio client is not configured, cannot send SMS.")

5
smsbot/utils/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from importlib.metadata import version
def get_smsbot_version() -> str:
return version("smsbot")

View File

@@ -1,23 +1,11 @@
from importlib.metadata import version class TwilioWebhookPayload:
@staticmethod
def parse(data: dict[str, str]) -> "TwilioCall | TwilioMessage | None":
def get_smsbot_version(): """Return the correct class for the incoming Twilio webhook payload"""
return version("smsbot") if "SmsMessageSid" in data:
return TwilioMessage(data)
if "CallSid" in data:
class TwilioMessage: return TwilioCall(data)
"""
Parses a Twilio webhook message.
"""
def __init__(self, data: dict) -> None:
self.from_number: str = data.get("From", "Unknown")
self.to_number: str = data.get("To", "Unknown")
self.body: str = data.get("Body", "")
self.media = []
for i in range(0, int(data.get("NumMedia", "0"))):
self.media.append(data.get(f"MediaUrl{i}"))
def _escape(self, text: str) -> str: def _escape(self, text: str) -> str:
"""Escape text for MarkdownV2""" """Escape text for MarkdownV2"""
@@ -45,6 +33,19 @@ class TwilioMessage:
text = text.replace(char, rf"\{char}") text = text.replace(char, rf"\{char}")
return text return text
class TwilioMessage(TwilioWebhookPayload):
"""Represents a Twilio SMS message"""
def __init__(self, data: dict) -> None:
self.from_number: str = data.get("From", "Unknown")
self.to_number: str = data.get("To", "Unknown")
self.body: str = data.get("Body", "")
self.media = []
for i in range(0, int(data.get("NumMedia", "0"))):
self.media.append(data.get(f"MediaUrl{i}"))
def __repr__(self) -> str: def __repr__(self) -> str:
return f"TwilioWebhookMessage(from={self.from_number}, to={self.to_number})" return f"TwilioWebhookMessage(from={self.from_number}, to={self.to_number})"
@@ -57,3 +58,22 @@ class TwilioMessage:
media_str = "\n".join([f"{self._escape(url)}" for url in self.media]) if self.media else "" media_str = "\n".join([f"{self._escape(url)}" for url in self.media]) if self.media else ""
msg = f"**From**: {self._escape(self.from_number)}\n**To**: {self._escape(self.to_number)}\n\n{self._escape(self.body)}\n\n{media_str}" msg = f"**From**: {self._escape(self.from_number)}\n**To**: {self._escape(self.to_number)}\n\n{self._escape(self.body)}\n\n{media_str}"
return msg return msg
class TwilioCall(TwilioWebhookPayload):
"""Represents a Twilio voice call"""
def __init__(self, data: dict) -> None:
self.from_number: str = data.get("From", "Unknown")
self.to_number: str = data.get("To", "Unknown")
def __repr__(self) -> str:
return f"TwilioCall(from={self.from_number}, to={self.to_number})"
def to_str(self) -> str:
msg = f"Call from {self.from_number}, rejected."
return msg
def to_markdownv2(self) -> str:
msg = f"Call from {self._escape(self.from_number)}, rejected\\."
return msg

109
smsbot/webhook.py Normal file
View File

@@ -0,0 +1,109 @@
from functools import wraps
from flask import Flask, abort, current_app, request
from prometheus_async.aio import time
from prometheus_client import Counter, Summary, make_wsgi_app
from twilio.request_validator import RequestValidator
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from smsbot.utils import get_smsbot_version
from smsbot.utils.twilio import TwilioWebhookPayload
REQUEST_TIME = Summary("webhook_request_processing_seconds", "Time spent processing request")
MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed")
CALL_COUNT = Counter("webhook_call_count", "Total number of calls processed")
class TwilioWebhookHandler(object):
"""
A wrapped Flask app handling webhooks received from Twilio
"""
def __init__(self, account_sid: str | None = None, auth_token: str | None = None):
self.app = Flask(self.__class__.__name__)
self.app.add_url_rule("/", "index", self.index, methods=["GET"])
self.app.add_url_rule("/health", "health", self.health, methods=["GET"])
self.app.add_url_rule("/message", "message", self.message, methods=["POST"])
self.app.add_url_rule("/call", "call", self.call, methods=["POST"])
# Twilio auth details
self.account_sid = account_sid
self.auth_token = auth_token
# Wrap validation around hook endpoints
self.message = self.validate_twilio_request(self.message)
self.call = self.validate_twilio_request(self.call)
# Add prometheus wsgi middleware to route /metrics requests
self.app.wsgi_app = DispatcherMiddleware(
self.app.wsgi_app,
{
"/metrics": make_wsgi_app(),
},
)
def validate_twilio_request(self, func):
"""Validates that incoming requests genuinely originated from Twilio"""
@wraps(func)
async def decorated_function(*args, **kwargs):
# Create an instance of the RequestValidator class
if not self.auth_token:
current_app.logger.warning("Twilio request validation skipped due to Twilio Auth Token missing")
return await func(*args, **kwargs)
validator = RequestValidator(self.auth_token)
# Validate the request using its URL, POST data,
# and X-TWILIO-SIGNATURE header
request_valid = validator.validate(
request.url,
request.form,
request.headers.get("X-TWILIO-SIGNATURE", ""),
)
# Continue processing the request if it's valid, return a 403 error if
# it's not
if request_valid or current_app.debug:
return await func(*args, **kwargs)
return abort(403)
return decorated_function
def set_telegram_application(self, app):
"""Set the Telegram application instance to use for any webhook calls"""
self.telegram_app = app
async def index(self) -> str:
return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>'
async def health(self) -> dict[str, str | int]:
"""Return basic health information"""
return {
"version": get_smsbot_version(),
"owners": self.telegram_app.owners,
"subscribers": len(self.telegram_app.subscribers),
}
@time(REQUEST_TIME)
async def message(self) -> str:
"""Handle incoming SMS messages from Twilio"""
current_app.logger.info("Received SMS from {From}: {Body}".format(**request.values.to_dict()))
hook_data = TwilioWebhookPayload.parse(request.values.to_dict())
if hook_data:
await self.telegram_app.send_subscribers(hook_data.to_markdownv2())
# Return a blank response
MESSAGE_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>'
@time(REQUEST_TIME)
async def call(self) -> str:
"""Handle incoming calls from Twilio"""
current_app.logger.info("Received Call from {From}".format(**request.values.to_dict()))
hook_data = TwilioWebhookPayload.parse(request.values.to_dict())
if hook_data:
await self.telegram_app.send_subscribers(hook_data.to_markdownv2())
# Always reject calls
CALL_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response><Reject/></Response>'

View File

@@ -1,111 +0,0 @@
import os
from functools import wraps
from flask import Flask, abort, current_app, request
from prometheus_async.aio import time
from prometheus_client import Counter, Summary, make_wsgi_app
from twilio.request_validator import RequestValidator
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from smsbot.utils import TwilioMessage, get_smsbot_version
REQUEST_TIME = Summary(
"webhook_request_processing_seconds", "Time spent processing request"
)
MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed")
CALL_COUNT = Counter("webhook_call_count", "Total number of calls processed")
def validate_twilio_request(func):
"""Validates that incoming requests genuinely originated from Twilio"""
@wraps(func)
async def decorated_function(*args, **kwargs):
# Create an instance of the RequestValidator class
twilio_token = os.environ.get("SMSBOT_TWILIO_AUTH_TOKEN")
if not twilio_token:
current_app.logger.warning(
"Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing"
)
return await func(*args, **kwargs)
validator = RequestValidator(twilio_token)
# Validate the request using its URL, POST data,
# and X-TWILIO-SIGNATURE header
request_valid = validator.validate(
request.url,
request.form,
request.headers.get("X-TWILIO-SIGNATURE", ""),
)
# Continue processing the request if it's valid, return a 403 error if
# it's not
if request_valid or current_app.debug:
return func(*args, **kwargs)
return abort(403)
return decorated_function
class TwilioWebhookHandler(object):
def __init__(self):
self.app = Flask(self.__class__.__name__)
self.app.add_url_rule("/", "index", self.index, methods=["GET"])
self.app.add_url_rule("/health", "health", self.health, methods=["GET"])
self.app.add_url_rule("/message", "message", self.message, methods=["POST"])
self.app.add_url_rule("/call", "call", self.call, methods=["POST"])
# Add prometheus wsgi middleware to route /metrics requests
self.app.wsgi_app = DispatcherMiddleware(
self.app.wsgi_app,
{
"/metrics": make_wsgi_app(),
},
)
def set_bot(self, bot):
self.bot = bot
async def index(self):
return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>'
async def health(self):
"""Return basic health information"""
return {
"version": get_smsbot_version(),
"owners": self.bot.owners,
"subscribers": self.bot.subscribers,
}
@time(REQUEST_TIME)
@validate_twilio_request
async def message(self):
"""Handle incoming SMS messages from Twilio"""
current_app.logger.info(
"Received SMS from {From}: {Body}".format(**request.values.to_dict())
)
await self.bot.send_subscribers(
TwilioMessage(request.values.to_dict()).to_markdownv2()
)
# Return a blank response
MESSAGE_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>'
@time(REQUEST_TIME)
@validate_twilio_request
async def call(self):
"""Handle incoming calls from Twilio"""
current_app.logger.info(
"Received Call from {From}".format(**request.values.to_dict())
)
await self.bot.send_subscribers(
"Received Call from {From}, rejecting.".format(**request.values.to_dict())
)
# Always reject calls
CALL_COUNT.inc()
return '<?xml version="1.0" encoding="UTF-8"?><Response><Reject/></Response>'

View File

@@ -1,4 +1,4 @@
from smsbot.utils import TwilioMessage from smsbot.utils.twilio import TwilioMessage
def test_twiliomessage_normal(): def test_twiliomessage_normal():

806
uv.lock generated

File diff suppressed because it is too large Load Diff