mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 16:28:16 +00:00
much done, worky
This commit is contained in:
parent
ccd8407588
commit
3561b91301
27 changed files with 862 additions and 11 deletions
|
|
@ -15,4 +15,6 @@ EXPOSE 3000
|
|||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
RUN prisma db push
|
||||
|
||||
CMD ["nephthys"]
|
||||
|
|
@ -7,6 +7,7 @@ from aiohttp import ClientSession
|
|||
from dotenv import load_dotenv
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from nephthys.tasks.update_helpers import update_helpers
|
||||
from nephthys.utils.delete_thread import process_queue
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
|
|
@ -30,6 +31,7 @@ async def main(_app: Starlette):
|
|||
env.session = session
|
||||
await env.db.connect()
|
||||
delete_msg_task = asyncio.create_task(process_queue())
|
||||
await update_helpers()
|
||||
handler = None
|
||||
if env.slack_app_token:
|
||||
if env.environment == "production":
|
||||
|
|
|
|||
0
nephthys/actions/assign.py
Normal file
0
nephthys/actions/assign.py
Normal file
87
nephthys/actions/assign_tag.py
Normal file
87
nephthys/actions/assign_tag.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from slack_bolt.async_app import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
|
||||
|
||||
async def assign_tag_callback(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
raw_tags = body["actions"][0]["selected_options"]
|
||||
tags = [
|
||||
{"name": tag["text"]["text"], "value": tag["value"]}
|
||||
for tag in raw_tags
|
||||
if "value" in tag
|
||||
]
|
||||
logging.info(tags)
|
||||
channel_id = body["channel"]["id"]
|
||||
ts = body["message"]["ts"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"id": user_id})
|
||||
if not user or not user.helper:
|
||||
await client.chat_postEphemeral(
|
||||
channel=channel_id,
|
||||
user=user_id,
|
||||
text="You are not authorized to assign tags.",
|
||||
)
|
||||
return
|
||||
|
||||
ticket = await env.db.ticket.find_unique(
|
||||
where={"ticketTs": ts}, include={"tagsOnTickets": True}
|
||||
)
|
||||
if not ticket:
|
||||
await send_heartbeat(
|
||||
f"Failed to find ticket with ts {ts} in channel {channel_id}."
|
||||
)
|
||||
return
|
||||
if ticket.tagsOnTickets:
|
||||
new_tags = [
|
||||
tag
|
||||
for tag in tags
|
||||
if tag["value"] not in [t.tagId for t in ticket.tagsOnTickets]
|
||||
]
|
||||
old_tags = [
|
||||
tag
|
||||
for tag in ticket.tagsOnTickets
|
||||
if tag.tagId not in [t["value"] for t in tags]
|
||||
]
|
||||
else:
|
||||
new_tags = tags
|
||||
old_tags = []
|
||||
logging.info(f"New: {new_tags}, Old: {old_tags}")
|
||||
|
||||
await env.db.tagsontickets.create_many(
|
||||
data=[{"tagId": tag["value"], "ticketId": ticket.id} for tag in new_tags]
|
||||
)
|
||||
|
||||
await env.db.tagsontickets.delete_many(
|
||||
where={"tagId": {"in": [tag.tagId for tag in old_tags]}, "ticketId": ts}
|
||||
)
|
||||
|
||||
tags = await env.db.usertagsubscription.find_many(
|
||||
where={"tagId": {"in": [tag["value"] for tag in new_tags]}}
|
||||
)
|
||||
|
||||
users = [
|
||||
{
|
||||
"id": tag.userId,
|
||||
"tags": [tag.tagId for tag in tags if tag.userId == tag.userId],
|
||||
}
|
||||
for tag in tags
|
||||
]
|
||||
url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{ticket.msgTs.replace('.', '')}"
|
||||
|
||||
for user in users:
|
||||
formatted_tags = ", ".join(
|
||||
[tag["name"] for tag in new_tags if tag["value"] in user["tags"]]
|
||||
)
|
||||
await client.chat_postMessage(
|
||||
channel=user["id"], text=f"New ticket for {formatted_tags}!\n{url}"
|
||||
)
|
||||
44
nephthys/actions/create_tag.py
Normal file
44
nephthys/actions/create_tag.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from slack_bolt.async_app import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.events.app_home_opened import open_app_home
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.views.modals.create_tag import get_create_tag_modal
|
||||
|
||||
|
||||
async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
|
||||
"""
|
||||
Callback for the create tag view submission
|
||||
"""
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"id": user_id})
|
||||
if not user or not user.admin:
|
||||
await send_heartbeat(f"Attempted to create tag by non-admin user <@{user_id}>")
|
||||
return
|
||||
|
||||
name = body["view"]["state"]["values"]["tag_name"]["tag_name"]["value"]
|
||||
await env.db.tag.create(data={"name": name})
|
||||
|
||||
await open_app_home("tags", client, user_id)
|
||||
|
||||
|
||||
async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
|
||||
"""
|
||||
Open modal to create a tag
|
||||
"""
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
trigger_id = body["trigger_id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"id": user_id})
|
||||
if not user or not user.admin:
|
||||
await send_heartbeat(
|
||||
f"Attempted to open create tag modal by non-admin user <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
view = get_create_tag_modal()
|
||||
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
from datetime import datetime
|
||||
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.data.transcript import Transcript
|
||||
|
|
@ -9,7 +11,7 @@ from prisma.enums import TicketStatus
|
|||
|
||||
|
||||
async def resolve(ts: str, resolver: str, client: AsyncWebClient):
|
||||
allowed = can_resolve(resolver, ts)
|
||||
allowed = await can_resolve(resolver, ts)
|
||||
if not allowed:
|
||||
await send_heartbeat(
|
||||
f"User {resolver} attempted to resolve ticket with ts {ts} without permission.",
|
||||
|
|
@ -22,9 +24,14 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
|
|||
if not ticket:
|
||||
return
|
||||
|
||||
now = datetime.now()
|
||||
tkt = await env.db.ticket.update(
|
||||
where={"msgTs": ts},
|
||||
data={"status": TicketStatus.CLOSED, "closedBy": {"connect": {"id": resolver}}},
|
||||
data={
|
||||
"status": TicketStatus.CLOSED,
|
||||
"closedBy": {"connect": {"id": resolver}},
|
||||
"closedAt": now,
|
||||
},
|
||||
)
|
||||
if not tkt:
|
||||
await send_heartbeat(
|
||||
|
|
@ -35,7 +42,7 @@ async def resolve(ts: str, resolver: str, client: AsyncWebClient):
|
|||
|
||||
await client.chat_postMessage(
|
||||
channel=env.slack_help_channel,
|
||||
text=Transcript.ticket_resolve,
|
||||
text=Transcript.ticket_resolve.format(user_id=resolver),
|
||||
thread_ts=ts,
|
||||
)
|
||||
|
||||
|
|
|
|||
44
nephthys/actions/tag_subscribe.py
Normal file
44
nephthys/actions/tag_subscribe.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from slack_bolt.async_app import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.events.app_home_opened import open_app_home
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
|
||||
|
||||
async def tag_subscribe_callback(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
"""
|
||||
Callback for the tag subscribe button
|
||||
"""
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"id": user_id})
|
||||
if not user:
|
||||
await send_heartbeat(
|
||||
f"Attempted to subscribe to tag by unknown user <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
tag_id, tag_name = body["actions"][0]["value"].split(";")
|
||||
# check if user is subcribed
|
||||
if await env.db.usertagsubscription.find_first(
|
||||
where={"userId": user_id, "tagId": tag_id}
|
||||
):
|
||||
await env.db.usertagsubscription.delete(
|
||||
where={"userId_tagId": {"tagId": tag_id, "userId": user_id}}
|
||||
)
|
||||
else:
|
||||
await env.db.usertagsubscription.create(
|
||||
data={
|
||||
"user": {"connect": {"id": user_id}},
|
||||
"tag": {"connect": {"id": tag_id}},
|
||||
}
|
||||
)
|
||||
|
||||
await open_app_home("tags", client, user_id)
|
||||
|
|
@ -13,5 +13,10 @@ someone should be along to help you soon but in the mean time i suggest you read
|
|||
"""
|
||||
|
||||
ticket_resolve = f"""
|
||||
oh, oh! it looks like this post has been marked as resolved by (user)! if you have any more questions, please make a new post in <#{env.slack_help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^
|
||||
oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{env.slack_help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^
|
||||
"""
|
||||
|
||||
home_unknown_user_title = ":upside-down_orpheus: woah, stop right there {name}!"
|
||||
home_unknown_user_text = f"heyyyy, heidi here! it looks like i'm not allowed to show ya this. sorry! if you think this is a mistake, please reach out to <@{env.slack_maintainer_id}> and she'll lmk what to do!"
|
||||
|
||||
not_allowed_channel = f"heya, it looks like you're not supposed to be in that channel, pls talk to <@{env.slack_maintainer_id}> if that's wrong"
|
||||
|
|
|
|||
73
nephthys/events/app_home_opened.py
Normal file
73
nephthys/events/app_home_opened.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.views.home.error import get_error_view
|
||||
from nephthys.views.home.helper import get_helper_view
|
||||
from nephthys.views.home.loading import get_loading_view
|
||||
from nephthys.views.home.stats import get_stats_view
|
||||
from nephthys.views.home.tags import get_manage_tags_view
|
||||
from nephthys.views.home.unknown_user import get_unknown_user_view
|
||||
|
||||
|
||||
async def on_app_home_opened(event: dict[str, Any], client: AsyncWebClient):
|
||||
user_id = event["user"]
|
||||
await open_app_home("default", client, user_id)
|
||||
|
||||
|
||||
async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str):
|
||||
try:
|
||||
await client.views_publish(view=get_loading_view(), user_id=user_id)
|
||||
|
||||
user = await env.db.user.find_unique(where={"id": user_id})
|
||||
|
||||
if not user:
|
||||
user_info = await client.users_info(user=user_id) or {}
|
||||
name = (
|
||||
user_info.get("user", {}).get("profile", {}).get("display_name")
|
||||
or user_info.get("user", {}).get("profile", {}).get("real_name")
|
||||
or "person"
|
||||
)
|
||||
view = get_unknown_user_view(name)
|
||||
else:
|
||||
logging.info(f"Opening {home_type} for {user_id}")
|
||||
match home_type:
|
||||
case "default" | "dashboard":
|
||||
view = await get_helper_view(user)
|
||||
case "tags":
|
||||
if user.admin:
|
||||
view = await get_manage_tags_view(user)
|
||||
else:
|
||||
view = get_error_view(
|
||||
"You do not have permission to access this page."
|
||||
)
|
||||
case "my-stats":
|
||||
view = await get_stats_view(user)
|
||||
case _:
|
||||
await send_heartbeat(
|
||||
f"Attempted to load unknown app home type {home_type} for <@{user_id}>"
|
||||
)
|
||||
view = get_error_view(
|
||||
f"This shouldn't happen, please tell <@{env.slack_maintainer_id}> that app home case `_` was hit with home type `{home_type}`"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error opening app home: {e}")
|
||||
tb = traceback.format_exception(e)
|
||||
|
||||
tb_str = "".join(tb)
|
||||
|
||||
view = get_error_view(
|
||||
f"An error occurred while opening the app home: {e}",
|
||||
traceback=tb_str,
|
||||
)
|
||||
err_type = type(e).__name__
|
||||
await send_heartbeat(
|
||||
f"`{err_type}` opening app home for <@{user_id}>",
|
||||
messages=[f"```{tb_str}```", f"cc <@{env.slack_maintainer_id}>"],
|
||||
)
|
||||
|
||||
await client.views_publish(user_id=user_id, view=view)
|
||||
23
nephthys/events/channel_join.py
Normal file
23
nephthys/events/channel_join.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from slack_bolt.context.ack.async_ack import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.data.transcript import Transcript
|
||||
from nephthys.tasks.update_helpers import update_helpers
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def channel_join(ack: AsyncAck, event: dict, client: AsyncWebClient):
|
||||
await ack()
|
||||
user_id = event["user"]
|
||||
channel_id = event["channel"]
|
||||
|
||||
if channel_id in [env.slack_bts_channel, env.slack_ticket_channel]:
|
||||
users = await client.usergroups_users_list(usergroup=env.slack_user_group)
|
||||
if user_id not in users.get("users", []):
|
||||
await client.conversations_kick(channel=channel_id, user=user_id)
|
||||
await client.chat_postMessage(
|
||||
channel=user_id, text=Transcript.not_allowed_channel
|
||||
)
|
||||
await update_helpers()
|
||||
else:
|
||||
return
|
||||
28
nephthys/events/channel_left.py
Normal file
28
nephthys/events/channel_left.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from slack_bolt.context.ack.async_ack import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def channel_left(ack: AsyncAck, event: dict, client: AsyncWebClient):
|
||||
await ack()
|
||||
user_id = event["user"]
|
||||
channel_id = event["channel"]
|
||||
|
||||
if channel_id == env.slack_help_channel:
|
||||
return
|
||||
|
||||
users = await client.usergroups_users_list(usergroup=env.slack_user_group)
|
||||
new_users = users.get("users", [])
|
||||
new_users.remove(user_id)
|
||||
await client.usergroups_users_update(
|
||||
usergroup=env.slack_user_group, users=new_users
|
||||
)
|
||||
|
||||
await env.db.user.update(where={"id": user_id}, data={"helper": False})
|
||||
|
||||
match channel_id:
|
||||
case env.slack_bts_channel:
|
||||
await client.conversations_kick(channel=channel_id, user=user_id)
|
||||
case env.slack_ticket_channel:
|
||||
await client.conversations_kick(channel=channel_id, user=user_id)
|
||||
|
|
@ -5,6 +5,7 @@ from slack_sdk.web.async_client import AsyncWebClient
|
|||
|
||||
from nephthys.data.transcript import Transcript
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
|
||||
ALLOWED_SUBTYPES = ["file_share", "me_message"]
|
||||
|
||||
|
|
@ -13,7 +14,6 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
|
|||
"""
|
||||
Handle incoming messages in Slack.
|
||||
"""
|
||||
print("Received message event:", event)
|
||||
if "subtype" in event and event["subtype"] not in ALLOWED_SUBTYPES:
|
||||
return
|
||||
|
||||
|
|
@ -30,11 +30,23 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
|
|||
past_tickets = await env.db.ticket.count(where={"openedById": user})
|
||||
else:
|
||||
past_tickets = 0
|
||||
user_info = await client.users_info(user=user) or {}
|
||||
username = user_info.get("user", {})[
|
||||
"name"
|
||||
] # this should never actually be empty but if it is, that is a major issue
|
||||
|
||||
if not username:
|
||||
await send_heartbeat(
|
||||
f"SOMETHING HAS GONE TERRIBLY WRONG <@{user}> has no username found - <@{env.slack_maintainer_id}>"
|
||||
)
|
||||
db_user = await env.db.user.upsert(
|
||||
where={
|
||||
"id": user,
|
||||
},
|
||||
data={"create": {"id": user}, "update": {"id": user}},
|
||||
data={
|
||||
"create": {"id": user, "username": username},
|
||||
"update": {"id": user, "username": username},
|
||||
},
|
||||
)
|
||||
|
||||
user_info = await client.users_info(user=user)
|
||||
|
|
@ -50,6 +62,16 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
|
|||
channel=env.slack_ticket_channel,
|
||||
text=f"New message from <@{user}>: {text}",
|
||||
blocks=[
|
||||
{
|
||||
"type": "input",
|
||||
"label": {"type": "plain_text", "text": "Tag ticket", "emoji": True},
|
||||
"element": {
|
||||
"action_id": "tag-list",
|
||||
"type": "multi_external_select",
|
||||
"placeholder": {"type": "plain_text", "text": "Select tags"},
|
||||
"min_query_length": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
|
|
@ -58,7 +80,7 @@ async def on_message(event: Dict[str, Any], client: AsyncWebClient):
|
|||
"text": f"Submitted by <@{user}>. They have {past_tickets} past tickets. <{thread_url}|View thread>.",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
username=display_name or None,
|
||||
icon_url=profile_pic or None,
|
||||
|
|
|
|||
23
nephthys/options/tags.py
Normal file
23
nephthys/options/tags.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from thefuzz import fuzz
|
||||
from thefuzz import process
|
||||
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
|
||||
tags = await env.db.tag.find_many()
|
||||
|
||||
keyword = payload.get("value")
|
||||
if keyword:
|
||||
tag_names = [tag.name for tag in tags]
|
||||
scores = process.extract(keyword, tag_names, scorer=fuzz.ratio, limit=100)
|
||||
old_tags = tags
|
||||
|
||||
tags = [old_tags[tag_names.index(score[0])] for score in scores]
|
||||
return [
|
||||
{
|
||||
"text": {"type": "plain_text", "text": f"{tag.name}"},
|
||||
"value": tag.id,
|
||||
}
|
||||
for tag in tags
|
||||
]
|
||||
46
nephthys/tasks/update_helpers.py
Normal file
46
nephthys/tasks/update_helpers.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import logging
|
||||
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def update_helpers():
|
||||
res = await env.slack_client.conversations_members(channel=env.slack_bts_channel)
|
||||
team_ids = res.get("members", [])
|
||||
|
||||
if not team_ids:
|
||||
# if this happens then something concerning has happened :p
|
||||
await env.slack_client.chat_postMessage(
|
||||
channel=env.slack_bts_channel,
|
||||
text=f"No members found in the bts channel. <@{env.slack_maintainer_id}>",
|
||||
)
|
||||
return
|
||||
|
||||
# unset helpers not in the team
|
||||
await env.db.user.update_many(
|
||||
where={"helper": True, "id": {"not_in": team_ids}},
|
||||
data={"helper": False},
|
||||
)
|
||||
|
||||
# update existing users in the db
|
||||
await env.db.user.update_many(
|
||||
where={"id": {"in": team_ids}},
|
||||
data={"helper": True},
|
||||
)
|
||||
|
||||
# create new users not in the db
|
||||
existing_users_in_db = await env.db.user.find_many(where={"id": {"in": team_ids}})
|
||||
existing_user_ids_in_db = {user.id for user in existing_users_in_db}
|
||||
|
||||
new_member_data_to_create = []
|
||||
for member_id in team_ids:
|
||||
if member_id not in existing_user_ids_in_db:
|
||||
user_info = await env.slack_client.users_info(user=member_id)
|
||||
logging.info(
|
||||
f"Creating new helper user {member_id} with info {user_info.get('name')}"
|
||||
)
|
||||
new_member_data_to_create.append(
|
||||
{"id": member_id, "helper": True, "username": user_info["name"]}
|
||||
)
|
||||
|
||||
if new_member_data_to_create:
|
||||
await env.db.user.create_many(data=new_member_data_to_create)
|
||||
|
|
@ -19,6 +19,9 @@ class Environment:
|
|||
self.environment = os.environ.get("ENVIRONMENT", "development")
|
||||
self.slack_help_channel = os.environ.get("SLACK_HELP_CHANNEL", "unset")
|
||||
self.slack_ticket_channel = os.environ.get("SLACK_TICKET_CHANNEL", "unset")
|
||||
self.slack_bts_channel = os.environ.get("SLACK_BTS_CHANNEL", "unset")
|
||||
self.slack_user_group = os.environ.get("SLACK_USER_GROUP", "unset")
|
||||
self.slack_maintainer_id = os.environ.get("SLACK_MAINTAINER_ID", "unset")
|
||||
|
||||
self.port = int(os.environ.get("PORT", 3000))
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,17 @@ from slack_bolt.async_app import AsyncApp
|
|||
from slack_bolt.context.ack.async_ack import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.actions.assign_tag import assign_tag_callback
|
||||
from nephthys.actions.create_tag import create_tag_btn_callback
|
||||
from nephthys.actions.create_tag import create_tag_view_callback
|
||||
from nephthys.actions.resolve import resolve
|
||||
from nephthys.actions.tag_subscribe import tag_subscribe_callback
|
||||
from nephthys.events.app_home_opened import on_app_home_opened
|
||||
from nephthys.events.app_home_opened import open_app_home
|
||||
from nephthys.events.channel_join import channel_join
|
||||
from nephthys.events.channel_left import channel_left
|
||||
from nephthys.events.message import on_message
|
||||
from nephthys.options.tags import get_tags
|
||||
from nephthys.utils.env import env
|
||||
|
||||
app = AsyncApp(token=env.slack_bot_token, signing_secret=env.slack_signing_secret)
|
||||
|
|
@ -26,3 +35,55 @@ async def handle_mark_resolved_button(
|
|||
value = body["actions"][0]["value"]
|
||||
resolver = body["user"]["id"]
|
||||
await resolve(value, resolver, client)
|
||||
|
||||
|
||||
@app.options("tag-list")
|
||||
async def handle_tag_list_options(ack: AsyncAck, payload: dict):
|
||||
tags = await get_tags(payload)
|
||||
await ack(options=tags)
|
||||
|
||||
|
||||
@app.event("app_home_opened")
|
||||
async def app_home_opened_handler(event: dict[str, Any], client: AsyncWebClient):
|
||||
await on_app_home_opened(event, client)
|
||||
|
||||
|
||||
@app.action("dashboard")
|
||||
@app.action("tags")
|
||||
@app.action("my-stats")
|
||||
async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient):
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
action_id = body["actions"][0]["action_id"]
|
||||
|
||||
await open_app_home(action_id, client, user_id)
|
||||
|
||||
|
||||
@app.event("member_joined_channel")
|
||||
async def handle_member_joined_channel(event: Dict[str, Any], client: AsyncWebClient):
|
||||
await channel_join(ack=AsyncAck(), event=event, client=client)
|
||||
|
||||
|
||||
@app.event("member_left_channel")
|
||||
async def handle_member_left_channel(event: Dict[str, Any], client: AsyncWebClient):
|
||||
await channel_left(ack=AsyncAck(), event=event, client=client)
|
||||
|
||||
|
||||
@app.action("create-tag")
|
||||
async def create_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await create_tag_btn_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.view("create_tag")
|
||||
async def create_tag_view(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await create_tag_view_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("tag-subscribe")
|
||||
async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await tag_subscribe_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("tag-list")
|
||||
async def assign_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await assign_tag_callback(ack, body, client)
|
||||
|
|
|
|||
35
nephthys/views/home/components/buttons.py
Normal file
35
nephthys/views/home/components/buttons.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from prisma.models import User
|
||||
|
||||
|
||||
def get_buttons(user: User, current: str = "dashboard"):
|
||||
buttons = []
|
||||
|
||||
buttons.append(
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Dashboard", "emoji": True},
|
||||
"action_id": "dashboard",
|
||||
**({"style": "primary"} if current != "dashboard" else {}),
|
||||
}
|
||||
)
|
||||
if user.admin:
|
||||
buttons.append(
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Tags", "emoji": True},
|
||||
"action_id": "tags",
|
||||
**({"style": "primary"} if current != "tags" else {}),
|
||||
}
|
||||
)
|
||||
|
||||
buttons.append(
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "My Stats", "emoji": True},
|
||||
"action_id": "my-stats",
|
||||
**({"style": "primary"} if current != "my-stats" else {}),
|
||||
}
|
||||
)
|
||||
|
||||
blocks = {"type": "actions", "elements": buttons}
|
||||
return blocks
|
||||
19
nephthys/views/home/error.py
Normal file
19
nephthys/views/home/error.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
def get_error_view(msg: str, traceback: str | None = None):
|
||||
if traceback:
|
||||
msg = f"{msg}\n\nTraceback:\n```{traceback}```"
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Sorry, something went wrong. Please try again later.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": f"Error message\n{msg}"},
|
||||
},
|
||||
],
|
||||
}
|
||||
42
nephthys/views/home/helper.py
Normal file
42
nephthys/views/home/helper.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from nephthys.utils.env import env
|
||||
from nephthys.views.home.components.buttons import get_buttons
|
||||
from prisma.enums import TicketStatus
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
async def get_helper_view(user: User):
|
||||
tickets = await env.db.ticket.find_many() or []
|
||||
|
||||
organised_tkts = {}
|
||||
for ticket in tickets:
|
||||
status = ticket.status
|
||||
if status not in organised_tkts:
|
||||
organised_tkts[status] = []
|
||||
organised_tkts[status].append(ticket)
|
||||
|
||||
formatted_msg = f"""
|
||||
*Requests*
|
||||
{len(tickets)} requests found
|
||||
{len(organised_tkts.get(TicketStatus.OPEN, []))} open
|
||||
{len(organised_tkts.get(TicketStatus.IN_PROGRESS, []))} in progress ({len([ticket for ticket in tickets if ticket.status == TicketStatus.IN_PROGRESS and ticket.assignedToId == user.id])} assigned to you)
|
||||
{len(organised_tkts.get(TicketStatus.CLOSED, []))} closed ({len([ticket for ticket in tickets if ticket.status == TicketStatus.CLOSED and ticket.closedById == user.id])} closed by you)
|
||||
"""
|
||||
|
||||
btns = get_buttons(user, "dashboard")
|
||||
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_cute: helper heidi",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
btns,
|
||||
{"type": "divider"},
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": formatted_msg}},
|
||||
],
|
||||
}
|
||||
21
nephthys/views/home/loading.py
Normal file
21
nephthys/views/home/loading.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
def get_loading_view():
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":hourglass_flowing_sand: loading...",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "divider",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"image_url": "https://hc-cdn.hel1.your-objectstorage.com/s/v3/1c1fc5fb03b8bf46c6ab047c97f962ed930616f0_loading-hugs.gif",
|
||||
"alt_text": "Loading...",
|
||||
},
|
||||
],
|
||||
}
|
||||
29
nephthys/views/home/stats.py
Normal file
29
nephthys/views/home/stats.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from nephthys.views.home.components.buttons import get_buttons
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
async def get_stats_view(user: User):
|
||||
btns = get_buttons(user, "my-stats")
|
||||
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_info: My Stats",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
btns,
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":rac_cute: weh im just a silly raccoon, did you expect me to have stats :rac_ded: i don't even have a job >:(",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
106
nephthys/views/home/tags.py
Normal file
106
nephthys/views/home/tags.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import logging
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.views.home.components.buttons import get_buttons
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
async def get_manage_tags_view(user: User) -> dict:
|
||||
btns = get_buttons(user, "tags")
|
||||
|
||||
tags = await env.db.tag.find_many(include={"userSubscriptions": True})
|
||||
|
||||
blocks = []
|
||||
|
||||
if not tags:
|
||||
blocks.append(
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":rac_nooo: i couldn't scrounge up any tags, you can make a new one below though",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
blocks.append(
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":rac_info: here are the tags i found!",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for tag in tags:
|
||||
logging.info(f"Tag {tag.name} with id {tag.id} found in the database")
|
||||
logging.info(
|
||||
f"Tag {tag.name} has {len(tag.userSubscriptions) if tag.userSubscriptions else 0} subscriptions"
|
||||
)
|
||||
if tag.userSubscriptions:
|
||||
subs = [user.userId for user in tag.userSubscriptions]
|
||||
else:
|
||||
subs = []
|
||||
stringified_subs = [f"<@{user}>" for user in subs]
|
||||
blocks.append(
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*{tag.name}* - {''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "tag-subscribe",
|
||||
"value": f"{tag.id};{tag.name}",
|
||||
"style": "primary" if user.id not in subs else "danger",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_info: Manage Tags",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
btns,
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":rac_thumbs: here you can manage tags and user subscriptions",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
*blocks,
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_cute: add a tag?",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "create-tag",
|
||||
"style": "primary",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
21
nephthys/views/home/unknown_user.py
Normal file
21
nephthys/views/home/unknown_user.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from nephthys.data.transcript import Transcript
|
||||
|
||||
|
||||
def get_unknown_user_view(name: str):
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": Transcript.home_unknown_user_title.format(name=name),
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": Transcript.home_unknown_user_text},
|
||||
},
|
||||
],
|
||||
}
|
||||
30
nephthys/views/modals/create_tag.py
Normal file
30
nephthys/views/modals/create_tag.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
def get_create_tag_modal():
|
||||
return {
|
||||
"type": "modal",
|
||||
"callback_id": "create_tag",
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_info: create a tag!",
|
||||
"emoji": True,
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "tag_name",
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "giv name?",
|
||||
"emoji": True,
|
||||
},
|
||||
"element": {
|
||||
"type": "plain_text_input",
|
||||
"action_id": "tag_name",
|
||||
},
|
||||
},
|
||||
],
|
||||
"submit": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_question: add tag?",
|
||||
"emoji": True,
|
||||
},
|
||||
}
|
||||
|
|
@ -16,13 +16,17 @@ enum TicketStatus {
|
|||
}
|
||||
|
||||
model User {
|
||||
id String @id @unique
|
||||
admin Boolean @default(false)
|
||||
id String @id @unique
|
||||
username String? @unique
|
||||
admin Boolean @default(false)
|
||||
|
||||
openedTickets Ticket[] @relation("OpenedTickets")
|
||||
closedTickets Ticket[] @relation("ClosedTickets")
|
||||
assignedTickets Ticket[] @relation("AssignedTickets")
|
||||
|
||||
tagSubscriptions UserTagSubscription[]
|
||||
helper Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
|
|
@ -38,12 +42,48 @@ model Ticket {
|
|||
openedBy User @relation("OpenedTickets", fields: [openedById], references: [id])
|
||||
openedById String
|
||||
|
||||
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
|
||||
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
|
||||
closedById String?
|
||||
closedAt DateTime?
|
||||
closedAt DateTime?
|
||||
|
||||
assignedTo User? @relation("AssignedTickets", fields: [assignedToId], references: [id])
|
||||
assignedToId String?
|
||||
|
||||
tagsOnTickets TagsOnTickets[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @unique @default(cuid())
|
||||
name String @unique
|
||||
|
||||
ticketsOnTags TagsOnTickets[]
|
||||
|
||||
userSubscriptions UserTagSubscription[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model TagsOnTickets {
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
ticketId String
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
tagId String
|
||||
|
||||
assignedAt DateTime @default(now())
|
||||
|
||||
@@id([ticketId, tagId])
|
||||
@@map("tags_on_tickets")
|
||||
}
|
||||
|
||||
model UserTagSubscription {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
tagId String
|
||||
|
||||
subscribedAt DateTime @default(now())
|
||||
|
||||
@@id([userId, tagId])
|
||||
@@map("user_tag_subscriptions")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ dependencies = [
|
|||
"python-dotenv>=1.1.0",
|
||||
"slack-bolt>=1.23.0",
|
||||
"starlette>=0.46.1",
|
||||
"thefuzz>=0.22.1",
|
||||
"uvicorn>=0.34.0",
|
||||
"uvloop>=0.21.0",
|
||||
]
|
||||
|
|
|
|||
37
uv.lock
generated
37
uv.lock
generated
|
|
@ -313,6 +313,7 @@ dependencies = [
|
|||
{ name = "python-dotenv" },
|
||||
{ name = "slack-bolt" },
|
||||
{ name = "starlette" },
|
||||
{ name = "thefuzz" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "uvloop" },
|
||||
]
|
||||
|
|
@ -330,6 +331,7 @@ requires-dist = [
|
|||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||
{ name = "slack-bolt", specifier = ">=1.23.0" },
|
||||
{ name = "starlette", specifier = ">=0.46.1" },
|
||||
{ name = "thefuzz", specifier = ">=0.22.1" },
|
||||
{ name = "uvicorn", specifier = ">=0.34.0" },
|
||||
{ name = "uvloop", specifier = ">=0.21.0" },
|
||||
]
|
||||
|
|
@ -500,6 +502,29 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rapidfuzz"
|
||||
version = "3.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/76/606e71e4227790750f1646f3c5c873e18d6cfeb6f9a77b2b8c4dec8f0f66/rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23", size = 1982282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/f5/d0b48c6b902607a59fd5932a54e3518dae8223814db8349b0176e6e9444b/rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae", size = 1439274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/cf/c3ac8c80d8ced6c1f99b5d9674d397ce5d0e9d0939d788d67c010e19c65f/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa", size = 1399854 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/5d/ca8698e452b349c8313faf07bfa84e7d1c2d2edf7ccc67bcfc49bee1259a/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611", size = 5308962 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0a/bebada332854e78e68f3d6c05226b23faca79d71362509dbcf7b002e33b7/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b", size = 1625016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/0c/9e58d4887b86d7121d1c519f7050d1be5eb189d8a8075f5417df6492b4f5/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527", size = 1600414 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/df/6096bc669c1311568840bdcbb5a893edc972d1c8d2b4b4325c21d54da5b1/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939", size = 3053179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/5179c583b75fce3e65a5cd79a3561bd19abd54518cb7c483a89b284bf2b9/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df", size = 2456856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/64/e9804212e3286d027ac35bbb66603c9456c2bce23f823b67d2f5cabc05c1/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798", size = 7567107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f2/7d69e7bf4daec62769b11757ffc31f69afb3ce248947aadbb109fefd9f65/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d", size = 2854192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/21/ab4ad7d7d0f653e6fe2e4ccf11d0245092bef94cdff587a21e534e57bda8/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566", size = 3398876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/45bba94c2489cb1ee0130dcb46e1df4fa2c2b25269e21ffd15240a80322b/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72", size = 4377077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f3/5e0c6ae452cbb74e5436d3445467447e8c32f3021f48f93f15934b8cffc2/rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8", size = 1822066 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e3/a98c25c4f74051df4dcf2f393176b8663bfd93c7afc6692c84e96de147a2/rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264", size = 1615100 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/b1/05cd5e697c00cd46d7791915f571b38c8531f714832eff2c5e34537c49ee/rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53", size = 858976 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slack-bolt"
|
||||
version = "1.23.0"
|
||||
|
|
@ -542,6 +567,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thefuzz"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "rapidfuzz" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.13.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue