├── .env.example ├── .vscode ├── settings.json └── launch.json ├── data.pickle ├── requirements.txt ├── utils └── auth.py ├── .gitignore ├── README.md ├── api.py └── main.py /.env.example: -------------------------------------------------------------------------------- 1 | MENDABLE_API_KEY='' 2 | TELEGRAM_BOT_TOKEN='' 3 | ALLOWED_CHAT_IDS='' -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic" 3 | } -------------------------------------------------------------------------------- /data.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klingefjord/chatgpt-telegram/HEAD/data.pickle -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-telegram-bot 2 | python-dotenv 3 | nest-asyncio 4 | langchain 5 | requests 6 | openai -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /utils/auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import dotenv 3 | import os 4 | 5 | dotenv.load_dotenv() 6 | 7 | allowed_chat_ids = os.getenv("ALLOWED_CHAT_IDS").split(",") 8 | 9 | 10 | def auth(): 11 | """Verify that the user is allowed to use the bot.""" 12 | 13 | def decorator(func: callable): 14 | @wraps(func) 15 | async def wrapper(update, context): 16 | if str(update.message.chat_id) in allowed_chat_ids: 17 | await func(update, context) 18 | else: 19 | await update.message.reply_text( 20 | "You are not authorized to use this bot" 21 | ) 22 | 23 | return wrapper 24 | 25 | return decorator 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea 6 | data/* 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 31 | __pypackages__/ 32 | 33 | # Environments 34 | .env 35 | .venv 36 | env/ 37 | venv/ 38 | pyenv/ 39 | ENV/ 40 | env.bak/ 41 | venv.bak/ 42 | 43 | # pytype static type analyzer 44 | .pytype/ 45 | 46 | # Cython debug symbols 47 | cython_debug/ 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Telegram Bot 2 | 3 | A telegram bot that uses a headless chrome wrapper to communicate with ChatGPT. 4 | 5 | see: https://github.com/Klingefjord/ChatGPT-API-Python 6 | 7 | ## How to Install 8 | 9 | ### Step 1: Create .venv 10 | `python -m venv ./venv` 11 | `source ./venv/bin/activate` 12 | 13 | ### Step 2: Set up your Telegram bot 14 | 15 | 1. Set up your Telegram bot token and user ID in the `.env` file. See [these instructions](https://core.telegram.org/bots/tutorial#obtain-your-bot-token) for more information on how to do this. 16 | 2. Edit the `.env.example` file, rename it to `.env`, and place your values in the appropriate fields. 17 | 18 | ## To run: 19 | 20 | `python main.py` 21 | 22 | ## Credits 23 | 24 | - Based on [@Altryne](https://twitter.com/altryne/status/1598902799625961472) on Twitter (https://github.com/altryne/chatGPT-telegram-bot) 25 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from pydantic import BaseModel 3 | 4 | 5 | class Mendable(BaseModel): 6 | """Mendable API""" 7 | 8 | api_key: str 9 | conversation_id: str | None = None 10 | history: list[dict] = [] 11 | 12 | async def _start_conversation(self) -> str: 13 | """Start a Mendable conversation""" 14 | 15 | data = {"api_key": self.api_key} 16 | 17 | async with aiohttp.ClientSession() as session: 18 | async with session.post( 19 | "https://api.mendable.ai/v0/newConversation", json=data 20 | ) as response: 21 | response_data = await response.json() 22 | return response_data["conversation_id"] 23 | 24 | async def call(self, query: str) -> dict[str, str]: 25 | """Call the Mendable API""" 26 | 27 | if self.conversation_id is None: 28 | self.conversation_id = await self._start_conversation() 29 | 30 | data = { 31 | "question": query, 32 | "shouldStream": False, 33 | "conversation_id": self.conversation_id, 34 | "history": self.history, 35 | "api_key": self.api_key, 36 | } 37 | 38 | async with aiohttp.ClientSession() as session: 39 | async with session.post( 40 | "https://api.mendable.ai/v0/mendableChat", json=data 41 | ) as response: 42 | response_data = await response.json() 43 | response_text = response_data["answer"]["text"] 44 | sources = response_data["sources"][:3] 45 | self.history.append({"prompt": query, "response": response_text}) 46 | 47 | if len(sources) > 0: 48 | response_text += "\n\n" 49 | response_text += "\n".join([s["link"] for s in sources]) 50 | 51 | return response_text 52 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytz 3 | import dotenv 4 | import telegram 5 | import logging 6 | import asyncio 7 | import dotenv 8 | from api import Mendable 9 | from utils.auth import auth 10 | from telegram.helpers import escape_markdown 11 | from telegram import Update 12 | from telegram.ext import ( 13 | Application, 14 | ContextTypes, 15 | CallbackContext, 16 | MessageHandler, 17 | CommandHandler, 18 | PicklePersistence, 19 | Defaults, 20 | filters, 21 | ) 22 | 23 | # Prepare the environment 24 | dotenv.load_dotenv() 25 | 26 | # logging 27 | logging.basicConfig( 28 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 29 | ) 30 | logger = logging.getLogger(__name__) 31 | 32 | # set the global timezone. 33 | os.environ["TZ"] = "Europe/Berlin" 34 | 35 | application = ( 36 | Application.builder() 37 | .token(os.environ.get("TELEGRAM_BOT_TOKEN")) 38 | .persistence(PicklePersistence(filepath="./data.pickle")) 39 | .defaults(defaults=Defaults(tzinfo=pytz.timezone(os.environ["TZ"]))) 40 | .build() 41 | ) 42 | 43 | mendable = Mendable(api_key=os.environ.get("MENDABLE_API_KEY")) 44 | 45 | 46 | async def send_typing_action(context: CallbackContext, chat_id): 47 | """Send typing action while processing the query.""" 48 | for _ in range(0, 3): 49 | await context.bot.send_chat_action(chat_id=chat_id, action="typing") 50 | await asyncio.sleep(5) 51 | 52 | 53 | @auth() 54 | async def send(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 55 | """Send message to Mendable""" 56 | # Start the typing task. 57 | typing_task = asyncio.create_task( 58 | send_typing_action(context, update.effective_chat.id) 59 | ) 60 | 61 | # send typing action 62 | await application.bot.send_chat_action(update.effective_chat.id, "typing") 63 | 64 | # get the response from the API 65 | response = await mendable.call(query=update.message.text) 66 | response = escape_markdown(response, version=2) 67 | 68 | # Cancel the typing task. 69 | typing_task.cancel() 70 | 71 | # send the response to the user 72 | await update.message.reply_text( 73 | response, 74 | disable_web_page_preview=True, 75 | parse_mode=telegram.constants.ParseMode.MARKDOWN_V2, 76 | ) 77 | 78 | 79 | @auth() 80 | async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 81 | """Reset the bot""" 82 | mendable.history = [] 83 | mendable.conversation_id = None 84 | await update.message.reply_text("Reset successful ✅") 85 | 86 | 87 | async def error(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 88 | """Log Errors caused by Updates.""" 89 | print(f"Update {update} caused error {context.error}") 90 | logger.warning('Error "%s"', context.error) 91 | 92 | 93 | def main(): 94 | # Handle messages 95 | application.add_handler( 96 | MessageHandler(filters.TEXT & ~filters.COMMAND, send), 97 | ) 98 | application.add_handler(CommandHandler("qa", send)) 99 | application.add_handler(CommandHandler("reset", reset)) 100 | application.add_error_handler(error) 101 | 102 | # Run the bot 103 | application.run_polling() 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | --------------------------------------------------------------------------------