├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── demo.gif └── supabase ├── .env.example ├── config.toml ├── functions └── telegram-bot │ ├── index.ts │ ├── openai.ts │ ├── supaClient.ts │ └── utils.ts ├── migrations ├── 20230302070150_initial.sql └── 20230808112319_add-settings.sql └── seed.sql /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .temp 4 | .branches 5 | todo.md 6 | notes.md -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.unstable": true, 4 | "deno.lint": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2023 Denis Marushchak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deploy Your Personal Telegram AI Assistant with Supabase Edge Functions 2 | 3 | ![demo](./demo.gif) 4 | 5 | 1. Start by creating a new Supabase project and executing the following commands: 6 | 7 | - `supabase link --project-ref ` - links your local CLI to your Supabase project. 8 | - `supabase db push` - it pushes your local database schema and any data to your Supabase project. 9 | 10 | 2. Contact [@BotFather](https://t.me/BotFather) on Telegram to create a bot and get its token. 11 | 12 | 3. Set up your environment variables by copying the example file: 13 | 14 | - `cp supabase/.env.example supabase/.env` 15 | 16 | 4. Fill out the environment variables in the `.env` file: 17 | 18 | - `BOT_TOKEN=your-telegram-bot-token` 19 | - `FUNCTION_SECRET=secret123` 20 | - `OPENAI_KEY=your-openai-api-secret` 21 | - `USERS='["user1", "user2", "user3"]'` (Note: wrap the usernames in double quotes) 22 | 23 | 5. Fill out `STARTING_PROMPT` in the `.env` file (not required): 24 | 25 | - `STARTING_PROMPT="You are a personal assistant. You will help the user with various tasks and requests. You should also provide information on how to complete the tasks or where to find the resources needed, if applicable. You are very helpful and provide clear and informational answers."` - Starting prompt allows up the bot to act the way you want. 26 | You can find more starting prompts [here](https://github.com/f/awesome-chatgpt-prompts). 27 | 28 | 6. Create the function by running the following command: 29 | 30 | - `supabase functions deploy --no-verify-jwt telegram-bot` 31 | 32 | 7. Set the secrets using the following command: 33 | 34 | - `supabase secrets set --env-file ./supabase/.env` 35 | 36 | 8. Set your bot's webhook URL by opening the following URL in your browser: 37 | - `https://api.telegram.org/bot/setWebhook?url=https://.functions.supabase.co/telegram-bot?secret=` 38 | - Replace `` and `` with your own values. 39 | - Alternatively, you can run the following command in your terminal: 40 | - `curl https://api.telegram.org/bot/setWebhook?url=https://.functions.supabase.co/telegram-bot?secret=` 41 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova4u/chatgpt-supabase-telegram-bot/75b6ff1cffb72b6247f0844255c1372ec098a352/demo.gif -------------------------------------------------------------------------------- /supabase/.env.example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN= 2 | FUNCTION_SECRET=supaSecret123123 3 | OPENAI_KEY= 4 | USERS="['user1', 'user2', 'user3']" 5 | STARTING_PROMPT="I want you to act as a personal assistant. You will help me with various tasks and requests that I have throughout the day. You should also provide information on how to complete the tasks or where to find the resources I need, if applicable. You are very helpful and provide clear and informational answers." 6 | 7 | 8 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "supabase" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; 2 | import { 3 | Bot, 4 | Context, 5 | GrammyError, 6 | HttpError, 7 | webhookCallback, 8 | } from "https://deno.land/x/grammy@v1.8.3/mod.ts"; 9 | 10 | import { Menu } from "https://deno.land/x/grammy_menu@v1.1.3/mod.ts"; 11 | 12 | import { 13 | changeSettings, 14 | checkUserSettings, 15 | clearUserMessageHistory, 16 | createInitialUserSettings, 17 | estimateTokens, 18 | formatMessageHistory, 19 | getAiResponse, 20 | getUserChatModel, 21 | getUserId, 22 | getUserMessageHistory, 23 | Message, 24 | updateUserMessageHistory, 25 | } from "./utils.ts"; 26 | 27 | const botToken = Deno.env.get("BOT_TOKEN") || ""; 28 | if (!botToken) { 29 | throw new Error(`Please specify the Telegram Bot Token.`); 30 | } 31 | 32 | const users: string[] = JSON.parse(Deno.env.get("USERS") || "[]"); 33 | if (!users.length) { 34 | throw new Error(`Please specify the users that have access to the bot.`); 35 | } 36 | 37 | const menu = new Menu("model") 38 | .text("GPT-3.5 turbo", async (ctx) => { 39 | await changeSettings(ctx.from?.id, { 40 | model: "gpt-3.5-turbo", 41 | }); 42 | return ctx.reply("The model has been changed to gpt-3.5-turbo"); 43 | }) 44 | .row() 45 | .text("GPT-4", async (ctx) => { 46 | await changeSettings(ctx.from?.id, { 47 | model: "gpt-4", 48 | }); 49 | return ctx.reply("The model has been changed to gpt-4"); 50 | }) 51 | .row() 52 | .text("GPT-4-Turbo", async (ctx) => { 53 | await changeSettings(ctx.from?.id, { 54 | model: "gpt-4-1106-preview", 55 | }); 56 | return ctx.reply("The model has been changed to gpt-4-1106-preview"); 57 | }); 58 | 59 | const bot = new Bot(botToken); 60 | 61 | export type BotContext = Context & { 62 | config: { 63 | isOwner: boolean; 64 | }; 65 | }; 66 | 67 | // deno-lint-ignore no-explicit-any 68 | bot.use(menu as any); 69 | 70 | bot.use(async (ctx, next) => { 71 | if (!ctx.from?.username) throw new Error("No user information found"); 72 | 73 | ctx.config = { isOwner: users.includes(ctx.from.username) }; 74 | if (!ctx.config.isOwner) 75 | return ctx.reply(`Sorry, you are not allowed. This is personal AI Bot`); 76 | 77 | const userId = getUserId(ctx); 78 | if (!userId) throw new Error("User ID could not be retrieved"); 79 | 80 | const userHasSettings = await checkUserSettings(userId); 81 | if (!userHasSettings) await createInitialUserSettings(userId); 82 | 83 | await next(); 84 | }); 85 | 86 | bot.command("start", (ctx) => 87 | ctx.reply("Welcome! I will be your personal AI Assistant.") 88 | ); 89 | 90 | bot.command("model", async (ctx) => { 91 | const userId = getUserId(ctx); 92 | const model = await getUserChatModel(userId!); 93 | return ctx.reply(`You are currently using ${model} model`); 94 | }); 95 | 96 | bot.command("changemodel", async (ctx) => { 97 | await ctx.reply("Please select the model you want to use", { 98 | reply_markup: menu, 99 | }); 100 | }); 101 | 102 | bot.command("history", async (ctx) => { 103 | const userId = getUserId(ctx); 104 | if (!userId) return ctx.reply(`No User Found`); 105 | 106 | const history = await getUserMessageHistory(userId); 107 | const aprxTokens = estimateTokens( 108 | formatMessageHistory(history).replaceAll("\n", "") 109 | ); 110 | console.log(formatMessageHistory(history).replaceAll("\n", "")); 111 | 112 | const reply = formatMessageHistory(history.filter((m) => m.role !== "system")) 113 | ? formatMessageHistory(history.filter((m) => m.role !== "system")) + 114 | `Approximate token usage for your query: ${aprxTokens}` 115 | : "History is empty"; 116 | 117 | return ctx.reply(reply, {}); 118 | }); 119 | 120 | bot.command("clear", async (ctx) => { 121 | const userId = getUserId(ctx); 122 | console.log(ctx.chat.id); 123 | 124 | if (!userId) { 125 | return ctx.reply(`No User Found`); 126 | } 127 | 128 | await clearUserMessageHistory(userId); 129 | 130 | return ctx.reply(`Your dialogue has been cleared`); 131 | }); 132 | 133 | bot.catch((err) => { 134 | const ctx = err.ctx; 135 | console.error(`Error while handling update ${ctx.update.update_id}:`); 136 | const e = err.error; 137 | if (e instanceof GrammyError) { 138 | console.error("Error in request:", e.description); 139 | } else if (e instanceof HttpError) { 140 | console.error("Could not contact Telegram:", e); 141 | } else { 142 | console.error("Unknown error:", e); 143 | } 144 | }); 145 | 146 | bot.on("message", async (ctx) => { 147 | try { 148 | const userId = ctx?.from?.id; 149 | const receivedMessage = ctx.update.message.text; 150 | 151 | if (!receivedMessage) { 152 | ctx.reply(`No message`); 153 | } 154 | 155 | const history = await getUserMessageHistory(userId); 156 | 157 | const lastRequest = history.findLast( 158 | (message) => message.role === "user" 159 | )?.content; 160 | 161 | if (lastRequest === receivedMessage) { 162 | return ctx.reply("Repeated requested!"); 163 | } 164 | 165 | const aprxTokens = +estimateTokens(formatMessageHistory(history)); 166 | 167 | if (aprxTokens > 2000) { 168 | await ctx.reply( 169 | `Just a heads up, you've used around *${Math.floor( 170 | +aprxTokens 171 | )}* tokens for this query. To help you manage your token usage, we recommend running the */clear* command every so oftens usage.`, 172 | { 173 | parse_mode: "Markdown", 174 | } 175 | ); 176 | } 177 | 178 | const message: Message = { 179 | role: "user", 180 | content: receivedMessage || "", 181 | }; 182 | const model = await getUserChatModel(userId); 183 | 184 | // console.log(`${model} used for the request`); 185 | // const sleep = (returnValue: string) => { 186 | // return new Promise((res) => 187 | // setTimeout(() => { 188 | // return res(returnValue); 189 | // }, 5000) 190 | // ); 191 | // }; 192 | 193 | const aiResponse = await getAiResponse([...history, message], model); 194 | // const aiResponse = await sleep(`Some response`); 195 | await ctx.reply(aiResponse).catch((e) => console.error(e)); 196 | await updateUserMessageHistory(userId, [ 197 | ...history, 198 | message, 199 | { role: "assistant", content: aiResponse + "\n" }, 200 | ]); 201 | } catch (error) { 202 | await ctx.reply(`Sorry an error has occured, please try again later.`); 203 | throw new Error(error.message); 204 | } 205 | }); 206 | 207 | await bot.api.setMyCommands([ 208 | { 209 | command: "/start", 210 | description: "Start the bot", 211 | }, 212 | { 213 | command: "/clear", 214 | description: "Clear the dialogue history.", 215 | }, 216 | { 217 | command: "/history", 218 | description: "Show the dialogue history.", 219 | }, 220 | { 221 | command: "/model", 222 | description: "Outputs a GPT model you are currently using.", 223 | }, 224 | { 225 | command: "/changemodel", 226 | description: "Change the model you are using.", 227 | }, 228 | { 229 | command: "/credits", 230 | description: "Show the amount of credits used.", 231 | }, 232 | ]); 233 | 234 | const handleUpdate = webhookCallback(bot, "std/http", "throw", 120_000); 235 | 236 | serve(async (req) => { 237 | try { 238 | const url = new URL(req.url); 239 | const isAllowed = 240 | url.searchParams.get("secret") === Deno.env.get("FUNCTION_SECRET"); 241 | 242 | if (!isAllowed) { 243 | return new Response("not allowed", { status: 405 }); 244 | } 245 | return await handleUpdate(req); 246 | } catch (err) { 247 | console.error(err); 248 | } 249 | }); 250 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/openai.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from "./utils.ts"; 2 | 3 | export interface CreateCompletionRequest { 4 | model: string; 5 | prompt?: string; 6 | max_tokens?: number; 7 | temperature?: number; 8 | top_p?: number; 9 | frequency_penalty?: number; 10 | presence_penalty?: number; 11 | logprobs?: number | null; 12 | echo?: boolean | null; 13 | stop?: string | string[] | null; 14 | } 15 | interface BillingResponse { 16 | object: string; 17 | total_granted: number; 18 | total_used: number; 19 | total_available: number; 20 | grants: { 21 | object: string; 22 | data: { 23 | object: string; 24 | id: string; 25 | grant_amount: number; 26 | used_amount: number; 27 | effective_at: number; 28 | expires_at: number; 29 | }; 30 | }; 31 | error?: OpenAIError; 32 | } 33 | 34 | interface OpenAIError { 35 | message: string; 36 | type: string; 37 | param: null; 38 | code: null; 39 | } 40 | 41 | interface Completion { 42 | id: string; 43 | object: string; 44 | created: number; 45 | model: string; 46 | choices: ChoiceChat[]; 47 | usage: Usage; 48 | error?: OpenAIError; 49 | } 50 | 51 | interface ChoiceChat { 52 | message: { 53 | role: string; 54 | content: string; 55 | }; 56 | finish_reason: string; 57 | index: number; 58 | } 59 | 60 | interface Usage { 61 | total_tokens: number; 62 | total_credits: number; 63 | credits_per_token: number; 64 | plan: string; 65 | } 66 | 67 | export class OpenAI { 68 | constructor(private API_KEY: string) {} 69 | 70 | public async getBilling() { 71 | const endpoint = "https://api.openai.com/dashboard/billing/credit_grants"; 72 | const { total_available, total_used, error } = await fetch(endpoint, { 73 | headers: { 74 | Authorization: `Bearer ${this.API_KEY}`, 75 | }, 76 | }).then((r) => r.json() as unknown as BillingResponse); 77 | if (error) { 78 | throw error.message; 79 | } 80 | return { total_available, total_used }; 81 | } 82 | 83 | public async createChatCompletion(messages: Messages, model: string) { 84 | const options: CreateCompletionRequest & { messages: Messages } = { 85 | model: model, 86 | messages, 87 | temperature: 0.6, 88 | max_tokens: 800, 89 | }; 90 | 91 | const response: Completion = await fetch( 92 | `https://api.openai.com/v1/chat/completions`, 93 | { 94 | body: JSON.stringify(options), 95 | headers: { 96 | Authorization: `Bearer ${this.API_KEY}`, 97 | "Content-Type": "application/json", 98 | }, 99 | method: "POST", 100 | } 101 | ).then((r) => r.json()); 102 | 103 | if (response?.error) { 104 | throw response.error.message; 105 | } 106 | 107 | console.log(response.usage); 108 | console.log(`Current model: ${response.model}`); 109 | 110 | return { 111 | answer: response.choices[0].message["content"], 112 | tokens: response.usage.total_tokens, 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/supaClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "https://esm.sh/@supabase/supabase-js@2.8.0"; 2 | 3 | export const supabaseClient = createClient( 4 | Deno.env.get("SUPABASE_URL") ?? "", 5 | Deno.env.get("SUPABASE_ANON_KEY") ?? "" 6 | ); 7 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/utils.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "./openai.ts"; 2 | import { supabaseClient } from "./supaClient.ts"; 3 | import { BotContext } from "./index.ts"; 4 | 5 | export type Messages = Message[]; 6 | export type Message = { 7 | role: "system" | "user" | "assistant"; 8 | content: string; 9 | }; 10 | 11 | const openAI = new OpenAI(Deno.env.get("OPENAI_KEY") || ""); 12 | const startingMessages: Messages = Deno.env.get("STARTING_PROMPT") 13 | ? [{ role: "system", content: Deno.env.get("STARTING_PROMPT") || "" }] 14 | : []; 15 | 16 | if (!openAI) { 17 | throw new Error(`Please specify the OpenAI API Key.`); 18 | } 19 | 20 | export const getUserId = (ctx: BotContext) => { 21 | return ctx.from?.id || null; 22 | }; 23 | 24 | export const getCredits = async () => { 25 | const { total_available, total_used } = await openAI.getBilling(); 26 | 27 | const formatNumber = (number: number) => { 28 | const formatter = new Intl.NumberFormat("en-US", { 29 | style: "currency", 30 | currency: "USD", 31 | minimumFractionDigits: 2, 32 | maximumFractionDigits: 2, 33 | }); 34 | 35 | return formatter.format(number); 36 | }; 37 | 38 | return { 39 | total_available: formatNumber(total_available), 40 | total_used: formatNumber(total_used), 41 | }; 42 | }; 43 | export function estimateTokens( 44 | text: string, 45 | method: "max" | "min" | "chars" | "words" | "average" = "max" 46 | ) { 47 | // method can be "average", "words", "chars", "max", "min", defaults to "max" 48 | // "average" is the average of words and chars 49 | // "words" is the word count divided by 0.75 50 | // "chars" is the char count divided by 4 51 | // "max" is the max of word and char 52 | // "min" is the min of word and char 53 | const word_count = text.split(" ").length; 54 | const char_count = text.length; 55 | const tokens_count_word_est = word_count / 0.75; 56 | const tokens_count_char_est = char_count / 4.0; 57 | let output = 0; 58 | switch (method) { 59 | case "average": 60 | output = (tokens_count_word_est + tokens_count_char_est) / 2; 61 | break; 62 | case "words": 63 | output = tokens_count_word_est; 64 | break; 65 | case "chars": 66 | output = tokens_count_char_est; 67 | break; 68 | case "max": 69 | output = Math.max(tokens_count_word_est, tokens_count_char_est); 70 | break; 71 | case "min": 72 | output = Math.min(tokens_count_word_est, tokens_count_char_est); 73 | break; 74 | default: 75 | // return invalid method message 76 | return "Invalid method. Use 'average', 'words', 'chars', 'max', or 'min'."; 77 | } 78 | return Math.ceil(output); 79 | } 80 | 81 | export const formatMessageHistory = (messages: Messages): string => { 82 | let output = ""; 83 | for (const message of messages) { 84 | const { role, content } = message; 85 | output += `${ 86 | role.charAt(0).toUpperCase() + role.slice(1) 87 | }: ${content.replace(/^\n+/, "")}\n`; 88 | } 89 | 90 | return output.replaceAll("User", "You").replaceAll("assistant", "AI:"); 91 | }; 92 | 93 | export const getUserMessageHistory = async (id: number): Promise => { 94 | const { data } = await supabaseClient 95 | .from("dialogues") 96 | .select("message") 97 | .eq("user_id", id); 98 | 99 | if (!data?.length) { 100 | const { error } = await supabaseClient 101 | .from("dialogues") 102 | .insert({ 103 | user_id: id, 104 | message: startingMessages, 105 | }) 106 | .eq("user_id", id); 107 | 108 | if (error) throw error.message; 109 | 110 | return startingMessages; 111 | } 112 | 113 | return data![0]["message"] || " "; 114 | }; 115 | 116 | export const getAiResponse = async ( 117 | messages: Messages, 118 | model: Settings["model"] = "gpt-3.5-turbo" 119 | ): Promise => { 120 | const { answer } = await openAI.createChatCompletion(messages, model); 121 | return answer; 122 | }; 123 | interface Settings { 124 | model: "gpt-3.5-turbo" | "gpt-4" | "gpt-4-1106-preview"; 125 | } 126 | 127 | const initSettings: Settings = { 128 | model: "gpt-3.5-turbo", 129 | }; 130 | 131 | export const checkUserSettings = async (id: number) => { 132 | const { data } = await supabaseClient 133 | .from("settings") 134 | .select("settings") 135 | .eq("user_id", id); 136 | console.log(!data?.length ? false : true); 137 | return !data?.length ? false : true; 138 | }; 139 | 140 | export const changeSettings = async (id: number, settings: Settings) => { 141 | const { error } = await supabaseClient 142 | .from("settings") 143 | .update({ 144 | user_id: id, 145 | settings, 146 | }) 147 | .eq("user_id", id); 148 | return error ? false : true; 149 | }; 150 | 151 | export const getUserSettings = async (id: number) => { 152 | const { data } = await supabaseClient 153 | .from("settings") 154 | .select("settings") 155 | .eq("user_id", id); 156 | 157 | if (!data) return null; 158 | 159 | return data[0].settings; 160 | }; 161 | export const getUserChatModel = async (id: number) => { 162 | const userSettings = await getUserSettings(id); 163 | if (!userSettings) return "gpt-3.5-turbo"; 164 | return userSettings.model; 165 | }; 166 | 167 | export const createInitialUserSettings = async (id: number) => { 168 | const { error, data } = await supabaseClient 169 | .from("settings") 170 | .insert({ 171 | user_id: id, 172 | settings: initSettings, 173 | }) 174 | .eq("user_id", id); 175 | 176 | console.log({ data, error }); 177 | if (error) throw error.message; 178 | }; 179 | 180 | export const updateUserMessageHistory = async ( 181 | id: number, 182 | message: Messages 183 | ) => { 184 | const { error } = await supabaseClient 185 | .from("dialogues") 186 | .update({ message: message }) 187 | .eq("user_id", id); 188 | 189 | if (error) throw error.message; 190 | }; 191 | 192 | export const clearUserMessageHistory = async (id: number) => { 193 | const { error } = await supabaseClient 194 | .from("dialogues") 195 | .update({ message: startingMessages }) 196 | .eq("user_id", id); 197 | 198 | if (error) throw error.message; 199 | }; 200 | -------------------------------------------------------------------------------- /supabase/migrations/20230302070150_initial.sql: -------------------------------------------------------------------------------- 1 | create table "public"."dialogues" ( 2 | "id" bigint generated by default as identity not null, 3 | "user_id" bigint, 4 | "created_at" timestamp with time zone default now(), 5 | "message" json 6 | ); 7 | 8 | 9 | CREATE UNIQUE INDEX dialogues_pkey ON public.dialogues USING btree (id); 10 | 11 | alter table "public"."dialogues" add constraint "dialogues_pkey" PRIMARY KEY using index "dialogues_pkey"; 12 | 13 | 14 | -------------------------------------------------------------------------------- /supabase/migrations/20230808112319_add-settings.sql: -------------------------------------------------------------------------------- 1 | create table "public"."settings" ( 2 | "id" bigint generated by default as identity not null, 3 | "user_id" bigint, 4 | "settings" json 5 | ); 6 | 7 | 8 | CREATE UNIQUE INDEX settings_pkey ON public.settings USING btree (id); 9 | 10 | alter table "public"."settings" add constraint "settings_pkey" PRIMARY KEY using index "settings_pkey"; 11 | 12 | 13 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova4u/chatgpt-supabase-telegram-bot/75b6ff1cffb72b6247f0844255c1372ec098a352/supabase/seed.sql --------------------------------------------------------------------------------