├── .gitignore ├── .vscode └── settings.json ├── deno.json ├── README.md ├── env.ts ├── import_map.json ├── main.ts ├── db.ts ├── gemini.ts └── bot.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *test.ts 3 | *.lock -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": "import_map.json", 3 | "tasks": { 4 | "test": "deno run -A main.ts --polling" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeminiChatBot 2 | 3 | Live: https://t.me/GeminiTalkBot 4 | 5 | Uses google's gemini api (free tier) 6 | 7 | # Credits 8 | 9 | - Google 10 | - [Aditya](https://xditya.me) 11 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { cleanEnv, str, url } from "envalid"; 3 | 4 | await config({ export: true }); 5 | 6 | export default cleanEnv(Deno.env.toObject(), { 7 | BOT_TOKEN: str(), 8 | OWNERS: str(), 9 | MONGO_URL: url(), 10 | DEFAULT_API_KEY: str(), 11 | }); 12 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "dotenv": "https://deno.land/std@0.154.0/dotenv/mod.ts", 4 | "envalid": "https://deno.land/x/envalid@0.1.2/mod.ts", 5 | "grammy/": "https://deno.land/x/grammy@v1.20.3/", 6 | "mongo": "https://deno.land/x/mongo@v0.32.0/mod.ts", 7 | "$env": "./env.ts" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { webhookCallback } from "grammy/mod.ts"; 2 | 3 | import bot from "./bot.ts"; 4 | 5 | if (Deno.args[0] == "--polling") { 6 | console.info(`Started as @${bot.botInfo.username} on long polling.`); 7 | 8 | bot.start(); 9 | Deno.addSignalListener("SIGINT", () => bot.stop()); 10 | Deno.addSignalListener( 11 | Deno.build.os != "windows" ? "SIGTERM" : "SIGINT", 12 | () => bot.stop(), 13 | ); 14 | } else { 15 | console.info(`Started as @${bot.botInfo.username} on webhooks.`); 16 | 17 | const handleUpdate = webhookCallback(bot, "std/http"); 18 | Deno.serve(async (req) => { 19 | if (req.method === "POST") { 20 | const url = new URL(req.url); 21 | if (url.pathname.slice(1) === bot.token) { 22 | try { 23 | return await handleUpdate(req); 24 | } catch (err) { 25 | console.error(err); 26 | } 27 | } 28 | } 29 | return new Response("Welcome!"); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 @xditya 3 | * Redistribution and use in source and binary forms, with or without modification, are permitted 4 | * provided the copyright header is included and attributes are preserved. 5 | */ 6 | 7 | import { MongoClient, ObjectId } from "mongo"; 8 | 9 | import config from "$env"; 10 | 11 | console.log("Connecting to MongoDB..."); 12 | const client = new MongoClient(); 13 | const MONGO_URL = new URL(config.MONGO_URL); 14 | if (!MONGO_URL.searchParams.has("authMechanism")) { 15 | MONGO_URL.searchParams.set("authMechanism", "SCRAM-SHA-1"); 16 | } 17 | try { 18 | await client.connect(MONGO_URL.href); 19 | } catch (err) { 20 | console.error("Error connecting to MongoDB", err); 21 | throw err; 22 | } 23 | const db = client.database("GeminiBot"); 24 | 25 | interface ConversationPart { 26 | text: string; 27 | } 28 | 29 | interface UserDataSchema { 30 | _id: ObjectId; 31 | user_id: number; 32 | conversation_history: Array>; 33 | reaction_settings: boolean; 34 | } 35 | 36 | const userDb = db.collection("UserData"); 37 | 38 | async function addUser(id: number) { 39 | const data = await userDb.findOne({ user_id: id }); 40 | if (data) return; 41 | await userDb.insertOne({ 42 | user_id: id, 43 | conversation_history: new Array>(), 44 | reaction_settings: true, 45 | }); 46 | } 47 | 48 | async function addConversation( 49 | id: number, 50 | conv: Array>, 51 | ) { 52 | const oldData = await userDb.findOne({ user_id: id }); 53 | if (!oldData) { 54 | await userDb.insertOne({ 55 | user_id: id, 56 | conversation_history: conv, 57 | reaction_settings: true, 58 | }); 59 | } else { 60 | const new_conv = oldData.conversation_history.concat(conv); 61 | await userDb.updateOne( 62 | { user_id: id }, 63 | { $set: { conversation_history: new_conv } }, 64 | ); 65 | } 66 | } 67 | 68 | async function resetConversation( 69 | id: number, 70 | conv: Array>, 71 | ) { 72 | await userDb.updateOne( 73 | { user_id: id }, 74 | { $set: { conversation_history: conv } }, 75 | ); 76 | } 77 | 78 | async function getConversations(id: number) { 79 | const data = await userDb.findOne({ user_id: id }); 80 | if (!data) return Array>(); 81 | return data.conversation_history; 82 | } 83 | 84 | async function getStats() { 85 | return await userDb.countDocuments(); 86 | } 87 | 88 | async function toggelReactionSettings(id: number) { 89 | const currentReactionSettings = await getUserReactionSettings(id); 90 | await userDb.updateOne( 91 | { user_id: id }, 92 | { $set: { reaction_settings: !currentReactionSettings } }, 93 | ); 94 | } 95 | 96 | async function getUserReactionSettings(id: number) { 97 | const data = await userDb.findOne({ user_id: id }); 98 | if (!data) return true; 99 | if (data.reaction_settings === undefined) return true; 100 | return data.reaction_settings; 101 | } 102 | 103 | export { 104 | addConversation, 105 | addUser, 106 | type ConversationPart, 107 | getConversations, 108 | getStats, 109 | getUserReactionSettings, 110 | resetConversation, 111 | toggelReactionSettings, 112 | }; 113 | -------------------------------------------------------------------------------- /gemini.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 @xditya 3 | * Redistribution and use in source and binary forms, with or without modification, are permitted 4 | * provided the copyright header is included and attributes are preserved. 5 | */ 6 | 7 | import config from "$env"; 8 | import { 9 | addConversation, 10 | type ConversationPart, 11 | getConversations, 12 | resetConversation, 13 | } from "./db.ts"; 14 | 15 | const baseUrl = 16 | "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + 17 | config.DEFAULT_API_KEY; 18 | 19 | async function getResponse(user_id: number, question: string) { 20 | const convArray = new Array>(); 21 | 22 | let userConv = new Map(); 23 | userConv 24 | .set("role", "user") 25 | .set("parts", [{ text: question }] as ConversationPart[]); 26 | 27 | const conversationHistory = await getConversations(user_id); 28 | 29 | await checkAndClearOldConversations(user_id, conversationHistory); 30 | 31 | convArray.push(userConv); 32 | 33 | let convHistoryToSend: Array> = 34 | conversationHistory; 35 | let jsonConvHistoryToSend; 36 | 37 | if (!conversationHistory || conversationHistory.length === 0) { 38 | convHistoryToSend = [userConv]; 39 | jsonConvHistoryToSend = convHistoryToSend.map((conversationMap) => { 40 | const conversationArray: [string, ConversationPart[]][] = Array.from( 41 | conversationMap.entries() 42 | ); 43 | return Object.fromEntries(conversationArray); 44 | }); 45 | } else { 46 | conversationHistory.push(userConv); 47 | jsonConvHistoryToSend = []; 48 | for (let i = 0; i < conversationHistory.length; i++) { 49 | const conversationMap = conversationHistory[i]; 50 | if (conversationMap instanceof Map) { 51 | const conversationArray: [string, ConversationPart[]][] = Array.from( 52 | conversationMap.entries() 53 | ); 54 | jsonConvHistoryToSend.push(Object.fromEntries(conversationArray)); 55 | } else { 56 | jsonConvHistoryToSend.push(conversationMap); 57 | } 58 | } 59 | } 60 | const response: Response = await fetch(baseUrl, { 61 | method: "POST", 62 | headers: { "Content-Type": "application/json" }, 63 | body: JSON.stringify({ 64 | contents: jsonConvHistoryToSend, 65 | safetySettings: [ 66 | { 67 | category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", 68 | threshold: "BLOCK_NONE", 69 | }, 70 | { 71 | category: "HARM_CATEGORY_HATE_SPEECH", 72 | threshold: "BLOCK_NONE", 73 | }, 74 | { 75 | category: "HARM_CATEGORY_HARASSMENT", 76 | threshold: "BLOCK_NONE", 77 | }, 78 | { 79 | category: "HARM_CATEGORY_DANGEROUS_CONTENT", 80 | threshold: "BLOCK_NONE", 81 | }, 82 | ], 83 | }), 84 | }); 85 | let textResponse = ""; 86 | try { 87 | const resp = await response.json(); 88 | textResponse = resp.candidates[0].content.parts[0].text; 89 | } catch (err) { 90 | console.error("User: ", user_id, "Error: ", err); 91 | return "Sorry, I'm having trouble understanding you. Could you please rephrase your question?"; 92 | } 93 | const modelConv = new Map(); 94 | modelConv 95 | .set("role", "model") 96 | .set("parts", [{ text: textResponse }] as ConversationPart[]); 97 | 98 | convArray.push(modelConv); 99 | 100 | await addConversation(user_id, convArray); 101 | return textResponse.replaceAll("* ", "→ "); 102 | } 103 | 104 | async function checkAndClearOldConversations( 105 | id: number, 106 | conversationHistory: Array> 107 | ) { 108 | // this function keeps removing the first 2 elements of the array until the total conversations length is less than 50 109 | if (conversationHistory.length > 50) { 110 | conversationHistory.shift(); 111 | conversationHistory.shift(); 112 | await resetConversation(id, conversationHistory); 113 | } 114 | } 115 | 116 | async function getModelInfo() { 117 | try { 118 | const data = await fetch( 119 | "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro?key=" + 120 | config.DEFAULT_API_KEY 121 | ); 122 | const resp = await data.json(); 123 | return ` 124 | Model Name:
${resp.displayName} v${resp.version} [${resp.name}]
125 | Description:
${resp.description}
126 | Input Token Limit:
${resp.inputTokenLimit}
127 | Output Token Limit:
${resp.outputTokenLimit}
128 | 129 | Bot developed and hosted by @BotzHub. 130 | `; 131 | } catch (err) { 132 | return "Could not fetch data. Try again later!"; 133 | } 134 | } 135 | 136 | export { getModelInfo, getResponse }; 137 | -------------------------------------------------------------------------------- /bot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 @xditya 3 | * Redistribution and use in source and binary forms, with or without modification, are permitted 4 | * provided the copyright header is included and attributes are preserved. 5 | */ 6 | 7 | import { Bot, GrammyError, HttpError, InlineKeyboard } from "grammy/mod.ts"; 8 | 9 | import config from "$env"; 10 | import { 11 | addUser, 12 | getStats, 13 | getUserReactionSettings, 14 | toggelReactionSettings, 15 | } from "./db.ts"; 16 | import { getModelInfo, getResponse } from "./gemini.ts"; 17 | 18 | const bot = new Bot(config.BOT_TOKEN); 19 | 20 | bot.catch((err) => { 21 | const ctx = err.ctx; 22 | console.error(`Error while handling update ${ctx.update.update_id}:`); 23 | const e = err.error; 24 | if (e instanceof GrammyError) { 25 | console.error("Error in request:", e.description); 26 | } else if (e instanceof HttpError) { 27 | console.error("Could not contact Telegram:", e); 28 | } else { 29 | console.error("Unknown error:", e); 30 | } 31 | }); 32 | 33 | async function checkJoin(user: number) { 34 | try { 35 | const member = await bot.api.getChatMember("@BotzHub", user); 36 | return ( 37 | member.status == "member" || 38 | member.status == "creator" || 39 | member.status == "administrator" 40 | ); 41 | } catch (err) { 42 | return false; 43 | } 44 | } 45 | 46 | bot.chatType("private").command("start", async (ctx) => { 47 | if (ctx.match && ctx.match === "how_to_remove") { 48 | await ctx.reply(`Join @BotzHub to remove this button from bots replies.`, { 49 | reply_markup: new InlineKeyboard().url( 50 | "Join Now!", 51 | "https://t.me/BotzHub" 52 | ), 53 | }); 54 | return; 55 | } 56 | 57 | await ctx.api.sendPhoto( 58 | ctx.chat!.id, 59 | "https://storage.googleapis.com/gweb-uniblog-publish-prod/images/final_keyword_header.width-1600.format-webp.webp", 60 | { 61 | caption: `Hey ${ 62 | ctx.from!.first_name 63 | }, I'm an AI assistant powered by Gemini! 64 | 65 |
I can act as a personal assistant, answering your questions and helping you with your day-to-day tasks. 😊
66 | 67 | I can remember your last 50 conversations, making your experience with me more personal and human-like! 🤖💬 68 | `, 69 | reply_markup: new InlineKeyboard() 70 | .text("⚙️ Settings", "settings") 71 | .url("🔄 Updates", "https://t.me/BotzHub") 72 | .row() 73 | .text("ℹ️ Information", "info"), 74 | parse_mode: "HTML", 75 | } 76 | ); 77 | await addUser(ctx.from!.id); 78 | }); 79 | 80 | bot.callbackQuery("back", async (ctx) => { 81 | await ctx.editMessageCaption({ 82 | caption: `Hey ${ 83 | ctx.from!.first_name 84 | }, I'm an AI assistant powered by Gemini! 85 | 86 |
I can act as a personal assistant, answering your questions and helping you with your day-to-day tasks. 😊
87 | 88 | I can remember your last 50 conversations, making your experience with me more personal and human-like! 🤖💬 89 | `, 90 | reply_markup: new InlineKeyboard() 91 | .text("⚙️ Settings", "settings") 92 | .url("🔄 Updates", "https://t.me/BotzHub") 93 | .row() 94 | .text("ℹ️ Information", "info"), 95 | parse_mode: "HTML", 96 | }); 97 | }); 98 | 99 | bot.callbackQuery("settings", async (ctx) => { 100 | const reactionsSettings = await getUserReactionSettings(ctx.from!.id); 101 | const userReaction = reactionsSettings ? "✅" : "❌"; 102 | const userReactionMsg = reactionsSettings 103 | ? "❌ Disable Reactions" 104 | : "✅ Enable Reactions"; 105 | await ctx.editMessageCaption({ 106 | caption: ` 107 | Settings Menu 108 | 109 | Reaction on message recieve: ${userReaction} 110 | `, 111 | parse_mode: "HTML", 112 | reply_markup: new InlineKeyboard() 113 | .text(userReactionMsg, "reaction_toggle") 114 | .url("Updates", "https://t.me/BotzHub") 115 | .row() 116 | .text("👈 Back", "back"), 117 | }); 118 | }); 119 | 120 | bot.callbackQuery("reaction_toggle", async (ctx) => { 121 | await toggelReactionSettings(ctx.from!.id); 122 | const reactionsSettings = await getUserReactionSettings(ctx.from!.id); 123 | const userReaction = reactionsSettings ? "✅" : "❌"; 124 | const userReactionMsg = reactionsSettings 125 | ? "❌ Disable Reactions" 126 | : "✅ Enable Reactions"; 127 | await ctx.editMessageCaption({ 128 | caption: ` 129 | Settings Menu 130 | 131 | Reaction on message recieve: ${userReaction} 132 | `, 133 | parse_mode: "HTML", 134 | reply_markup: new InlineKeyboard() 135 | .text(userReactionMsg, "reaction_toggle") 136 | .url("Updates", "https://t.me/BotzHub") 137 | .row() 138 | .text("👈 Back", "back"), 139 | }); 140 | }); 141 | 142 | bot.callbackQuery("info", async (ctx) => { 143 | const resp = await getModelInfo(); 144 | await ctx.editMessageCaption({ 145 | caption: resp, 146 | parse_mode: "HTML", 147 | reply_markup: new InlineKeyboard() 148 | .url("Updates", "https://t.me/BotzHub") 149 | .row() 150 | .text("👈 Back", "back"), 151 | }); 152 | }); 153 | 154 | const owners: number[] = []; 155 | for (const owner of config.OWNERS.split(" ")) { 156 | owners.push(parseInt(owner)); 157 | } 158 | 159 | bot 160 | .filter((ctx) => owners.includes(ctx.from?.id || 0)) 161 | .command("stats", async (ctx) => { 162 | await ctx.reply(`Total users: ${await getStats()}`); 163 | }); 164 | 165 | bot.chatType("private").on("message:text", async (ctx) => { 166 | if (await getUserReactionSettings(ctx.from!.id)) { 167 | await ctx.react("⚡"); 168 | } 169 | await ctx.api.sendChatAction(ctx.chat!.id, "typing"); 170 | const text = ctx.message!.text; 171 | const response = await getResponse(ctx.from!.id, text); 172 | let buttons = new InlineKeyboard(); 173 | if (!(await checkJoin(ctx.from!.id))) { 174 | buttons = buttons.url( 175 | "Join Now", 176 | `https://t.me/${bot.botInfo?.username}?start=how_to_remove` 177 | ); 178 | } 179 | 180 | if (response.length > 4096) { 181 | const splitMsg = response.match(/[\s\S]{1,4096}/g) || []; 182 | for (const msg of splitMsg) { 183 | try { 184 | await ctx.reply(msg, { 185 | parse_mode: "Markdown", 186 | reply_markup: buttons, 187 | }); 188 | } catch (err) { 189 | await ctx.reply(`
${msg}
`, { 190 | parse_mode: "HTML", 191 | reply_markup: buttons, 192 | }); 193 | } 194 | } 195 | } else { 196 | try { 197 | await ctx.reply(response, { 198 | parse_mode: "Markdown", 199 | reply_markup: buttons, 200 | }); 201 | } catch (err) { 202 | await ctx.reply(`
${response}
`, { 203 | parse_mode: "HTML", 204 | reply_markup: buttons, 205 | }); 206 | } 207 | } 208 | }); 209 | 210 | await bot.init(); 211 | export default bot; 212 | --------------------------------------------------------------------------------