Skip to content

Latest commit

 

History

History
317 lines (227 loc) · 6.48 KB

README.md

File metadata and controls

317 lines (227 loc) · 6.48 KB

Chat toolkit

GitHub Tag NPM Downloads GitHub Actions Workflow Status

What is it ?

it's library/toolkit for easier chat-bot state managment/persistion

currently only telegram API supported, further extension for discrod API possible

Content tree

Adding new state

Describe state using actions like say, expect, switchState and others

export async function mainState() {

  await say('Enter a')

  const a = Number(await expect_())

  await say('Enter b')

  const b = Number(await expect_())

  await say("Reuslt is " + (a + b))
  
  await switchState('mainState')
  // or 
  await stateSwitcher.mainState()

}

And then add that state to AllStates to safe typing

declare module 'chat-toolkit' {
  interface AllStates {
    mainState: typeof mainState
  }
}

Default / Main state

You need to specify what is the entry point state and what should run first

that's done at setup, see this for details

const handler
  = createTelegramHandler({
    bot,
    allStates,
    defaultState: 'mainState'
  }, dbParams)

Setup

Ensure prisma config and file structure is right

Currently this npm relies on prisma (in future planned ability to chose over other orms) and it assumes you have enabled your prismaSchemaFolder in schema.prisma and it lies in prisma/schema/ folder

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["typedSql", "prismaSchemaFolder"]
}

Generate models

Then you need to run

npx chat-toolkit setup 

which would create needed models in your project folder

Apply prisma migrations

After that you'll have to apply migration migrate

yarn prisma migrate dev --name add_chat_toolkit_models

Adjust code to supply events to handler

Example with Telegraf:

const bot = new Telegraf(process.env.TG_TOKEN)
const prisma = new PrismaClient()

const dbParams = {
  findOrCreateUser: findOrCreateUserPrisma(prisma as any), 
  stateManager: defaultPrismaStateManagerImplementation(prisma as any),
  // unfrotunatelly cast to any needed, since local prisma is not the same as chat-toolkits 
  // right now idk how to avoid this but it's works just fine
}

const allStates = {
  mainState
}

const handler
  = createTelegramHandler({
    bot,
    allStates,
    defaultState: 'mainState'
  }, dbParams)


bot.start(async ctx => {
  await handler.handlePrivateMessage(ctx, true)
})


bot.on('message', async ctx => {
  await handler.handlePrivateMessage(ctx as any, false)
}) 

Actions

Avaliable actions

say :: String -> Effect ()

^ Just sends text message to user

expect_ :: () -> Effect (String)

^ Expects text message from user

expectAny :: (Message -> a) -> Effect a 

^ In case if if need to wait for specific user message or combined

for example

await expectAny(msg => {
  if ('photo' in msg) {
    return msg.photo[0]
  }
  if ('text' in msg) {
    return msg.text
  }
})
random :: () -> Effect (Number)

^ since states can do anything it's better not to use Math.random directly so it's just wrapper over it (needed for restoring state)

TODO:

suggest

suggestIt

_disableRecording _onRestoreDoRun

switchState escape_

Modifing actions

TODO

Notification / Interrupt state

TODO

Inline keyboard handler

Suppose case when you need inline keyboard for example [like, dislike]

then each button should execute some kind of logic

in this case you can get use of createCallbackHandle:

await bot.sendMessage('you liked that post?', {
  reply_markup: { 
    inline_keyboard: [[
      {
        text: "Like",
        callback_data: await recordingObject.like({post_id: post.id}),
      },
      {
        text: "Dislike",
        callback_data: await recordingObject.dislike({post_id: post.id}),
      }
    ]]
  }
}) 

where recordingObject would be defined like this:

const recordingObject = createCallbackHandle({
  namespace: 'post-likes',
  handler: ctx => ({
    async like({post_id}: { post_id: number }) {
      const user = await User.find(ctx.from.id)
      await Post.find(post_id).likeBy(user)
      await ctx.editMessageText //...
    },

    dislike({post_id}: { post_id: number }) {
      const user = await User.find(ctx.from.id)
      await Post.find(post_id).likeBy(user)
      await ctx.editMessageText //...
    }
  })
}) 

To use this you need to enable handler

const redis = new Redis() // from ioredis npm
const bot = new Telegraf(process.env.TG_TOKEN)

/// ...

export const { 
  setupCallbackHandler, 
  createCallbackHandle,
 } = createRedisInlineKeyboardHandler({
  redis,
  projectName: 'mybot', 
  ivalidateIn: duration(1, 'day'),
 })

// ...

setupCallbackHandler(bot)

Each call to recordingObject generates a UUID that is stored in callback_data on the Telegram side. When a button is pressed, the corresponding function is invoked.

On our end, the arguments for the handler function are stored in Redis under the key ${projectName}:callbackquery:${uuid} for 24 hours (this is the default duration and covers 99% of use cases).

Adjusting global state

TODO

declare module 'chat-toolkit' {
  interface GlobalSharedAppContext {
   
  }
}

Adjusting escape ctx

declare module 'chat-toolkit' {
  interface EscapeData {
    user: User
  }
}

TODO

  • Notifications
  • Custom expects
  • GC
  • Detect long states
  • Error handling strategy