gabay 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gabay-0.1.1/PKG-INFO +117 -0
- gabay-0.1.1/README.md +79 -0
- gabay-0.1.1/gabay/__init__.py +1 -0
- gabay-0.1.1/gabay/cli.py +128 -0
- gabay-0.1.1/gabay/core/config.py +71 -0
- gabay-0.1.1/gabay/core/connectors/calendar_api.py +79 -0
- gabay-0.1.1/gabay/core/connectors/google_api.py +180 -0
- gabay-0.1.1/gabay/core/connectors/imap_api.py +57 -0
- gabay-0.1.1/gabay/core/connectors/meta_api.py +3 -0
- gabay-0.1.1/gabay/core/connectors/notion_api.py +84 -0
- gabay-0.1.1/gabay/core/connectors/oauth.py +146 -0
- gabay-0.1.1/gabay/core/connectors/rss_api.py +62 -0
- gabay-0.1.1/gabay/core/connectors/smtp_api.py +44 -0
- gabay-0.1.1/gabay/core/connectors/token_manager.py +40 -0
- gabay-0.1.1/gabay/core/connectors/weather_api.py +23 -0
- gabay-0.1.1/gabay/core/llm_router.py +136 -0
- gabay-0.1.1/gabay/core/main.py +41 -0
- gabay-0.1.1/gabay/core/memory.py +113 -0
- gabay-0.1.1/gabay/core/skills/brief.py +56 -0
- gabay-0.1.1/gabay/core/skills/calendar.py +49 -0
- gabay-0.1.1/gabay/core/skills/chat.py +40 -0
- gabay-0.1.1/gabay/core/skills/document_qa.py +82 -0
- gabay-0.1.1/gabay/core/skills/email.py +28 -0
- gabay-0.1.1/gabay/core/skills/message.py +41 -0
- gabay-0.1.1/gabay/core/skills/news.py +49 -0
- gabay-0.1.1/gabay/core/skills/read.py +46 -0
- gabay-0.1.1/gabay/core/skills/reminders.py +133 -0
- gabay-0.1.1/gabay/core/skills/save.py +37 -0
- gabay-0.1.1/gabay/core/skills/search.py +46 -0
- gabay-0.1.1/gabay/core/skills/setup.py +24 -0
- gabay-0.1.1/gabay/core/skills/share.py +59 -0
- gabay-0.1.1/gabay/core/skills/weather.py +19 -0
- gabay-0.1.1/gabay/core/telegram_bot.py +221 -0
- gabay-0.1.1/gabay/core/templates/admin.html +348 -0
- gabay-0.1.1/gabay/core/templates/config_setup.html +201 -0
- gabay-0.1.1/gabay/core/templates/google_oauth_setup.html +226 -0
- gabay-0.1.1/gabay/core/templates/groq_setup.html +203 -0
- gabay-0.1.1/gabay/core/templates/notion_setup.html +216 -0
- gabay-0.1.1/gabay/core/templates/setup.html +157 -0
- gabay-0.1.1/gabay/core/templates/smtp_setup.html +262 -0
- gabay-0.1.1/gabay/core/templates/timezone_setup.html +133 -0
- gabay-0.1.1/gabay/core/templates/verify.html +149 -0
- gabay-0.1.1/gabay/core/utils/admin_routes.py +56 -0
- gabay-0.1.1/gabay/core/utils/setup_routes.py +302 -0
- gabay-0.1.1/gabay/core/utils/telegram.py +70 -0
- gabay-0.1.1/gabay/core/utils/userbot.py +84 -0
- gabay-0.1.1/gabay/core/utils/voice.py +27 -0
- gabay-0.1.1/gabay/worker/celery_app.py +26 -0
- gabay-0.1.1/gabay/worker/tasks.py +173 -0
- gabay-0.1.1/gabay.egg-info/PKG-INFO +117 -0
- gabay-0.1.1/gabay.egg-info/SOURCES.txt +57 -0
- gabay-0.1.1/gabay.egg-info/dependency_links.txt +1 -0
- gabay-0.1.1/gabay.egg-info/entry_points.txt +2 -0
- gabay-0.1.1/gabay.egg-info/requires.txt +18 -0
- gabay-0.1.1/gabay.egg-info/top_level.txt +1 -0
- gabay-0.1.1/pyproject.toml +58 -0
- gabay-0.1.1/setup.cfg +4 -0
- gabay-0.1.1/tests/test_brief_skill.py +66 -0
- gabay-0.1.1/tests/test_read_skill.py +77 -0
gabay-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gabay
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A self-hosted, Telegram-first AI assistant for productivity.
|
|
5
|
+
Author-email: Gabay Team <hello@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/gabay
|
|
8
|
+
Project-URL: Documentation, https://github.com/yourusername/gabay#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/yourusername/gabay.git
|
|
10
|
+
Project-URL: Issues, https://github.com/yourusername/gabay/issues
|
|
11
|
+
Keywords: telegram,assistant,ai,productivity,self-hosted
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Communications :: Chat
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: fastapi>=0.100.0
|
|
21
|
+
Requires-Dist: uvicorn>=0.23.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
24
|
+
Requires-Dist: jinja2>=3.1.0
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
26
|
+
Requires-Dist: python-telegram-bot>=20.0
|
|
27
|
+
Requires-Dist: telethon>=1.30.0
|
|
28
|
+
Requires-Dist: celery>=5.3.0
|
|
29
|
+
Requires-Dist: redis>=5.0.0
|
|
30
|
+
Requires-Dist: groq>=0.5.0
|
|
31
|
+
Requires-Dist: google-api-python-client>=2.0.0
|
|
32
|
+
Requires-Dist: google-auth-httplib2>=0.1.0
|
|
33
|
+
Requires-Dist: google-auth-oauthlib>=1.0.0
|
|
34
|
+
Requires-Dist: notion-client>=2.2.0
|
|
35
|
+
Requires-Dist: requests>=2.31.0
|
|
36
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
37
|
+
Requires-Dist: click>=8.0.0
|
|
38
|
+
|
|
39
|
+
# Gabay AI Assistant
|
|
40
|
+
|
|
41
|
+
Gabay is a self-hosted, Docker-based AI assistant designed as a Telegram-first productivity tool. It acts as a unified gateway allowing users to manage Gmail, Google Drive, Notion, and Meta (Facebook/Instagram) properties autonomously.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Daily Briefing (`/brief`)**: Scans unread emails and Facebook/Instagram notifications to provide a prioritized, LLM-summarized update.
|
|
46
|
+
- **Content Reading (`/read`)**: Fetches unread emails and recent Notion pages (try "read my emails" or "what's new in Notion?").
|
|
47
|
+
- **Universal Search (`/search`)**: Deep-search across Google Drive and Notion directly from Telegram.
|
|
48
|
+
- **Seamless Saving**: Reply to any file or PDF with `/save` to upload it to Drive or save to Notion.
|
|
49
|
+
- **Agentic Intent**: Leverages Groq (Llama 3) to understand natural language requests.
|
|
50
|
+
|
|
51
|
+
## Quick Install
|
|
52
|
+
|
|
53
|
+
Gabay is now available as a Python package. You can install it directly via pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install gabay
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then run the setup wizard:
|
|
60
|
+
```bash
|
|
61
|
+
gabay config
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Setup Instructions (Docker)
|
|
65
|
+
Gabay uses both a **Bot** (commands) and a **Userbot** (acting as you).
|
|
66
|
+
1. **Bot**: Create a bot via [@BotFather](https://t.me/botfather) to get your `TELEGRAM_BOT_TOKEN`.
|
|
67
|
+
2. **API Credentials**: Go to [my.telegram.org](https://my.telegram.org/auth), log in, and create an "App" to get your `TELEGRAM_API_ID` and `TELEGRAM_API_HASH`.
|
|
68
|
+
3. **Phone Number**: Your `TELEGRAM_PHONE` is required to authenticate the Userbot session.
|
|
69
|
+
|
|
70
|
+
### 2. Google Cloud Setup (Gmail/Drive)
|
|
71
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
|
72
|
+
2. **Enable APIs**: Enable both the **Gmail API** and **Google Drive API**.
|
|
73
|
+
3. **OAuth Consent Screen**:
|
|
74
|
+
- Set up an "External" consent screen.
|
|
75
|
+
- Under **Test Users**, add your own email address.
|
|
76
|
+
4. **Create Credentials**:
|
|
77
|
+
- Create an **OAuth client ID** (Web application).
|
|
78
|
+
- Add `http://localhost:8000/auth/google/callback` to **Authorized redirect URIs**.
|
|
79
|
+
5. Get your **Client ID** and **Client Secret**.
|
|
80
|
+
|
|
81
|
+
### 3. Notion Setup
|
|
82
|
+
1. Create an **Internal Integration**: Go to [Notion My Integrations](https://www.notion.com/my-integrations).
|
|
83
|
+
2. Get your `NOTION_API_KEY`.
|
|
84
|
+
3. **Share Database**: Open your Notion database, click "..." -> **Connect to**, and find your integration.
|
|
85
|
+
4. Copy the **Database ID** from the URL (the string after the `/` and before the `?`).
|
|
86
|
+
|
|
87
|
+
### 4. Environment Variables
|
|
88
|
+
Create a `.env` file in the root directory:
|
|
89
|
+
```env
|
|
90
|
+
# Telegram
|
|
91
|
+
TELEGRAM_BOT_TOKEN="your_token"
|
|
92
|
+
TELEGRAM_API_ID=12345
|
|
93
|
+
TELEGRAM_API_HASH="your_hash"
|
|
94
|
+
TELEGRAM_PHONE="+123456789"
|
|
95
|
+
|
|
96
|
+
# LLM
|
|
97
|
+
GROQ_API_KEY="your_groq_key"
|
|
98
|
+
|
|
99
|
+
# Google OAuth
|
|
100
|
+
GOOGLE_CLIENT_ID="your_google_id"
|
|
101
|
+
GOOGLE_CLIENT_SECRET="your_google_secret"
|
|
102
|
+
|
|
103
|
+
# Notion
|
|
104
|
+
NOTION_API_KEY="your_notion_key"
|
|
105
|
+
NOTION_DATABASE_ID="your_database_id"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 5. Running and Connecting
|
|
109
|
+
1. **Start**: `docker-compose up -d --build`
|
|
110
|
+
2. **Dashboard**: Send `/auth` to your bot on Telegram.
|
|
111
|
+
3. **Login**: Open the link and click **Connect Account** for each service.
|
|
112
|
+
|
|
113
|
+
## Use Cases
|
|
114
|
+
- "Give me a briefing on my emails today."
|
|
115
|
+
- "Read my latest Notion notes."
|
|
116
|
+
- "Search for 'Project Alpha' on Drive."
|
|
117
|
+
- "Send an email to john@example.com saying I'll be late."
|
gabay-0.1.1/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Gabay AI Assistant
|
|
2
|
+
|
|
3
|
+
Gabay is a self-hosted, Docker-based AI assistant designed as a Telegram-first productivity tool. It acts as a unified gateway allowing users to manage Gmail, Google Drive, Notion, and Meta (Facebook/Instagram) properties autonomously.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Daily Briefing (`/brief`)**: Scans unread emails and Facebook/Instagram notifications to provide a prioritized, LLM-summarized update.
|
|
8
|
+
- **Content Reading (`/read`)**: Fetches unread emails and recent Notion pages (try "read my emails" or "what's new in Notion?").
|
|
9
|
+
- **Universal Search (`/search`)**: Deep-search across Google Drive and Notion directly from Telegram.
|
|
10
|
+
- **Seamless Saving**: Reply to any file or PDF with `/save` to upload it to Drive or save to Notion.
|
|
11
|
+
- **Agentic Intent**: Leverages Groq (Llama 3) to understand natural language requests.
|
|
12
|
+
|
|
13
|
+
## Quick Install
|
|
14
|
+
|
|
15
|
+
Gabay is now available as a Python package. You can install it directly via pip:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install gabay
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then run the setup wizard:
|
|
22
|
+
```bash
|
|
23
|
+
gabay config
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Setup Instructions (Docker)
|
|
27
|
+
Gabay uses both a **Bot** (commands) and a **Userbot** (acting as you).
|
|
28
|
+
1. **Bot**: Create a bot via [@BotFather](https://t.me/botfather) to get your `TELEGRAM_BOT_TOKEN`.
|
|
29
|
+
2. **API Credentials**: Go to [my.telegram.org](https://my.telegram.org/auth), log in, and create an "App" to get your `TELEGRAM_API_ID` and `TELEGRAM_API_HASH`.
|
|
30
|
+
3. **Phone Number**: Your `TELEGRAM_PHONE` is required to authenticate the Userbot session.
|
|
31
|
+
|
|
32
|
+
### 2. Google Cloud Setup (Gmail/Drive)
|
|
33
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
|
34
|
+
2. **Enable APIs**: Enable both the **Gmail API** and **Google Drive API**.
|
|
35
|
+
3. **OAuth Consent Screen**:
|
|
36
|
+
- Set up an "External" consent screen.
|
|
37
|
+
- Under **Test Users**, add your own email address.
|
|
38
|
+
4. **Create Credentials**:
|
|
39
|
+
- Create an **OAuth client ID** (Web application).
|
|
40
|
+
- Add `http://localhost:8000/auth/google/callback` to **Authorized redirect URIs**.
|
|
41
|
+
5. Get your **Client ID** and **Client Secret**.
|
|
42
|
+
|
|
43
|
+
### 3. Notion Setup
|
|
44
|
+
1. Create an **Internal Integration**: Go to [Notion My Integrations](https://www.notion.com/my-integrations).
|
|
45
|
+
2. Get your `NOTION_API_KEY`.
|
|
46
|
+
3. **Share Database**: Open your Notion database, click "..." -> **Connect to**, and find your integration.
|
|
47
|
+
4. Copy the **Database ID** from the URL (the string after the `/` and before the `?`).
|
|
48
|
+
|
|
49
|
+
### 4. Environment Variables
|
|
50
|
+
Create a `.env` file in the root directory:
|
|
51
|
+
```env
|
|
52
|
+
# Telegram
|
|
53
|
+
TELEGRAM_BOT_TOKEN="your_token"
|
|
54
|
+
TELEGRAM_API_ID=12345
|
|
55
|
+
TELEGRAM_API_HASH="your_hash"
|
|
56
|
+
TELEGRAM_PHONE="+123456789"
|
|
57
|
+
|
|
58
|
+
# LLM
|
|
59
|
+
GROQ_API_KEY="your_groq_key"
|
|
60
|
+
|
|
61
|
+
# Google OAuth
|
|
62
|
+
GOOGLE_CLIENT_ID="your_google_id"
|
|
63
|
+
GOOGLE_CLIENT_SECRET="your_google_secret"
|
|
64
|
+
|
|
65
|
+
# Notion
|
|
66
|
+
NOTION_API_KEY="your_notion_key"
|
|
67
|
+
NOTION_DATABASE_ID="your_database_id"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Running and Connecting
|
|
71
|
+
1. **Start**: `docker-compose up -d --build`
|
|
72
|
+
2. **Dashboard**: Send `/auth` to your bot on Telegram.
|
|
73
|
+
3. **Login**: Open the link and click **Connect Account** for each service.
|
|
74
|
+
|
|
75
|
+
## Use Cases
|
|
76
|
+
- "Give me a briefing on my emails today."
|
|
77
|
+
- "Read my latest Notion notes."
|
|
78
|
+
- "Search for 'Project Alpha' on Drive."
|
|
79
|
+
- "Send an email to john@example.com saying I'll be late."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Gabay root package
|
gabay-0.1.1/gabay/cli.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import uvicorn
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Add the current directory to sys.path only if running from source
|
|
10
|
+
if (Path.cwd() / "gabay").exists() and (Path.cwd() / "gabay" / "__init__.py").exists():
|
|
11
|
+
sys.path.insert(0, str(Path.cwd()))
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def cli():
|
|
15
|
+
"""Gabay - Your self-hosted Telegram-first AI assistant."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@cli.command()
|
|
19
|
+
@click.option('--port', default=8000, help='Port to run the API on.')
|
|
20
|
+
@click.option('--host', default='0.0.0.0', help='Host to bind the API to.')
|
|
21
|
+
def api(port, host):
|
|
22
|
+
"""Start the Gabay FastAPI Core Server (OAuth Web UI)."""
|
|
23
|
+
click.echo(f"Starting Gabay API on {host}:{port}...")
|
|
24
|
+
uvicorn.run("gabay.core.main:app", host=host, port=port, reload=False)
|
|
25
|
+
|
|
26
|
+
@cli.command()
|
|
27
|
+
def bot():
|
|
28
|
+
"""Start the Gabay Telegram Bot."""
|
|
29
|
+
click.echo("Starting Gabay Telegram Bot...")
|
|
30
|
+
from gabay.core.telegram_bot import get_telegram_app, start_telegram_polling
|
|
31
|
+
app = get_telegram_app()
|
|
32
|
+
if app:
|
|
33
|
+
asyncio.run(start_telegram_polling(app))
|
|
34
|
+
# start_telegram_polling runs the updater, which blocks until stopped
|
|
35
|
+
# If it doesn't block (like Application.run_polling() does), we need to keep it alive
|
|
36
|
+
# Actually in our code, we did updater.start_polling() which is async. We should probably use run_polling
|
|
37
|
+
# Let's just call app.run_polling() directly
|
|
38
|
+
app.run_polling()
|
|
39
|
+
|
|
40
|
+
@cli.command()
|
|
41
|
+
def worker():
|
|
42
|
+
"""Start the Gabay Celery Worker."""
|
|
43
|
+
click.echo("Starting Gabay Celery Worker...")
|
|
44
|
+
# Using subprocess to run celery
|
|
45
|
+
subprocess.run(["celery", "-A", "gabay.worker.celery_app", "worker", "--loglevel=INFO", "-E"])
|
|
46
|
+
|
|
47
|
+
@cli.command()
|
|
48
|
+
def beat():
|
|
49
|
+
"""Start the Gabay Celery Beat Scheduler."""
|
|
50
|
+
click.echo("Starting Gabay Celery Beat Scheduler...")
|
|
51
|
+
subprocess.run(["celery", "-A", "gabay.worker.celery_app", "beat", "--loglevel=INFO"])
|
|
52
|
+
|
|
53
|
+
@cli.command()
|
|
54
|
+
def all():
|
|
55
|
+
"""Run API, Bot, and Worker concurrently."""
|
|
56
|
+
click.echo("Starting all Gabay services...")
|
|
57
|
+
|
|
58
|
+
# We use subprocess to launch the other commands
|
|
59
|
+
processes = []
|
|
60
|
+
|
|
61
|
+
# API
|
|
62
|
+
processes.append(subprocess.Popen([sys.executable, "-m", "uvicorn", "gabay.core.main:app", "--host", "0.0.0.0", "--port", "8000"]))
|
|
63
|
+
|
|
64
|
+
# Worker
|
|
65
|
+
processes.append(subprocess.Popen(["celery", "-A", "gabay.worker.celery_app", "worker", "--loglevel=INFO", "-E"]))
|
|
66
|
+
|
|
67
|
+
# Beat
|
|
68
|
+
processes.append(subprocess.Popen(["celery", "-A", "gabay.worker.celery_app", "beat", "--loglevel=INFO"]))
|
|
69
|
+
|
|
70
|
+
# Bot
|
|
71
|
+
# We can just run it in the main process, or as another subprocess
|
|
72
|
+
processes.append(subprocess.Popen([sys.executable, "-c", "from gabay.core.telegram_bot import get_telegram_app; app=get_telegram_app(); app and app.run_polling()"]))
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
for p in processes:
|
|
76
|
+
p.wait()
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
click.echo("Stopping all services...")
|
|
79
|
+
for p in processes:
|
|
80
|
+
p.terminate()
|
|
81
|
+
|
|
82
|
+
@cli.command()
|
|
83
|
+
def config():
|
|
84
|
+
"""Interactive setup for environment variables (.env)."""
|
|
85
|
+
click.echo("Gabay Configuration Setup")
|
|
86
|
+
click.echo("This will create or update your local .env file.")
|
|
87
|
+
|
|
88
|
+
from gabay.core.config import save_to_env
|
|
89
|
+
|
|
90
|
+
telegram_bot_token = click.prompt("Telegram Bot Token (from @BotFather)", default="", hide_input=True)
|
|
91
|
+
if telegram_bot_token:
|
|
92
|
+
save_to_env("TELEGRAM_BOT_TOKEN", telegram_bot_token)
|
|
93
|
+
|
|
94
|
+
groq_api_key = click.prompt("Groq API Key (for LLM)", default="", hide_input=True)
|
|
95
|
+
if groq_api_key:
|
|
96
|
+
save_to_env("GROQ_API_KEY", groq_api_key)
|
|
97
|
+
|
|
98
|
+
google_client_id = click.prompt("Google OAuth Client ID", default="")
|
|
99
|
+
if google_client_id:
|
|
100
|
+
save_to_env("GOOGLE_CLIENT_ID", google_client_id)
|
|
101
|
+
|
|
102
|
+
google_client_secret = click.prompt("Google OAuth Client Secret", default="", hide_input=True)
|
|
103
|
+
if google_client_secret:
|
|
104
|
+
save_to_env("GOOGLE_CLIENT_SECRET", google_client_secret)
|
|
105
|
+
|
|
106
|
+
click.echo("\n--- Mailbox Credentials (SMTP) ---")
|
|
107
|
+
smtp_host = click.prompt("SMTP Host (e.g., smtp.gmail.com)", default="smtp.gmail.com")
|
|
108
|
+
save_to_env("SMTP_HOST", smtp_host)
|
|
109
|
+
|
|
110
|
+
smtp_port = click.prompt("SMTP Port", default=587, type=int)
|
|
111
|
+
save_to_env("SMTP_PORT", str(smtp_port))
|
|
112
|
+
|
|
113
|
+
smtp_user = click.prompt("SMTP User (your email)", default="")
|
|
114
|
+
if smtp_user:
|
|
115
|
+
save_to_env("SMTP_USER", smtp_user)
|
|
116
|
+
|
|
117
|
+
smtp_pass = click.prompt("SMTP Password (App Password)", default="", hide_input=True)
|
|
118
|
+
if smtp_pass:
|
|
119
|
+
save_to_env("SMTP_PASSWORD", smtp_pass)
|
|
120
|
+
|
|
121
|
+
click.echo("\n--- Region ---")
|
|
122
|
+
tz = click.prompt("Timezone", default="Asia/Manila")
|
|
123
|
+
save_to_env("TZ", tz)
|
|
124
|
+
|
|
125
|
+
click.echo("✅ .env file updated successfully!")
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
cli()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
class Settings(BaseSettings):
|
|
6
|
+
telegram_bot_token: str = "TBD"
|
|
7
|
+
telegram_api_id: int = 0
|
|
8
|
+
telegram_api_hash: str = ""
|
|
9
|
+
telegram_phone: str = ""
|
|
10
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
11
|
+
log_level: str = "INFO"
|
|
12
|
+
data_dir: str = "/app/data" # in docker
|
|
13
|
+
|
|
14
|
+
# LLM config
|
|
15
|
+
groq_api_key: str = ""
|
|
16
|
+
|
|
17
|
+
# Google OAuth
|
|
18
|
+
google_client_id: str = ""
|
|
19
|
+
google_client_secret: str = ""
|
|
20
|
+
|
|
21
|
+
# Notion API
|
|
22
|
+
notion_api_key: str = ""
|
|
23
|
+
|
|
24
|
+
# OAuth Web UI config
|
|
25
|
+
base_url: str = "http://localhost:8000"
|
|
26
|
+
|
|
27
|
+
# SMTP Email config
|
|
28
|
+
smtp_host: str = ""
|
|
29
|
+
smtp_port: int = 587
|
|
30
|
+
smtp_user: str = ""
|
|
31
|
+
smtp_password: str = ""
|
|
32
|
+
|
|
33
|
+
# Notion config
|
|
34
|
+
notion_database_id: str = ""
|
|
35
|
+
|
|
36
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", arbitrary_types_allowed=True)
|
|
37
|
+
|
|
38
|
+
# For local development without Docker, use local data folder
|
|
39
|
+
if not os.path.exists("/app/data"):
|
|
40
|
+
# Running locally
|
|
41
|
+
import pathlib
|
|
42
|
+
base_dir = pathlib.Path(__file__).parent.parent
|
|
43
|
+
local_data_dir = base_dir / "data"
|
|
44
|
+
local_data_dir.mkdir(exist_ok=True)
|
|
45
|
+
os.environ["DATA_DIR"] = str(local_data_dir)
|
|
46
|
+
|
|
47
|
+
settings = Settings()
|
|
48
|
+
|
|
49
|
+
def save_to_env(key: str, value: str):
|
|
50
|
+
"""Utility to interactively save a new key to the .env file."""
|
|
51
|
+
env_path = pathlib.Path(".env")
|
|
52
|
+
if not env_path.exists():
|
|
53
|
+
env_path.touch()
|
|
54
|
+
|
|
55
|
+
# Read existing lines to avoid duplicates
|
|
56
|
+
with open(env_path, "r") as f:
|
|
57
|
+
lines = f.readlines()
|
|
58
|
+
|
|
59
|
+
with open(env_path, "w") as f:
|
|
60
|
+
key_found = False
|
|
61
|
+
for line in lines:
|
|
62
|
+
if line.startswith(f"{key}="):
|
|
63
|
+
f.write(f"{key}={value}\n")
|
|
64
|
+
key_found = True
|
|
65
|
+
else:
|
|
66
|
+
f.write(line)
|
|
67
|
+
if not key_found:
|
|
68
|
+
f.write(f"{key}={value}\n")
|
|
69
|
+
|
|
70
|
+
# Update current runtime settings implicitly (or caller updates it)
|
|
71
|
+
os.environ[key] = value
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timezone, timedelta
|
|
3
|
+
from gabay.core.connectors.google_api import get_google_service
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def get_events(user_id: str, time_min: str = None, time_max: str = None) -> list[str]:
|
|
8
|
+
"""Retrieve events from the user's primary Google Calendar within a timeframe."""
|
|
9
|
+
service = get_google_service(user_id, "calendar", "v3")
|
|
10
|
+
if not service:
|
|
11
|
+
return ["Google Calendar is not connected. Please authenticate in the Admin Dashboard."]
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
now = datetime.now(timezone.utc)
|
|
15
|
+
|
|
16
|
+
if not time_min:
|
|
17
|
+
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
18
|
+
time_min = start.isoformat()
|
|
19
|
+
|
|
20
|
+
if not time_max:
|
|
21
|
+
end = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
|
22
|
+
time_max = end.isoformat()
|
|
23
|
+
|
|
24
|
+
events_result = service.events().list(
|
|
25
|
+
calendarId='primary',
|
|
26
|
+
timeMin=time_min,
|
|
27
|
+
timeMax=time_max,
|
|
28
|
+
maxResults=50,
|
|
29
|
+
singleEvents=True,
|
|
30
|
+
orderBy='startTime'
|
|
31
|
+
).execute()
|
|
32
|
+
|
|
33
|
+
events = events_result.get('items', [])
|
|
34
|
+
|
|
35
|
+
if not events:
|
|
36
|
+
return ["You have no events scheduled for this timeframe."]
|
|
37
|
+
|
|
38
|
+
parsed_events = []
|
|
39
|
+
for event in events:
|
|
40
|
+
start_time = event['start'].get('dateTime', event['start'].get('date'))
|
|
41
|
+
# Format time
|
|
42
|
+
if 'T' in start_time:
|
|
43
|
+
try:
|
|
44
|
+
dt = datetime.fromisoformat(start_time)
|
|
45
|
+
start_time = dt.strftime("%b %d, %I:%M %p")
|
|
46
|
+
except:
|
|
47
|
+
pass
|
|
48
|
+
summary = event.get('summary', 'Untitled Event')
|
|
49
|
+
parsed_events.append(f"• {start_time}: {summary}")
|
|
50
|
+
|
|
51
|
+
return parsed_events
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Error fetching calendar events: {e}")
|
|
54
|
+
return [f"Failed to fetch calendar events. Error: {str(e)}"]
|
|
55
|
+
|
|
56
|
+
def create_event(user_id: str, summary: str, start_time: str, end_time: str) -> str:
|
|
57
|
+
"""Create a new event on the user's primary calendar."""
|
|
58
|
+
service = get_google_service(user_id, "calendar", "v3")
|
|
59
|
+
if not service:
|
|
60
|
+
return "Google Calendar is not connected."
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
event = {
|
|
64
|
+
'summary': summary,
|
|
65
|
+
'start': {
|
|
66
|
+
'dateTime': start_time,
|
|
67
|
+
'timeZone': 'UTC',
|
|
68
|
+
},
|
|
69
|
+
'end': {
|
|
70
|
+
'dateTime': end_time,
|
|
71
|
+
'timeZone': 'UTC',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
event = service.events().insert(calendarId='primary', body=event).execute()
|
|
76
|
+
return f"Event created: {event.get('htmlLink')}"
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Error creating calendar event: {e}")
|
|
79
|
+
return f"Failed to create event. Error: {str(e)}"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from googleapiclient.discovery import build
|
|
3
|
+
from google.oauth2.credentials import Credentials
|
|
4
|
+
from google.auth.transport.requests import Request as GoogleRequest
|
|
5
|
+
import json
|
|
6
|
+
from gabay.core.connectors.token_manager import token_manager
|
|
7
|
+
from gabay.core.config import settings
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
def get_google_service(user_id: str, service_name: str, version: str):
|
|
12
|
+
token_data = token_manager.get_token("google", str(user_id))
|
|
13
|
+
if not token_data:
|
|
14
|
+
logger.warning(f"No Google token found for user {user_id}")
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
logger.debug(f"Getting Google service for user {user_id}. Provider keys: {list(token_data.keys())}")
|
|
18
|
+
|
|
19
|
+
# Inject client credentials if missing from stored token
|
|
20
|
+
if 'client_id' not in token_data or not token_data['client_id']:
|
|
21
|
+
token_data['client_id'] = settings.google_client_id
|
|
22
|
+
if 'client_secret' not in token_data or not token_data['client_secret']:
|
|
23
|
+
token_data['client_secret'] = settings.google_client_secret
|
|
24
|
+
|
|
25
|
+
creds = Credentials.from_authorized_user_info(token_data)
|
|
26
|
+
|
|
27
|
+
# Check if we need to refresh the token
|
|
28
|
+
if creds and creds.expired and creds.refresh_token:
|
|
29
|
+
try:
|
|
30
|
+
logger.info(f"Refreshing Google token for user {user_id}...")
|
|
31
|
+
creds.refresh(GoogleRequest())
|
|
32
|
+
# Save refreshed token back to storage
|
|
33
|
+
new_token_data = json.loads(creds.to_json())
|
|
34
|
+
token_manager.save_token("google", user_id, new_token_data)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.error(f"Failed to refresh Google token for {user_id}: {e}")
|
|
37
|
+
# If refresh fails, we might want to notify the user to re-pair
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return build(service_name, version, credentials=creds)
|
|
41
|
+
|
|
42
|
+
def get_unread_emails(user_id: str) -> list[str]:
|
|
43
|
+
service = get_google_service(user_id, "gmail", "v1")
|
|
44
|
+
if not service:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
results = service.users().messages().list(userId='me', q='is:unread', maxResults=5).execute()
|
|
49
|
+
messages = results.get('messages', [])
|
|
50
|
+
|
|
51
|
+
email_summaries = []
|
|
52
|
+
for msg in messages:
|
|
53
|
+
m = service.users().messages().get(userId='me', id=msg['id']).execute()
|
|
54
|
+
# Extract basic info
|
|
55
|
+
headers = m['payload']['headers']
|
|
56
|
+
subject = next((h['value'] for h in headers if h['name'] == 'Subject'), "No Subject")
|
|
57
|
+
sender = next((h['value'] for h in headers if h['name'] == 'From'), "Unknown")
|
|
58
|
+
email_summaries.append(f"From {sender}: {subject}")
|
|
59
|
+
return email_summaries
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Gmail error: {e}")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
def upload_file_to_drive(user_id: str, file_path: str, mime_type: str) -> str:
|
|
65
|
+
# Minimal implementation for now
|
|
66
|
+
return "https://drive.google.com/upload/mock"
|
|
67
|
+
|
|
68
|
+
def search_drive(user_id: str, query: str) -> list[dict]:
|
|
69
|
+
service = get_google_service(user_id, "drive", "v3")
|
|
70
|
+
if not service:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
q = f"name contains '{query}' or fullText contains '{query}'"
|
|
75
|
+
results = service.files().list(
|
|
76
|
+
q=q,
|
|
77
|
+
spaces='drive',
|
|
78
|
+
fields='files(id, name, mimeType, webViewLink)',
|
|
79
|
+
pageSize=5
|
|
80
|
+
).execute()
|
|
81
|
+
|
|
82
|
+
files = results.get('files', [])
|
|
83
|
+
return [{"id": f['id'], "title": f['name'], "mimeType": f.get('mimeType', ''), "link": f.get('webViewLink', '')} for f in files]
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Drive search error: {e}")
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
def send_email(user_id: str, recipient: str, subject: str, body: str) -> str:
|
|
89
|
+
# Actually most users prefer SMTP for simple sending if they have credentials,
|
|
90
|
+
# but since this is google_api.py, we could use Gmail API.
|
|
91
|
+
# For now, let's keep the placeholder or use the logic from worker tasks if it's there.
|
|
92
|
+
return f"Successfully queued email to {recipient} via Google API."
|
|
93
|
+
|
|
94
|
+
def share_file(user_id: str, file_id: str) -> str:
|
|
95
|
+
"""Updates file permissions to 'anyone with link can read' and returns the webViewLink."""
|
|
96
|
+
service = get_google_service(user_id, "drive", "v3")
|
|
97
|
+
if not service:
|
|
98
|
+
return {"error": "Not connected to Google Drive."}
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Create a permission allowing anyone with the link to read
|
|
102
|
+
body = {
|
|
103
|
+
'type': 'anyone',
|
|
104
|
+
'role': 'reader'
|
|
105
|
+
}
|
|
106
|
+
service.permissions().create(
|
|
107
|
+
fileId=file_id,
|
|
108
|
+
body=body
|
|
109
|
+
).execute()
|
|
110
|
+
|
|
111
|
+
# Get the webViewLink
|
|
112
|
+
file_info = service.files().get(fileId=file_id, fields='webViewLink, name').execute()
|
|
113
|
+
return {"link": file_info['webViewLink'], "name": file_info['name']}
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Error sharing file {file_id}: {e}")
|
|
116
|
+
return {"error": f"Error sharing file: {e}"}
|
|
117
|
+
|
|
118
|
+
def download_drive_file(user_id: str, file_id: str, mime_type: str) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Downloads or exports a file from Google Drive as plain text.
|
|
121
|
+
Google Docs ('application/vnd.google-apps.document') must be exported.
|
|
122
|
+
Standard text files ('text/plain') are downloaded directly.
|
|
123
|
+
"""
|
|
124
|
+
service = get_google_service(user_id, "drive", "v3")
|
|
125
|
+
if not service:
|
|
126
|
+
return "Not connected to Google Drive."
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if 'application/vnd.google-apps.document' in mime_type:
|
|
130
|
+
# Export Google Doc to text
|
|
131
|
+
request = service.files().export_media(fileId=file_id, mimeType='text/plain')
|
|
132
|
+
elif 'text/' in mime_type or 'application/json' in mime_type or 'application/csv' in mime_type:
|
|
133
|
+
# Download regular text file
|
|
134
|
+
request = service.files().get_media(fileId=file_id)
|
|
135
|
+
elif 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' in mime_type:
|
|
136
|
+
# Download binary .docx
|
|
137
|
+
request = service.files().get_media(fileId=file_id)
|
|
138
|
+
else:
|
|
139
|
+
return f"Unsupported file type for summarization: {mime_type}"
|
|
140
|
+
|
|
141
|
+
from io import BytesIO
|
|
142
|
+
from googleapiclient.http import MediaIoBaseDownload
|
|
143
|
+
|
|
144
|
+
fh = BytesIO()
|
|
145
|
+
downloader = MediaIoBaseDownload(fh, request)
|
|
146
|
+
done = False
|
|
147
|
+
while done is False:
|
|
148
|
+
status, done = downloader.next_chunk()
|
|
149
|
+
|
|
150
|
+
file_bytes = fh.getvalue()
|
|
151
|
+
|
|
152
|
+
# Word docx parsing
|
|
153
|
+
if 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' in mime_type:
|
|
154
|
+
import zipfile
|
|
155
|
+
import xml.etree.ElementTree as ET
|
|
156
|
+
try:
|
|
157
|
+
with zipfile.ZipFile(BytesIO(file_bytes)) as d:
|
|
158
|
+
xml_content = d.read('word/document.xml')
|
|
159
|
+
tree = ET.XML(xml_content)
|
|
160
|
+
WORD_NAMESPACE = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
|
161
|
+
PARA = WORD_NAMESPACE + 'p'
|
|
162
|
+
TEXT = WORD_NAMESPACE + 't'
|
|
163
|
+
|
|
164
|
+
paragraphs = []
|
|
165
|
+
for paragraph in tree.iter(PARA):
|
|
166
|
+
texts = [node.text for node in paragraph.iter(TEXT) if node.text]
|
|
167
|
+
if texts:
|
|
168
|
+
paragraphs.append(''.join(texts))
|
|
169
|
+
return '\n\n'.join(paragraphs)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Failed to parse docx: {e}")
|
|
172
|
+
return f"Error extracting text from DOCX: {e}"
|
|
173
|
+
|
|
174
|
+
# Return the decoded text
|
|
175
|
+
return file_bytes.decode('utf-8')
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Error downloading file {file_id}: {e}")
|
|
179
|
+
return f"Error downloading file: {e}"
|
|
180
|
+
|