From 15fdb69b9619dcee2c7821b616c7fe913c295a84 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 16 Oct 2020 16:29:54 +0700 Subject: [PATCH] Use Twitter API to fetch profile photos. fixes #133 --- backend/mempool-config.sample.json | 3 +- backend/src/api/donations.ts | 125 ++++++++++++++---- .../app/components/about/about.component.html | 6 +- .../app/components/about/about.component.scss | 9 +- .../app/components/about/about.component.ts | 6 +- mariadb-structure.sql | 3 + 6 files changed, 119 insertions(+), 33 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index fa989d6b1..76965bc08 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -25,5 +25,6 @@ "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem", "BTCPAY_URL": "", "BTCPAY_WEBHOOK_URL": "", - "BTCPAY_AUTH": "" + "BTCPAY_AUTH": "", + "TWITTER_BEARER_AUTH": "" } diff --git a/backend/src/api/donations.ts b/backend/src/api/donations.ts index d5873e111..0cca3e664 100644 --- a/backend/src/api/donations.ts +++ b/backend/src/api/donations.ts @@ -13,7 +13,9 @@ class Donations { }, }; - constructor() { } + constructor() { + this.runMigration(); + } setNotfyDonationStatusCallback(fn: any) { this.notifyDonationStatusCallback = fn; @@ -65,20 +67,48 @@ class Donations { return; } - let imageUrl = ''; + let imageBlob = ''; let handle = ''; + let imageUrl = ''; + let twitter_id = null; if (response.orderId !== '') { try { - const hiveData = await this.$getTwitterImageUrl(response.orderId); - imageUrl = hiveData.imageUrl; - handle = hiveData.screenName; + const userData = await this.$getTwitterUserData(response.orderId); + imageUrl = userData.profile_image_url.replace('normal', '200x200'); + imageBlob = await this.$downloadProfileImageBlob(imageUrl); + handle = userData.screen_name; + twitter_id = userData.id; } catch (e) { - logger.err('Error fetching twitter image' + e.message); + logger.err('Error fetching twitter data: ' + e.message); } } logger.debug('Creating database entry for donation with invoice id: ' + response.id); - this.$addDonationToDatabase(response.btcPaid, handle, response.id, imageUrl); + this.$addDonationToDatabase(response.btcPaid, handle, twitter_id, response.id, imageUrl, imageBlob); + } + + async $getDonationsFromDatabase() { + try { + const connection = await DB.pool.getConnection(); + const query = `SELECT handle, imageUrl, TO_BASE64(image) AS image_64 FROM donations WHERE handle != '' ORDER BY id DESC`; + const [rows] = await connection.query(query); + connection.release(); + return rows; + } catch (e) { + logger.err('$getDonationsFromDatabase() error' + e); + } + } + + private async $getLegacyDonations() { + try { + const connection = await DB.pool.getConnection(); + const query = `SELECT * FROM donations WHERE twitter_id IS NULL AND handle != ''`; + const [rows] = await connection.query(query); + connection.release(); + return rows; + } catch (e) { + logger.err('$getLegacyDonations() error' + e); + } } private getStatus(id: string): Promise { @@ -96,27 +126,18 @@ class Donations { }); } - async $getDonationsFromDatabase() { + private async $addDonationToDatabase(btcPaid: number, handle: string, twitter_id: number | null, + orderId: string, imageUrl: string, image: string): Promise { try { const connection = await DB.pool.getConnection(); - const query = `SELECT handle, imageUrl FROM donations WHERE handle != '' ORDER BY id DESC`; - const [rows] = await connection.query(query); - connection.release(); - return rows; - } catch (e) { - logger.err('$getDonationsFromDatabase() error' + e); - } - } - - private async $addDonationToDatabase(btcPaid: number, handle: string, orderId: string, imageUrl: string): Promise { - try { - const connection = await DB.pool.getConnection(); - const query = `INSERT IGNORE INTO donations(added, amount, handle, order_id, imageUrl) VALUES (NOW(), ?, ?, ?, ?)`; - const params: (string | number)[] = [ + const query = `INSERT IGNORE INTO donations(added, amount, handle, twitter_id, order_id, imageUrl, image) VALUES (NOW(), ?, ?, ?, ?, ?, FROM_BASE64(?))`; + const params: (string | number | null)[] = [ btcPaid, handle, + twitter_id, orderId, imageUrl, + image, ]; const [result]: any = await connection.query(query, params); connection.release(); @@ -125,19 +146,69 @@ class Donations { } } - private async $getTwitterImageUrl(handle: string): Promise { + private async $updateDonation(id: number, handle: string, twitterId: number, imageUrl: string, image: string): Promise { + try { + const connection = await DB.pool.getConnection(); + const query = `UPDATE donations SET handle = ?, twitter_id = ?, imageUrl = ?, image = FROM_BASE64(?) WHERE id = ?`; + const params: (string | number)[] = [ + handle, + twitterId, + imageUrl, + image, + id, + ]; + const [result]: any = await connection.query(query, params); + connection.release(); + } catch (e) { + logger.err('$updateDonation() error' + e); + } + } + + private async $getTwitterUserData(handle: string): Promise { return new Promise((resolve, reject) => { - logger.debug('Fetching Hive.one data...'); + logger.debug('Fetching Twitter API data...'); request.get({ - uri: `https://api.hive.one/v1/influencers/screen_name/${handle}/?format=json`, + uri: `https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`, json: true, + headers: { + Authorization: 'Bearer ' + config.TWITTER_BEARER_AUTH + }, }, (err, res, body) => { if (err) { return reject(err); } - logger.debug('Hive.one data fetched:' + JSON.stringify(body.data)); - resolve(body.data); + logger.debug('Twitter user data fetched:' + JSON.stringify(body.data)); + resolve(body); }); }); } + + private async $downloadProfileImageBlob(url: string): Promise { + return new Promise((resolve, reject) => { + logger.debug('Fetching image blob...'); + request.get({ + uri: url, + encoding: null, + }, (err, res, body) => { + if (err) { return reject(err); } + logger.debug('Image downloaded.'); + resolve(Buffer.from(body, 'utf8').toString('base64')); + }); + }); + } + + private async runMigration() { + const legacyDonations = await this.$getLegacyDonations(); + legacyDonations.forEach(async (donation: any) => { + logger.debug('Migrating donation for handle: ' + donation.handle); + try { + const twitterData = await this.$getTwitterUserData(donation.handle); + const imageUrl = twitterData.profile_image_url.replace('normal', '200x200'); + const imageBlob = await this.$downloadProfileImageBlob(imageUrl); + await this.$updateDonation(donation.id, twitterData.screen_name, twitterData.id, imageUrl, imageBlob); + } catch (e) { + logger.err('Failed to migrate donation for handle: ' + donation.handle + '. ' + (e.message || e)); + } + }); + } } export default new Donations(); diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 675459dad..49029a6a9 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -44,7 +44,9 @@ -
+
+ +


@@ -82,7 +84,7 @@
diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index fb95e701c..0ff87851e 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -12,6 +12,13 @@ margin: 10px; } +.profile_img { + width: 80px; + height: 80px; + border-radius: 50%; + border: 0; +} + .text-small { font-size: 12px; -} \ No newline at end of file +} diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index aea5f0f63..49a3c867f 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -22,7 +22,6 @@ export class AboutComponent implements OnInit { donationObj: any; sponsorsEnabled = env.SPONSORS_ENABLED; sponsors = null; - bitcoinUrl: SafeUrl; constructor( private websocketService: WebsocketService, @@ -63,8 +62,11 @@ export class AboutComponent implements OnInit { .subscribe((response) => { this.websocketService.trackDonation(response.id); this.donationObj = response; - this.bitcoinUrl = this.sanitizer.bypassSecurityTrustUrl('bitcoin:' + this.donationObj.address + '?amount=' + this.donationObj.amount); this.donationStatus = 3; }); } + + bypassSecurityTrustUrl(text: string): SafeUrl { + return this.sanitizer.bypassSecurityTrustUrl(text); + } } diff --git a/mariadb-structure.sql b/mariadb-structure.sql index b7eed37c4..052f0fb1f 100644 --- a/mariadb-structure.sql +++ b/mariadb-structure.sql @@ -102,3 +102,6 @@ ALTER TABLE `donations` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; ALTER TABLE `donations` ADD UNIQUE(`order_id`); + +ALTER TABLE `donations` ADD `image` BLOB NULL AFTER `imageUrl`; +ALTER TABLE `donations` ADD `twitter_id` INT NULL AFTER `handle`;