allow user to be added via id

This commit is contained in:
2026-05-11 11:34:38 +01:00
parent 5b434abd06
commit 7a2db35495
8 changed files with 170 additions and 9 deletions

View File

@@ -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
View File

@@ -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))

View File

@@ -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