-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #374 from kaogeek/dev-shiorin
Auto Post from facebook webhook
- Loading branch information
Showing
8 changed files
with
376 additions
and
2 deletions.
There are no files selected for viewing
208 changes: 208 additions & 0 deletions
208
api-spanboon/src/api/controllers/FacebookWebhookController.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
/* | ||
* @license Spanboon Platform v0.1 | ||
* (c) 2020-2021 KaoGeek. http://kaogeek.dev | ||
* License: MIT. https://opensource.org/licenses/MIT | ||
* Author: shiorin <[email protected]> | ||
*/ | ||
|
||
import 'reflect-metadata'; | ||
import moment from 'moment'; | ||
import { JsonController, Res, QueryParams, Get, Body } from 'routing-controllers'; | ||
import { ObjectID } from 'mongodb'; | ||
import { PROVIDER } from '../../constants/LoginProvider'; | ||
import { PageSocialAccountService } from '../services/PageSocialAccountService'; | ||
import { FacebookWebhookLogsService } from '../services/FacebookWebhookLogsService'; | ||
import { PageService } from '../services/PageService'; | ||
import { PostsService } from '../services/PostsService'; | ||
import { SocialPostService } from '../services/SocialPostService'; | ||
import { AssetService } from '../services/AssetService'; | ||
import { PostsGalleryService } from '../services/PostsGalleryService'; | ||
import { FacebookWebhookLogs } from '../models/FacebookWebhookLogs'; | ||
import { Posts } from '../models/Posts'; | ||
import { PostsGallery } from '../models/PostsGallery'; | ||
import { SocialPost } from '../models/SocialPost'; | ||
import { POST_TYPE } from '../../constants/PostType'; | ||
import { ASSET_PATH } from '../../constants/AssetScope'; | ||
import { facebook_setup } from '../../env'; | ||
|
||
@JsonController('/fb_webhook') | ||
export class FacebookWebhookController { | ||
constructor(private pageSocialAccountService: PageSocialAccountService, private facebookWebhookLogsService: FacebookWebhookLogsService, | ||
private pageService: PageService, private postsService: PostsService, private socialPostService: SocialPostService, | ||
private assetService: AssetService, private postsGalleryService: PostsGalleryService) { } | ||
|
||
/** | ||
* @api {get} /api/fb_webhook/page_feeds WebHook for page feed | ||
* @apiGroup Facebook | ||
* @apiSuccessExample {json} Success | ||
* HTTP/1.1 200 OK | ||
* { | ||
* "message": "", | ||
* "data":{ | ||
* } | ||
* "status": "1" | ||
* } | ||
* @apiSampleRequest /api/fb_webhook/page_feeds | ||
* @apiErrorExample {json} WebHook for page feed | ||
* HTTP/1.1 500 Internal Server Error | ||
*/ | ||
@Get('/page_feeds') | ||
public async verifyPageFeedWebhook(@QueryParams() params: any, @Body({ validate: true }) body: any, @Res() res: any): Promise<any> { | ||
const VERIFY_TOKEN = facebook_setup.FACEBOOK_VERIFY_TOKEN; | ||
|
||
// Parse the query params | ||
const mode = params['hub.mode']; | ||
const token = params['hub.verify_token']; | ||
const challenge = params['hub.challenge']; | ||
|
||
// Checks if a token and mode is in the query string of the request | ||
if (mode && token) { | ||
// Checks the mode and token sent is correct | ||
if (mode === 'subscribe' && token === VERIFY_TOKEN) { | ||
|
||
// Responds with the challenge token from the request | ||
console.log('FACEBOOK_WEBHOOK_VERIFIED'); | ||
return res.status(200).send(challenge); | ||
} else { | ||
// Responds with '403 Forbidden' if verify tokens do not match | ||
return res.sendStatus(403); | ||
} | ||
} | ||
|
||
let createLog = true; | ||
if (body !== undefined) { | ||
if (body.object === 'page') { | ||
if (body.entry !== undefined) { | ||
for (let index = 0; index < body.entry.length; index++) { | ||
const element = body.entry[index]; | ||
if (element.changes !== undefined) { | ||
for (const item of element.changes) { | ||
const pageId = item.value.from.id; | ||
const fbPostId = item.value.post_id; | ||
const message = item.value.message; | ||
const link = item.value.link; // photo link, can be undefined | ||
const photos = item.value.photos; // array of photo, can be undefined | ||
// const createdTime = item.value.created_time; | ||
// const verb = item.value.verb; | ||
|
||
// check if contains pageSocialAccount | ||
const pageSocialAccount = await this.pageSocialAccountService.findOne({ providerPageId: pageId, providerName: PROVIDER.FACEBOOK }); | ||
if (pageSocialAccount === undefined) { | ||
createLog = false; | ||
continue; | ||
} | ||
|
||
const spanboonPage = await this.pageService.findOne({ _id: pageSocialAccount.page, banned: false }); | ||
if (spanboonPage === undefined) { | ||
createLog = false; | ||
continue; | ||
} | ||
|
||
// check if fbPostId was post by page | ||
const hasSocialPosted = await this.socialPostService.findOne({ pageId: pageSocialAccount.page, socialId: fbPostId, socialType: PROVIDER.FACEBOOK }); | ||
if (hasSocialPosted !== undefined) { | ||
createLog = false; | ||
continue; | ||
} | ||
|
||
// create post | ||
let post = this.createPagePostModel(pageSocialAccount.page, spanboonPage.ownerUser, undefined, (message ? message : ''), undefined); | ||
post = await this.postsService.create(post); | ||
|
||
const photoGallery = []; | ||
if (link !== undefined && link !== '') { | ||
// this is one photo mode | ||
try { | ||
const asset = await this.assetService.createAssetFromURL(link, spanboonPage.ownerUser); | ||
if (asset !== undefined) { | ||
photoGallery.push(asset); | ||
} | ||
} catch (error) { | ||
console.log('error create asset from url', error); | ||
} | ||
} else if (photos !== undefined && photos.length > 0) { | ||
// this is many photo mode | ||
for (const plink of photos) { | ||
try { | ||
const asset = await this.assetService.createAssetFromURL(plink, spanboonPage.ownerUser); | ||
if (asset !== undefined) { | ||
photoGallery.push(asset); | ||
} | ||
} catch (error) { | ||
console.log('error create asset from url', error); | ||
} | ||
} | ||
} | ||
|
||
if (post !== undefined) { | ||
if (photoGallery.length > 0) { | ||
// create post gallery | ||
for (const asset of photoGallery) { | ||
const gallery = new PostsGallery(); | ||
gallery.fileId = asset.id; | ||
gallery.post = post.id; | ||
gallery.imageURL = asset ? ASSET_PATH + asset.id : ''; | ||
gallery.s3ImageURL = asset.s3FilePath; | ||
gallery.ordering = 1; | ||
await this.postsGalleryService.create(gallery); | ||
} | ||
} | ||
|
||
const socialPost = new SocialPost(); | ||
socialPost.pageId = pageSocialAccount.page; | ||
socialPost.postId = post.id; | ||
socialPost.postBy = pageSocialAccount.page; | ||
socialPost.postByType = 'PAGE'; | ||
socialPost.socialId = fbPostId; | ||
socialPost.socialType = PROVIDER.FACEBOOK; | ||
|
||
await this.socialPostService.create(socialPost); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (createLog) { | ||
const logs = new FacebookWebhookLogs(); | ||
logs.data = body; | ||
this.facebookWebhookLogsService.create(logs); | ||
} | ||
} | ||
|
||
return res.status(200).send(challenge); | ||
} | ||
|
||
private createPagePostModel(pageObjId: ObjectID, userObjId: ObjectID, title: string, detail: string, photoLinks: string[]): Posts { | ||
const today = moment().toDate(); | ||
|
||
const post = new Posts(); | ||
post.deleted = false; | ||
post.pageId = pageObjId; | ||
post.referencePost = null; | ||
post.rootReferencePost = null; | ||
post.visibility = null; | ||
post.ranges = null; | ||
post.title = title; | ||
post.detail = detail; | ||
post.isDraft = false; | ||
post.hidden = false; | ||
post.type = POST_TYPE.GENERAL; | ||
post.userTags = []; | ||
post.coverImage = ''; | ||
post.pinned = false; | ||
post.deleted = false; | ||
post.ownerUser = userObjId; | ||
post.commentCount = 0; | ||
post.repostCount = 0; | ||
post.shareCount = 0; | ||
post.likeCount = 0; | ||
post.viewCount = 0; | ||
post.createdDate = today; | ||
post.startDateTime = today; | ||
post.story = null; | ||
|
||
return post; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Column, Entity, BeforeInsert, ObjectIdColumn, ObjectID } from 'typeorm'; | ||
import moment = require('moment/moment'); | ||
import { IsNotEmpty, IsMongoId } from 'class-validator'; | ||
import { BaseModel } from './BaseModel'; | ||
|
||
@Entity('FacebookWebhookLogs') | ||
export class FacebookWebhookLogs extends BaseModel { | ||
|
||
@ObjectIdColumn({ name: '_id' }) | ||
@IsNotEmpty() | ||
@IsMongoId() | ||
public id: ObjectID; | ||
|
||
@Column({ name: 'data' }) | ||
public data: any; | ||
|
||
@BeforeInsert() | ||
public async createDetails(): Promise<void> { | ||
this.createdDate = moment().toDate(); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
api-spanboon/src/api/repositories/FacebookWebhookLogsRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* @license Spanboon Platform v0.1 | ||
* (c) 2020-2021 KaoGeek. http://kaogeek.dev | ||
* License: MIT. https://opensource.org/licenses/MIT | ||
* Author: shiorin <[email protected]> | ||
*/ | ||
|
||
import { EntityRepository, MongoRepository } from 'typeorm'; | ||
import { FacebookWebhookLogs } from '../models/FacebookWebhookLogs'; | ||
|
||
@EntityRepository(FacebookWebhookLogs) | ||
export class FacebookWebhookLogsRepository extends MongoRepository<FacebookWebhookLogs> { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
* Author: shiorin <[email protected]>, chalucks <[email protected]> | ||
*/ | ||
|
||
import * as https from 'https'; | ||
import { Service } from 'typedi'; | ||
import { OrmRepository } from 'typeorm-typedi-extensions'; | ||
import { Asset } from '../models/Asset'; | ||
|
@@ -15,6 +16,8 @@ import { ASSET_CONFIG_NAME, DEFAULT_ASSET_CONFIG_VALUE } from '../../constants/S | |
import { S3Service } from '../services/S3Service'; | ||
import { ConfigService } from '../services/ConfigService'; | ||
import { aws_setup } from '../../env'; | ||
import { ASSET_SCOPE } from '../../constants/AssetScope'; | ||
import { ObjectID } from 'mongodb'; | ||
|
||
@Service() | ||
export class AssetService { | ||
|
@@ -156,6 +159,60 @@ export class AssetService { | |
return asset; | ||
} | ||
|
||
public createAssetFromURL(url: string, userId: ObjectID): Promise<Asset> { | ||
if (url === undefined || url === null || url === '') { | ||
return Promise.resolve(undefined); | ||
} | ||
|
||
return new Promise((resolve, reject) => { | ||
https.get(url, (res) => { | ||
res.setEncoding('base64'); | ||
console.log(`createAssetFromURL STATUS: ${res.statusCode}`); | ||
|
||
let data = ''; | ||
res.on('data', (d) => { | ||
data += d; | ||
}); | ||
|
||
res.on('end', () => { | ||
if (data !== undefined && data !== '') { | ||
const mimeType = res.headers['content-type']; | ||
const fileName = FileUtil.renameFile(); | ||
|
||
if (mimeType === undefined) { | ||
resolve(undefined); | ||
return; | ||
} | ||
|
||
const buffData = Buffer.from(data, 'base64'); | ||
const asset: Asset = new Asset(); | ||
asset.scope = ASSET_SCOPE.PUBLIC; | ||
asset.userId = userId; | ||
asset.fileName = fileName; | ||
asset.data = data; | ||
asset.mimeType = mimeType; | ||
asset.size = buffData.length; | ||
asset.expirationDate = null; | ||
|
||
this.create(asset).then((savedAsset) => { | ||
resolve(savedAsset); | ||
}).catch((err) => { | ||
console.log('err: ' + err); | ||
reject(err); | ||
}); | ||
} else { | ||
resolve(undefined); | ||
} | ||
}); | ||
|
||
}).on('error', (err) => { | ||
// Handle error | ||
console.log('err: ' + err); | ||
reject(err); | ||
}).end(); | ||
}); | ||
} | ||
|
||
private async _isUploadToS3(): Promise<boolean> { | ||
// s3 upload by cofig | ||
const assetUploadToS3Cfg = await this.configService.getConfig(ASSET_CONFIG_NAME.S3_STORAGE_UPLOAD); | ||
|
40 changes: 40 additions & 0 deletions
40
api-spanboon/src/api/services/FacebookWebhookLogsService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* @license Spanboon Platform v0.1 | ||
* (c) 2020-2021 KaoGeek. http://kaogeek.dev | ||
* License: MIT. https://opensource.org/licenses/MIT | ||
* Author: shiorin <[email protected]>, chalucks <[email protected]> | ||
*/ | ||
|
||
import { Service } from 'typedi'; | ||
import { OrmRepository } from 'typeorm-typedi-extensions'; | ||
import { FacebookWebhookLogsRepository } from '../repositories/FacebookWebhookLogsRepository'; | ||
|
||
@Service() | ||
export class FacebookWebhookLogsService { | ||
constructor(@OrmRepository() private fbLogRepository: FacebookWebhookLogsRepository) { } | ||
|
||
// create actionLog | ||
public async create(actionLog: any): Promise<any> { | ||
return await this.fbLogRepository.save(actionLog); | ||
} | ||
|
||
// find one actionLog | ||
public async findOne(actionLog: any): Promise<any> { | ||
return await this.fbLogRepository.findOne(actionLog); | ||
} | ||
|
||
// find all actionLog | ||
public async findAll(): Promise<any> { | ||
return await this.fbLogRepository.find(); | ||
} | ||
|
||
// edit actionLog | ||
public async update(query: any, newValue: any): Promise<any> { | ||
return await this.fbLogRepository.updateOne(query, newValue); | ||
} | ||
|
||
// delete actionLog | ||
public async delete(query: any, options?: any): Promise<any> { | ||
return await this.fbLogRepository.deleteOne(query, options); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.