Browse Source

Fix interval

Ryan Wright 4 days ago
parent
commit
cdc355c9ad
3 changed files with 208 additions and 135 deletions
  1. BIN
      bun.lockb
  2. 3 1
      docker-compose.yml
  3. 205 134
      index.ts

BIN
bun.lockb


+ 3 - 1
docker-compose.yml

@@ -6,5 +6,7 @@ services:
       dockerfile: Dockerfile
     env_file: ".env"
     volumes:
-      - "./db:/usr/src/app/db"
+      - "db:/usr/src/app/db"
     restart: unless-stopped
+volumes:
+  db:

+ 205 - 134
index.ts

@@ -7,10 +7,15 @@ const APP_NAME = Bun.env.APP_NAME!;
 
 const TICKETS_LIST_LIMIT = 10;
 const TICKET_CODE_REGEX = /^#T\d+$/;
-const DB_CLEANUP_INTERVAL = 24 * 60 * 60 * 60 * 1000; // Day
+const DB_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // Day
 
-const db = new Database('./db/tickets.db');
-db.run(`
+(async () => {
+  if (!(await Bun.file("./db/tickets.db").exists())) {
+    await Bun.write("./db/tickets.db", "");
+  }
+
+  const db = new Database("./db/tickets.db");
+  db.run(`
 CREATE TABLE IF NOT EXISTS counters (
   key TEXT NOT NULL PRIMARY KEY,
   counter INTEGER DEFAULT 0
@@ -25,29 +30,33 @@ CREATE TABLE IF NOT EXISTS tickets (
   closed_at TIMESTAMP NULL
 );
 CREATE INDEX IF NOT EXISTS chat_id_idx ON tickets (chat_id)`);
-type Ticket = {
-  id: number;
-  chat_id: string;
-  first_message: string;
-  status: 'open' | 'closed';
-  created_at: string;
-  closed_at: string | null;
-};
-const bot = new Bot(TOKEN);
-
-const supportChatFilter = async (ctx: Context) => {
-  const chat = await ctx.getChat();
-  if (String(chat.id) !== TELEGRAM_SUPPORT_CHAT_ID) {
-    await ctx.reply(`Этот бот доступен только для сервиса "${APP_NAME}"`);
-    return false;
-  }
-
-  return true;
-}
-
-const getOpenedUserTicket = db.prepare<Ticket, string>('SELECT * FROM tickets WHERE chat_id = ? AND status = "open"');
-const updateCounterAndGet = db.prepare<{ counter: number }, []>(`UPDATE counters SET counter = counter + 1 WHERE key = 'last_ticket_id' RETURNING counter`);
-const createTicketAndReturn = db.prepare<Ticket, [number, string, string]>(`
+  type Ticket = {
+    id: number;
+    chat_id: string;
+    first_message: string;
+    status: "open" | "closed";
+    created_at: string;
+    closed_at: string | null;
+  };
+  const bot = new Bot(TOKEN);
+
+  const supportChatFilter = async (ctx: Context) => {
+    const chat = await ctx.getChat();
+    if (String(chat.id) !== TELEGRAM_SUPPORT_CHAT_ID) {
+      await ctx.reply(`Этот бот доступен только для сервиса "${APP_NAME}"`);
+      return false;
+    }
+
+    return true;
+  };
+
+  const getOpenedUserTicket = db.prepare<Ticket, string>(
+    'SELECT * FROM tickets WHERE chat_id = ? AND status = "open"'
+  );
+  const updateCounterAndGet = db.prepare<{ counter: number }, []>(
+    `UPDATE counters SET counter = counter + 1 WHERE key = 'last_ticket_id' RETURNING counter`
+  );
+  const createTicketAndReturn = db.prepare<Ticket, [number, string, string]>(`
   INSERT INTO tickets (
     id, chat_id, first_message, status
   ) VALUES (
@@ -56,122 +65,184 @@ const createTicketAndReturn = db.prepare<Ticket, [number, string, string]>(`
     ?, 
     'open'
   ) RETURNING *`);
-const getTickets = db.query<{ id: number, created_at: string }, number>('SELECT id, created_at FROM tickets WHERE status = "open" ORDER BY created_at DESC LIMIT ?');
-const getOpenedTicketById = db.prepare<Ticket, number>('SELECT * FROM tickets WHERE id = ? AND status = "open"');
-const closeTicket = db.prepare<{ id: number }, number>('UPDATE tickets SET status = "closed", closed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = "open" RETURNING id');
-const getClosedTicketsCount = db.query<{ count: number }, []>('SELECT count(*) as count FROM tickets WHERE status = "closed"');
-
-let blockUpdateDb = false;
-// Clear db
-setInterval(() => {
-  blockUpdateDb = true;
-  db.run('DELETE FROM tickets WHERE status = "closed"');
-  const lastTicket = db.query<Ticket, []>('SELECT * FROM tickets ORDER BY created_at DESC LIMIT 1').get();
-  const newCounter = lastTicket?.id ?? 0;
-  db.query<null, number>('UPDATE counters SET counter = ? WHERE key = "last_ticket_id"').run(newCounter);
-  console.log(new Date(), 'DB cleared. New counter is', newCounter);
-  blockUpdateDb = false;
-}, DB_CLEANUP_INTERVAL)
-
-const notifyTicketCreated = async (ticket: Ticket) => {
-  const code = `#T${ticket.id}`;
-  await bot.api.sendMessage(TELEGRAM_SUPPORT_CHAT_ID, `Новое обращение ${code}
+  const getTickets = db.query<{ id: number; created_at: string }, number>(
+    'SELECT id, created_at FROM tickets WHERE status = "open" ORDER BY created_at DESC LIMIT ?'
+  );
+  const getOpenedTicketById = db.prepare<Ticket, number>(
+    'SELECT * FROM tickets WHERE id = ? AND status = "open"'
+  );
+  const closeTicket = db.prepare<{ id: number }, number>(
+    'UPDATE tickets SET status = "closed", closed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = "open" RETURNING id'
+  );
+  const getClosedTicketsCount = db.query<{ count: number }, []>(
+    'SELECT count(*) as count FROM tickets WHERE status = "closed"'
+  );
+
+  let blockUpdateDb = false;
+  // Clear db
+  setInterval(() => {
+    blockUpdateDb = true;
+    db.run('DELETE FROM tickets WHERE status = "closed"');
+    const lastTicket = db
+      .query<Ticket, []>(
+        "SELECT * FROM tickets ORDER BY created_at DESC LIMIT 1"
+      )
+      .get();
+    const newCounter = lastTicket?.id ?? 0;
+    db.query<null, number>(
+      'UPDATE counters SET counter = ? WHERE key = "last_ticket_id"'
+    ).run(newCounter);
+    console.log(new Date(), "DB cleared. New counter is", newCounter);
+    blockUpdateDb = false;
+  }, DB_CLEANUP_INTERVAL);
+
+  const notifyTicketCreated = async (ticket: Ticket) => {
+    const code = `#T${ticket.id}`;
+    await bot.api.sendMessage(
+      TELEGRAM_SUPPORT_CHAT_ID,
+      `Новое обращение ${code}
 Время регистрации ${ticket.created_at}
 Сообщение:
 
 ${ticket.first_message}
 
-Чтобы ответить используйте \`/answer ${code} текст\`. Чтобы закрыть тикет используйте \`/close ${code}\`. Закрытые тикеты через время удаляются`, { parse_mode: 'Markdown' });
-}
-const notifyNewTicketMessage = async (ticket: Ticket, message: string) => {
-  const code = `#T${ticket.id}`;
-  await bot.api.sendMessage(TELEGRAM_SUPPORT_CHAT_ID, `${code}
+Чтобы ответить используйте \`/answer ${code} текст\`. Чтобы закрыть тикет используйте \`/close ${code}\`. Закрытые тикеты через время удаляются`,
+      { parse_mode: "Markdown" }
+    );
+  };
+  const notifyNewTicketMessage = async (ticket: Ticket, message: string) => {
+    const code = `#T${ticket.id}`;
+    await bot.api.sendMessage(
+      TELEGRAM_SUPPORT_CHAT_ID,
+      `${code}
 ${message}
 
 Чтобы ответить используйте \`/answer ${code} текст\`. Чтобы закрыть тикет используйте \`/close ${code}\`.
-`, { parse_mode: 'Markdown' });
-}
-
-const chatBot = bot.chatType('private');
-
-chatBot.command('start', (ctx) => ctx.reply(`👋 Привет! Это бот службы поддержки приложения ${APP_NAME}! Вы можете написать нам ваши предложения по улучшению приложения или пожаловаться на проблемы при работе с ним. Наши менеджеры вам ответят.`));
-chatBot.on('message', async (ctx) => {
-  if (blockUpdateDb) return ctx.reply('🛠️ Просим прощения, идут технические работы. Пока вы читали это сообщение, мы уже закончили! Пожалуйста, повторите свой запрос.');
-
-  const { id } = await ctx.getChat();
-  const messageText = ctx.message?.text;
-  if (!messageText) return;
-
-  const chatId = String(id);
-  const ticket = getOpenedUserTicket.get(chatId);
-  if (!ticket) {
-    const newId = updateCounterAndGet.get();
-    if (!newId) throw new Error('Не удалось добавить тикет');
-    const newTicket = createTicketAndReturn.get(newId.counter, chatId, messageText);
-    if (!newTicket) throw new Error('Не удалось добавить тикет');
-    await notifyTicketCreated(newTicket);
-    ctx.reply('📨 Ваше сообщение отправлено сотрудникам службы поддержки. Мы скоро вам ответим в этом чате!');
-    return;
-  }
-
-  await notifyNewTicketMessage(ticket, messageText);
-});
-
-const groupBot = bot.chatType('group').filter(supportChatFilter);
-
-groupBot.command('help', (ctx) => {
-  ctx.reply(`Привет! Вам доступны следующие команды:
+`,
+      { parse_mode: "Markdown" }
+    );
+  };
+
+  const chatBot = bot.chatType("private");
+
+  chatBot.command("start", (ctx) =>
+    ctx.reply(
+      `👋 Привет! Это бот службы поддержки приложения ${APP_NAME}! Вы можете написать нам ваши предложения по улучшению приложения или пожаловаться на проблемы при работе с ним. Наши менеджеры вам ответят.`
+    )
+  );
+  chatBot.on("message", async (ctx) => {
+    if (blockUpdateDb)
+      return ctx.reply(
+        "🛠️ Просим прощения, идут технические работы. Пока вы читали это сообщение, мы уже закончили! Пожалуйста, повторите свой запрос."
+      );
+
+    const { id } = await ctx.getChat();
+    const messageText = ctx.message?.text;
+    if (!messageText) return;
+
+    const chatId = String(id);
+    const ticket = getOpenedUserTicket.get(chatId);
+    if (!ticket) {
+      const newId = updateCounterAndGet.get();
+      if (!newId) throw new Error("Не удалось добавить тикет");
+      const newTicket = createTicketAndReturn.get(
+        newId.counter,
+        chatId,
+        messageText
+      );
+      if (!newTicket) throw new Error("Не удалось добавить тикет");
+      await notifyTicketCreated(newTicket);
+      ctx.reply(
+        "📨 Ваше сообщение отправлено сотрудникам службы поддержки. Мы скоро вам ответим в этом чате!"
+      );
+      return;
+    }
+
+    await notifyNewTicketMessage(ticket, messageText);
+  });
+
+  const groupBot = bot.chatType("group").filter(supportChatFilter);
+
+  groupBot.command("help", (ctx) => {
+    ctx.reply(
+      `Привет! Вам доступны следующие команды:
     \`/answer #ID текст\` - ответить по тикету
     \`/close #ID\` - закрыть тикет и отправить клиенту сообщение с благодарностью за обрашение
     /closed - показать количество закрытых тикетов
-    /tickets - показать все открытые тикеты`, { parse_mode: 'Markdown' });
-});
-groupBot.command('tickets', (ctx) => {
-  const tickets = getTickets.all(TICKETS_LIST_LIMIT + 1);
-  if (!tickets.length) return ctx.reply('Нет открытых тикетов');
-
-  let answer = tickets.map((ticket) => `#T${ticket.id} ${ticket.created_at}`).join('\n');
-  if (tickets.length > TICKETS_LIST_LIMIT) {
-    answer += `\n...`;
-  }
-  ctx.reply(answer);
-});
-groupBot.command('close', (ctx) => {
-  if (!TICKET_CODE_REGEX.test(ctx.match)) return ctx.reply('Неверный формат команды. Укажите код тикета в таком формате: \`#T0\`', { parse_mode: 'Markdown' });
-  const id = ctx.match.slice(2);
-  const idNumber = Number.parseInt(id);
-  const ticket = getOpenedTicketById.get(idNumber);
-  if (!ticket) return ctx.reply(`Тикет ${ctx.match} не найден или уже закрыт`);
-
-  const removedId = closeTicket.get(idNumber);
-  if (removedId?.id !== idNumber) return ctx.reply(`Не удалось закрыть тикет ${ctx.match}`);
-
-  ctx.reply(`Тикет ${ctx.match} закрыт`);
-  bot.api.sendMessage(ticket.chat_id, `🎉 Работа по вашему обращению завершена.
-
-Благодарим за обращение! Вы всегда можете написать нам снова!`);
-});
-groupBot.command('answer', (ctx) => {
-  const ticketIdMatch = ctx.match.match(/^#T\d+/);
-  if (!ticketIdMatch) return ctx.reply('Неверный формат команды. Укажите код тикета в таком формате: \`#T0\` и через пробел напишите свое сообщение', { parse_mode: 'Markdown' });
-  const [ticketId] = ticketIdMatch;
-  const message = ctx.match.slice(ticketId.length + 1);
-  if (!message) return ctx.reply('Неверный формат команды. Вы не написали сообщение. Формат \`/answer #ID текст\`', { parse_mode: 'Markdown' });
-
-  const id = ticketId.slice(2);
-  const idNumber = Number.parseInt(id);
-  const ticket = getOpenedTicketById.get(idNumber);
-  if (!ticket) return ctx.reply(`Тикет ${ticketId} не найден или уже закрыт`);
-
-  bot.api.sendMessage(ticket.chat_id, `_Ответ от сотрудника поддержки_
-
-${message}`, { parse_mode: 'Markdown' });
-});
-groupBot.command('closed', (ctx) => {
-  const result = getClosedTicketsCount.get();
-  if (!result) return ctx.reply('Запрос в базу провален');
-
-  ctx.reply(`Закрытых тикетов ${result.count}`);
-});
-
-bot.start();
+    /tickets - показать все открытые тикеты`,
+      { parse_mode: "Markdown" }
+    );
+  });
+  groupBot.command("tickets", (ctx) => {
+    const tickets = getTickets.all(TICKETS_LIST_LIMIT + 1);
+    if (!tickets.length) return ctx.reply("Нет открытых тикетов");
+
+    let answer = tickets
+      .map((ticket) => `#T${ticket.id} ${ticket.created_at}`)
+      .join("\n");
+    if (tickets.length > TICKETS_LIST_LIMIT) {
+      answer += `\n...`;
+    }
+    ctx.reply(answer);
+  });
+  groupBot.command("close", (ctx) => {
+    if (!TICKET_CODE_REGEX.test(ctx.match))
+      return ctx.reply(
+        "Неверный формат команды. Укажите код тикета в таком формате: `#T0`",
+        { parse_mode: "Markdown" }
+      );
+    const id = ctx.match.slice(2);
+    const idNumber = Number.parseInt(id);
+    const ticket = getOpenedTicketById.get(idNumber);
+    if (!ticket)
+      return ctx.reply(`Тикет ${ctx.match} не найден или уже закрыт`);
+
+    const removedId = closeTicket.get(idNumber);
+    if (removedId?.id !== idNumber)
+      return ctx.reply(`Не удалось закрыть тикет ${ctx.match}`);
+
+    ctx.reply(`Тикет ${ctx.match} закрыт`);
+    bot.api.sendMessage(
+      ticket.chat_id,
+      `🎉 Работа по вашему обращению завершена.
+
+Благодарим за обращение! Вы всегда можете написать нам снова!`
+    );
+  });
+  groupBot.command("answer", (ctx) => {
+    const ticketIdMatch = ctx.match.match(/^#T\d+/);
+    if (!ticketIdMatch)
+      return ctx.reply(
+        "Неверный формат команды. Укажите код тикета в таком формате: `#T0` и через пробел напишите свое сообщение",
+        { parse_mode: "Markdown" }
+      );
+    const [ticketId] = ticketIdMatch;
+    const message = ctx.match.slice(ticketId.length + 1);
+    if (!message)
+      return ctx.reply(
+        "Неверный формат команды. Вы не написали сообщение. Формат `/answer #ID текст`",
+        { parse_mode: "Markdown" }
+      );
+
+    const id = ticketId.slice(2);
+    const idNumber = Number.parseInt(id);
+    const ticket = getOpenedTicketById.get(idNumber);
+    if (!ticket) return ctx.reply(`Тикет ${ticketId} не найден или уже закрыт`);
+
+    bot.api.sendMessage(
+      ticket.chat_id,
+      `_Ответ от сотрудника поддержки_
+
+${message}`,
+      { parse_mode: "Markdown" }
+    );
+  });
+  groupBot.command("closed", (ctx) => {
+    const result = getClosedTicketsCount.get();
+    if (!result) return ctx.reply("Запрос в базу провален");
+
+    ctx.reply(`Закрытых тикетов ${result.count}`);
+  });
+
+  bot.start();
+})();