Skip to content

Commit

Permalink
Support for Slack Shortcuts, Models, & Celery Beat Scheduled Tasks
Browse files Browse the repository at this point in the history
+ Added support for Slack Shortcuts
  + Also included support for External data sources to pull lists into Slack options/options groups
+ Added support for Slack Modals
+ Added support for scheduled tasks using Celery's Beat feature
+ Updated and improved all Slack response messages and notifications
+ Other minor cleanup and standardizations
  + Ability to enable/disable Slack Slash Commands and Shortcuts via pkgbot_config.yaml
  • Loading branch information
MLBZ521 committed Feb 11, 2023
2 parents 910160e + eb75a04 commit 4a379bd
Show file tree
Hide file tree
Showing 18 changed files with 772 additions and 121 deletions.
146 changes: 135 additions & 11 deletions Settings/blocks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from itertools import groupby
from operator import itemgetter

from pkgbot import config
from pkgbot.db import models


config = config.load_config()

secure = "s" if config.PkgBot.get("enable_ssl") else ""
pkgbot_server = f"http{secure}://{config.PkgBot.get('host')}:{config.PkgBot.get('port')}"
SECURE = "s" if config.PkgBot.get("enable_ssl") else ""
PKGBOT_SERVER = f"http{SECURE}://{config.PkgBot.get('host')}:{config.PkgBot.get('port')}"


async def brick_header(pkg_object: models.Package_In):
Expand All @@ -29,7 +32,7 @@ async def brick_main(pkg_object: models.Package_In):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{pkg_object.dict().get('icon')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{pkg_object.dict().get('icon')}",
"alt_text": ":new:"
}
}
Expand Down Expand Up @@ -134,7 +137,7 @@ async def brick_error(recipe_id, error):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{config.PkgBot.get('icon_error')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{config.PkgBot.get('icon_error')}",
"alt_text": ":x:"
}
},
Expand Down Expand Up @@ -198,7 +201,7 @@ async def brick_update_trust_error_msg(trust_object, msg):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{config.PkgBot.get('icon_error')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{config.PkgBot.get('icon_error')}",
"alt_text": ":x:"
}
}]
Expand Down Expand Up @@ -226,7 +229,7 @@ async def brick_deny_trust(trust_object):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{config.PkgBot.get('icon_denied')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{config.PkgBot.get('icon_denied')}",
"alt_text": ":denied:"
}
}
Expand Down Expand Up @@ -254,7 +257,7 @@ async def brick_trust_diff_main(recipe):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{config.PkgBot.get('icon_warning')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{config.PkgBot.get('icon_warning')}",
"alt_text": ":warning:"
}
}
Expand Down Expand Up @@ -321,7 +324,7 @@ async def unauthorized(user):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{config.PkgBot.get('icon_permission_denied')}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{config.PkgBot.get('icon_permission_denied')}",
"alt_text": ":denied:"
}
}
Expand All @@ -340,12 +343,14 @@ async def brick_section_text(text):
}


async def brick_accessory_image(image, alt_text=":notification:"):
async def brick_accessory_image(image, alt_text):

alt_text = alt_text or ":notification:"

return {
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{image}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{image}",
"alt_text": alt_text
}
}
Expand All @@ -371,7 +376,7 @@ async def brick_disk_space_msg(header, msg, image):
},
"accessory": {
"type": "image",
"image_url": f"{pkgbot_server}/static/icons/{image}",
"image_url": f"{PKGBOT_SERVER}/static/icons/{image}",
"alt_text": ":warning:"
}
},
Expand All @@ -391,3 +396,122 @@ async def brick_disk_space_msg(header, msg, image):
]
}
]


async def modal_notification(title_txt: str, button_text: str):
# An undocumented limitation: maximum 26 characters in the `title.text` string

return {
"type": "modal",
"callback_id": "notification",
# "private_metadata": f"{private_metadata}",
"title": {
"type": "plain_text",
"text": f"{title_txt}",
"emoji": True
},
"close": {
"type": "plain_text",
"text": f"{button_text}",
"emoji": True
}
}


async def modal_promote_pkg(pkg_name: str):

return {
"type": "modal",
"callback_id": "promote_pkg",
"private_metadata": f"{pkg_name}",
"title": {
"type": "plain_text",
"text": "Promote pkg to Jamf Pro"
},
"submit": {
"type": "plain_text",
"text": "Promote :rocket:",
"emoji": True
},
"close": {
"type": "plain_text",
"text": "Cancel"
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Select Policy to add or update the existing version of the selected pkg."
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"Pkg to promote: `{pkg_name}`"
}
},
{
"type": "input",
"element": {
"type": "external_select",
"placeholder": {
"type": "plain_text",
"text": "Select a Policy",
"emoji": True
},
"min_query_length": 5,
"action_id": "policy_list"
},
"label": {
"type": "plain_text",
"text": "Select a Policy and :ship_it_parrot:",
"emoji": True
}
}
]
}


async def policy_list(policies: str):

option_groups = []
policies = sorted(policies, key=itemgetter("site"))

# This _is_ a documented limitation: maximum of 100 options can be included in a list
for site, value in groupby(policies[:99], key=itemgetter("site")):
options = []

for policy in value:
options.append(await create_static_option(policy))

if options:
option_groups.append({
"label": {
"type": "plain_text",
"text": f"Site: {site}"
},
"options": options
})

return {
"option_groups": option_groups
}


async def create_static_option(policy):

return {
"text": {
"type": "plain_text",
# This seems to be an undocumented limitation:
# maximum of 76 characters in the `text` string
"text": policy.get("name")[:75],
"emoji": True
},
"value": f"{policy.get('policy_id')}"
}
18 changes: 18 additions & 0 deletions Settings/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,21 @@ async def basic(text: str, image: str | None = None, alt_image_text: str | None
async def disk_space(header: str, msg: str, image: str | None = None):

return await format_json(await block.brick_disk_space_msg(header, msg, image))


async def modal_notification(title_txt: str, msg_text: str,
button_text: str, image: str | None = None, alt_image_text: str | None = None):

blocks = await block.brick_section_text(msg_text)

if image:
blocks = blocks | await block.brick_accessory_image(image, alt_image_text)

blocks = await block.modal_notification(title_txt, button_text) | {"blocks": [blocks]}

return await format_json(blocks)


async def modal_promote_pkg(pkg_name: str):

return await format_json(await block.modal_promote_pkg(pkg_name))
3 changes: 2 additions & 1 deletion examples/settings/pkgbot_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ Slack:
bot_name: PkgBot
slack_id:
channel:

slash_cmds_enabled: False
shortcuts_enabled: False

PkgBot:
# enable_ssl: True
Expand Down
2 changes: 1 addition & 1 deletion pkgbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def create_pkgbot() -> FastAPI:
title="PkgBot API",
description="A framework to manage software packaging, testing, and promoting from a "
"development to production environment.",
version="0.4.0",
version="0.5.0",
openapi_tags=settings.api.tags_metadata,
docs_url="/api"
)
Expand Down
22 changes: 14 additions & 8 deletions pkgbot/api/slackbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,20 @@ async def receive(request: Request):
payload_type = payload_object.get("type")
# log.debug(f"Received Payload Type: {payload_type}")

if payload_type == "message_action":
await core.chatbot.events.message_shortcut(payload_object)

elif (
payload_type == "block_actions" and
payload_object.get("actions")[0].get("type") == "button"
):
await core.chatbot.events.button_click(payload_object)
match payload_type:

case "block_actions":
if payload_object.get("actions")[0].get("type") == "button":
await core.chatbot.events.button_click(payload_object)

case "block_suggestion":
return await core.chatbot.events.external_lists(payload_object)

case "message_action":
await core.chatbot.events.message_shortcut(payload_object)

case "view_submission":
await core.chatbot.events.view_submission(payload_object)

return Response(status_code=status.HTTP_200_OK)

Expand Down
1 change: 1 addition & 0 deletions pkgbot/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from . import error
from . import jamf_pro
from . import package
from . import policy
from . import recipe
from . import user

Expand Down
5 changes: 2 additions & 3 deletions pkgbot/core/autopkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def run_recipe(recipe_id: str, autopkg_cmd: models.AutoPkgCMD_Run):

if a_recipe.enabled:

queued_task = pkgbot_celery_app.send_task(
return pkgbot_celery_app.send_task(
"autopkg:verb_parser",
kwargs = {
"recipes": [ a_recipe.dict() ],
Expand All @@ -136,8 +136,7 @@ async def run_recipe(recipe_id: str, autopkg_cmd: models.AutoPkgCMD_Run):
queue="autopkg",
priority=4
)

return { "result": "Queued background task" , "task_id": queued_task.id }
# return { "result": "Queued background task" , "task_id": queued_task.id }

log.info(f"Recipe '{recipe_id}' is disabled.")
return { "result": "Recipe is disabled" }
Expand Down
12 changes: 12 additions & 0 deletions pkgbot/core/chatbot/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,15 @@ async def basic_msg(text: str, image: str | None = None, alt_image_text: str | N
async def disk_space_msg(header: str, msg: str, image: str | None = None):

return await messages.disk_space(header, msg, image)


async def modal_notification(title_txt: str, msg_text: str,
button_text: str, image: str | None = None, alt_image_text: str | None = None):

return await messages.modal_notification(
title_txt, msg_text, button_text, image, alt_image_text)


async def modal_promote_pkg(pkg_name: str):

return await messages.modal_promote_pkg(pkg_name)
Loading

0 comments on commit 4a379bd

Please sign in to comment.