• Akademia
  • Blog
  • O Serverless
  • O stronie

Jak stworzy膰 twitterowego bota, kt贸ry rozpoznaje obrazy?


alt text

Serverless?

W ci膮gu ostatnich kilku lat architektura serverless zyska艂a na popularno艣ci. Programi艣ci i firmy zmieniaj膮 swoje podej艣cie do tworzenia, utrzymywania i wdra偶ania aplikacji internetowych. Ale czym dok艂adnie jest serverless? Jak zdefiniowa艂o to https://serverless-stack.com/:

Serverless to model programowania, w kt贸rym dostawca chmury (AWS, Azure lub Google Cloud) jest odpowiedzialny za wykonanie fragmentu kodu poprzez dynamiczne przydzielanie zasob贸w. Op艂aty za taka us艂ug臋 pobierane s膮 tylko za ilo艣膰 zasob贸w u偶ywanych do uruchomienia kodu. Kod jest zwykle uruchamiany w kontenerach, kt贸re mog膮 by膰 uruchamiane przez r贸偶ne eventy, w tym 偶膮dania HTTP, zapytania do bazy danych, us艂ugi kolejkowania, alerty monitorowania, przesy艂anie plik贸w, zaplanowane eventy (cron) itp. Kod wysy艂any do dostawcy w chmurze ma zazwyczaj posta膰 funkcji. Dlatego serverless jest czasami okre艣lane jako Funkcje jako Us艂uga lub FaaS.

TIP: Sprawd藕 ich samouczek - jest naprawd臋 niesamowity i pomo偶e ci zrozumie膰 lepiej, o co chodzi w podej艣ciu serverless.

Co b臋dziemy budowa膰?

W tym samouczku poka偶臋, jak zbudowa膰 Twitter bota, kt贸ry po otrzymaniu tweeta ze zdj臋ciem jakiego艣 zwierzaka, rozpozna, co jest na tym zdj臋ciu (je艣li jest to zwierz臋) i ode艣le tweeta z prawid艂ow膮 odpowiedzi膮. Na przyk艂ad, je艣li wy艣lesz zdj臋cie 偶yrafy, bot u偶yje naszej architektury serverless i natychmiast odpowie ci czym艣 w stylu - 鈥濰ej, na twoim zdj臋ciu jest 偶yrafa!鈥.
Aby to osi膮gn膮膰, wykorzystamy Framework Serverless. To fantastyczne narz臋dzie, kt贸re pozwala 艂atwo skonfigurowa膰 wszystkie us艂ugi potrzebne do projektu, w jednym pliku konfiguracyjnym. Poza tym jest niezale偶ny od dostawcy, wi臋c nie musisz wybiera膰 mi臋dzy AWS, Azure lub Google Cloud, mo偶esz korzysta膰 z nich wszystkich.
W tym przyk艂adzie u偶yjesz Amazon Web Services - AWS. Ma dziesi膮tki 艣wietnych us艂ug w chmurze, ale u偶yjesz tylko kilku - S3 Bucket, Lambda Functions, API Gateway i Image Recognition. Poni偶ej mo偶esz zobaczy膰 na schemacie, jak b臋dzie dzia艂a膰 nasz program.

alt text

Zacznijmy od pocz膮tku

Zanim zaczniesz u偶ywa膰 Serverless Framework, musisz upewni膰 si臋, 偶e masz poprawnie skonfigurowane API Twittera.
Utw贸rz konto programisty na Twitterze i dodaj now膮 aplikacj臋 na https://developer.twitter.com. Po zako艅czeniu przejd藕 do sekcji uprawnie艅 i upewnij si臋, 偶e zmieni艂e艣 je na 鈥濩zytaj, pisz i kieruj wiadomo艣ciami鈥. W sekcji kluczy i token贸w dost臋pu upewnij si臋, 偶e wygenerowa艂e艣 token dost臋pu i tajny token dost臋pu. B臋dziesz ich p贸藕niej potrzebowa膰 do komunikacji z API.
Aby umo偶liwi膰 wysy艂anie danych do webhooka, musisz uzyska膰 dost臋p do Account Activity API. Zarejestruj si臋 tutaj. Nie przejmuj si臋 informacja m贸wi膮ca ze jest to us艂uga premium. Do naszych cel贸w wystarczy darmowy tryb sandbox.
Przejd藕 do 艢rodowiska programist贸w i utw贸rz 艣rodowisko dla Account Activity API. Zanotuj nazw臋 艣rodowiska programisty, poniewa偶 b臋dzie ona nam potrzebna p贸藕niej.

Zarejestruj webhook na Twitterze

Spos贸b, w jaki dzia艂a Account Activity API, mo偶e pocz膮tkowo wydawa膰 si臋 nieco skomplikowany, ale w rzeczywisto艣ci jest do艣膰 prosty. Oto kroki wymagane do uruchomienia:

  1. Wy艣lij zapytanie do interfejsu API Twittera z informacj膮 o endpoincie URL, kt贸ry zajmie si臋 Twitter Challenge Response Check
  2. Interfejs API Twittera wysy艂a 偶膮danie GET w celu spe艂nienia Twitter Challenge Response Check
  3. Tw贸j endpoint odpowiada poprawnie sformatowan膮 odpowiedzi膮 JSON - Webhook jest zarejestrowany (tak!).
  4. Wy艣lij 偶膮danie POST do interfejsu API Twittera, aby zasubskrybowa膰 aplikacj臋 do aplikacji Twitter.

Aby obs艂u偶y膰 wszystkie te 偶膮dania, utworzymy klas臋 kontroler贸w API Twittera.
Po pierwsze, stw贸rzmy wszystkie w艂a艣ciwo艣ci, kt贸re b臋dziemy musieli wykorzysta膰 w naszych metodach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const request = require('request-promise');

module.exports = class TwitterController {
constructor(consumerKey, consumerSecret, token, tokenSecret, urlBase, environment, crcUrl) {
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.token = token;
this.tokenSecret = tokenSecret;
this.urlBase = urlBase;
this.environment = environment;
this.crcUrl = crcUrl;
this.credentials = {
consumer_key: this.consumerKey,
consumer_secret: this.consumerSecret,
token: this.token,
token_secret: this.tokenSecret,
};

this.registerWebhook = this.registerWebhook.bind(this);
}
};

twittercontroller.js

Wszystkie w艂a艣ciwo艣ci, kt贸re przeka偶emy konstruktorowi, zostan膮 zapisane w pliku serverless.env.yml w katalogu g艂贸wnym projektu. Wr贸cimy do tego p贸藕niej.

Teraz przyjrzyjmy si臋 metodom, kt贸re b臋d膮 obs艂ugiwa艂y komunikacj臋 z API Twittera.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
setRequestOptions(type, webhhokId) {
let url = null;
let content = {};
const { urlBase, environment, credentials, crcUrl } = this;

switch (type) {
case ('registerWebhook'):
url = `${urlBase}${environment}/webhooks.json`;
content = {
form: {
url: crcUrl,
},
};
break;
case ('getWebhook'):
url = `${urlBase}${environment}/webhooks.json`;
break;
case ('deleteWebhook'):
url = `${urlBase}${environment}/webhooks/${webhhokId}.json`;
break;
case ('registerSubscription'):
url = `${urlBase}${environment}/subscriptions.json`;
break;
case ('createTweet'):
url = `${urlBase}update.json`;
break;
default:
url = `${urlBase}${environment}/webhooks.json`;
}
return Object.assign({}, {
url,
oauth: credentials,
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
resolveWithFullResponse: true,
}, content);
}

async registerWebhook() {
const requestOptions = this.setRequestOptions('registerWebhook');

try {
const response = await request.post(requestOptions);
console.log(response);
console.log('Succesfully register webhook');
} catch (err) {
console.log(err);
console.log('Cannot register webhook');
}
}

async registerSubscription() {
const requestOptions = this.setRequestOptions('registerSubscription');

try {
const response = await request.post(requestOptions);
if (response.statusCode === 204) {
console.log('Subscription added. Yay!');
}
} catch (err) {
console.log(err);
console.log('Cannot register subscription');
}
}

async createTweet(status, tweetID) {
const requestOptions = Object.assign({}, this.setRequestOptions('createTweet'), {
form: {
status,
in_reply_to_status_id: tweetID,
auto_populate_reply_metadata: true,
},
});

try {
await request.post(requestOptions);
} catch (err) {
console.log(err);
console.log('Cannot post tweet.');
}
}

twittercontroller.js

Wi臋kszo艣膰 z tych metod to funkcje asynchroniczne, kt贸re maj膮 na celu utworzenie komunikacji z API. Do wys艂ania zada艅 b臋dziemy korzysta膰 z biblioteki request-promise. Wyja艣nijmy pokr贸tce metody:

    • setRequestOptions * - tworzy obiekt z parametrami, potrzebnymi do komunikacji z API
    • registerWebhook * - wysy艂a 偶膮danie POST do API, z adresem URL zawieraj膮cym Twitter Challenge Response Check
    • registerSubscription * - wysy艂a 偶膮danie POST do API, aby zarejestrowa膰 subskrypcj臋 naszego webhooka
    • createTweet * - wysy艂a 偶膮danie POST do API i tworzy nowy Tweet

The Serverless

Aby rozpocz膮膰 prac臋 z Serverless, musimy go zainstalowa膰 (duh!). Otw贸rz terminal i zainstaluj framework globalnie.

1
$ npm install serverless -g

Nast臋pnie przejd藕 do folderu projektu i uruchom:

1
$ serverless create --template aws-nodejs

To polecenie utworzy domy艣lny plik konfiguracyjny node.js + AWS. Wygenerowany plik yaml zawiera wiele zakomentowanego kodu. Nie b臋dziemy go tutaj potrzebowa膰, wi臋c mo偶emy go usun膮膰. Jedyne, na czym nam teraz zale偶y, to:

1
2
3
4
5
6
7
service: aws-nodejs
provider:
name: aws
runtime: nodejs8.10
functions:
hello:
handler: handler.hello

Jest to minimalna, podstawowa konfiguracja. Zanim przejdziemy dalej, musisz utworzy膰 konto AWS (je艣li jeszcze go nie masz) i skonfigurowa膰 AWS dla Serverless. Nie b臋d臋 wchodzi艂 w szczeg贸艂y tego procesu, mo偶esz zobaczy膰, jak to zrobi膰 [tutaj] (https://www.youtube.com/watch?v=KngM5bfpttA).

Po skonfigurowaniu uprawnie艅 mo偶esz rozpocz膮膰 dodawanie szczeg贸艂贸w konfiguracji. Zwykle Serverless domy艣lnie u偶ywa nazwy profilu i regionu AWS, kt贸rego u偶ywasz, ale je艣li masz wiele profili na swoim komputerze (prywatny, s艂u偶bowy itp.), dobr膮 praktyk膮 jest zdefiniowanie go w pliku serverless.yaml w nast臋puj膮cy spos贸b:

1
2
3
4
5
provider:
name: aws
runtime: nodejs8.10
profile: aws-private # your profile name
region: eu-west-1 # aws region

TIP: W wierszu polece艅 mo偶esz u偶y膰 skr贸tu - zamiast 鈥瀞erverles鈥︹ mo偶esz po prostu wpisa膰 鈥瀞ls鈥︹.

Plik ENV

Jak wspomnia艂em wcze艣niej, do przechowywania naszych kluczy, token贸w i innych zmiennych utworzymy plik serverless.env.yml w folderze g艂贸wnym. Powinno to wygl膮da膰 tak:

1
2
3
4
5
6
7
8
TWITTER_CONSUMER_KEY: ########
TWITTER_CONSUMER_SECRET: ########
TWITTER_TOKEN: ########
TWITTER_TOKEN_SECRET: ########
ENVIRONMENT: ########
URL_BASE: 'https://api.twitter.com/1.1/account_activity/all/'
URL_CREATE: 'https://api.twitter.com/1.1/statuses/'
CRC_URL: ########

Pierwsze pi臋膰 z nich, to te o kt贸rych wspominali艣my wcze艣niej, podczas tworzenia aplikacji na koncie deweloperskim Twittera. Znajduje si臋 tu r贸wnie偶 base URL API Twittera - dobrze jest trzyma膰 tego typu globalne zmienne w jednym miejscu.
W dalszej cz臋艣ci artyku艂y utworzymy adres URL obs艂uguj膮cy Twitter Challenge Response Check, z wykorzystaniem Serverless Framework i AWS.

Z gotowym plikiem env mo偶esz wstawia膰 zmienne do swojego kodu, umieszczaj膮c je w pliku serverless.yml. Oto spos贸b:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
custom:
CRC_URL: ${file(./serverless.env.yml):CRC_URL}
ENVIRONMENT: ${file(./serverless.env.yml):ENVIRONMENT}
TWITTER_CONSUMER_KEY: ${file(./serverless.env.yml):TWITTER_CONSUMER_KEY}
TWITTER_CONSUMER_SECRET: ${file(./serverless.env.yml):TWITTER_CONSUMER_SECRET}
TWITTER_TOKEN: ${file(./serverless.env.yml):TWITTER_TOKEN}
TWITTER_TOKEN_SECRET: ${file(./serverless.env.yml):TWITTER_TOKEN_SECRET}
URL_BASE: ${file(./serverless.env.yml):URL_BASE}
provider:
name: aws
runtime: nodejs8.10
profile: aws-private
region: eu-west-1
environment:
TWITTER_CONSUMER_KEY: ${self:custom.TWITTER_CONSUMER_KEY}
TWITTER_CONSUMER_SECRET: ${self:custom.TWITTER_CONSUMER_SECRET}
TWITTER_TOKEN: ${self:custom.TWITTER_TOKEN}
TWITTER_TOKEN_SECRET: ${self:custom.TWITTER_TOKEN_SECRET}
ENVIRONMENT: ${self:custom.ENVIRONMENT}
CRC_URL: ${self:custom.CRC_URL}
URL_BASE: ${self:custom.URL_BASE}

Dodaj膮c zmienne w obiekcie provider.environment, mo偶emy uzyska膰 do nich dost臋p w dowolnej funkcji, kt贸r膮 zdefiniujemy w pliku konfiguracyjnym Serverless. Mo偶emy r贸wnie偶 przekaza膰 go osobno w ka偶dej funkcji, ale poka偶臋 ten przyk艂ad w dalszej cz臋艣ci samouczka.

Funkcje

Przejd藕my teraz do g艂贸wnej cz臋艣ci naszego projektu - funkcji lambda. Zacznijmy od zdefiniowania pierwszego z nich w naszym pliku konfiguracyjnym.

1
2
3
4
5
6
7
functions:
handleCrc:
handler: src/lambda_functions/handleCrc.handler
events:
- http:
path: twitter/webhook/handleapi
method: get

Tworzymy pierwsz膮 funkcj臋 lambda o nazwie handleCrc. W sekcji events okre艣lasz, kiedy funkcja ma zosta膰 wywo艂ana.
Jak wida膰, po wys艂aniu 偶膮dania GET do w艂a艣nie stworzonego przez nas endpointu - twitter/webhook/handleapi, uruchomi si臋 funkcja handleCrc.
To podstawowy spos贸b tworzenia konfiguracji funkcji lambda w Serverless Framework. Istnieje wiele opcji definiowania zdarze艅, na przyk艂ad -
obraz zosta艂 przes艂any do S3 Bucket, nowe dane zosta艂y dodane do bazy danych itp.

Sp贸jrzmy teraz na w艂a艣ciwa funkcje:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const crypto = require('crypto');

const encodeCrc = (crcToken, consumerSecret) => crypto.createHmac('sha256', consumerSecret).update(crcToken).digest('base64');

module.exports.handler = async (event) => {
const responseToken = encodeCrc(
event.queryStringParameters.crc_token,
process.env.TWITTER_CONSUMER_SECRET,
);
return {
statusCode: 200,
body: JSON.stringify({ response_token: `sha256=${responseToken}` }),
};
};

handleCrc.js

U偶yjemy biblioteki Crypto do odkodowania odpowiedzi z API Twittera. Jak wida膰, jest to do艣膰 proste. Musisz przekaza膰 token Twitter Challenge Response Check
i sw贸j tajny klucz z API Twittera, aby zakodowa膰 funkcj臋 CRC i zwr贸ci膰 wynik. Zauwa偶, 偶e nasz sekret pobieramy z obiektu process.env.
Mo偶emy uzyska膰 do niego dost臋p dzi臋ki wcze艣niejszemu zdefiniowaniu go w pliku serverless.yml.

Teraz mo偶emy zrobi膰 deploy naszej funkcji, aby uzyska膰 adres URL Twitter Challenge Response Check, kt贸rego b臋dziemy potrzebowa膰 p贸藕niej.

Aby wdro偶y膰 nasz膮 funkcj臋, po prostu uruchom z terminala polecenie:

1
$ sls deploy

Spowoduje to utworzenie nowego szablonu AWS CloudFormation i przes艂anie funkcji do S3 Bucket. Je艣li wszystko posz艂o dobrze, powiniene艣 zobaczy膰 co艣 podobnego:

![alt text](https://i.postimg.cc/50hZxqZL/1-LOH0-MAFMn-K-Nz7-Djgd4v-Q.png 鈥濳onsola鈥)

Tutaj mo偶esz znale藕膰 wszystkie informacje o swoim projekcie: etap, nazw臋 projektu, endpointy, funkcje itp. To co, interesuje nas w tej chwili to endpoint CRD.
Jak wspomnia艂em wcze艣niej, b臋dziesz potrzebowa膰 tego adresu URL, aby przej艣膰 Twitter Challenge Response Check. Skopiuj i wklej do pliku serverless.env.yml.

TIP: Je艣li chcesz dowiedzie膰 si臋, co faktycznie dzieje si臋 za kulisami, podczas uruchamiania $ sls, mo偶esz przej艣膰 [tutaj] (https://serverless.com/framework/docs/providers/aws/guide/deploying/ #aws 鈥 wdra偶anie) i przeczytaj o tym wszystko.

Register webhook and subscription

Teraz, dodajmy funkcje lambda, kt贸re b臋d膮 odpowiedzialne za rejestracj臋 webhooka i subskrypcji u偶ytkownika.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
functions:
...
registerWebhook:
handler: src/lambda_functions/registerWebhook.handler
events:
- http:
path: twitter/webhook/register
method: get
registerSubscription:
handler: src/lambda_functions/registerSubscription.handler
events:
- http:
path: twitter/subscription/register
method: get

Funkcje same w sobie s膮 naprawd臋 proste. Og贸lnie rzecz bior膮c ka偶da z nich wywo艂uje odpowiedni膮 metod臋 z klasy TwitterController, kt贸r膮 wcze艣niej stworzyli艣my.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const TwitterController = require('../TwitterController');

module.exports.handler = async () => {
const controller = new TwitterController(
process.env.TWITTER_CONSUMER_KEY,
process.env.TWITTER_CONSUMER_SECRET,
process.env.TWITTER_TOKEN,
process.env.TWITTER_TOKEN_SECRET,
process.env.URL_BASE,
process.env.ENVIRONMENT,
process.env.CRC_URL,
);

await controller.registerSubscription();
};

registerSubscription.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const TwitterController = require('../TwitterController');

module.exports.handler = async () => {
const controller = new TwitterController(
process.env.TWITTER_CONSUMER_KEY,
process.env.TWITTER_CONSUMER_SECRET,
process.env.TWITTER_TOKEN,
process.env.TWITTER_TOKEN_SECRET,
process.env.URL_BASE,
process.env.ENVIRONMENT,
process.env.CRC_URL,
);

await controller.registerWebhook();
};

registerWebhook.js

Jak widzisz, nie ma tu 偶adnej magii. Tworzysz nowa instancje klasy, przekazujesz wszystkie dane dost臋pu i uruchamiasz funkcje. Teraz powinni艣my zrobi膰 redploy naszej aplikacji:

1
$ sls deploy

Powinien zosta膰 wy艣wietlony 鈥瀝aport鈥 (podobny do tego, kt贸ry otrzymali艣my po pierwszym deploy) z adresami URL endpointow. W tym momencie mamy wszystko, aby zarejestrowa膰 nasz webhook.

Mo偶esz dos艂ownie wklei膰 adres do paska przegl膮darki. Zanim to zrobimy z metod膮 registerWebhook, zobaczmy, jak mo偶emy monitorowa膰 nasze funkcje.

1
$ sls logs -f registerWebhook

Je艣li uruchomisz to w swoim terminalu, otrzymasz raport log贸w z ostatniego wywo艂ania funkcji. Opcjonalnie mo偶esz nas艂uchiwa膰 wywo艂a艅 funkcji wywo艂uj膮c komend臋 z flag膮 -t:

1
$ sls logs -f registerWebhook -t

UWAGA: Ta opcja dzia艂a tylko wtedy, gdy twoje funkcja zosta艂y wcze艣niej wywo艂ane przynajmniej raz.

Teraz mo偶esz wklei膰 adres URL endpointa registerWebhook w przegl膮darce. Nast臋pnie przejd藕 do terminala i uruchom logi. Je艣li wszystko jest w porz膮dku, powiniene艣 zobaczy膰 komunikat:

1
Successfully register webhook

Powt贸rz te same kroki dla funkcji registerSubscription. Super! W艂a艣nie zarejestrowali艣my nasze webhooki.

Obs艂uga odpowiedzi z API Twittera

Od tego momentu jakakolwiek aktywno艣膰 na Twoim koncie spowoduje wywo艂anie 偶膮dania POST zawieraj膮cego wszystkie dane dotycz膮ce tej aktywno艣ci. Aby zobaczy膰 dane, musisz utworzy膰 funkcj臋 lambda, kt贸ra obs艂u偶y to 偶膮danie.

1
2
3
4
5
6
7
8
9
/* serverless.yml */
functions:
...
handleTweet:
handler: src/lambda_functions/handleTweet.handler
events:
- http:
path: twitter/webhook/handleapi
method: post
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = (username, labels = []) => {
let message = '';
const ANIMAL_LABELS = ['Animal', 'Mammal', 'Bird', 'Fish', 'Reptile', 'Amphibian'];
const isAnimal = labels.length && labels.some(label => ANIMAL_LABELS.includes(label.Name));

if (labels.length === 0) {
message = `Sorry @${username}, you need to upload an image.`;
} else if (isAnimal) {
const recongizedLabels = labels.map(label => label.Name);
message = `Hi @${username}. On your image, I can recognize: ${recongizedLabels.join(', ')}.`;
} else {
message = `Ooops @${username} looks like it's not an animal on your image.`;
}

return message;
};

createMessage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const uploadImage = require('../helpers/uploadImage');
const createMessage = require('../helpers/createMessage');
const TwitterController = require('../TwitterController');

module.exports.handler = async (event) => {
const tweet = JSON.parse(event.body);
const tweetData = await tweet.tweet_create_events;

if (typeof tweetData === 'undefined' || tweetData.length < 1) {
return console.log('Not a new tweet event');
}

if (tweet.for_user_id === tweetData[0].user.id_str) {
return console.log('Same user, not sending response.');
}

const { id_str, user, entities } = tweetData[0];
const key = `${id_str}___---${user.screen_name}`;

// If tweet containes image
if (entities.hasOwnProperty('media')) {
const imageUrl = tweetData[0].entities.media[0].media_url_https;
await uploadImage(imageUrl, {
bucket: process.env.BUCKET,
key,
});
} else {
const controller = new TwitterController(
process.env.TWITTER_CONSUMER_KEY,
process.env.TWITTER_CONSUMER_SECRET,
process.env.TWITTER_TOKEN,
process.env.TWITTER_TOKEN_SECRET,
process.env.URL_CREATE,
process.env.ENVIRONMENT,
process.env.CRC_URL,
);
const message = createMessage(user.screen_name);
await controller.createTweet(message, key);
}
};

handleTweet.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fetch = require('node-fetch');
const AWS = require('aws-sdk');

const s3 = new AWS.S3();

module.exports = async (image, meta) => {
console.log('Uploading image....');

const mediaResponse = await fetch(image);
const bufferedMedia = await mediaResponse.buffer();
const params = {
Bucket: meta.bucket,
Key: meta.key,
Body: bufferedMedia,
};

try {
const uploadedImage = await s3.putObject(params).promise();
console.log(uploadedImage, 'Image uploaded.');
} catch (err) {
console.log(err);
console.log('Cannot upload.');
}
};

uploadImage.js

W pliku handleTweet.js:

  1. sprawdzanie obiektu zdarzenia, czy rzeczywi艣cie jest to tweet (mo偶e to by膰 prywatna wiadomo艣膰 lub co艣 innego) i czy tweet pochodzi od innego u偶ytkownika (nie chcemy tworzy膰 niesko艅czonej p臋tli podczas wysy艂ania odpowiedzi)
  2. sprawdzenie czy tweet posiada dodane zdj臋cie, je艣li tak-przeslemy to zdj臋cie do S3, je艣li nie-ode艣lemy tweet z odpowiednim przypomnieniem o jego braku.

UWAGA: W wierszu 18 tworzymy nazw臋 pliku ze zmiennych - identyfikator tweeta i nazw臋 u偶ytkownika oraz kilka my艣lnik贸w / podkre艣le艅. Robimy to w taki spos贸b, aby 艂atwo odczyta膰 te zmienne w dalszej cz臋艣ci.

W pliku uploadImage.js:

  1. zainstaluj node-fetch za pomoc膮 npm i - uzyjemy tej paczki go do pobrania obrazu zapisanego na serwerach Twittera
  2. przekonwertuj obraz do danych binarnych i przeka偶 je, jako tre艣膰 w parametrach
  3. zainstaluj pakiet aws-sdk, aby korzysta膰 z metod AWS bezpo艣rednio w kodzie
  4. za艂aduj obraz do S3 za pomoc膮 metody s3.putObject

TIP: Mo偶esz zwr贸ci膰 promise zamiast callback w wi臋kszo艣ci metod aws-sdk, uruchamiaj膮c na nich metod臋 promise (). Zobacz wi臋cej tutaj.

Upload zdj臋膰 do S3

Teraz ustawimy funkcj臋 lambda, kt贸ra b臋dzie si臋 uruchamia膰, za ka偶dym razem, gdy nowe zdj臋cie zostanie przes艂ane do naszego kontenera w S3. Aby to zrobi膰, musimy doda膰 konfiguracj臋 do servereless.yml

1
2
3
4
5
6
7
8
/* serverless.yml */
functions:
...
respondToTweetWithImage:
handler: src/lambda_functions/respondToTweetWithImage.handler
events:
- s3:
bucket: ${self:custom.BUCKET}

Funckaj respondToTweetWithImage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AWS = require('aws-sdk');

module.exports = async (meta) => {
const rekognition = new AWS.Rekognition();
const params = {
Image: {
S3Object: {
Bucket: meta.bucket.name,
Name: meta.object.key,
},
},
MaxLabels: 5,
MinConfidence: 85,
};

try {
const data = await rekognition.detectLabels(params).promise();
return data.Labels;
} catch (err) {
console.log(err);
console.log('Cannot recognize image');
}
};

recognizeImage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const AWS = require('aws-sdk');

module.exports = (meta) => {
const s3 = new AWS.S3();
const params = {
Bucket: meta.bucket.name,
Key: meta.object.key,
};

try {
s3.deleteObject(params).promise();
} catch (err) {
console.log(err);
console.log('Cannot delete image.');
}
};

removeImage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const recognizeImage = require('../helpers/recognizeImage');
const removeImage = require('../helpers/removeImage');
const createMessage = require('../helpers/createMessage');
const TwitterController = require('../TwitterController');

module.exports.handler = async (event) => {
const { s3 } = event.Records[0];
const tweetId = s3.object.key.split('___---')[0];
const username = s3.object.key.split('___---')[1];

const labels = await recognizeImage(s3);
const message = createMessage(username, labels);
const controller = new TwitterController(
process.env.TWITTER_CONSUMER_KEY,
process.env.TWITTER_CONSUMER_SECRET,
process.env.TWITTER_TOKEN,
process.env.TWITTER_TOKEN_SECRET,
process.env.URL_CREATE,
process.env.ENVIRONMENT,
process.env.CRC_URL,
);
await controller.createTweet(message, tweetId);
removeImage(s3);
};

respondToTweetWithImage.js

Przeanalizujmy funckcje:

  1. po przes艂aniu obrazu do S3 funkcja otrzyma obiekt ze wszystkimi danymi o zdarzeniu
  2. dzi臋ki specyficznej konstrukcji nazwy pliku obrazu mo偶emy uzyska膰 oryginalny identyfikator tweeta i nazw臋 u偶ytkownika, kt贸ry go opublikowa艂
  3. nast臋pnie funkcja przeka偶e dane o zdarzeniu do AWS Rekognition Class
  4. nast臋pnie rozpoznaje zawarto艣膰 obrazu i zwraca j膮 do funkcji createMessage
  5. utworzona wiadomo艣膰 jest wysy艂ana jako Tweet
  6. obraz jest usuwany z S3, poniewa偶 nie jest ju偶 potrzebny

Podsumowanie

Uda艂o Ci si臋 utworzy膰 Twitter-bota, kt贸ry automatycznie rozpozna obraz i ode艣le odpowiedz. Zach臋cam do eksperymentowania z t膮 funkcjonalno艣ci膮 - rozpoznawania r贸偶nych rodzaj贸w obraz贸w, tworzenia bardziej szczeg贸艂owych komunikat贸w itp. Ten przyk艂ad by艂 tylko kr贸tkim om贸wieniem Serverless i tego, jak mo偶na budowa膰 ciekawe aplikacje bez znajomo艣ci back-endu.

Je艣li masz jakie艣 uwagi lub s膮dzisz, 偶e co艣 w tym artykule mo偶e by膰 nie tak, wy艣lij mi wiadomo艣膰 lub zostaw komentarz.

Autor: Maciej Grzybek Programista JavaScript z Bia艂egostoku. Obecnie mieszka i pracuje w Leeds. Lubi przekszta艂ca膰 z艂o偶one problemy w proste, pi臋kne i intuicyjne interfejsy. Gdy nie koduje, prawdopodobnie znajdziesz go w kuchni, robi膮cego swoje ulubione danie: sushi.