Python backend for a Slack's kudos plugin.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'develop' of github.com:Dekalabs/kefi-backend into develop

+214 -7
+12 -4
.fleet/run.json
··· 12 12 "type": "python", 13 13 "module": "uvicorn", 14 14 "arguments": [ 15 - "kefi.main:app" 15 + "kefi.main:app", 16 + "--reload" 16 17 ] 17 18 }, 18 19 { ··· 24 25 }, 25 26 { 26 27 "type": "python-tests", 27 - "name": "Run commands models", 28 + "name": "Run models tests", 28 29 "testFramework": "pytest", 29 30 "target": "kefi/tests/test_models", 30 31 "arguments": [] 31 32 }, 32 33 { 33 34 "type": "python-tests", 34 - "name": "Run commands responses", 35 + "name": "Run responses tests", 35 36 "testFramework": "pytest", 36 37 "target": "kefi/tests/test_responses", 37 38 "arguments": [] 38 39 }, 39 40 { 40 41 "type": "python-tests", 41 - "name": "Run commands slack", 42 + "name": "Run slack tests", 42 43 "testFramework": "pytest", 43 44 "target": "kefi/tests/test_slack", 45 + "arguments": [] 46 + }, 47 + { 48 + "type": "python-tests", 49 + "name": "Run interactivity tests", 50 + "testFramework": "pytest", 51 + "target": "kefi/tests/test_interactivity", 44 52 "arguments": [] 45 53 } 46 54 ]
+11
kefi/constants.py
··· 5 5 CONGRATS: str = "congrats" 6 6 HIGH_FIVE: str = "highfive" 7 7 REWARD: str = "reward" 8 + 9 + 10 + class InteractionType: 11 + SHORTCUT: str = "shortcut" 12 + BLOCK_ACTIONS: str = "block_actions" 13 + VIEW_SUBMISSION: str = "view_submission" 14 + 15 + 16 + class ViewType: 17 + JOIN_MEET: str = "meet_join" 18 + LEAVE_MEET: str = "meet_leave"
+5
kefi/dependencies.py
··· 4 4 from kefi.models.database import engine 5 5 6 6 7 + class InteractionParams: 8 + def __init__(self, payload: str = Form(...)): 9 + self.payload = payload 10 + 11 + 7 12 class SlashCommandParams: 8 13 def __init__( 9 14 self,
+2 -1
kefi/main.py
··· 4 4 from kefi.models.core.helpers import create_users 5 5 from kefi.models.database import engine 6 6 from kefi.models.kudos.helpers import create_default_actions 7 - from kefi.routers import commands 7 + from kefi.routers import commands, interactivity 8 8 9 9 app = FastAPI() 10 10 app.include_router(commands.router) 11 + app.include_router(interactivity.router) 11 12 12 13 13 14 @app.on_event("startup")
+39 -2
kefi/routers/helpers.py
··· 1 + import json 1 2 import re 2 3 from collections.abc import Callable 3 4 4 5 from sqlmodel import Session 5 6 6 - from kefi.constants import Command 7 - from kefi.dependencies import SlashCommandParams 7 + from kefi.constants import Command, InteractionType 8 + from kefi.dependencies import InteractionParams, SlashCommandParams 8 9 from kefi.models.core.helpers import ( 9 10 available_balance, 10 11 find_user_by_slack_user_id, ··· 26 27 SlackResponse, 27 28 WalletResponse, 28 29 ) 30 + from kefi.routers.views import JoinMeetView, LeaveMeetView 31 + from kefi.slack import Slack 29 32 30 33 31 34 class CommandHandler: ··· 140 143 141 144 def not_found(self, *args, **kwargs) -> "SlackResponse": 142 145 return NotFoundResponse() 146 + 147 + 148 + class InteractionHandler: 149 + def __init__(self, interaction: InteractionParams, session: Session): 150 + print(interaction.payload) 151 + self.payload = json.loads(interaction.payload) 152 + self.slack = Slack() 153 + 154 + def response(self): 155 + handlers: dict[str, Callable] = { 156 + InteractionType.SHORTCUT: self.interaction_shortcut, 157 + InteractionType.BLOCK_ACTIONS: self.interaction_block_actions, 158 + InteractionType.VIEW_SUBMISSION: self.interaction_view_submission, 159 + } 160 + type = self.payload["type"] 161 + response = handlers.get(type, self.not_found)(payload=self.payload) 162 + return response 163 + 164 + def interaction_shortcut(self, *args, **kwargs): 165 + trigger_id = self.payload["trigger_id"] 166 + self.slack.open_view(trigger_id=trigger_id, view=JoinMeetView()) 167 + return [] 168 + 169 + def interaction_block_actions(self, *args, **kwargs): 170 + view_id = self.payload["view"]["id"] 171 + self.slack.update_view(view_id=view_id, view=LeaveMeetView()) 172 + return [] 173 + 174 + def interaction_view_submission(self, *args, **kwargs): 175 + print("im here") 176 + return [] 177 + 178 + def not_found(self, *args, **kwargs): 179 + return []
+19
kefi/routers/interactivity.py
··· 1 + import json 2 + 3 + from fastapi import APIRouter, Depends 4 + from sqlmodel import Session 5 + 6 + from kefi.dependencies import InteractionParams, get_session 7 + from kefi.routers.helpers import InteractionHandler 8 + 9 + router = APIRouter() 10 + 11 + 12 + @router.post("/interactivity/", tags=["interactivity"]) 13 + def handle_interactivity( 14 + session: Session = Depends(get_session), 15 + interaction: InteractionParams = Depends(InteractionParams), 16 + ): 17 + """Calls the interaction handler and gets the response.""" 18 + interaction_handler = InteractionHandler(interaction=interaction, session=session) 19 + return interaction_handler.response()
+79
kefi/routers/views.py
··· 1 + from slack_sdk.models.blocks import ( 2 + ActionsBlock, 3 + ButtonElement, 4 + ContextBlock, 5 + DividerBlock, 6 + ImageElement, 7 + MarkdownTextObject, 8 + PlainTextObject, 9 + SectionBlock, 10 + ) 11 + from slack_sdk.models.views import View 12 + 13 + from kefi.constants import ViewType 14 + 15 + 16 + class JoinMeetView(View): 17 + def __init__(self, *args, **kwargs): 18 + super().__init__( 19 + callback_id=ViewType.JOIN_MEET, 20 + type="modal", 21 + title=PlainTextObject(text="Kefi"), 22 + close=PlainTextObject(text="Cancelar"), 23 + submit=PlainTextObject(text="¡Me apunto!"), 24 + blocks=[ 25 + SectionBlock( 26 + text=MarkdownTextObject( 27 + text="*:wave: ¡Hola! ¿Te vienes a la Kefi Plaza?*" 28 + ) 29 + ), 30 + DividerBlock(), 31 + SectionBlock( 32 + text=MarkdownTextObject( 33 + text="*Próximo encuentro*\nViernes, 11 Noviembre \n10:00-10:30am\n" 34 + ), 35 + accessory=ImageElement( 36 + image_url="https://api.slack.com/img/blocks/bkb_template_images/notifications.png", 37 + alt_text="calendar thumbnail", 38 + ), 39 + ), 40 + ], 41 + ) 42 + 43 + 44 + class LeaveMeetView(View): 45 + def __init__(self, *args, **kwargs): 46 + super().__init__( 47 + callback_id=ViewType.LEAVE_MEET, 48 + type="modal", 49 + title=PlainTextObject(text="Kefi"), 50 + close=PlainTextObject(text="Cancelar"), 51 + submit=PlainTextObject(text="No asistiré"), 52 + blocks=[ 53 + SectionBlock( 54 + text=MarkdownTextObject( 55 + text="*:wave: ¡Ei! ¡Nos vemos en la Kefi Plaza!*" 56 + ) 57 + ), 58 + DividerBlock(), 59 + SectionBlock( 60 + text=MarkdownTextObject( 61 + text="*Kefi Plaza*\nViernes, 11 Noviembre \n10:00-10:30am\n" 62 + ), 63 + accessory=ImageElement( 64 + image_url="https://api.slack.com/img/blocks/bkb_template_images/notifications.png", 65 + alt_text="calendar thumbnail", 66 + ), 67 + ), 68 + DividerBlock(), 69 + ContextBlock( 70 + elements=[ 71 + MarkdownTextObject( 72 + text="*:white_check_mark: Has indicado que asistirás.*" 73 + ) 74 + ] 75 + ), 76 + DividerBlock(), 77 + SectionBlock(text=MarkdownTextObject(text="*¿Cambio de planes?*")), 78 + ], 79 + )
+10
kefi/slack.py
··· 1 1 import uuid 2 2 from collections.abc import Sequence 3 3 4 + from slack_sdk.models.views import View 4 5 from slack_sdk.web.client import WebClient 5 6 6 7 from kefi.config import settings ··· 44 45 self, user_id: str, blocks: Sequence[dict], text: str | None = None 45 46 ): 46 47 self.client.chat_postMessage(channel=user_id, blocks=blocks, text=text) 48 + 49 + def open_view(self, trigger_id: str, view: View): 50 + self.client.views_open(trigger_id=trigger_id, view=view) 51 + 52 + def update_view(self, view_id: str, view: View): 53 + self.client.views_update(view_id=view_id, view=view) 54 + 55 + def push_view(self, trigger_id: str, view: View): 56 + self.client.views_push(trigger_id=trigger_id, view=view) 47 57 48 58 def create_group_call( 49 59 self, url: str, users: Sequence[str], external_unique_id: str | None = None
+37
kefi/tests/test_interactivity.py
··· 1 + import json 2 + 3 + from fastapi.testclient import TestClient 4 + from pytest_mock import MockerFixture 5 + from slack_sdk.web.client import WebClient 6 + from sqlmodel import Session 7 + 8 + 9 + def test_default_interactivity( 10 + session: Session, client: TestClient, mocker: MockerFixture 11 + ): 12 + views_open = mocker.patch.object(WebClient, "views_open") 13 + views_open_response: dict = {} # Adds expected response here 14 + views_open.side_effect = [views_open_response] 15 + response = client.post( 16 + "/interactivity/", 17 + data={ 18 + "payload": json.dumps( 19 + { 20 + "type": "shortcut", 21 + "token": "7n993LRqLRydOnIkEhymVcYD", 22 + "action_ts": "1667577118.758775", 23 + "team": {"id": "T0RS507QX", "domain": "dekalabs"}, 24 + "user": { 25 + "id": "UCBKX2GQ4", 26 + "username": "marcos", 27 + "team_id": "T0RS507QX", 28 + }, 29 + "is_enterprise_install": False, 30 + "enterprise": None, 31 + "callback_id": "kefi_meets", 32 + "trigger_id": "4322087874981.25889007847.4301795f6ef2096cbc96f70c5d7bd48b", 33 + } 34 + ) 35 + }, 36 + ) 37 + assert response.status_code == 200