<commit_before>import axios, { AxiosInstance } from 'axios'
import * as sdk from 'botpress/sdk'
import crypto from 'crypto'
import { json as expressJson, Router } from 'express'
import _ from 'lodash'

import { Config } from '../config'

const debug = DEBUG('channel-messenger')
const debugMessages = debug.sub('messages')
const debugHttp = debug.sub('http')
const debugWebhook = debugHttp.sub('webhook')
const debugHttpOut = debugHttp.sub('out')

const outgoingTypes = ['text', 'typing', 'login_prompt', 'carousel']
type MessengerAction = 'typing_on' | 'typing_off' | 'mark_seen'

type MountedBot = { pageId: string; botId: string; client: MessengerClient }

export class MessengerService {
  private readonly http = axios.create({ baseURL: 'https://graph.facebook.com/v3.2/me' })
  private mountedBots: MountedBot[] = []
  private router: Router & sdk.http.RouterExtension
  private appSecret: string

  constructor(private bp: typeof sdk) {}

  async initialize() {
    const config = (await this.bp.config.getModuleConfig('channel-messenger')) as Config

    if (!config.verifyToken || !config.verifyToken.length) {
      throw new Error(
        'You need to set a non-empty value for "verifyToken" in data/global/config/channel-messenger.json'
      )
    }

    if (!config.appSecret || !config.appSecret.length) {
      throw new Error(`You need to set a non-empty value for "appSecret" in data/global/config/channel-messenger.json`)
    }

    this.appSecret = config.appSecret

    this.router = this.bp.http.createRouterForBot('channel-messenger', {
      checkAuthentication: false,
      enableJsonBodyParser: false // we use our custom json body parser instead, see below
    })

    this.router.getPublicPath().then(publicPath => {
      if (publicPath.indexOf('https://') !== 0) {
        this.bp.logger.warn('Messenger requires HTTPS to be setup to work properly. See EXTERNAL_URL botpress config.')
      }

      this.bp.logger.info(`Messenger Webhook URL is ${publicPath.replace('BOT_ID', '___')}/webhook`)
    })

    this.router.use(
      expressJson({
        verify: this._verifySignature.bind(this)
      })
    )

    this.router.get('/webhook', this._setupWebhook.bind(this))
    this.router.post('/webhook', this._handleIncomingMessage.bind(this))

    this.bp.events.registerMiddleware({
      description: 'Sends outgoing messages for the messenger channel',
      direction: 'outgoing',
      handler: this._handleOutgoingEvent.bind(this),
      name: 'messenger.sendMessages',
      order: 200
    })
  }

  async mountBot(botId: string) {
    const config = (await this.bp.config.getModuleConfigForBot('channel-messenger', botId)) as Config
    if (config.enabled) {
      if (!config.accessToken) {
        return this.bp.logger
          .forBot(botId)
          .error('You need to configure an Access Token to enable it. Messenger Channel is disabled for this bot.')
      }

      const { data } = await this.http.get('/', { params: { access_token: config.accessToken } })

      if (!data || !data.id) {
        return this.bp.logger
          .forBot(botId)
          .error(
            'Could not register bot, are you sure your Access Token is valid? Messenger Channel is disabled for this bot.'
          )
      }

      const pageId = data.id
      const client = new MessengerClient(botId, this.bp, this.http)
      this.mountedBots.push({ botId: botId, client, pageId })

      await client.setupGreeting()
      await client.setupGetStarted()
      await client.setupPersistentMenu()
    }
  }

  async unmountBot(botId: string) {
    this.mountedBots = _.remove(this.mountedBots, x => x.botId === botId)
  }

  getMessengerClientByBotId(botId: string): MessengerClient {
    const entry = _.find(this.mountedBots, x => x.botId === botId)

    if (!entry) {
      throw new Error(`Can't find a MessengerClient for bot "${botId}"`)
    }

    return entry.client
  }

  // See: https://developers.facebook.com/docs/messenger-platform/webhook#security
  private _verifySignature(req, res, buffer) {
    const signatureError = new Error("Couldn't validate the request signature.")

    if (!/^\/webhook/i.test(req.path)) {
      return
    }

    const signature = req.headers['x-hub-signature']
    if (!signature || !this.appSecret) {
      throw signatureError
    } else {
      const [, hash] = signature.split('=')

      const expectedHash = crypto
        .createHmac('sha1', this.appSecret)
        .update(buffer)
        .digest('hex')

      if (hash !== expectedHash) {
        debugWebhook('invalid signature', req.path)
        throw signatureError
      } else {
        debugWebhook('signed', req.path)
      }
    }
  }

  private async _handleIncomingMessage(req, res) {
    const body = req.body

    if (body.object !== 'page') {
      // TODO: Handle other cases here
    } else {
      res.status(200).send('EVENT_RECEIVED')
    }

    for (const entry of body.entry) {
      const pageId = entry.id
      const messages = entry.messaging

      const bot = _.find<MountedBot>(this.mountedBots, { pageId })
      if (!bot) {
        debugMessages('could not find a bot for page id =', pageId)
        continue
      }
      if (!messages) {
         debugMessages('incoming event without messaging entry')
         return
       }
      for (const webhookEvent of messages) {
        if (!webhookEvent.sender) {
          return
        }

        debugMessages('incoming', webhookEvent)
        const senderId = webhookEvent.sender.id

        await bot.client.sendAction(senderId, 'mark_seen')

        if (webhookEvent.message) {
          await this._sendEvent(bot.botId, senderId, webhookEvent.message, { type: 'message' })
        } else if (webhookEvent.postback) {
          await this._sendEvent(bot.botId, senderId, { text: webhookEvent.postback.payload }, { type: 'callback' })
        }
      }
    }<commit_msg>fix: continue instead of return<commit_after>import axios, { AxiosInstance } from 'axios'
import * as sdk from 'botpress/sdk'
import crypto from 'crypto'
import { json as expressJson, Router } from 'express'
import _ from 'lodash'

import { Config } from '../config'

const debug = DEBUG('channel-messenger')
const debugMessages = debug.sub('messages')
const debugHttp = debug.sub('http')
const debugWebhook = debugHttp.sub('webhook')
const debugHttpOut = debugHttp.sub('out')

const outgoingTypes = ['text', 'typing', 'login_prompt', 'carousel']
type MessengerAction = 'typing_on' | 'typing_off' | 'mark_seen'

type MountedBot = { pageId: string; botId: string; client: MessengerClient }

export class MessengerService {
  private readonly http = axios.create({ baseURL: 'https://graph.facebook.com/v3.2/me' })
  private mountedBots: MountedBot[] = []
  private router: Router & sdk.http.RouterExtension
  private appSecret: string

  constructor(private bp: typeof sdk) {}

  async initialize() {
    const config = (await this.bp.config.getModuleConfig('channel-messenger')) as Config

    if (!config.verifyToken || !config.verifyToken.length) {
      throw new Error(
        'You need to set a non-empty value for "verifyToken" in data/global/config/channel-messenger.json'
      )
    }

    if (!config.appSecret || !config.appSecret.length) {
      throw new Error(`You need to set a non-empty value for "appSecret" in data/global/config/channel-messenger.json`)
    }

    this.appSecret = config.appSecret

    this.router = this.bp.http.createRouterForBot('channel-messenger', {
      checkAuthentication: false,
      enableJsonBodyParser: false // we use our custom json body parser instead, see below
    })

    this.router.getPublicPath().then(publicPath => {
      if (publicPath.indexOf('https://') !== 0) {
        this.bp.logger.warn('Messenger requires HTTPS to be setup to work properly. See EXTERNAL_URL botpress config.')
      }

      this.bp.logger.info(`Messenger Webhook URL is ${publicPath.replace('BOT_ID', '___')}/webhook`)
    })

    this.router.use(
      expressJson({
        verify: this._verifySignature.bind(this)
      })
    )

    this.router.get('/webhook', this._setupWebhook.bind(this))
    this.router.post('/webhook', this._handleIncomingMessage.bind(this))

    this.bp.events.registerMiddleware({
      description: 'Sends outgoing messages for the messenger channel',
      direction: 'outgoing',
      handler: this._handleOutgoingEvent.bind(this),
      name: 'messenger.sendMessages',
      order: 200
    })
  }

  async mountBot(botId: string) {
    const config = (await this.bp.config.getModuleConfigForBot('channel-messenger', botId)) as Config
    if (config.enabled) {
      if (!config.accessToken) {
        return this.bp.logger
          .forBot(botId)
          .error('You need to configure an Access Token to enable it. Messenger Channel is disabled for this bot.')
      }

      const { data } = await this.http.get('/', { params: { access_token: config.accessToken } })

      if (!data || !data.id) {
        return this.bp.logger
          .forBot(botId)
          .error(
            'Could not register bot, are you sure your Access Token is valid? Messenger Channel is disabled for this bot.'
          )
      }

      const pageId = data.id
      const client = new MessengerClient(botId, this.bp, this.http)
      this.mountedBots.push({ botId: botId, client, pageId })

      await client.setupGreeting()
      await client.setupGetStarted()
      await client.setupPersistentMenu()
    }
  }

  async unmountBot(botId: string) {
    this.mountedBots = _.remove(this.mountedBots, x => x.botId === botId)
  }

  getMessengerClientByBotId(botId: string): MessengerClient {
    const entry = _.find(this.mountedBots, x => x.botId === botId)

    if (!entry) {
      throw new Error(`Can't find a MessengerClient for bot "${botId}"`)
    }

    return entry.client
  }

  // See: https://developers.facebook.com/docs/messenger-platform/webhook#security
  private _verifySignature(req, res, buffer) {
    const signatureError = new Error("Couldn't validate the request signature.")

    if (!/^\/webhook/i.test(req.path)) {
      return
    }

    const signature = req.headers['x-hub-signature']
    if (!signature || !this.appSecret) {
      throw signatureError
    } else {
      const [, hash] = signature.split('=')

      const expectedHash = crypto
        .createHmac('sha1', this.appSecret)
        .update(buffer)
        .digest('hex')

      if (hash !== expectedHash) {
        debugWebhook('invalid signature', req.path)
        throw signatureError
      } else {
        debugWebhook('signed', req.path)
      }
    }
  }

  private async _handleIncomingMessage(req, res) {
    const body = req.body

    if (body.object !== 'page') {
      // TODO: Handle other cases here
    } else {
      res.status(200).send('EVENT_RECEIVED')
    }

    for (const entry of body.entry) {
      const pageId = entry.id
      const messages = entry.messaging

      const bot = _.find<MountedBot>(this.mountedBots, { pageId })
      if (!bot) {
        debugMessages('could not find a bot for page id =', pageId)
        continue
      }
      if (!messages) {
         debugMessages('incoming event without messaging entry')
         continue
       }
      for (const webhookEvent of messages) {
        if (!webhookEvent.sender) {
          continue
        }

        debugMessages('incoming', webhookEvent)
        const senderId = webhookEvent.sender.id

        await bot.client.sendAction(senderId, 'mark_seen')

        if (webhookEvent.message) {
          await this._sendEvent(bot.botId, senderId, webhookEvent.message, { type: 'message' })
        } else if (webhookEvent.postback) {
          await this._sendEvent(bot.botId, senderId, { text: webhookEvent.postback.payload }, { type: 'callback' })
        }
      }
    }
{'fraction_non_alphanumeric': 0.12235393734123624, 'fraction_numerical': 0.001693480101608806, 'mean_word_length': 2.8661211129296236, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '523847', 'n_tokens_mistral': 3606, 'n_tokens_neox': 3482, 'n_words': 888}