| from datetime import datetime, timedelta |
| import json |
| import os |
| import secrets |
| import sys |
| import time |
| from urllib.parse import urlencode |
| from flask import Flask, jsonify, redirect, request |
| import requests |
|
|
| |
| os.environ["ASANA_CLIENT_ID"] = "1211551350187489" |
| os.environ["ASANA_CLIENT_SECRET"] = "a4d944583e2e3fd199b678ece03762b0" |
| os.environ["ASANA_REDIRECT_URI"] = "http://localhost:8000/api/auth/asana/callback" |
|
|
| |
| SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID", "YOUR_SLACK_CLIENT_ID") |
| SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET", "YOUR_SLACK_CLIENT_SECRET") |
| SLACK_REDIRECT_URI = os.getenv( |
| "SLACK_REDIRECT_URI", "http://localhost:8000/api/auth/slack/callback" |
| ) |
|
|
| |
| SLACK_AUTH_URL = "https://slack.com/oauth/v2/authorize" |
| SLACK_TOKEN_URL = "https://slack.com/api/oauth.v2.access" |
|
|
| |
| SLACK_SCOPES = [ |
| "channels:read", |
| "channels:history", |
| "groups:read", |
| "groups:history", |
| "im:read", |
| "im:history", |
| "mpim:read", |
| "mpim:history", |
| "chat:write", |
| "chat:write.public", |
| "users:read", |
| "users:read.email", |
| "team:read", |
| ] |
|
|
| |
| app = Flask(__name__) |
| app.config["SECRET_KEY"] = "atom-backend-with-slack-integration" |
|
|
| |
| sessions = {} |
| slack_tokens = {} |
|
|
|
|
| class SlackService: |
| """Simple Slack service for basic operations""" |
|
|
| def __init__(self, access_token=None): |
| self.access_token = access_token |
| self.base_url = "https://slack.com/api" |
|
|
| def _make_request(self, endpoint, method="GET", data=None): |
| """Make authenticated request to Slack API""" |
| if not self.access_token: |
| return {"ok": False, "error": "No access token available"} |
|
|
| headers = { |
| "Authorization": f"Bearer {self.access_token}", |
| "Content-Type": "application/json", |
| } |
|
|
| url = f"{self.base_url}/{endpoint}" |
|
|
| try: |
| if method == "GET": |
| response = requests.get(url, headers=headers, timeout=30) |
| else: |
| response = requests.post(url, headers=headers, json=data, timeout=30) |
|
|
| return response.json() |
| except Exception as e: |
| return {"ok": False, "error": str(e)} |
|
|
| def get_channels(self): |
| """Get list of channels""" |
| return self._make_request( |
| "conversations.list?types=public_channel,private_channel" |
| ) |
|
|
| def get_users(self): |
| """Get list of users""" |
| return self._make_request("users.list") |
|
|
| def send_message(self, channel, text): |
| """Send message to channel""" |
| data = {"channel": channel, "text": text} |
| return self._make_request("chat.postMessage", method="POST", data=data) |
|
|
| def get_channel_history(self, channel, limit=100): |
| """Get channel message history""" |
| return self._make_request( |
| f"conversations.history?channel={channel}&limit={limit}" |
| ) |
|
|
| def get_user_info(self, user_id): |
| """Get user information""" |
| return self._make_request(f"users.info?user={user_id}") |
|
|
|
|
| |
| slack_service = SlackService() |
|
|
|
|
| @app.route("/health") |
| def health(): |
| return jsonify( |
| { |
| "status": "ok", |
| "service": "atom-backend-with-slack-integration", |
| "version": "1.0.0", |
| "timestamp": time.time(), |
| "asana_configured": bool( |
| os.getenv("ASANA_CLIENT_ID") and os.getenv("ASANA_CLIENT_SECRET") |
| ), |
| "slack_configured": bool( |
| SLACK_CLIENT_ID != "YOUR_SLACK_CLIENT_ID" |
| and SLACK_CLIENT_SECRET != "YOUR_SLACK_CLIENT_SECRET" |
| ), |
| } |
| ) |
|
|
|
|
| @app.route("/") |
| def root(): |
| return jsonify( |
| { |
| "name": "ATOM Backend with Slack Integration", |
| "status": "running", |
| "asana_client_id": os.getenv("ASANA_CLIENT_ID", "not_set"), |
| "slack_client_id": SLACK_CLIENT_ID |
| if SLACK_CLIENT_ID != "YOUR_SLACK_CLIENT_ID" |
| else "not_set", |
| "endpoints": { |
| "health": "/health", |
| "asana_health": "/api/asana/health", |
| "asana_oauth": "/api/auth/asana/authorize", |
| "asana_callback": "/api/auth/asana/callback", |
| "slack_health": "/api/slack/health", |
| "slack_oauth": "/api/auth/slack/authorize", |
| "slack_callback": "/api/auth/slack/callback", |
| "slack_channels": "/api/slack/channels", |
| "slack_users": "/api/slack/users", |
| "slack_send_message": "/api/slack/send-message", |
| }, |
| } |
| ) |
|
|
|
|
| |
| @app.route("/api/asana/health") |
| def asana_health(): |
| client_id = os.getenv("ASANA_CLIENT_ID") |
| return jsonify( |
| { |
| "ok": True, |
| "service": "asana", |
| "status": "ready", |
| "client_id_configured": bool(client_id), |
| "message": "Asana integration ready for OAuth flow", |
| "endpoints": { |
| "oauth_authorize": "/api/auth/asana/authorize", |
| "oauth_callback": "/api/auth/asana/callback", |
| }, |
| } |
| ) |
|
|
|
|
| @app.route("/api/auth/asana/authorize") |
| def asana_authorize(): |
| user_id = request.args.get("user_id", "default_user") |
| client_id = os.getenv("ASANA_CLIENT_ID") |
| redirect_uri = os.getenv("ASANA_REDIRECT_URI") |
|
|
| if not client_id: |
| return jsonify({"ok": False, "error": "ASANA_CLIENT_ID not configured"}), 400 |
|
|
| |
| state = secrets.token_urlsafe(16) |
| sessions[state] = {"user_id": user_id, "timestamp": time.time()} |
|
|
| |
| params = { |
| "client_id": client_id, |
| "redirect_uri": redirect_uri, |
| "response_type": "code", |
| "state": state, |
| "scope": "default", |
| } |
|
|
| auth_url = f"https://app.asana.com/-/oauth_authorize?{urlencode(params)}" |
|
|
| return jsonify( |
| { |
| "ok": True, |
| "auth_url": auth_url, |
| "user_id": user_id, |
| "state": state, |
| } |
| ) |
|
|
|
|
| @app.route("/api/auth/asana/callback") |
| def asana_callback(): |
| code = request.args.get("code") |
| state = request.args.get("state") |
| error = request.args.get("error") |
|
|
| if error: |
| return jsonify({"ok": False, "error": error}), 400 |
|
|
| if not code: |
| return jsonify({"ok": False, "error": "No authorization code received"}), 400 |
|
|
| if not state or state not in sessions: |
| return jsonify({"ok": False, "error": "Invalid state parameter"}), 400 |
|
|
| user_session = sessions[state] |
| user_id = user_session["user_id"] |
|
|
| |
| client_id = os.getenv("ASANA_CLIENT_ID") |
| client_secret = os.getenv("ASANA_CLIENT_SECRET") |
| redirect_uri = os.getenv("ASANA_REDIRECT_URI") |
|
|
| token_data = { |
| "grant_type": "authorization_code", |
| "client_id": client_id, |
| "client_secret": client_secret, |
| "code": code, |
| "redirect_uri": redirect_uri, |
| } |
|
|
| try: |
| response = requests.post("https://app.asana.com/-/oauth_token", data=token_data) |
| token_info = response.json() |
|
|
| if "access_token" in token_info: |
| |
| sessions[f"asana_token_{user_id}"] = { |
| "access_token": token_info["access_token"], |
| "expires_at": time.time() + token_info.get("expires_in", 3600), |
| "refresh_token": token_info.get("refresh_token"), |
| } |
|
|
| return jsonify( |
| { |
| "ok": True, |
| "message": "Asana connected successfully", |
| "user_id": user_id, |
| "access_token": token_info["access_token"], |
| } |
| ) |
| else: |
| return jsonify( |
| {"ok": False, "error": token_info.get("error", "Token exchange failed")} |
| ), 400 |
|
|
| except Exception as e: |
| return jsonify({"ok": False, "error": str(e)}), 500 |
|
|
|
|
| |
| @app.route("/api/slack/health") |
| def slack_health(): |
| return jsonify( |
| { |
| "ok": True, |
| "service": "slack", |
| "status": "ready", |
| "client_id_configured": bool(SLACK_CLIENT_ID != "YOUR_SLACK_CLIENT_ID"), |
| "message": "Slack integration ready for OAuth flow", |
| "endpoints": { |
| "oauth_authorize": "/api/auth/slack/authorize", |
| "oauth_callback": "/api/auth/slack/callback", |
| "channels": "/api/slack/channels", |
| "users": "/api/slack/users", |
| "send_message": "/api/slack/send-message", |
| }, |
| } |
| ) |
|
|
|
|
| @app.route("/api/auth/slack/authorize") |
| def slack_authorize(): |
| """Initiate Slack OAuth flow""" |
| user_id = request.args.get("user_id", "default_user") |
|
|
| if SLACK_CLIENT_ID == "YOUR_SLACK_CLIENT_ID": |
| return jsonify({"ok": False, "error": "SLACK_CLIENT_ID not configured"}), 400 |
|
|
| |
| state = secrets.token_urlsafe(32) |
| sessions[f"slack_state_{state}"] = {"user_id": user_id, "timestamp": time.time()} |
|
|
| |
| auth_params = { |
| "client_id": SLACK_CLIENT_ID, |
| "redirect_uri": SLACK_REDIRECT_URI, |
| "scope": ",".join(SLACK_SCOPES), |
| "state": state, |
| "user_scope": "chat:write,users:read", |
| } |
|
|
| auth_url = f"{SLACK_AUTH_URL}?{urlencode(auth_params)}" |
|
|
| return jsonify( |
| { |
| "ok": True, |
| "auth_url": auth_url, |
| "user_id": user_id, |
| "state": state, |
| "scopes": SLACK_SCOPES, |
| } |
| ) |
|
|
|
|
| @app.route("/api/auth/slack/callback") |
| def slack_callback(): |
| """Handle Slack OAuth callback""" |
| code = request.args.get("code") |
| state = request.args.get("state") |
| error = request.args.get("error") |
|
|
| if error: |
| return jsonify({"ok": False, "error": error}), 400 |
|
|
| if not code: |
| return jsonify({"ok": False, "error": "No authorization code received"}), 400 |
|
|
| state_key = f"slack_state_{state}" |
| if not state or state_key not in sessions: |
| return jsonify({"ok": False, "error": "Invalid state parameter"}), 400 |
|
|
| user_session = sessions[state_key] |
| user_id = user_session["user_id"] |
|
|
| |
| token_data = { |
| "client_id": SLACK_CLIENT_ID, |
| "client_secret": SLACK_CLIENT_SECRET, |
| "code": code, |
| "redirect_uri": SLACK_REDIRECT_URI, |
| } |
|
|
| try: |
| response = requests.post(SLACK_TOKEN_URL, data=token_data) |
| token_info = response.json() |
|
|
| if token_info.get("ok"): |
| access_token = token_info["access_token"] |
| refresh_token = token_info.get("refresh_token") |
| expires_in = token_info.get("expires_in", 3600) |
| team_id = token_info.get("team", {}).get("id") |
| team_name = token_info.get("team", {}).get("name") |
|
|
| |
| slack_tokens[user_id] = { |
| "access_token": access_token, |
| "refresh_token": refresh_token, |
| "expires_at": time.time() + expires_in, |
| "team_id": team_id, |
| "team_name": team_name, |
| } |
|
|
| |
| slack_service.access_token = access_token |
|
|
| |
| del sessions[state_key] |
|
|
| return jsonify( |
| { |
| "ok": True, |
| "message": "Slack connected successfully", |
| "user_id": user_id, |
| "team_name": team_name, |
| "team_id": team_id, |
| "access_token": access_token, |
| } |
| ) |
| else: |
| return jsonify( |
| {"ok": False, "error": token_info.get("error", "Token exchange failed")} |
| ), 400 |
|
|
| except Exception as e: |
| return jsonify({"ok": False, "error": str(e)}), 500 |
|
|
|
|
| @app.route("/api/slack/channels") |
| def slack_channels(): |
| """Get list of Slack channels""" |
| user_id = request.args.get("user_id", "default_user") |
|
|
| if user_id not in slack_tokens: |
| return jsonify({"ok": False, "error": "User not authenticated with Slack"}), 401 |
|
|
| |
| slack_service.access_token = slack_tokens[user_id]["access_token"] |
|
|
| result = slack_service.get_channels() |
| return jsonify(result) |
|
|
|
|
| @app.route("/api/slack/users") |
| def slack_users(): |
| """Get list of Slack users""" |
| user_id = request.args.get("user_id", "default_user") |
|
|
| if user_id not in slack_tokens: |
| return jsonify({"ok": False, "error": "User not authenticated with Slack"}), 401 |
|
|
| |
| slack_service.access_token = slack_tokens[user_id]["access_token"] |
|
|
| result = slack_service.get_users() |
| return jsonify(result) |
|
|
|
|
| @app.route("/api/slack/send-message", methods=["POST"]) |
| def slack_send_message(): |
| """Send message to Slack channel""" |
| user_id = request.json.get("user_id", "default_user") |
| channel = request.json.get("channel") |
| text = request.json.get("text") |
|
|
| if not channel or not text: |
| return jsonify({"ok": False, "error": "Channel and text are required"}), 400 |
|
|
| if user_id not in slack_tokens: |
| return jsonify({"ok": False, "error": "User not authenticated with Slack"}), 401 |
|
|
| |
| slack_service.access_token = slack_tokens[user_id]["access_token"] |
|
|
| result = slack_service.send_message(channel, text) |
| return jsonify(result) |
|
|
|
|
| @app.route("/api/slack/status") |
| def slack_status(): |
| """Get Slack connection status""" |
| user_id = request.args.get("user_id", "default_user") |
|
|
| if user_id in slack_tokens: |
| token_info = slack_tokens[user_id] |
| is_expired = time.time() > token_info["expires_at"] |
|
|
| return jsonify( |
| { |
| "ok": True, |
| "connected": True, |
| "expired": is_expired, |
| "team_name": token_info.get("team_name"), |
| "team_id": token_info.get("team_id"), |
| } |
| ) |
| else: |
| return jsonify({"ok": True, "connected": False, "expired": False}) |
|
|
|
|
| if __name__ == "__main__": |
| print("🚀 STARTING ATOM BACKEND WITH SLACK INTEGRATION") |
| print("================================================") |
| print("📋 Configuration:") |
| print(f" • Asana Client ID: {os.getenv('ASANA_CLIENT_ID', 'Not set')}") |
| print( |
| f" • Slack Client ID: {SLACK_CLIENT_ID if SLACK_CLIENT_ID != 'YOUR_SLACK_CLIENT_ID' else 'Not set'}" |
| ) |
| print("") |
| print("🌐 Available Endpoints:") |
| print(" • Health: http://localhost:8000/health") |
| print( |
| " • Asana OAuth: http://localhost:8000/api/auth/asana/authorize?user_id=test" |
| ) |
| print( |
| " • Slack OAuth: http://localhost:8000/api/auth/slack/authorize?user_id=test" |
| ) |
| print(" • Slack Channels: http://localhost:8000/api/slack/channels?user_id=test") |
| print(" • Slack Users: http://localhost:8000/api/slack/users?user_id=test") |
| print("") |
| print("🔐 OAuth Flows:") |
| print(" 1. Visit /api/auth/slack/authorize to get auth URL") |
| print(" 2. Complete authorization in Slack") |
| print(" 3. Slack redirects to /api/auth/slack/callback") |
| print(" 4. Backend exchanges code for access token") |
| print("") |
|
|
| app.run(host="0.0.0.0", port=8000, debug=True) |
|
|