Skip to content

Commit f837a6e

Browse files
authored
Feat/shareable brains send link be (QuivrHQ#599)
* 🗃️ new table for invitations to subscribe to brain * ✨ new BrainSubscription class * ✨ new subscription router * 👽️ add RESEND_API_KEY to .env in BE * 📦 add 'resend' lib to requirements * ♻️ fix some stanGPT
1 parent 8749ffd commit f837a6e

File tree

8 files changed

+149
-9
lines changed

8 files changed

+149
-9
lines changed

.backend_env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ MAX_REQUESTS_NUMBER=200
1313
PRIVATE=False
1414
MODEL_PATH=./local_models/ggml-gpt4all-j-v1.3-groovy.bin
1515
MODEL_N_CTX=1000
16-
MODEL_N_BATCH=8
16+
MODEL_N_BATCH=8
17+
18+
#RESEND
19+
RESEND_API_KEY=

backend/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from routes.crawl_routes import crawl_router
1313
from routes.explore_routes import explore_router
1414
from routes.misc_routes import misc_router
15+
from routes.subscription_routes import subscription_router
1516
from routes.upload_routes import upload_router
1617
from routes.user_routes import user_router
1718

@@ -46,7 +47,7 @@ async def startup_event():
4647
app.include_router(upload_router)
4748
app.include_router(user_router)
4849
app.include_router(api_key_router)
49-
50+
app.include_router(subscription_router)
5051

5152
@app.exception_handler(HTTPException)
5253
async def http_exception_handler(_, exc):

backend/models/brains.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,13 @@ class Brain(BaseModel):
2020
max_tokens: Optional[int] = 256
2121
max_brain_size: Optional[int] = int(os.getenv("MAX_BRAIN_SIZE", 0))
2222
files: List[Any] = []
23-
_commons: Optional[CommonsDep] = None
2423

2524
class Config:
2625
arbitrary_types_allowed = True
2726

2827
@property
2928
def commons(self) -> CommonsDep:
30-
if not self._commons:
31-
self.__class__._commons = common_dependencies()
32-
return self._commons # pyright: ignore reportPrivateUsage=none
29+
return common_dependencies()
3330

3431
@property
3532
def brain_size(self):
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
from typing import Optional
3+
from uuid import UUID
4+
5+
import resend
6+
from logger import get_logger
7+
from models.settings import CommonsDep, common_dependencies
8+
from pydantic import BaseModel
9+
10+
logger = get_logger(__name__)
11+
12+
13+
class BrainSubscription(BaseModel):
14+
brain_id: Optional[UUID] = None
15+
inviter_email: Optional[str]
16+
email: Optional[str]
17+
rights: Optional[str]
18+
19+
class Config:
20+
arbitrary_types_allowed = True
21+
22+
@property
23+
def commons(self) -> CommonsDep:
24+
return common_dependencies()
25+
26+
def create_subscription_invitation(self):
27+
logger.info("Creating subscription invitation")
28+
response = (
29+
self.commons["supabase"]
30+
.table("brain_subscription_invitations")
31+
.insert({"brain_id": str(self.brain_id), "email": self.email, "rights": self.rights})
32+
.execute()
33+
)
34+
return response.data
35+
36+
def update_subscription_invitation(self):
37+
logger.info('Updating subscription invitation')
38+
response = (
39+
self.commons["supabase"]
40+
.table("brain_subscription_invitations")
41+
.update({"rights": self.rights})
42+
.eq("brain_id", str(self.brain_id))
43+
.eq("email", self.email)
44+
.execute()
45+
)
46+
return response.data
47+
48+
def create_or_update_subscription_invitation(self):
49+
response = self.commons["supabase"].table("brain_subscription_invitations").select("*").eq("brain_id", str(self.brain_id)).eq("email", self.email).execute()
50+
51+
if response.data:
52+
response = self.update_subscription_invitation()
53+
else:
54+
response = self.create_subscription_invitation()
55+
56+
return response
57+
58+
def get_brain_url(self) -> str:
59+
"""Generates the brain URL based on the brain_id."""
60+
base_url = "https://www.quivr.app/chat"
61+
return f"{base_url}?brain_subscription_invitation={self.brain_id}"
62+
63+
def resend_invitation_email(self):
64+
resend.api_key = os.getenv("RESEND_API_KEY")
65+
66+
brain_url = self.get_brain_url()
67+
68+
html_body = f"""
69+
<p>This brain has been shared with you by {self.inviter_email}.</p>
70+
<p><a href='{brain_url}'>Click here</a> to access your brain.</p>
71+
"""
72+
73+
try:
74+
r = resend.Emails.send({
75+
"from": "[email protected]",
76+
"to": self.email,
77+
"subject": "Quivr - Brain Shared With You",
78+
"html": html_body
79+
})
80+
print('Resend response', r)
81+
except Exception as e:
82+
logger.error(f"Error sending email: {e}")
83+
return
84+
85+
return r

backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ flake8==6.0.0
2323
flake8-black==0.3.6
2424
sentence_transformers>=2.0.0
2525
sentry-sdk==1.26.0
26-
pyright==1.1.316
26+
pyright==1.1.316
27+
resend==0.5.1
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import List
2+
from uuid import UUID
3+
4+
from auth.auth_bearer import get_current_user
5+
from fastapi import APIRouter, Depends, HTTPException
6+
from models.brains_subscription_invitations import BrainSubscription
7+
from models.users import User
8+
9+
subscription_router = APIRouter()
10+
11+
12+
@subscription_router.post("/brain/{brain_id}/subscription")
13+
async def invite_user_to_brain(brain_id: UUID, users: List[dict], current_user: User = Depends(get_current_user)):
14+
# TODO: Ensure the current user has permissions to invite users to this brain
15+
16+
for user in users:
17+
subscription = BrainSubscription(brain_id=brain_id, email=user['email'], rights=user['rights'], inviter_email=current_user.email or "Quivr")
18+
19+
try:
20+
subscription.create_or_update_subscription_invitation()
21+
subscription.resend_invitation_email()
22+
except Exception as e:
23+
raise HTTPException(status_code=400, detail=f"Error inviting user: {e}")
24+
25+
return {"message": "Invitations sent successfully"}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
BEGIN;
2+
3+
-- Create brain_subscription_invitations table if it doesn't exist
4+
CREATE TABLE IF NOT EXISTS brain_subscription_invitations (
5+
brain_id UUID,
6+
email VARCHAR(255),
7+
rights VARCHAR(255),
8+
PRIMARY KEY (brain_id, email),
9+
FOREIGN KEY (brain_id) REFERENCES Brains (brain_id)
10+
);
11+
12+
INSERT INTO migrations (name)
13+
SELECT '202307111517030_add_subscription_invitations_table'
14+
WHERE NOT EXISTS (
15+
SELECT 1 FROM migrations WHERE name = '202307111517030_add_subscription_invitations_table'
16+
);
17+
18+
COMMIT;

scripts/tables.sql

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,23 @@ CREATE TABLE IF NOT EXISTS brains_vectors (
158158
FOREIGN KEY (brain_id) REFERENCES brains (brain_id)
159159
);
160160

161+
-- Create brains X vectors table
162+
CREATE TABLE IF NOT EXISTS brain_subscription_invitations (
163+
brain_id UUID,
164+
email VARCHAR(255),
165+
rights VARCHAR(255),
166+
PRIMARY KEY (brain_id, email),
167+
FOREIGN KEY (brain_id) REFERENCES Brains (brain_id)
168+
);
169+
170+
161171
CREATE TABLE IF NOT EXISTS migrations (
162172
name VARCHAR(255) PRIMARY KEY,
163173
executed_at TIMESTAMPTZ DEFAULT current_timestamp
164174
);
165175

166176
INSERT INTO migrations (name)
167-
SELECT '20230629143400_add_file_sha1_brains_vectors'
177+
SELECT '202307111517030_add_subscription_invitations_table'
168178
WHERE NOT EXISTS (
169-
SELECT 1 FROM migrations WHERE name = '20230629143400_add_file_sha1_brains_vectors'
179+
SELECT 1 FROM migrations WHERE name = '202307111517030_add_subscription_invitations_table'
170180
);

0 commit comments

Comments
 (0)