implemented teams integration
This commit is contained in:
@@ -2339,6 +2339,22 @@ async def update_post_status(
|
||||
)
|
||||
except Exception as tg_err:
|
||||
logger.warning(f"Telegram notification failed: {tg_err}")
|
||||
# Send Teams Adaptive Card with approve/reject buttons
|
||||
if settings.teams_enabled:
|
||||
try:
|
||||
from src.services.teams_service import teams_service as _ts
|
||||
teams_account = await db.get_teams_account(post.user_id)
|
||||
if _ts and teams_account and teams_account.is_active:
|
||||
await _ts.send_approval_card(
|
||||
teams_account=teams_account,
|
||||
post_title=post.topic_title or "Untitled Post",
|
||||
post_content=post.post_content or "",
|
||||
approve_token=approve_token,
|
||||
reject_token=reject_token,
|
||||
company_name=company_name,
|
||||
)
|
||||
except Exception as teams_err:
|
||||
logger.warning(f"Teams notification failed: {teams_err}")
|
||||
else:
|
||||
# Employee/owner moving their own post to "approved": no email/telegram notification
|
||||
pass
|
||||
@@ -2507,6 +2523,11 @@ async def settings_page(request: Request):
|
||||
if settings.telegram_enabled:
|
||||
telegram_account = await db.get_telegram_account(user_id)
|
||||
|
||||
# Get Teams account if feature is enabled
|
||||
teams_account = None
|
||||
if settings.teams_enabled:
|
||||
teams_account = await db.get_teams_account(user_id)
|
||||
|
||||
# Get company permissions if user is an employee with a company
|
||||
company_permissions = None
|
||||
company = None
|
||||
@@ -2523,6 +2544,9 @@ async def settings_page(request: Request):
|
||||
"linkedin_account": linkedin_account,
|
||||
"telegram_enabled": settings.telegram_enabled,
|
||||
"telegram_account": telegram_account,
|
||||
"teams_enabled": settings.teams_enabled,
|
||||
"teams_account": teams_account,
|
||||
"microsoft_app_id": settings.microsoft_app_id,
|
||||
"company_permissions": company_permissions,
|
||||
"company": company,
|
||||
})
|
||||
@@ -5595,3 +5619,152 @@ if settings.telegram_enabled:
|
||||
|
||||
await db.delete_telegram_account(UUID(session.user_id))
|
||||
return JSONResponse({"success": True})
|
||||
|
||||
|
||||
# ==================== MICROSOFT TEAMS BOT ====================
|
||||
|
||||
if settings.teams_enabled:
|
||||
import secrets as _secrets_teams
|
||||
from src.services.teams_service import teams_service as _teams_service
|
||||
|
||||
@user_router.get("/settings/teams/start")
|
||||
async def teams_start_link(request: Request):
|
||||
"""Generate a one-time Teams linking token and return the deep link."""
|
||||
session = require_user_session(request)
|
||||
if not session:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
from src.services.redis_client import get_redis
|
||||
token = _secrets_teams.token_urlsafe(32)
|
||||
try:
|
||||
redis = await get_redis()
|
||||
await redis.setex(f"teams_link:{token}", 600, session.user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store teams link token: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to generate link")
|
||||
|
||||
deep_link = (
|
||||
f"https://teams.microsoft.com/l/chat/0/0"
|
||||
f"?users=28:{settings.microsoft_app_id}&message=link:{token}"
|
||||
)
|
||||
return JSONResponse({"deep_link": deep_link, "token": token})
|
||||
|
||||
async def _handle_teams_link(activity: dict, link_token: str) -> None:
|
||||
"""Handle account linking when user sends 'link:{token}' to the bot."""
|
||||
from src.services.redis_client import get_redis
|
||||
from src.database.models import TeamsAccount
|
||||
|
||||
try:
|
||||
redis = await get_redis()
|
||||
link_key = f"teams_link:{link_token}"
|
||||
raw_user_id = await redis.get(link_key)
|
||||
if not raw_user_id:
|
||||
logger.warning(f"Teams link token not found or expired: {link_token[:8]}…")
|
||||
if _teams_service:
|
||||
conv_id = activity.get("conversation", {}).get("id", "")
|
||||
service_url = activity.get("serviceUrl", "")
|
||||
if conv_id and service_url:
|
||||
from src.database.models import TeamsAccount as _TA
|
||||
dummy = _TA(
|
||||
user_id=UUID("00000000-0000-0000-0000-000000000000"),
|
||||
teams_service_url=service_url,
|
||||
teams_conversation_id=conv_id,
|
||||
)
|
||||
await _teams_service.send_text_message(
|
||||
dummy,
|
||||
"❌ Dieser Link ist ungültig oder abgelaufen. Bitte erstelle einen neuen Link in der App.",
|
||||
)
|
||||
return
|
||||
|
||||
user_id_str = raw_user_id.decode() if isinstance(raw_user_id, bytes) else raw_user_id
|
||||
await redis.delete(link_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to look up teams link token: {e}")
|
||||
return
|
||||
|
||||
# Extract conversation reference from incoming activity
|
||||
service_url = activity.get("serviceUrl", "")
|
||||
conversation_id = activity.get("conversation", {}).get("id", "")
|
||||
teams_user_id = activity.get("from", {}).get("id", "")
|
||||
tenant_id = activity.get("channelData", {}).get("tenant", {}).get("id")
|
||||
|
||||
if not service_url or not conversation_id:
|
||||
logger.error("Teams link activity missing serviceUrl or conversation.id")
|
||||
return
|
||||
|
||||
account = TeamsAccount(
|
||||
user_id=UUID(user_id_str),
|
||||
teams_user_id=teams_user_id or None,
|
||||
teams_tenant_id=tenant_id,
|
||||
teams_service_url=service_url,
|
||||
teams_conversation_id=conversation_id,
|
||||
is_active=True,
|
||||
)
|
||||
await db.save_teams_account(account)
|
||||
|
||||
if _teams_service:
|
||||
await _teams_service.send_text_message(
|
||||
account,
|
||||
"✅ Dein Teams-Konto ist jetzt verbunden! Du erhältst ab sofort Freigabe-Anfragen direkt hier.",
|
||||
)
|
||||
|
||||
async def _handle_teams_approval(activity: dict, action: str, token: str) -> None:
|
||||
"""Handle approve/reject invoke from an Adaptive Card button."""
|
||||
from src.services.email_service import validate_token, mark_all_post_tokens_used
|
||||
|
||||
token_data = await validate_token(token)
|
||||
if not token_data:
|
||||
logger.warning(f"Teams approval: invalid or used token {token[:8]}…")
|
||||
return
|
||||
|
||||
post_id = UUID(token_data["post_id"])
|
||||
new_status = "ready" if action == "approve" else "draft"
|
||||
await db.update_generated_post(post_id, {"status": new_status})
|
||||
await db.mark_all_post_tokens_used(post_id)
|
||||
|
||||
logger.info(f"Teams approval handled: post={post_id} action={action} status={new_status}")
|
||||
|
||||
@user_router.post("/api/teams/webhook")
|
||||
async def teams_webhook(request: Request):
|
||||
"""Receive Microsoft Teams bot activities."""
|
||||
# Validate incoming JWT
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if _teams_service and not await _teams_service.validate_incoming_token(auth_header):
|
||||
raise HTTPException(status_code=403, detail="Invalid Teams JWT")
|
||||
|
||||
try:
|
||||
activity = await request.json()
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
|
||||
activity_type = activity.get("type", "")
|
||||
|
||||
if activity_type == "message":
|
||||
text = (activity.get("text") or "").strip()
|
||||
if text.startswith("link:"):
|
||||
link_token = text[5:].strip()
|
||||
await _handle_teams_link(activity, link_token)
|
||||
|
||||
elif activity_type == "invoke" and activity.get("name") == "adaptiveCard/action":
|
||||
value = activity.get("value", {})
|
||||
action = value.get("action", {})
|
||||
action_data = action.get("data", {}) if isinstance(action, dict) else {}
|
||||
act = action_data.get("action", "")
|
||||
token = action_data.get("token", "")
|
||||
if act in ("approve", "reject") and token:
|
||||
await _handle_teams_approval(activity, act, token)
|
||||
# Teams requires a synchronous 200 with statusCode for invoke
|
||||
return JSONResponse({"statusCode": 200})
|
||||
|
||||
# conversationUpdate and other types — ignore
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
@user_router.post("/api/settings/teams/disconnect")
|
||||
async def teams_disconnect(request: Request):
|
||||
"""Disconnect Teams account."""
|
||||
session = require_user_session(request)
|
||||
if not session:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
await db.delete_teams_account(UUID(session.user_id))
|
||||
return JSONResponse({"success": True})
|
||||
|
||||
Reference in New Issue
Block a user