1 Commits

Author SHA1 Message Date
a2fec2a4af chore(deps): lock file maintenance
All checks were successful
Enforce Conventional Commit PR Title / Validate PR Title (pull_request_target) Successful in 4s
CI / ci (pull_request) Successful in 2m27s
2025-02-10 00:10:09 +00:00
13 changed files with 462 additions and 594 deletions

View File

@@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: Run Hadolint
uses: hadolint/hadolint-action@v3.3.0
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
output-file: hadolint.out
@@ -25,9 +25,9 @@ jobs:
no-fail: true
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.14"
python-version: "3.13"
- name: uv cache
uses: actions/cache@v4
@@ -40,15 +40,11 @@ jobs:
- name: Install dependencies
run: uv sync
# - name: Lint
# run: |
# uv run pylint --fail-under=8 --recursive=yes --output-format=parseable --output=lintreport.txt app/ tests/
# cat lintreport.txt
- name: Lint
run: |
uv run pylint --fail-under=8 --recursive=yes --output-format=parseable app/ tests/
uv run pylint --fail-under=8 --recursive=yes --output-format=parseable --output=lintreport.txt app/ tests/
cat lintreport.txt
- name: Unit Test
run: |
@@ -58,38 +54,18 @@ jobs:
- name: Minimize uv cache
run: uv cache prune --ci
- name: Set up environment for Snyk
run: |
uv pip freeze > requirements.txt
mv pyproject.toml pyproject.toml.bak
mv uv.lock uv.lock.bak
- name: Snyk SAST Scan
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4.2.1
env:
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
- name: Snyk Vulnerability Scan
uses: snyk/actions/python@master
continue-on-error: true # Sometimes vulns aren't immediately fixable
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
# command: snyk
args: snyk code test #--all-projects --exclude=.archive
# - name: SonarQube Scan
# uses: SonarSource/sonarqube-scan-action@v5.2.0
# env:
# SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }}
# SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
# - name: Snyk Vulnerability Scan
# uses: snyk/actions/python@master
# continue-on-error: true # Sometimes vulns aren't immediately fixable
# env:
# SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# with:
# command: snyk
# args: test --all-projects
- name: Reverse set up environment for Snyk
run: |
rm -f requirements.txt
mv pyproject.toml.bak pyproject.toml
mv uv.lock.bak uv.lock
command: snyk
args: test --all-projects

View File

@@ -1,48 +1,104 @@
name: Release
on:
workflow_dispatch:
schedule:
- cron: '0 9 * * 0'
- cron: "0 9 * * 0"
issue_comment:
types: [created]
jobs:
# test:
# name: Test
# uses: https://git.tainton.uk/${{ gitea.repository }}/.gitea/workflows/ci.yml@main
manual_trigger:
name: Manual Trigger Cleanup
runs-on: ubuntu-latest
if: ${{ gitea.event_name == 'issue_comment' }}
steps:
- name: Log event metadata
run: |
echo "Issue: ${{ gitea.event.issue.number }}"
echo "Comment: ${{ gitea.event.comment.body }}"
echo "User: ${{ gitea.event.comment.user.login }}"
tag:
name: Tag release
uses: https://git.tainton.uk/actions/gha-workflows/.gitea/workflows/release-with-tag.yaml@main
- name: Stop workflow if required conditions are not met
if: ${{ !contains(gitea.event.issue.number, '436') || !contains(gitea.event.comment.body, '/trigger-release') || !contains(gitea.event.comment.user.login, 'luke') }}
run: exit 1
- name: Delete issue comment
run: |
curl -X DELETE \
-H "Authorization: token ${{ gitea.token }}" \
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/issues/comments/${{ gitea.event.comment.id }}"
# test:
# name: Unit Test
# uses: https://git.tainton.uk/public/webexmemebot/.gitea/workflows/ci.yml@main
# continue-on-error: true
create_release:
name: Create Release
needs: tag
uses: https://git.tainton.uk/actions/gha-workflows/.gitea/workflows/create-release-preexisting-tag.yaml@main
with:
tag: ${{ needs.tag.outputs.tag_name }}
body: ${{ needs.tag.outputs.changelog }}
secrets:
ACTIONS_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
# get_release_id:
# name: Get Release ID
# runs-on: ubuntu-latest
# needs: create_release
# outputs:
# releaseid: ${{ steps.getid.outputs.releaseid }}
# steps:
# - name: Get Release ID
# id: getid
# run: |
# rid=$(curl -s -X 'GET' \
# -H 'accept: application/json' \
# '${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/latest' | jq -r '.id')
# echo "releaseid=$rid" >> "$GITEA_OUTPUT"
# echo "$rid"
create_docker:
name: Publish Docker Images
runs-on: ubuntu-latest
needs: [tag, create_release]
# needs: test
outputs:
release_name: ${{ steps.get_next_version.outputs.tag }}
steps:
- name: Check out repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: Changes since last tag
id: changes
run: |
rm -f .changes
git log $(git describe --tags --abbrev=0)..HEAD --no-merges --oneline >> .changes
cat .changes
- name: Check for changes
run: |
if [[ -z $(grep '[^[:space:]]' .changes) ]] ; then
echo "changes=false"
echo "changes=false" >> "$GITEA_OUTPUT"
else
echo "changes=true"
echo "changes=true" >> "$GITEA_OUTPUT"
fi
- name: Cancel if no changes
if: steps.changes.outputs.changes == 'false'
run: exit 1
- name: Set server URL
id: set_srvurl
run: |
SRVURL=$(echo "${{ gitea.server_url }}" | sed 's/https:\/\/\(.*\)/\1/')
echo "srvurl=$SRVURL" >> "$GITEA_OUTPUT"
- name: Get next version
uses: TriPSs/conventional-changelog-action@v6
id: get_next_version
with:
git-url: ${{ steps.set_srvurl.outputs.srvurl }}
github-token: ${{ gitea.token }}
preset: "conventionalcommits"
# preset: "angular" # This is the default
skip-commit: true
release-count: 1
output-file: false
create-summary: true
skip-on-empty: true
skip-version-file: true
skip-tag: true
- name: Create release
run: |
curl -s -X POST \
-H "Authorization: token ${{ secrets.ACTIONS_TOKEN }}" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${{ steps.get_next_version.outputs.tag }}\", \"name\": \"${{ steps.get_next_version.outputs.tag }}\", \"body\": \"${{ steps.get_next_version.outputs.changelog }}\"}" \
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases"
build_docker:
name: Build Docker Images
needs: create_release
steps:
- name: Update Docker configuration
continue-on-error: true
@@ -54,17 +110,11 @@ jobs:
echo "DOCKER_OPTS=\"--insecure-registry ${{ vars.PACKAGES_REGISTRY_URL }}\"" >> /etc/default/docker
echo "{\"insecure-registries\": [\"${{ vars.PACKAGES_REGISTRY_URL }}\"]}" > /etc/docker/daemon.json
- name: Get repo name
id: split
run: echo "repo=${REPO##*/}" >> "$GITEA_OUTPUT"
env:
REPO: ${{ gitea.repository }}
- name: Check out repository
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.tag.outputs.tag_name }}
ref: ${{ needs.create_release.outputs.release_name }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -87,10 +137,10 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
tags: type=semver,pattern=v{{version}},value=${{ needs.tag.outputs.tag_name }}
images: |
ghcr.io/${{ vars.GHCR_USERNAME }}/${{ steps.split.outputs.repo }}
ghcr.io/${{ vars.GHCR_USERNAME }}/webexmemebot
${{ vars.PACKAGES_REGISTRY_URL }}/${{ gitea.repository }}
tags: type=semver,pattern=v{{version}},value=${{ needs.create_release.outputs.release_name }}
- name: Print metadata
run: |

View File

@@ -9,25 +9,23 @@ on:
- cron: "@daily"
jobs:
# sonarqube:
# name: SonarQube
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repo
# uses: actions/checkout@v4.2.2
# - name: SonarQube Scan
# uses: SonarSource/sonarqube-scan-action@v5.2.0
# env:
# SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }}
# SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
snyk:
name: Snyk
sonarqube:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.2.2
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4.2.1
env:
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
snyk:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4.2.2
- name: Snyk
uses: snyk/actions/python@master

View File

@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.13-slim
LABEL maintainer="Luke Tainton <luke@tainton.uk>"
USER root

View File

@@ -1,13 +1,8 @@
"""Command module for handling the 'exit' command in the Webex meme bot."""
from webex_bot.models.command import Command
class ExitCommand(Command):
"""Command to handle the 'exit' command in the Webex meme bot."""
def __init__(self) -> None:
"""Initialize the ExitCommand with command keyword and help message."""
super().__init__(
command_keyword="exit",
help_message="Exit",
@@ -15,14 +10,11 @@ class ExitCommand(Command):
)
self.sender: str = ""
def pre_execute(self, message, attachment_actions, activity) -> None: # pylint: disable=unused-argument
"""Pre-execution logic for the exit command."""
def pre_execute(self, message, attachment_actions, activity) -> None:
return
def execute(self, message, attachment_actions, activity) -> None: # pylint: disable=unused-argument
"""Execute the exit command."""
def execute(self, message, attachment_actions, activity) -> None:
return
def post_execute(self, message, attachment_actions, activity) -> None: # pylint: disable=unused-argument
"""Post-execution logic for the exit command."""
def post_execute(self, message, attachment_actions, activity) -> None:
return

View File

@@ -1,5 +1,3 @@
"""Generates meme images using the memegen.link API."""
import requests
CHAR_REPLACEMENTS: list = [
@@ -19,11 +17,6 @@ CHAR_REPLACEMENTS: list = [
def get_templates() -> list[dict]:
"""Fetches available meme templates from the memegen.link API.
Returns:
list[dict]: A list of dictionaries containing meme template information.
"""
url: str = "https://api.memegen.link/templates"
req: requests.Response = requests.get(url=url, timeout=10)
req.raise_for_status()
@@ -47,14 +40,6 @@ def get_templates() -> list[dict]:
def format_meme_string(input_string: str) -> str:
"""Formats a string for use in a meme image URL.
Args:
input_string (str): The string to format.
Returns:
str: The formatted string suitable for meme image URLs.
"""
# https://memegen.link/#special-characters
out_string: str = input_string
for char_replacement in CHAR_REPLACEMENTS:
@@ -63,16 +48,6 @@ def format_meme_string(input_string: str) -> str:
def generate_api_url(template: str, top_str: str, btm_str: str) -> str:
"""Generates a meme image URL using the memegen.link API.
Args:
template (str): The template identifier in the format "name.ext".
top_str (str): The text for the top line of the meme.
btm_str (str): The text for the bottom line of the meme.
Returns:
str: The complete URL for the meme image.
"""
tmpl_name: str
tmpl_ext: str
tmpl_name, tmpl_ext = template.split(".")
@@ -80,5 +55,7 @@ def generate_api_url(template: str, top_str: str, btm_str: str) -> str:
top_str = format_meme_string(top_str)
btm_str = format_meme_string(btm_str)
url: str = f"https://api.memegen.link/images/{tmpl_name}/{top_str}/{btm_str}.{tmpl_ext}"
url: str = (
f"https://api.memegen.link/images/{tmpl_name}/{top_str}/{btm_str}.{tmpl_ext}"
)
return url

View File

@@ -1,7 +1,5 @@
#!/usr/local/bin/python3
"""Main entry point for the Webex Bot application."""
from webex_bot.webex_bot import WebexBot
from app import close, meme
@@ -20,7 +18,6 @@ def create_bot() -> WebexBot:
def main() -> None:
"""Main function to run the Webex Bot."""
bot: WebexBot = create_bot()
bot.add_command(meme.MakeMemeCommand())
bot.add_command(close.ExitCommand())

View File

@@ -1,11 +1,9 @@
"""Generates meme images using the memegen.link API."""
from webex_bot.models.command import Command
from webex_bot.models.response import Response, response_from_adaptive_card
from webexpythonsdk.models.cards import (
from webexteamssdk.models.cards import (
AdaptiveCard,
Choice,
ChoiceSet,
Choices,
Column,
ColumnSet,
FontSize,
@@ -13,7 +11,7 @@ from webexpythonsdk.models.cards import (
Text,
TextBlock,
)
from webexpythonsdk.models.cards.actions import OpenUrl, Submit
from webexteamssdk.models.cards.actions import OpenUrl, Submit
from app import img
@@ -24,7 +22,6 @@ class MakeMemeCommand(Command):
"""Class for initial Webex interactive card."""
def __init__(self) -> None:
"""Initialize the MakeMemeCommand with command keyword and help message."""
super().__init__(
command_keyword="/meme",
help_message="Make a Meme",
@@ -32,12 +29,10 @@ class MakeMemeCommand(Command):
delete_previous_message=True,
)
def pre_execute(self, message, attachment_actions, activity) -> None: # pylint: disable=unused-argument
"""Pre-execution logic for the MakeMemeCommand."""
def pre_execute(self, message, attachment_actions, activity) -> None:
return
def execute(self, message, attachment_actions, activity) -> Response: # pylint: disable=unused-argument
"""Execute the MakeMemeCommand and return an adaptive card."""
def execute(self, message, attachment_actions, activity) -> Response:
card_body: list = [
ColumnSet(
columns=[
@@ -50,13 +45,13 @@ class MakeMemeCommand(Command):
size=FontSize.MEDIUM,
),
TextBlock(
"This bot uses memegen.link to generate memes. Click 'View Templates' to view available templates.", # pylint: disable=line-too-long
"This bot uses memegen.link to generate memes. Click 'View Templates' to view available templates.",
weight=FontWeight.LIGHTER,
size=FontSize.SMALL,
wrap=True,
),
TextBlock(
"Both fields are required. If you do not want to specify a value, please type a space.", # pylint: disable=line-too-long
"Both fields are required. If you do not want to specify a value, please type a space.",
weight=FontWeight.LIGHTER,
size=FontSize.SMALL,
wrap=True,
@@ -70,10 +65,13 @@ class MakeMemeCommand(Command):
Column(
width=1,
items=[
ChoiceSet(
Choices(
id="meme_type",
isMultiSelect=False,
choices=[Choice(title=x["name"], value=x["choiceval"]) for x in TEMPLATES],
choices=[
Choice(title=x["name"], value=x["choiceval"])
for x in TEMPLATES
],
),
Text(id="text_top", placeholder="Top Text", maxLength=100),
Text(
@@ -105,7 +103,6 @@ class MakeMemeCallback(Command):
"""Class to process user data and return meme."""
def __init__(self) -> None:
"""Initialize the MakeMemeCallback with command keyword and help message."""
super().__init__(
card_callback_keyword="make_meme_callback_rbamzfyx",
delete_previous_message=True,
@@ -116,8 +113,7 @@ class MakeMemeCallback(Command):
self.meme: str = ""
self.meme_filename: str = ""
def pre_execute(self, message, attachment_actions, activity) -> str: # pylint: disable=unused-argument
"""Pre-execution logic for the MakeMemeCallback."""
def pre_execute(self, message, attachment_actions, activity) -> str:
self.meme: str = attachment_actions.inputs.get("meme_type")
self.text_top: str = attachment_actions.inputs.get("text_top")
self.text_bottom: str = attachment_actions.inputs.get("text_bottom")
@@ -131,12 +127,13 @@ class MakeMemeCallback(Command):
return "Generating your meme..."
def execute(self, message, attachment_actions, activity) -> Response | None: # pylint: disable=unused-argument
"""Execute the MakeMemeCallback and return a response with the meme image."""
def execute(self, message, attachment_actions, activity) -> Response | None:
if self.error:
return None
self.meme_filename: str = img.generate_api_url(self.meme, self.text_top, self.text_bottom)
self.meme_filename: str = img.generate_api_url(
self.meme, self.text_top, self.text_bottom
)
msg: Response = Response(
attributes={
"roomId": activity["target"]["globalId"],
@@ -146,6 +143,5 @@ class MakeMemeCallback(Command):
)
return msg
def post_execute(self, message, attachment_actions, activity) -> None: # pylint: disable=unused-argument
"""Post-execution logic for the MakeMemeCallback."""
def post_execute(self, message, attachment_actions, activity) -> None:
return

View File

@@ -8,17 +8,17 @@ authors = [
]
requires-python = ">=3.11.2"
dependencies = [
"webex-bot<1.1.0,>=1.0.3",
"pillow<12.0.1,>=12.0.0",
"astroid<=4.0.1",
"webex-bot<1.0.0,>=0.5.2",
"pillow<12.0.0,>=11.0.0",
"astroid<=3.3.8",
]
[dependency-groups]
dev = [
"black<25.9.1,>=25.9.0",
[tool.uv]
dev-dependencies = [
"black<25.2.0,>=25.1.0",
"coverage<8.0.0,>=7.6.10",
"isort<7.0.1,>=7.0.0",
"pylint<4.1.0,>=4.0.0",
"isort<6.1.0,>=6.0.0",
"pylint<4.0.0,>=3.3.2",
"pylint-exit<2.0.0,>=1.2.0",
"pytest<9.0.0,>=8.3.4",
"pre-commit<5.0.0,>=4.0.1",
@@ -32,6 +32,3 @@ includes = []
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.black]
line-length = 120

View File

@@ -1,6 +1,6 @@
{
"assignAutomerge": false,
"assigneesFromCodeOwners": false,
"assignAutomerge": true,
"assigneesFromCodeOwners": true,
"dependencyDashboardAutoclose": true,
"extends": ["config:recommended"],
"ignorePaths": ["**/.archive/**"],

View File

@@ -2,22 +2,19 @@
import os
env_vars: dict = {
vars: dict = {
"APP_VERSION": "dev",
"WEBEX_API_KEY": "testing",
}
for var, value in env_vars.items():
for var, value in vars.items():
os.environ[var] = value
# needs to be imported AFTER environment variables are set
from app.config import (
config,
) # pylint: disable=wrong-import-position # pragma: no cover # noqa: E402
from app.config import config # pragma: no cover # noqa: E402
def test_config() -> None:
"""Test the configuration settings."""
assert config.webex_token == env_vars["WEBEX_API_KEY"]
assert config.version == env_vars["APP_VERSION"]
assert config.webex_token == vars["WEBEX_API_KEY"]
assert config.version == vars["APP_VERSION"]

View File

@@ -29,4 +29,8 @@ def test_error_false() -> None:
callback.text_top = "TEST"
callback.text_bottom = "TEST"
result: Response = callback.execute(None, None, {"target": {"globalId": "TEST"}})
assert isinstance(result, Response) and result.roomId == "TEST" and result.files[0] == callback.meme_filename
assert (
isinstance(result, Response)
and result.roomId == "TEST"
and result.files[0] == callback.meme_filename
)

700
uv.lock generated

File diff suppressed because it is too large Load Diff