diff --git a/vue-apps-py/README.MD b/vue-apps-py/README.MD index 19ffda0..6ea9a7a 100644 --- a/vue-apps-py/README.MD +++ b/vue-apps-py/README.MD @@ -11,10 +11,21 @@ python3 -m pip install "uvicorn[standard]" After importing an app such as `main.py`: ```python +from typing import List from src.vue_apps_py.security import SecurityHelper -from src.vue_apps_py.server import VueApp +from src.vue_apps_py.server import VueApp, name_intents from src.vue_apps_py.client import ClientAPI -from src.vue_apps_py.server_utils.models import Intent +from src.vue_apps_py.server_utils.intents import Intent, InlineQuery +from src.vue_apps_py.server_utils.models import AutoModel, SuggestionsModel + + +def autocomplete_handler(query_model: AutoModel) -> SuggestionsModel: + # Just log the query + print(query_model.json()) + + # Send back empty model + return SuggestionsModel() + # Initialize API api = ClientAPI(client_id='vue-app-0', client_secret='abccf12389efab222') @@ -22,8 +33,12 @@ api = ClientAPI(client_id='vue-app-0', client_secret='abccf12389efab222') # Prepare Security key_holder = SecurityHelper(public_key='abbcbbbcaksjdhf/skdjhfnnsn/sjdkfjj21234=') +# Define intents +intents: List[Intent] = [InlineQuery(autocomplete_handler)] +named_intents = name_intents(intents) + # Get Vue Application Server -vue_app = VueApp(client_api=api, security_helper=key_holder, intents=[Intent.INLINE_QUERY], debug=True) +vue_app = VueApp(client_api=api, security_helper=key_holder, named_intents=named_intents, debug=True) ``` You can launch web server: diff --git a/vue-apps-py/src/vue_apps_py/security.py b/vue-apps-py/src/vue_apps_py/security.py index 652f394..5ae13d4 100644 --- a/vue-apps-py/src/vue_apps_py/security.py +++ b/vue-apps-py/src/vue_apps_py/security.py @@ -7,7 +7,7 @@ class SecurityHelper: def __init__(self, public_key: str): self.public_key = VerifyingKey(public_key.encode(), encoding='base64') - def check(self, http: bytes, signature: bytes) -> bool: + def is_valid(self, http: bytes, signature: bytes) -> bool: try: self.public_key.verify(signature, http, encoding='base64') return True diff --git a/vue-apps-py/src/vue_apps_py/server.py b/vue-apps-py/src/vue_apps_py/server.py index ed9aedb..879ff8d 100644 --- a/vue-apps-py/src/vue_apps_py/server.py +++ b/vue-apps-py/src/vue_apps_py/server.py @@ -1,38 +1,89 @@ -from typing import List +from typing import Dict, List -from fastapi import FastAPI, Request, HTTPException +from fastapi import FastAPI, Request, HTTPException, status -from client import ClientAPI -from security import SecurityHelper -from server_utils.models import PongModel, Intent +from .client import ClientAPI +from .security import SecurityHelper +from .server_utils.intents import Intent, InlineQuery, PageMentions, Interactions, PageApp +from .server_utils.models import PongModel, SuggestionsModel, AutoModel, MentionModel, InteractionModel, PageModel -def VueApp(client_api: ClientAPI, security_helper: SecurityHelper, intents: List[Intent], debug: bool = False): +def name_intents(intents: List[Intent]) -> Dict[str, Intent]: + dictionary: Dict[str, Intent] = dict() + for intent in intents: + if issubclass(type(intent), Intent): + dictionary[str(intent)] = intent + return dictionary + + +def VueApp(client_api: ClientAPI, + security_helper: SecurityHelper, + named_intents: Dict[str, Intent], + debug: bool = False) -> FastAPI: app = FastAPI(debug=debug, title='VueApp', description='Vue Application Server', version='0.0.1') - async def check_request(request: Request) -> bool: + async def check_request(request: Request): if 'X-Signature-Ed25519' not in request.headers or 'X-Signature-Timestamp' not in request.headers: - return False + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Verification of signature failed') body_to_verify = await request.body() timestamp = request.headers['X-Signature-Timestamp'].encode('utf-8') signature = request.headers['X-Signature-Ed25519'].encode('utf-8') - return security_helper.check(timestamp + body_to_verify, signature) + if not security_helper.is_valid(timestamp + body_to_verify, signature): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Verification of signature failed') @app.post(path='/ping', response_model=PongModel) async def handle_ping(request: Request) -> PongModel: - ok = await check_request(request) - if not ok: - raise HTTPException(status_code=401, detail='Verification of signature failed') - + await check_request(request) return PongModel(message='pong', token=client_api.get_token()) - if Intent.INLINE_QUERY in intents: - @app.post(path='/auto', response_model=PongModel) - async def handle_autocomplete(request: Request) -> PongModel: - ok = await check_request(request) - if not ok: - raise HTTPException(status_code=401, detail='Verification of signature failed') + if PageApp.intent_name in named_intents and isinstance(named_intents[PageApp.intent_name], PageApp): + page_app_intent: PageApp = named_intents[PageApp.intent_name] + start_handler = page_app_intent.added_page_handler + stop_handler = page_app_intent.removed_page_handler - return PongModel(message='pong', token=client_api.get_token()) + @app.post(path='/start', status_code=status.HTTP_201_CREATED) + async def handle_integration(request: Request, model: PageModel): + await check_request(request) + acknowledged = start_handler(model) + if not acknowledged: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + + @app.post(path='/stop', status_code=status.HTTP_204_NO_CONTENT) + async def handle_disconnect(request: Request, model: PageModel): + await check_request(request) + acknowledged = stop_handler(model) + if not acknowledged: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + + if InlineQuery.intent_name in named_intents and isinstance(named_intents[InlineQuery.intent_name], InlineQuery): + inline_query_intent: InlineQuery = named_intents[InlineQuery.intent_name] + auto_handler = inline_query_intent.query_handler + + @app.post(path='/auto', response_model=SuggestionsModel) + async def handle_autocomplete(request: Request, model: AutoModel) -> SuggestionsModel: + await check_request(request) + return auto_handler(model) + + if PageMentions.intent_name in named_intents and isinstance(named_intents[PageMentions.intent_name], PageMentions): + page_mentions_intent: PageMentions = named_intents[PageMentions.intent_name] + mentions_handler = page_mentions_intent.query_handler + + @app.post(path='/mention', status_code=status.HTTP_202_ACCEPTED) + async def handle_mentions(request: Request, model: MentionModel): + await check_request(request) + acknowledged = mentions_handler(model) + if not acknowledged: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + + if Interactions.intent_name in named_intents and isinstance(named_intents[Interactions.intent_name], Interactions): + interactions_intent: Interactions = named_intents[Interactions.intent_name] + interactions_handler = interactions_intent.query_handler + + @app.post(path='/interact', status_code=status.HTTP_202_ACCEPTED) + async def handle_interactions(request: Request, model: InteractionModel): + await check_request(request) + acknowledged = interactions_handler(model) + if not acknowledged: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) return app diff --git a/vue-apps-py/src/vue_apps_py/server_utils/intents.py b/vue-apps-py/src/vue_apps_py/server_utils/intents.py new file mode 100644 index 0000000..b562bc0 --- /dev/null +++ b/vue-apps-py/src/vue_apps_py/server_utils/intents.py @@ -0,0 +1,46 @@ +from typing import Callable + +from .models import AutoModel, SuggestionsModel, MentionModel, InteractionModel, PageModel + + +class Intent: + intent_name: str + + def __str__(self) -> str: + return self.intent_name + + +class PageApp(Intent): + intent_name = 'PAGE_APP' + added_page_handler: Callable[[PageModel], bool] + removed_page_handler: Callable[[PageModel], bool] + + def __init__(self, + added_page_handler: Callable[[PageModel], bool], + removed_page_handler: Callable[[PageModel], bool]): + self.added_page_handler = added_page_handler + self.removed_page_handler = removed_page_handler + + +class InlineQuery(Intent): + intent_name = 'INLINE_QUERY' + query_handler: Callable[[AutoModel], SuggestionsModel] + + def __init__(self, query_handler: Callable[[AutoModel], SuggestionsModel]): + self.query_handler = query_handler + + +class PageMentions(Intent): + intent_name = 'PAGE_MENTIONS' + query_handler: Callable[[MentionModel], bool] + + def __init__(self, query_handler: Callable[[MentionModel], bool]): + self.query_handler = query_handler + + +class Interactions(Intent): + intent_name = 'INTERACTIONS' + query_handler: Callable[[InteractionModel], bool] + + def __init__(self, query_handler: Callable[[InteractionModel], bool]): + self.query_handler = query_handler diff --git a/vue-apps-py/src/vue_apps_py/server_utils/models.py b/vue-apps-py/src/vue_apps_py/server_utils/models.py index efb4efc..2821deb 100644 --- a/vue-apps-py/src/vue_apps_py/server_utils/models.py +++ b/vue-apps-py/src/vue_apps_py/server_utils/models.py @@ -1,4 +1,3 @@ -from enum import Enum, auto from typing import Optional from pydantic import BaseModel, Field @@ -13,8 +12,21 @@ class PongModel(BaseModel): token: Optional[str] = Field(default=None, title='Token', description='Token for API to check') -class Intent(Enum): - PAGE_APP = auto() - PAGE_MENTIONS = auto() - INLINE_QUERY = auto() - INTERACTIONS = auto() +class AutoModel(BaseModel): + pass + + +class SuggestionsModel(BaseModel): + pass + + +class MentionModel(BaseModel): + pass + + +class InteractionModel(BaseModel): + pass + + +class PageModel(BaseModel): + pass