Define Server API scheme

API contains optional endpoints defined by list of Intents
This commit is contained in:
havlong
2022-12-16 17:06:59 +03:00
parent e184c49a1f
commit d6574f2bed
5 changed files with 154 additions and 30 deletions

View File

@@ -11,10 +11,21 @@ python3 -m pip install "uvicorn[standard]"
After importing an app such as `main.py`: After importing an app such as `main.py`:
```python ```python
from typing import List
from src.vue_apps_py.security import SecurityHelper 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.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 # Initialize API
api = ClientAPI(client_id='vue-app-0', client_secret='abccf12389efab222') 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 # Prepare Security
key_holder = SecurityHelper(public_key='abbcbbbcaksjdhf/skdjhfnnsn/sjdkfjj21234=') 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 # 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: You can launch web server:

View File

@@ -7,7 +7,7 @@ class SecurityHelper:
def __init__(self, public_key: str): def __init__(self, public_key: str):
self.public_key = VerifyingKey(public_key.encode(), encoding='base64') 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: try:
self.public_key.verify(signature, http, encoding='base64') self.public_key.verify(signature, http, encoding='base64')
return True return True

View File

@@ -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 .client import ClientAPI
from security import SecurityHelper from .security import SecurityHelper
from server_utils.models import PongModel, Intent 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') 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: 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() body_to_verify = await request.body()
timestamp = request.headers['X-Signature-Timestamp'].encode('utf-8') timestamp = request.headers['X-Signature-Timestamp'].encode('utf-8')
signature = request.headers['X-Signature-Ed25519'].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) @app.post(path='/ping', response_model=PongModel)
async def handle_ping(request: Request) -> PongModel: async def handle_ping(request: Request) -> PongModel:
ok = await check_request(request) await check_request(request)
if not ok:
raise HTTPException(status_code=401, detail='Verification of signature failed')
return PongModel(message='pong', token=client_api.get_token()) return PongModel(message='pong', token=client_api.get_token())
if Intent.INLINE_QUERY in intents: if PageApp.intent_name in named_intents and isinstance(named_intents[PageApp.intent_name], PageApp):
@app.post(path='/auto', response_model=PongModel) page_app_intent: PageApp = named_intents[PageApp.intent_name]
async def handle_autocomplete(request: Request) -> PongModel: start_handler = page_app_intent.added_page_handler
ok = await check_request(request) stop_handler = page_app_intent.removed_page_handler
if not ok:
raise HTTPException(status_code=401, detail='Verification of signature failed')
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 return app

View File

@@ -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

View File

@@ -1,4 +1,3 @@
from enum import Enum, auto
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field 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') token: Optional[str] = Field(default=None, title='Token', description='Token for API to check')
class Intent(Enum): class AutoModel(BaseModel):
PAGE_APP = auto() pass
PAGE_MENTIONS = auto()
INLINE_QUERY = auto()
INTERACTIONS = auto() class SuggestionsModel(BaseModel):
pass
class MentionModel(BaseModel):
pass
class InteractionModel(BaseModel):
pass
class PageModel(BaseModel):
pass