Skip to content

Commit

Permalink
Release 0.0.1-beta.0
Browse files Browse the repository at this point in the history
  • Loading branch information
webNeat committed Jun 23, 2021
1 parent 26fdb6a commit 7013bd8
Show file tree
Hide file tree
Showing 29 changed files with 830 additions and 366 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ jobs:
with:
path: ./node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install
- run: yarn build
- run: yarn
- run: yarn test-coverage
- uses: coverallsapp/github-action@master
with:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ jobs:
with:
path: ./node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install
- run: yarn build
- run: yarn
- name: Publish to npm
uses: pascalgn/npm-publish-action@1.3.3
env:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ jobs:
with:
path: ./node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install
- run: yarn build
- run: yarn
- run: yarn test-coverage
- uses: coverallsapp/github-action@master
with:
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"semi": false,
"printWidth": 120,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false
Expand Down
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "messenger-chat",
"author": "Amine Ben hammou",
"version": "0.0.1",
"version": "0.0.1-beta",
"license": "MIT",
"description": "A library to create chat bots for Messenger",
"module": "dist/messenger-chat.esm.js",
Expand All @@ -24,13 +24,8 @@
},
"peerDependencies": {},
"devDependencies": {
"@types/lodash": "^4.14.170",
"tsdx": "^0.14.1",
"tslib": "^2.3.0",
"typescript": "^4.3.2"
},
"dependencies": {
"lodash": "^4.17.21",
"lodash-es": "^4.17.21"
}
}
11 changes: 11 additions & 0 deletions src/bot/addStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {BotConfig, ChatStep} from './types'

export async function addStep(config: BotConfig, name: string, step: ChatStep) {
if (!name) {
throw `The chat step name should not be empty`
}
if (config.steps[name]) {
throw `The chat step '${name}' is already defined`
}
config.steps[name] = step
}
62 changes: 62 additions & 0 deletions src/bot/handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {WebhookEvent, EventEntry} from '../events'
import { MessagingItem } from '../events/types'
import {Message, MessageReply, MessageBuilder} from '../messages'
import {BotConfig, ChatState} from './types'

export async function handle(config: BotConfig, event: WebhookEvent) {
if (event.object !== 'page') {
throw `Unknown event.object value '${event.object}', it should be 'page'`
}
await Promise.all(event.entry.map(e => handleEntry(config, e)))
}

async function handleEntry(config: BotConfig, entry: EventEntry) {
const messaging = entry.messaging[0]
const sender = messaging.sender
const state = await getState(config, sender.id)
const step = await listen(config, state, messaging)
const reply = makeReply(sender, await step.send(state.context))
await Promise.all([config.send(config.accessToken, reply), setState(config, sender.id, state)])
}

async function listen(config: BotConfig, state: ChatState, messaging: MessagingItem) {
if (!state.step) {
state.step = 'start'
return config.steps['start']
}
const {sender, message, postback} = messaging as any
const step = config.steps[state.step]
const setContext = async (value: any) => {
state.context = value
return setState(config, sender.id, state)
}
const nextStep = await step.listen({message, postback, context: state.context, setContext})
if (!nextStep) {
return step
}
if (!config.steps[nextStep]) {
throw `Unknown chat step '${nextStep}'`
}
state.step = nextStep
return config.steps[nextStep]
}

async function getState(config: BotConfig, key: string): Promise<ChatState> {
const value = await config.storage.get(key)
if (value == undefined) {
return {step: '', context: {...config.initialContext}}
}
return JSON.parse(value)
}

async function setState(config: BotConfig, key: string, state: ChatState): Promise<void> {
await config.storage.set(key, JSON.stringify(state))
}

function makeReply(recipient: {id: string}, message: MessageBuilder<Message>): MessageReply {
return {
recipient,
messaging_type: 'RESPONSE',
message: message.get(),
}
}
21 changes: 21 additions & 0 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {send} from './send'
import {handle} from './handle'
import {verify} from './verify'
import {addStep} from './addStep'
import {Bot, BotConfig} from './types'
import {memoryStorage} from '../storages'

export function bot(config: Partial<BotConfig>): Bot {
if (!config.accessToken) throw `The accessToken is missing on the bot configuration!`
if (!config.verifyToken) throw `The verifyToken is missing on the bot configuration!`
if (!config.send) config.send = send
if (!config.storage) config.storage = memoryStorage()
if (!config.initialContext) config.initialContext = {}
if (!config.steps) config.steps = {}

return {
on: (name, step) => addStep(config as BotConfig, name, step),
verify: query => verify(config as BotConfig, query),
handle: event => handle(config as BotConfig, event),
}
}
19 changes: 19 additions & 0 deletions src/bot/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import https from 'https'

export async function send(accessToken: string, data: any) {
data = JSON.stringify(data)
const req = https.request({
hostname: 'graph.facebook.com',
port: 443,
path: '/v11.0/me/messages?access_token=' + accessToken,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length,
},
})
return new Promise<void>((resolve, reject) => {
req.on('error', err => reject(err))
req.end(data, () => resolve())
})
}
41 changes: 41 additions & 0 deletions src/bot/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {WebhookEvent, PostbackEvent, MessageEvent} from '../events'
import {Message, MessageBuilder} from '../messages'
import {ContextStorage} from '../storages'

export type BotConfig = {
accessToken: string
verifyToken: string
initialContext: any
steps: Record<string, ChatStep>
storage: ContextStorage
send: (accessToken: string, data: any) => Promise<void>
}

export interface Bot {
on(name: string, step: ChatStep): void
verify(query: VerificationQuery): string
handle(event: WebhookEvent): Promise<void>
}

export type VerificationQuery = {
'hub.mode': string
'hub.challenge': string
'hub.verify_token': string
}

export type ChatStep = {
send: (context: any) => Promise<MessageBuilder<Message>>
listen: (entry: ChatEntry) => Promise<string | void>
}

export type ChatEntry = {
context: any
setContext: (value: Record<string, any>) => Promise<void>
message?: MessageEvent
postback?: PostbackEvent
}

export type ChatState = {
step: string
context: any
}
8 changes: 8 additions & 0 deletions src/bot/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {BotConfig, VerificationQuery} from './types'

export function verify({verifyToken}: BotConfig, query: VerificationQuery) {
if (query['hub.mode'] !== 'subscribe' || query['hub.verify_token'] !== verifyToken) {
throw `The query parameters are incorrect`
}
return query['hub.challenge']
}
50 changes: 22 additions & 28 deletions src/event.ts → src/events/EventBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import {WebhookAttachment, WebhookEntry, WebhookEvent, WebhookMessage, WebhookPostback} from './types'
import {Builder} from '../utils'
import {EventAttachment, EventEntry, MessageEvent, PostbackEvent, WebhookEvent} from './types'

type EventData = {
export type EventBuilderData = {
senderId?: string
recipientId?: string
timestamp: number
items: Array<EventItem>
items: Array<MessageItem | PostbackItem>
}
type EventItem = EventMessageItem | EventPostbackItem
type EventMessageItem = {timestamp: number; message: WebhookMessage}
type EventPostbackItem = {timestamp: number; postback: WebhookPostback}
type MessageItem = {timestamp: number; message: MessageEvent}
type PostbackItem = {timestamp: number; postback: PostbackEvent}

export function event(data?: EventData) {
return new Event(data || {timestamp: Date.now(), items: []})
}

let nexttMessageId = 1

class Event {
constructor(private data: EventData) {}
let nextFakeMessageId = 1

export class EventBuilder extends Builder<WebhookEvent, EventBuilderData> {
from(senderId: string) {
return this.clone({senderId})
}
Expand All @@ -31,9 +25,9 @@ class Event {
return this.clone({timestamp})
}

postback(title: string, payload: string) {
const mid = `m-${nexttMessageId++}`
const item = {timestamp: this.data.timestamp, postback: {mid, title, payload}}
postback(title: string, payload?: string) {
const mid = `m-${nextFakeMessageId++}`
const item = {timestamp: this.data.timestamp, postback: {mid, title, payload: payload || title}}
return this.clone({items: [...this.data.items, item]})
}

Expand Down Expand Up @@ -69,12 +63,16 @@ class Event {
return this.attachment({type: 'location', payload: {url, coordinates: {lat, long}}})
}

get(): WebhookEvent {
protected validate() {
const {recipientId, senderId, items} = this.data
if (recipientId === undefined) throw `The recipient is missing, use .to(recipientId) to set the recipient`
if (recipientId === undefined)
throw `The recipient is missing, use .to(recipientId) to set the recipient`
if (senderId === undefined) throw `The sender is missing, use .from(senderId) to set the sender`
if (items.length === 0) throw `The message to send with the event is missing`
}

protected build() {
const {recipientId, senderId, items} = this.data
const entry = items.map(({timestamp, ...data}) => {
return {
id: recipientId,
Expand All @@ -87,22 +85,18 @@ class Event {
...data,
},
],
} as WebhookEntry
} as EventEntry
})
return {object: 'page', entry}
return {object: 'page', entry} as WebhookEvent
}

private message(data: Omit<WebhookMessage, 'mid'>) {
const mid = `m-${nexttMessageId++}`
private message(data: Omit<MessageEvent, 'mid'>) {
const mid = `m-${nextFakeMessageId++}`
const item = {timestamp: this.data.timestamp, message: {mid, ...data}}
return this.clone({items: [...this.data.items, item]})
}

private attachment(data: WebhookAttachment) {
private attachment(data: EventAttachment) {
return this.message({attachments: [data]})
}

private clone(data: Partial<EventData>) {
return event({...this.data, ...data})
}
}
7 changes: 7 additions & 0 deletions src/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {EventBuilder, EventBuilderData} from './EventBuilder'

export {WebhookEvent, EventEntry, PostbackEvent, MessageEvent} from './types'

export function event(data?: EventBuilderData) {
return new EventBuilder(data || {timestamp: Date.now(), items: []})
}
Loading

0 comments on commit 7013bd8

Please sign in to comment.