allow user to be added via id
This commit is contained in:
@@ -45,6 +45,7 @@ POCKETBASE_ADMIN_PASSWORD=secret
|
|||||||
|
|
||||||
- Notes:
|
- Notes:
|
||||||
- `ALLOWED_USERS` should be a comma-separated list of Telegram user IDs (no brackets).
|
- `ALLOWED_USERS` should be a comma-separated list of Telegram user IDs (no brackets).
|
||||||
|
- `ALLOWED_USERS` acts as the bootstrap allowlist; the bot also checks the PocketBase `Telegram` collection for persisted access.
|
||||||
- The bot reads `TG_TOKEN` and `ALLOWED_USERS` from the environment.
|
- The bot reads `TG_TOKEN` and `ALLOWED_USERS` from the environment.
|
||||||
|
|
||||||
6. Ollama (local LLM) setup
|
6. Ollama (local LLM) setup
|
||||||
@@ -99,6 +100,7 @@ python main.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
The bot listens for commands:
|
The bot listens for commands:
|
||||||
|
- `/add <id>` — grant a Telegram user ID access through the `Telegram` collection
|
||||||
- `/op <url or paste>` — parse an opportunity
|
- `/op <url or paste>` — parse an opportunity
|
||||||
- `/ev <url or paste>` — parse an event
|
- `/ev <url or paste>` — parse an event
|
||||||
- If you send a URL directly in chat, the bot will ask whether to process it as an event or an opportunity using buttons.
|
- If you send a URL directly in chat, the bot will ask whether to process it as an event or an opportunity using buttons.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
69
bot.py
69
bot.py
@@ -13,7 +13,7 @@ from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, fil
|
|||||||
|
|
||||||
# Import your existing logic
|
# Import your existing logic
|
||||||
from agent import parse_page
|
from agent import parse_page
|
||||||
from database import upload_entry
|
from database import add_telegram_user_id, get_telegram_user_ids, upload_entry
|
||||||
from scraper import get_clean_content as _get_clean_content
|
from scraper import get_clean_content as _get_clean_content
|
||||||
|
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ TOKEN = os.getenv("TG_TOKEN")
|
|||||||
_allowed_env = os.getenv("ALLOWED_USERS", "")
|
_allowed_env = os.getenv("ALLOWED_USERS", "")
|
||||||
if _allowed_env:
|
if _allowed_env:
|
||||||
try:
|
try:
|
||||||
ALLOWED_IDS = [int(x.strip()) for x in _allowed_env.split(',') if x.strip()]
|
ALLOWED_IDS = {int(x.strip()) for x in _allowed_env.split(',') if x.strip()}
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning("Failed to parse ALLOWED_USERS from .env; defaulting to empty list")
|
logging.warning("Failed to parse ALLOWED_USERS from .env; defaulting to empty list")
|
||||||
ALLOWED_IDS = []
|
ALLOWED_IDS = set()
|
||||||
else:
|
else:
|
||||||
ALLOWED_IDS = []
|
ALLOWED_IDS = set()
|
||||||
|
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
logging.warning("TG_TOKEN not set in .env; bot will not start without a token")
|
logging.warning("TG_TOKEN not set in .env; bot will not start without a token")
|
||||||
@@ -95,6 +95,17 @@ def retry(max_attempts=3, backoff_factor=2, initial_delay=1):
|
|||||||
task_queue = asyncio.Queue()
|
task_queue = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
|
def is_authorized_user(user_id: int) -> bool:
|
||||||
|
if user_id in ALLOWED_IDS:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
return user_id in set(get_telegram_user_ids())
|
||||||
|
except Exception as exc:
|
||||||
|
logging.warning("Failed to fetch Telegram access list: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_http_url(text: str) -> bool:
|
def is_http_url(text: str) -> bool:
|
||||||
return bool(re.match(r'^https?://\S+$', text.strip()))
|
return bool(re.match(r'^https?://\S+$', text.strip()))
|
||||||
|
|
||||||
@@ -188,19 +199,56 @@ async def process_link(update, context, source_value, entry_type="opportunity",
|
|||||||
# --- Handlers ---
|
# --- Handlers ---
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if update.effective_user.id not in ALLOWED_IDS:
|
if not is_authorized_user(update.effective_user.id):
|
||||||
return
|
return
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"Welcome! I can extract arts opportunities and events.\n\n"
|
"Welcome! I can extract arts opportunities and events.\n\n"
|
||||||
"📋 **Commands:**\n"
|
"📋 **Commands:**\n"
|
||||||
|
"/add <id> - Allow a Telegram user ID access\n"
|
||||||
"/op <url> - Extract an opportunity\n"
|
"/op <url> - Extract an opportunity\n"
|
||||||
"/ev <url> - Extract an event\n\n"
|
"/ev <url> - Extract an event\n\n"
|
||||||
"You can also send a URL directly and I will ask whether to process it as an event or opportunity."
|
"You can also send a URL directly and I will ask whether to process it as an event or opportunity."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
requester_id = update.effective_user.id
|
||||||
|
if not is_authorized_user(requester_id):
|
||||||
|
await update.message.reply_text("Unauthorized. User ID needs to be added!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not context.args:
|
||||||
|
await update.message.reply_text("Usage: /add <telegram_user_id>")
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_user_id = context.args[0].strip()
|
||||||
|
try:
|
||||||
|
user_id = int(raw_user_id)
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text("Please provide a valid numeric Telegram user ID. Usage: /add <telegram_user_id>")
|
||||||
|
return
|
||||||
|
|
||||||
|
if user_id in ALLOWED_IDS:
|
||||||
|
await update.message.reply_text(f"{user_id} already has access.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_ids = set(get_telegram_user_ids())
|
||||||
|
if user_id in existing_ids:
|
||||||
|
ALLOWED_IDS.add(user_id)
|
||||||
|
await update.message.reply_text(f"{user_id} already has access.")
|
||||||
|
return
|
||||||
|
|
||||||
|
add_telegram_user_id(user_id)
|
||||||
|
ALLOWED_IDS.add(user_id)
|
||||||
|
await update.message.reply_text(f"Added {user_id} to Telegram access.")
|
||||||
|
except Exception as exc:
|
||||||
|
logging.exception("Failed to add Telegram user ID %s", user_id)
|
||||||
|
await update.message.reply_text(f"Failed to add {user_id}: {exc}")
|
||||||
|
|
||||||
async def handle_opportunity(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_opportunity(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id not in ALLOWED_IDS:
|
if not is_authorized_user(user_id):
|
||||||
await update.message.reply_text("Unauthorized. User ID needs to be added!")
|
await update.message.reply_text("Unauthorized. User ID needs to be added!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -220,7 +268,7 @@ async def handle_opportunity(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
|
|
||||||
async def handle_event(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_event(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id not in ALLOWED_IDS:
|
if not is_authorized_user(user_id):
|
||||||
await update.message.reply_text("Unauthorized. User ID needs to be added!")
|
await update.message.reply_text("Unauthorized. User ID needs to be added!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -239,7 +287,7 @@ async def handle_event(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
await task_queue.put((update, context, input_text, "event", source_kind))
|
await task_queue.put((update, context, input_text, "event", source_kind))
|
||||||
|
|
||||||
async def handle_followup_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_followup_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if update.effective_user.id not in ALLOWED_IDS:
|
if not is_authorized_user(update.effective_user.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not context.user_data.get('awaiting_save_url'):
|
if not context.user_data.get('awaiting_save_url'):
|
||||||
@@ -279,6 +327,10 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
|
if not is_authorized_user(query.from_user.id):
|
||||||
|
await query.edit_message_text("Unauthorized. User ID needs to be added!")
|
||||||
|
return
|
||||||
|
|
||||||
if query.data.startswith('choose_type:'):
|
if query.data.startswith('choose_type:'):
|
||||||
pending_url = context.user_data.get('pending_url_to_process')
|
pending_url = context.user_data.get('pending_url_to_process')
|
||||||
if not pending_url:
|
if not pending_url:
|
||||||
@@ -325,6 +377,7 @@ async def _main():
|
|||||||
application = ApplicationBuilder().token(TOKEN).build()
|
application = ApplicationBuilder().token(TOKEN).build()
|
||||||
|
|
||||||
application.add_handler(CommandHandler("start", start))
|
application.add_handler(CommandHandler("start", start))
|
||||||
|
application.add_handler(CommandHandler("add", handle_add_user))
|
||||||
application.add_handler(CommandHandler("op", handle_opportunity))
|
application.add_handler(CommandHandler("op", handle_opportunity))
|
||||||
application.add_handler(CommandHandler("ev", handle_event))
|
application.add_handler(CommandHandler("ev", handle_event))
|
||||||
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle_followup_text))
|
application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle_followup_text))
|
||||||
|
|||||||
108
database.py
108
database.py
@@ -9,6 +9,112 @@ load_dotenv()
|
|||||||
pb = PocketBase(os.getenv('POCKETBASE_URL'))
|
pb = PocketBase(os.getenv('POCKETBASE_URL'))
|
||||||
admin_data = pb.admins.auth_with_password(os.getenv('POCKETBASE_ADMIN_EMAIL'), os.getenv('POCKETBASE_ADMIN_PASSWORD'))
|
admin_data = pb.admins.auth_with_password(os.getenv('POCKETBASE_ADMIN_EMAIL'), os.getenv('POCKETBASE_ADMIN_PASSWORD'))
|
||||||
show_debug_msg = False
|
show_debug_msg = False
|
||||||
|
TELEGRAM_COLLECTION_NAME = 'Telegram'
|
||||||
|
|
||||||
|
|
||||||
|
def _record_value(record, key, default=None):
|
||||||
|
def _lookup(mapping):
|
||||||
|
if not isinstance(mapping, dict):
|
||||||
|
return default
|
||||||
|
|
||||||
|
if key in mapping:
|
||||||
|
return mapping.get(key, default)
|
||||||
|
|
||||||
|
lower_key = key.lower()
|
||||||
|
for existing_key, existing_value in mapping.items():
|
||||||
|
if str(existing_key).lower() == lower_key:
|
||||||
|
return existing_value
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
if isinstance(record, dict):
|
||||||
|
return _lookup(record)
|
||||||
|
|
||||||
|
if hasattr(record, 'get'):
|
||||||
|
try:
|
||||||
|
value = record.get(key, default)
|
||||||
|
if value is not default:
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(record, 'data'):
|
||||||
|
try:
|
||||||
|
value = _lookup(record.data)
|
||||||
|
if value is not default:
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(record, 'model_dump'):
|
||||||
|
try:
|
||||||
|
value = _lookup(record.model_dump())
|
||||||
|
if value is not default:
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(record, 'to_dict'):
|
||||||
|
try:
|
||||||
|
value = _lookup(record.to_dict())
|
||||||
|
if value is not default:
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(record, '__dict__'):
|
||||||
|
value = _lookup(record.__dict__)
|
||||||
|
if value is not default:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if hasattr(record, key):
|
||||||
|
return getattr(record, key)
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_telegram_id(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(str(value).strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_telegram_user_ids():
|
||||||
|
records = pb.collection(TELEGRAM_COLLECTION_NAME).get_full_list()
|
||||||
|
telegram_ids = set()
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
raw_telegram_id = _record_value(record, 'TGID')
|
||||||
|
telegram_id = _normalize_telegram_id(raw_telegram_id)
|
||||||
|
if telegram_id is not None:
|
||||||
|
telegram_ids.add(telegram_id)
|
||||||
|
|
||||||
|
record_id = _normalize_telegram_id(_record_value(record, 'id'))
|
||||||
|
if record_id is not None:
|
||||||
|
telegram_ids.add(record_id)
|
||||||
|
|
||||||
|
return sorted(telegram_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def add_telegram_user_id(user_id: int):
|
||||||
|
collection = pb.collection(TELEGRAM_COLLECTION_NAME)
|
||||||
|
payload_candidates = [
|
||||||
|
{'TGID': user_id},
|
||||||
|
{'id': str(user_id)},
|
||||||
|
]
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for payload in payload_candidates:
|
||||||
|
try:
|
||||||
|
return collection.create(payload)
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
|
||||||
|
raise last_error
|
||||||
|
|
||||||
def convert_datetime_to_pocketbase(date_time_str):
|
def convert_datetime_to_pocketbase(date_time_str):
|
||||||
"""
|
"""
|
||||||
@@ -52,7 +158,7 @@ def upload_entry(data, entry_type='opportunity', url=None):
|
|||||||
entry_type: 'opportunity' or 'event'
|
entry_type: 'opportunity' or 'event'
|
||||||
url: The source URL of the entry
|
url: The source URL of the entry
|
||||||
"""
|
"""
|
||||||
print(f"[DEBUG] Uploading {entry_type} entry. Data: {data["title"]}")
|
print(f"[DEBUG] Uploading {entry_type} entry. Data: {data['title']}")
|
||||||
data = dict(data)
|
data = dict(data)
|
||||||
|
|
||||||
# Add URL to data if provided
|
# Add URL to data if provided
|
||||||
|
|||||||
Reference in New Issue
Block a user