diff --git a/packages/bottender-facebook/src/FacebookConnector.ts b/packages/bottender-facebook/src/FacebookConnector.ts index 8627c5910..c7fc9fb73 100644 --- a/packages/bottender-facebook/src/FacebookConnector.ts +++ b/packages/bottender-facebook/src/FacebookConnector.ts @@ -14,6 +14,8 @@ import { import FacebookClient from './FacebookClient'; import FacebookContext from './FacebookContext'; import FacebookEvent from './FacebookEvent'; +import InstagramContext from './InstagramContext'; +import InstagramEvent from './InstagramEvent'; type FacebookRequestBody = Record; @@ -64,12 +66,14 @@ export default class FacebookConnector return 'facebook'; } - getUniqueSessionKey(event: FacebookEvent | MessengerEvent): string | null { + getUniqueSessionKey( + event: FacebookEvent | MessengerEvent | InstagramEvent + ): string | null { if (event instanceof MessengerEvent) { return this._messengerConnector.getUniqueSessionKey(event); } - // TODO: How to determine session key in facebook feed events + // TODO: How to determine session key in facebook feed events and instagram return null; } @@ -108,45 +112,59 @@ export default class FacebookConnector public mapRequestToEvents( body: FacebookRequestBody - ): (FacebookEvent | MessengerEvent)[] { - // TODO: returns InstagramEvent (object === 'instagram') - if (body.object !== 'page') { - return []; + ): (FacebookEvent | MessengerEvent | InstagramEvent)[] { + if (body.object === 'instagram') { + return body.entry + .map((rawEvent: any) => { + const businessAccountId = rawEvent.id; + if (rawEvent.changes) { + return new InstagramEvent(rawEvent.changes[0], { + businessAccountId, + }); + } + + return null; + }) + .filter((event: any) => event !== null); } - return body.entry - .map((rawEvent: any) => { - const pageId = rawEvent.id; - if (rawEvent.messaging) { - return new MessengerEvent(rawEvent.messaging[0], { - pageId, - isStandby: false, - }); - } - - if (rawEvent.standby) { - return new MessengerEvent(rawEvent.standby[0], { - pageId, - isStandby: true, - }); - } - - if (rawEvent.changes) { - return new FacebookEvent(rawEvent.changes[0], { pageId }); - } - - return null; - }) - .filter((event: any) => event !== null); + if (body.object === 'page') { + return body.entry + .map((rawEvent: any) => { + const pageId = rawEvent.id; + if (rawEvent.messaging) { + return new MessengerEvent(rawEvent.messaging[0], { + pageId, + isStandby: false, + }); + } + + if (rawEvent.standby) { + return new MessengerEvent(rawEvent.standby[0], { + pageId, + isStandby: true, + }); + } + + if (rawEvent.changes) { + return new FacebookEvent(rawEvent.changes[0], { pageId }); + } + + return null; + }) + .filter((event: any) => event !== null); + } + + return []; } public async createContext(params: { - event: FacebookEvent | MessengerEvent; + event: FacebookEvent | MessengerEvent | InstagramEvent; session?: Session; initialState?: Record; requestContext?: RequestContext; emitter?: EventEmitter; - }): Promise { + }): Promise { let customAccessToken; if (this._mapPageToAccessToken) { @@ -169,6 +187,16 @@ export default class FacebookConnector appId: this._appId, }); } + if (params.event instanceof InstagramEvent) { + return new InstagramContext({ + ...params, + event: params.event, + client: this._client, + customAccessToken, + batchQueue: this._batchQueue, + appId: this._appId, + }); + } return new MessengerContext({ ...params, event: params.event, diff --git a/packages/bottender-facebook/src/InstagramContext.ts b/packages/bottender-facebook/src/InstagramContext.ts new file mode 100644 index 000000000..175768739 --- /dev/null +++ b/packages/bottender-facebook/src/InstagramContext.ts @@ -0,0 +1,85 @@ +import { EventEmitter } from 'events'; + +import { Context, RequestContext } from 'bottender'; +import { MessengerBatchQueue } from 'messenger-batch'; + +import FacebookClient from './FacebookClient'; +import InstagramBatch from './InstagramBatch'; +import InstagramEvent from './InstagramEvent'; + +// TODO: use exported type +type Session = Record; + +type Options = { + appId?: string; + client: FacebookClient; + event: InstagramEvent; + session?: Session; + initialState?: Record; + requestContext?: RequestContext; + customAccessToken?: string; + batchQueue?: MessengerBatchQueue | null; + emitter?: EventEmitter; +}; + +export default class InstagramContext extends Context< + FacebookClient, + InstagramEvent +> { + _appId: string | undefined; + + _customAccessToken: string | undefined; + + _batchQueue: MessengerBatchQueue | undefined; + + public constructor({ + appId, + client, + event, + session, + initialState, + requestContext, + customAccessToken, + batchQueue, + emitter, + }: Options) { + super({ client, event, session, initialState, requestContext, emitter }); + this._customAccessToken = customAccessToken; + this._batchQueue = batchQueue || undefined; + this._appId = appId; + } + + /** + * The name of the platform. + * + */ + get platform(): 'instagram' { + return 'instagram'; + } + + /** + * + * @param text The text to be sent in the reply. + */ + public async sendText(text: string) { + // + } + + /** + * @see https://developers.facebook.com/docs/instagram-api/guides/comment-moderation + */ + + public async hideComment() {} + + public async unhideComment() {} + + public async deleteComment() {} + + public async getReplies() {} + + public async sendReplies() {} + + public async disableComment() {} + + public async enableComment() {} +} diff --git a/packages/bottender-facebook/src/InstagramEvent.ts b/packages/bottender-facebook/src/InstagramEvent.ts new file mode 100644 index 000000000..6f5a9d941 --- /dev/null +++ b/packages/bottender-facebook/src/InstagramEvent.ts @@ -0,0 +1,91 @@ +export type InstagramRawEvent = + /** + * Sends a notification to your endpoint when an Instagram user comments on a media object owned by an Instagram Business or Creator Account. + */ + | { + field: 'comments'; + value: Record; // TODO + } + /** + * Sends a notification to your endpoint when an Instagram user @mentions an Instagram Business or Creator Account in a comment or caption on a media object not owned by the Business or Creator. + */ + | { + field: 'mentions'; + value: { + commentId?: string; + mediaId: string; + }; + } + /** + * Sends a notification to your endpoint when a story owned by an Instagram Business or Creator Account expires. + */ + | { + field: 'story_insights'; + value: { + mediaId: string; + exits: number; + replies: number; + reach: number; + tapsForward: number; + tapsBack: number; + impressions: number; + }; + }; + +export default class InstagramEvent { + _rawEvent: InstagramRawEvent; + + _businessAccountId: string | undefined; + + constructor( + rawEvent: InstagramRawEvent, + options: { businessAccountId?: string } = {} + ) { + this._rawEvent = rawEvent; + this._businessAccountId = options.businessAccountId; + } + + /** + * Underlying raw event from Instagram. + * + */ + get rawEvent(): InstagramRawEvent { + return this._rawEvent; + } + + /** + * Determine if the event is a message event. + * + */ + get isMessage(): boolean { + return false; + } + + /** + * Determine if the event is a message event which includes text. + * + */ + get isText(): boolean { + return false; + } + + /** + * The text string from Instagram raw event. + * + */ + get text(): string | null { + return null; + } + + get isComment(): boolean { + return this.rawEvent.field === 'comments'; + } + + get isMention(): boolean { + return this.rawEvent.field === 'mentions'; + } + + get isStoryInsights(): boolean { + return this.rawEvent.field === 'story_insights'; + } +}