Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.0.1-beta.0 #1

Merged
merged 1 commit into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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