Как создать самообновляющийся заголовок Twitter с динамическим контентом

Первое, что открывается взору при нажатии на профиль в Twitter,  —  изображение заголовка. Так почему бы не отображать в нем какой-нибудь динамический контент? Для этого не нужно быть семи пядей во лбу, зато возможности безграничны.

В статье я расскажу, как легко это сделать с помощью простого скрипта Node.js, Twitter, API Medium и AWS Lambda.

Содержание: 

  • получение учетных данных для работы с Twitter API;  
  • создание скрипта Node.js для сбора информации о недавних подписчиках; 
  • извлечение свежей статьи блога через RSS-поток Medium; 
  • обновление статического заголовка путем отображения аватарок подписчиков и названия статьи; 
  • размещение созданного кода в функцию Lambda на AWS, выполняющуюся каждые 60 секунд. 

💡 Несмотря на то, что мы создаем инфраструктуру на AWS, избранный подход ничего не будет стоить благодаря бесплатному уровню AWS Lambda.

Начнем работу над новым проектом с установки npm или yarn и добавим требуемые зависимости: 

  • axios —  простой, но эффективный клиент HTTP; 
  • jimp —  управление изображением; 
  • sharp —  преобразование буферов ответов в изображения; 
  • twitter-api-client —  взаимодействие с Twitter API; 
  • rss-to-json —  для преобразования XML (потоки Medium) в JSON;
  • serverless —  подготовка AWS инфраструктуры к работе посредством Serverless Framework. Глобальная установка! 

Создадим один файл скрипта в src/handler.js, содержащий все необходимые операции. 

Получение доступа к Twitter API 

Далее нам необходим доступ к Twitter API, поэтому создаем новое клиентское приложение на портале разработчиков Twitter’s developer portal.

После этого обязательно копируем API-ключи: публичный и приватный. Вернувшись на экран обзора, выбираем новое приложение и нажимаем на Edit в левом верхнем углу. Устанавливаем полный перечень разрешений Read and Write and Direct Messages (чтение, запись, личные сообщения), чтобы впоследствии получить доступ ко всей необходимой информации и обновить заголовок. 

На завершающем этапе создаем токены аутентификации Access Token и Secret, выбирая приложение и переключаясь на вкладку Keys and token сразу под заголовком. 

Вероятнее всего Twitter еще раз покажет публичный и приватный API-ключи вместе с новыми токенами. Их необходимо записать, поскольку доступа к этим данным уже не будет. 

Инициализация клиента Twitter

Наличие учетных данных позволяет настроить клиент для взаимодействия с Twitter API. Сначала помещаем все публичные и приватные ключи, а также дескрипторы Twitter и Medium в creds.json на корневом уровне. 

{
"TWITTER_API_KEY": "5Hn9Z28...BW4lEzD",
"TWITTER_API_SECRET_KEY": "yndh2s5sXLuh...33sbWsbbREZYp4",
"TWITTER_API_ACCESS_TOKEN": "42399689...jQIBa3Ew4RDi5ed",
"TWITTER_API_ACCESS_SECRET": "QgYTQoIueX5S...JHk44vVuty9ewwo",
"TWITTER_HANDLE": "tpschmidt_",
"MEDIUM_HANDLE": "tpschmidt"
}

Теперь с помощью следующего кода создаем клиента: 

const { TwitterClient } = require('twitter-api-client')

function getVariable(name) {
if (fs.existsSync('../creds.json')) {
return require('../creds.json')[name]
}
return process.env[name]
}

const twitterClient = new TwitterClient({
apiKey: getVariable('TWITTER_API_KEY'),
apiSecret: getVariable('TWITTER_API_SECRET_KEY'),
accessToken: getVariable('TWITTER_API_ACCESS_TOKEN'),
accessTokenSecret: getVariable('TWITTER_API_ACCESS_SECRET'),
});

Для локального тестирования будем напрямую обращаться к файлу creds.json, а при развертывании на AWS воспользуемся переменными среды, которые настроим позже. 

Сбор информации о недавних подписчиках 

Теперь с помощью клиента Twitter API получим информацию о недавних подписчиках. Ответ состоит из списка пользователей, каждый из которых содержит profile_image_url_https.

Скачиваем аватары пользователей, меняем размер изображения посредством sharp и сохраняем во временном файле. 

const axios = require('axios')
const sharp = require('sharp')

const numberOfFollowers = 3
const widthHeightFollowerImage = 90

async function saveAvatar(user, path) {
console.log(`Retrieving avatar...`)
const response = await axios({
url: user.profile_image_url_https,
responseType: 'arraybuffer'
})
await sharp(response.data)
.resize(widthHeightFollowerImage, widthHeightFollowerImage)
.toFile(path)
}

async function getImagesOfLatestFollowers() {
console.log(`Retrieving followers...`)
const data = await twitterClient
.accountsAndUsers
.followersList({
screen_name: getVariable('TWITTER_HANDLE'),
count: numberOfFollowers
})
await Promise.all(data.users
.map((user, index) => saveAvatar(user, `/tmp/${index}.png`)))
}

Работа с персональными RSS-потоками Medium 

Следующая задача  —  получить название свежей статьи из потока Medium. Если вы ведете блог на этом сайте, то все метаданные ваших лент сообщений доступны на https://medium.com/feed/@username. 

Поскольку мы имеем дело с JavaScript, то с помощью rss-to-json преобразуем XML-ответ в JSON, что позволяет без особого труда извлечь требуемое название. Добавление небольших отступов слева и справа в соответствии с размером названия обеспечит его выравнивание по центру относительно определенной точки заголовка. 

const Feed = require('rss-to-json')

async function getLatestArticleHeadline() {
console.log(`Retrieving headline...`)
const rss = await Feed.load(`https://medium.com/feed/@${getVariable('MEDIUM_HANDLE')}`)
const title = rss.items[0].title
console.log(`Retrieved headline: ${title}`)
// добавляем отступы слева и справа для выравнивания по центру
const padding = ' '.repeat(Math.ceil((60 - title.length) / 2))
return `${padding}${title}${padding}`;
}

Конечно же, можно не ограничиваться Medium, а добыть все что угодно, даже простую HTML-страницу, и извлечь материалы, которые нужно показать. 

Обновление изображения заголовка 

Все самое сложное уже сделано. Нам удалось извлечь динамический контент! Осталось лишь добавить основное изображение для заголовка проекта в новый каталог assets. Приведу пример моего: 

Как видно, я оставил пустое пространство для отображения названия статьи блога и аватарок недавних подписчиков. В дальнейшем в соответствии с изображением необходимо настроить координаты областей, куда будут добавляться верхние слои для динамического контента. 

Кроме того, я подобрал одну маску (белый круг на темном фоне) для наложения на аватарки подписчиков, чтобы придать им округлые края. При этом следует убедиться, что ее размер соответствует этим изображениям (в нашем примере он составляет 90×90 пикселей). Если вам больше нравится квадратная форма, то просто удалите вызовы mask

const Jimp = require('jimp')

async function createBanner(headline) {
const banner = await Jimp.read(`${__dirname}/../assets/${bannerFileName}`)
const mask = await Jimp.read(`${__dirname}/../assets/${maskFileName}`)
const font = await Jimp.loadFont(Jimp.FONT_SANS_32_WHITE)
// создаем заголовок
console.log(`Adding followers...`)
await Promise.all([...Array(numberOfFollowers)].map((_, i) => {
return new Promise(async resolve => {
const image = await Jimp.read(`/tmp/${i}.png`)
const x = 600 + i * (widthHeightFollowerImage + 10);
console.log(`Appending image ${i} with x=${x}`)
banner.composite(image.mask(mask, 0, 0), x, 350);
resolve()
})
}))
console.log(`Adding headline...`)
banner.print(font, 380, 65, headline);
await banner.writeAsync('/tmp/1500x500_final.png');
}

Здесь ничего необычного не происходит: 

  • загружаем активы и ранее сохраненные изображения подписчиков; 
  • задаем шрифт, цвет и размер текста; 
  • добавляем изображения и название статьи в заголовок. 

В результате получаем новый файл в 1500x500_final.png в /tmp.

В завершении осталось лишь обновить новый заголовок на Twitter!

async function uploadBanner() {
console.log(`Uploading to twitter...`)
const base64 = await fs.readFileSync('/tmp/1500x500_final.png', { encoding: 'base64' });
await twitterClient.accountsAndUsers
.accountUpdateProfileBanner({ banner: base64 })
}

Автоматизация процесса посредством Lambda и правил Eventbridge 

Итак, теперь у нас есть локальный скрипт для обновления заголовка, который должен запускаться автоматически и так часто, как только позволяет Twitter API. 

Эта задача легко выполняется посредством AWS Lambda. Если вы впервые имеете дело с AWS, то нужно зарегистрироваться (сервис бесплатный; в течение первого года без оплаты предоставляются дополнительные объемы услуг, и это при том, что для нескольких сервисов, подобных Lambda, уже изначально установлены уровни бесплатного пользования). 

Далее предварительно предстоит сделать следующее:

  • перейти на страницу Security Credentials (Данные для доступа) и создать новые публичный и приватный ключи; 
  • установить AWS CLI (например, через Homebrew);  
  • выполнить aws configure для настройки учетных данных из первого шага. 

Поскольку мы уже глобально установили Serverless Framework, то можем инициализировать новый файл шаблона, выполнив sls или serverless. В результате получим файл serverless.yml, который определяет все инфраструктуры. 

В итоге нужно создать: 

  • функцию Lambda, запускающую скрипт; 
  • уровень Lambda, содержащий зависимости; 
  • правило EventBridge, вызывающее функцию каждые 60 секунд. 

С помощью Serverless Framework мы легко это делаем буквально в несколько строк кода: 

service: twitter-banner

frameworkVersion: '2'

custom:
appName: twitter-banner

provider:
lambdaHashingVersion: 20201221
name: aws
runtime: nodejs12.x
region: eu-central-1
logRetentionInDays: 7

layers:
common:
package:
artifact: deploy/layer-common.zip
name: ${self:custom.appName}-common-layer
description: Common dependencies for Lambdas
compatibleRuntimes:
- nodejs12.x
retain: false

package:
individually: true

functions:
twitterBanner:
handler: src/handler.handler
package:
artifact: deploy/lambda.zip
name: ${self:custom.appName}
description: Function to regularly update the Twitter banner
reservedConcurrency: 1
memorySize: 2048
timeout: 10
layers:
- { Ref: CommonLambdaLayer }
events:
- schedule: rate(1 minute)
environment:
TWITTER_API_KEY: ${file(creds.json):TWITTER_API_KEY}
TWITTER_API_SECRET_KEY: ${file(creds.json):TWITTER_API_SECRET_KEY}
TWITTER_API_ACCESS_TOKEN: ${file(creds.json):TWITTER_API_ACCESS_TOKEN}
TWITTER_API_ACCESS_SECRET: ${file(creds.json):TWITTER_API_ACCESS_SECRET}
TWITTER_HANDLE: ${file(creds.json):TWITTER_HANDLE}
MEDIUM_HANDLE: ${file(creds.json):MEDIUM_HANDLE}

Рассмотрим 3 основные части: 

  • provider  —  указывает на то, что мы используем AWS, и включает ряд основных пунктов, например срок хранения логов, который составляет 7 дней во избежание затрат в будущем; 
  • layers —  определяет уровень, содержащий все зависимости node_modules;
  • functions —  это блок, в котором мы создаем функцию Lambda.

С помощью поля events мы указываем, что EventBridge будет периодически вызывать функцию. 

Перед упаковкой файлов добавим в скрипт прокси-функцию, которая впоследствии будет вызываться Lambda. 

module.exports.handler = async () => {
await getImagesOfLatestFollowers()
const title = await getLatestArticleHeadline()
await createBanner(title)
await uploadBanner()
}

Если вы посмотрите еще раз, то увидите, что мы ссылались на src/handler.handler в serverless.yml. В нашем случае источник находится в src/handler.js. Так Lambda узнает, откуда следует начинать выполнение скрипта node.js

Заключительный этап: упаковка функции и уровня. 

#!/usr/bin/env bash

rm -rf deploy
mkdir deploy

# упаковка lambda
zip -rq deploy/lambda.zip src/* assets/*

# упаковка layer
mkdir -p deploy
rm -rf tmp/common
mkdir -p tmp/common/nodejs
cp package.json tmp/common/nodejs
pushd tmp/common/nodejs 2>&1>/dev/null
docker run -v "$PWD":/var/task lambci/lambda:build-nodejs12.x npm install --no-optional --only=prod
popd 2>&1>/dev/null
pushd tmp/common 2>&1>/dev/null
rm nodejs/package.json
zip -r ../../deploy/layer-common.zip . 2>&1>/dev/null
popd 2>&1>/dev/null
if [[ ! -f deploy/layer-common.zip ]];then
echo "Packaging failed! Distribution package ZIP file could not be found."
exit 1
fi

Готово! Теперь осталось лишь развернуть весь стек посредством sls deploy, и процесс запущен. Если что-то не работает, для выявления проблемы изучите соответствующие потоки логов CloudWatch. Если же все идет по плану, то вы увидите логи отладки: 

INFO   Retrieving followers...
INFO Retrieving avatar...
INFO Retrieving avatar...
INFO Retrieving avatar...
INFO Retrieving headline...
INFO Retrieved headline: Distributed Tracing Matters
INFO Adding followers...
INFO Appending image 0 with x=600
INFO Appending image 1 with x=700
INFO Appending image 2 with x=800
INFO Adding headline...
INFO Uploading to twitter...

Если вы дошли до этого этапа, и у вас все получилось, то принимайте мои поздравления! 🎉

💡 Пусть вас не пугает тот факт, что код будет вызываться каждые 60 секунд. Как уже ранее упоминалось, мы располагаем уровнем бесплатного пользования AWS Lambda, который включает 1 млн бесплатных запросов и 400 000 ГБ‑секунд вычислений в месяц. А так как наша функция выполняется всего лишь несколько секунд, то мы никогда не превысим этот лимит. 

Весь код доступен на GitHub

Заключение 

Сегодня результатом нашей с вами работы, конечно же, никого уже не удивишь. Однако эксперименты с динамическим контентом в ленте сообщений Twitter несомненно приведут к расширению аудитории. Кроме того, вы наверняка узнаете что-нибудь новое и получите удовольствие от процесса. 

Вы можете делать и добавлять практически все что угодно без всяких ограничений, например автоматически обновлять информацию о себе или аватарку. 

Творите и делитесь плодами трудов своих с миром!

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Tobias Schmidt: How To Build a Self-Updating Twitter Banner With Dynamic Content

Предыдущая статьяKotlin 1.5.30 и KMM/KMP
Следующая статьяКонтейнеризацию невозможно сдержать