negezor / vk-io

Modern VK API SDK for Node.js
https://npm.im/vk-io
MIT License
548 stars 85 forks source link

Сцены выполняются без ожидания #252

Closed devis-hub closed 4 years ago

devis-hub commented 4 years ago

Заметил такую особенность при работе с LongPoll сервером. Сцены выполняются без ожидания действия. К примеру нужно реализовать сцену где идёт опрос и запись введенных данных в stage. Взять пример из документации. Он так же идёт без ожидания. Я пробовал сравнивать последнее сообщение из прошлой сцены и сообщение в новой сцене сохраняя createAt в stage.

Работает, но не совсем корректно, и насколько это целесообразно? Не создает ли это лишних загрузок, так как сцена выполняется много-много раз до тех пор пока не случился условие где даты сообщений с прошлой сцены и последнее сообщение не сравниваются. Может уже есть тот, кто решил подобную проблемы. Извиняюсь если такой вопрос уже был задан, я изучил и вроде как не нашёл ответов на подобное.

mulfyx commented 4 years ago

Можно ли посмотреть на код?

devis-hub commented 4 years ago

Это главный класс бота. По сути ничего не обычного, я его экспортирую в index.js и вызываю его run() в конце файла.

` class Bot {

public VK
public sessionManager
public sceneManager

constructor() {
    this.VK = new VK({
        token: config.vk.access_token
    })
    this.sessionManager = new SessionManager()
    this.sceneManager = new SceneManager()

    this.VK.updates.on('message', this.sessionManager.middleware)
    this.VK.updates.on('message', this.sceneManager.middleware)
    this.VK.updates.on('message', this.sceneManager.middlewareIntercept)
    this.VK.updates.on('message', (ctx, next) => {
        const {messagePayload} = ctx
        ctx.state.command = messagePayload && messagePayload.command
            ? messagePayload.command
            : null

        return next()
    })
}

public run = () => {

    import('../database')
    import('../services/commands')

    Database.run().then(() => {
        // Database init
        if (Configuration.db.force) {
            City.create({name: 'Челябинск'}).then((city) => {
                College.create({name: "ЧГПГТ им. Яковлева", cityId: city['id']});
                College.create({name: "ЮУгк", cityId: city['id']});
            })
        }

        this.VK.updates.startPolling().then(() => {
            console.info("Application started")
        }).catch(console.error)

    }).catch(() => console.error("Application starting failed"))
}

} `

А вот пример сцены. На самом деле я уже что только не пробовал. И использовать async await и возвращать return в условиях. Тут не реализовано условия проверки времени сообщений как я писал выше.

` Bot.sceneManager.addScene(new StepScene("registerScene", [ async (ctx) => {

    if (ctx.scene.step.firstTime || !ctx.text) {
        await City.findAll().then((cities) => {
            let keyboards = [];

            for (let city of cities) {
                keyboards.push(Keyboard.textButton({
                    label: city['name'],
                    color: Keyboard.PRIMARY_COLOR
                }))
            }

            return ctx.send({
                message: '🤖 - Регистрация не займёт много времени. Мне лишь нужно узнать с какого ты города, в каком учебном учреждении обучаешься и в каком(ой) классе(группе)\n\nВ каком городе находится твоё учебное учреждение?',
                keyboard: Keyboard.keyboard(keyboards).oneTime(true)
            });

        });
    }

    return ctx.scene.step.next()
},
async (ctx) => {
    if (ctx.scene.step.firstTime || !ctx.text) {
        return ctx.send({
            message: '🤖 ЭТО ОТПРАВЛЯЕТСЯ СРАЗУ ЖЕ БЕЗ ОЖИДАНИЙ ПРОШЛОЙ СЦЕНЫ',
        });
    }
}

])) `

negezor commented 4 years ago

Ну так всё логично, это всё из-за того что здесь перемешан Promise API и async/await.

if (ctx.scene.step.firstTime || !ctx.text) {
-        await City.findAll().then((cities) => {
+        return City.findAll().then((cities) => {

Или переписать код нормально:

Bot.sceneManager.addScene(new StepScene("registerScene", [
    async (ctx) => {
        if (ctx.scene.step.firstTime || !ctx.text) {
            const keyboard = Keyboard.builder()
                .oneTime(true);

            const cities = await City.findAll();

            for (const city of cities) {
                keyboard
                    .textButton({
                        label: city.name,
                        color: Keyboard.PRIMARY_COLOR
                    })
                    .row()
            }

            return ctx.send({
                message: '🤖 - Регистрация не займёт много времени. Мне лишь нужно узнать с какого ты города, в каком учебном учреждении обучаешься и в каком(ой) классе(группе)\n\nВ каком городе находится твоё учебное учреждение?',
                keyboard
            });
        }

        return ctx.scene.step.next()
    },
    async (ctx) => {
        if (ctx.scene.step.firstTime || !ctx.text) {
            return ctx.send({
                message: '🤖 ЭТО УЖЕ ОТПРАВИТСЯ ПОСЛЕ ПРОШЛОГО ШАГА',
            });
        }
    }
]));
devis-hub commented 4 years ago

Ну так всё логично, это всё из-за того что здесь перемешан Promise API и async/await.

if (ctx.scene.step.firstTime || !ctx.text) {
-        await City.findAll().then((cities) => {
+        return City.findAll().then((cities) => {

Или переписать код нормально:

Bot.sceneManager.addScene(new StepScene("registerScene", [
    async (ctx) => {
        if (ctx.scene.step.firstTime || !ctx.text) {
            const keyboard = Keyboard.builder()
                .oneTime(true);

            const cities = await City.findAll();

            for (const city of cities) {
                keyboard
                    .textButton({
                        label: city.name,
                        color: Keyboard.PRIMARY_COLOR
                    })
                    .row()
            }

            return ctx.send({
                message: '🤖 - Регистрация не займёт много времени. Мне лишь нужно узнать с какого ты города, в каком учебном учреждении обучаешься и в каком(ой) классе(группе)\n\nВ каком городе находится твоё учебное учреждение?',
                keyboard
            });
        }

        return ctx.scene.step.next()
    },
    async (ctx) => {
        if (ctx.scene.step.firstTime || !ctx.text) {
            return ctx.send({
                message: '🤖 ЭТО УЖЕ ОТПРАВИТСЯ ПОСЛЕ ПРОШЛОГО ШАГА',
            });
        }
    }
]));

Увы не в этом проблема. Взять пример из документации и немного его переделав под свой код я получаю это.

Bot.sceneManager.addScene(new StepScene('signup', [
    (context) => {
        if (context.scene.step.firstTime || !context.text) {
            return context.send('What\'s your name?');
        }

        context.scene.state.firstName = context.text;

        return context.scene.step.next();
    },
    (context) => {
        if (context.scene.step.firstTime || !context.text) {
            return context.send('How old are you?');
        }

        context.scene.state.age = Number(context.text);

        return context.scene.step.next();
    },
    async (context) => {
        const { firstName, age } = context.scene.state;

        await context.send(`👤 ${firstName} ${age} ages`);

        return context.scene.step.next(); // Automatic exit, since this is the last scene
    }
]))

Ввожу команду которая делает ctx.scene.enter('signup'). И что я получаю в ответ? Спам сообщений от бота без задержек между сценами.

Нету никаких ожиданий пользователя. Должно было быть что-то вроде

Я долгое время искал проблему и нашёл только то, что бот использует свой же ответы и записывает их в stage сцены. Это можно увидеть в ответе

Когда ответ должен быть

Проблема в том, что он не ждёт ответов или действий от пользователя. Если с кнопками я еще смогу разобраться, то с ожидание ввода текста всё сложнее, так как я не могу выяснить от кого сообщение, он в любом случае говорит что это юзер пишет, не важно даже что бот ответил на свое же сообщение, считает что это сообщения юзера, методов не находил нужных. И на сколько помню эта проблема связана с работой longpoll

negezor commented 4 years ago

Ну так снова логично, ВКонтакте присылает исходящие сообщение бота, код их пропускает - значит всё в порядке. Варианты решения:

devis-hub commented 4 years ago
vk.updates.on('message', (context, next) => (
  context.isOutbox
     ? next()
      : undefined
))

Это мне кажется странным но как я вставил ваш midleware в свой код он перестал отвечать на любые сообщения от пользователя. Вывел в консоль context.isOutbox, выдает false, то есть не пропускает. Я зашел в сообщения группы и написал от имени группы пользователю, и это работает, он вернул true и даже была некая задержка в сцене. Подумал, что достаточно будет поменять сделать !context.isOutbox.

Но нет, это работает как и ранее, т.е он не ожидает никаких шагов на сцене и шлёт их дальше, воспринимая любые ответы как ответы пользователя, даже те что отправил бот средством ctx.send или ctx.reply. Пробовал отключать так же флажки исходящих сообщений, оставив только входящие, это так же не решило моей проблемы.

Я так же видел прошлый вопрос (решение ниже) и это не работает для меня. Хотя должно если подумать логически. Я уже давно борюсь с этим и не могу понять в чём может быть причина. Уже руки опускаются. Последняя надежда что мне помогут тут.

vk.updates.on('message', (context, next) => {
    if (context.isOutbox) {
        return;
    }

    return next();
});
devis-hub commented 4 years ago

Причина была банальной... Ваш ответ помог

vk.updates.on('message', (context, next) => (
  context.isOutbox
     ? next()
      : undefined
))

Только я его переделал на

vk.updates.on('message', (ctx, next) => ctx.isOutbox ? undefined : next())

то есть поменял местами next() и undefined. Главная моя проблема была в том что я вставлял этот midleware после midleware сцены.

// НУЖНО ПРОПИСЫВАТЬ ВЫШЕ, ВОТ СУДА
// vk.updates.on('message', (ctx, next) => ctx.isOutbox ? undefined : next())

vk.updates.on('message', this.sceneManager.middleware)
vk.updates.on('message', this.sceneManager.middlewareIntercept)

//  Я ПРОПИСАЛ НИЖЕ
vk.updates.on('message', (ctx, next) => ctx.isOutbox ? undefined : next())

Как только я перенёс свой midleware выше остальных, все заработало. Порой такие банальные вещи не могут придти в голову и сидишь ломаешь её несколько дней. Прошу прощение за потраченное время, без ваших догадок я думаю не додумался до порядка выполнения midlewares. Спасибо большое. Закрываю вопрос. Решение я описал в этом ответе.