仕様の検討
作成するBotの仕様を考えます。
サッカー関係で多数RTされているツイートを自動でRTしたいので、以下のようになりました。
事前準備
- 事前にサッカー関係のインフルエンサーをBotのアカウントで複数フォローしておく
以下を定期的に実行
- タイムライン上でRT数が一定以上のツイートをRT
- 特定のキーワード(ワールドカップなど)で検索し、一定以上のRT、かつ24時間以内に投稿されたツイートをRT
- トレンドのキーワードのうち、TLのツイートに含まれるものをサッカーネタとみなし、該当キーワードで検索。24時間以内、かつ一定以上のツイートをRT
この機能を満たすように実装していきたいと思います。
フォルダ構成
MVCライクにしたいのですが、ビジネスロジックをどこに置くべきか悩みます。
「prisma」はDBスキーマやマイグレーションの配置用なので、新たに「models」フォルダを追加。
また、設定ファイルなどを置く「etc」フォルダ、ログ出力用の「logs」も作成。
以下のように使い分けたいと思います。
フォルダの用途
- routes … コントローラを配置
- models … モデル(ビジネスロジック)を配置
- prisma … Prismaで使用するDBスキーマ、SQLiteDBファイルを配置
- logs … ログを配置
- etc … 定数、アプリケーション固有の設定ファイルを配置
ついでにgitのリポジトリ生成とREADME.mdも追加。
フォルダ構成は以下のようになりました。
ファイル構成
実装するファイルの構成は以下のようにしたいと思います。
app.jsからtweetControllerをルーティングで呼び出し、そこからビジネスロジックを呼び出す形です。
- etc/
- constants.js … 定数定義用(複数Twitterアカウントで使い回せる値)
- appConfig.js … 各Twitterアカウント固有の設定定義用
- models/
- dbAccessor.js … DB操作用のビジネスロジック
- twitterAPIAccessor.js … TwitterAPI操作用のビジネスロジック
- routes/
- tweetController.js … コントローラ
スキーマの定義
prismaフォルダ内にあるschema.prismaでスキーマを定義します。
RTしたツイートを保存するテーブルを作成します。
//======================================================
//
// Prismaスキーマファイル
//
// [索引]
// □ 1. モデル定義 Tweet
//
//======================================================
// クライアント生成
generator client {
provider = "prisma-client-js"
}
// DB設定
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
//======================================================
//
// 1. モデル定義 Tweet
//
//======================================================
// Tweetモデル
// ・ツイート1件分のデータ
//
model Tweet {
// DB内のID。連番
id Int @id @default (autoincrement())
// Twitter内の数値形式のツイートID 例'101010101011000'
id_str_in_twitter String
// Twitter内のユーザ名 例:田中
user_name String
// Twitter内のユーザの文字列形式のID 例:@test
user_screen_name String
// ツイート本文
tweet_text String
// RT数
rt_count Int
// クライアント名(source) 例:TweetBot
client_name String
// 投稿日時
posted_date DateTime
// DBデータ更新日時
udate_date DateTime @updatedAt
}
以下コマンドでマイグレーション実施。SQLite用のDBが生成され、テーブルも自動作成されます。
prisma migrate dev --name initial
実装
実際の実装内容はGitHubを参照ください。
肝の部分だけ書いておきます。
TwitterAPI関連
TwitterAPIでTLから一定以上のRTのツイートを取得する処理です。
APIv1のhomeTimeLine関数を使用し、そこから一定以上のRT数のものを取り出します。
各関数に渡すパラメータ、受け取る値は以下を参照ください。
//======================================================
//
// TwitterAPI操作用モジュール
//
// [必要ライブラリ]
// ・twitter-api-v2
// ・log4js
//
// [索引]
// □ 1. ホームタイムラインから一定のRT数以上のツイートを取得
// □ 2. 複数件のツイートをRT
// □ 3. 対象キーワードで検索を実行し、一定のRT以上のツイートIDのリストを返す
// □ 4. トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
// □ 5. 対象ツイートの情報を取得して投稿日時をセット
// □ 6. 自分がフォローしているユーザのIDを全件取得
//
//======================================================
//======================================================
// モジュール読込
//======================================================
// 定数
const _constants = require('../etc/constants');
// twitter-api-v2ライブラリ
const _twitterAPI = require('twitter-api-v2');
// ロギング用ライブラリ
const _log4js = require('log4js');
// DB操作用
const _dbAccessor = require('./dbAccessor');
//======================================================
// 変数宣言
//======================================================
// ロギング用設定
_log4js.configure(_constants.LOG4JS_CONFIG_VALS_DICT);
// ロギング用
const _logger = _log4js.getLogger();
// TwitterAPI生成
const _twAPI = new _twitterAPI.TwitterApi({
appKey: _constants.CONSUMER_KEY,
appSecret: _constants.CONSUMER_SECRET,
accessToken: _constants.ACCESS_TOKEN_KEY,
accessSecret: _constants.ACCESS_TOKEN_SECRET
});
// Twitter操作用クライアント生成
const _twClient = _twAPI.readWrite;
// 自分のツイッターID
var _myTwitterID = '';
//======================================================
//
// 1. ホームタイムラインから一定のRT数以上のツイートを取得
//
//======================================================
/**
* ホームタイムラインから一定のRT数以上のツイートを取得
* ・タイムラインから200件分のツイートを取得
* ・自分のアカウントでRT済のものはスキップ
* ・RT数が一定のものをPrismaのTweetモデルデータに変換し、配列に格納して返す
*
* @return {array} Tweetモデルデータの配列
*/
module.exports.getManyRTTweetsFromTimeLine = async function() {
var manyRtTweets = [];
try {
// タイムラインからツイート取得
const homeTimeline = await _twClient.v1.homeTimeline({'count': _constants.TWEET_GET_COUNT_FROM_TL});
console.log(homeTimeline.tweets.length, 'tweet fetched from timeline.');
// ツイートを走査
for (const tObj of homeTimeline.tweets) {
// RT済はスキップ
if (tObj.retweeted) {
continue;
}
// APIのツイートデータをDB保存用のツイートデータに変換
const tw = _dbAccessor.getTweetModelFromTweetObj(tObj);
// RT数が一定以上なら配列に追加
if (_constants.RETWEET_LEAST_RT < tw.rt_count) {
manyRtTweets.push(tw);
}
}
} catch (error) {
_logger.error(error);
}
return manyRtTweets;
}
続けてリツイート処理。v2のretweet関数を使用。
APIでのRT時には自分のTwitterID(数値形式)が必要なので、APIv2でIDを取得してRT前にセットしています。
//======================================================
//
// 2. 複数件のツイートをRT
//
//======================================================
/**
* 複数件のツイートをRT
* ・自分のTwitterIDをセット
* ・対象ツイートの件数、RT実行
*
* @param {array} tws RT対象のTweetモデルデータの配列
*/
module.exports.retweetTargetTweets = async function(tws) {
try {
// 自分のTwitterIDをセット
await setMyTwitterID();
// 各ツイートをRT
for (const d of tws) {
retweetTargetIDTweet(d.id_str_in_twitter);
_logger.debug('[RT実行] ' + d.rt_count + 'RT ' + d.user_screen_name + ' ' + d.tweet_text);
}
} catch (error) {
_logger.error(error);
}
}
//======================================================
// 自分のTwitterIDをセット
//======================================================
/**
* 自分のTwitterIDをセット
* ・APIで自分のTwitterIDを取得し、_myTwitterIDにセット
*/
async function setMyTwitterID() {
try {
// 自分のTwitterIDをセット
const mDict = await _twClient.v2.me({ expansions: ['pinned_tweet_id'] });
_myTwitterID = mDict.data.id.toString();
} catch (error) {
_logger.error(error);
}
}
//======================================================
// 1件のツイートのRTを実行
//======================================================
/**
* 1件のツイートのRTを実行
*
* @param tidStr RTするツイートのID
*/
async function retweetTargetIDTweet(tidStr) {
try {
// RT実行
await _twClient.v2.retweet(_myTwitterID, tidStr);
console.log('retweeted. ' + tidStr);
} catch (error) {
_logger.error(error);
}
}
続けて検索処理。APIv2使用。
こちらもAPI発行時にはTwitterIDが必要です。
対象キーワードで検索して一定のRT以上のツイートを取得します。
//======================================================
//
// 3. 対象キーワードで検索を実行し、一定のRT以上のツイートIDのリストを返す
//
//======================================================
/**
* 対象キーワードで検索を実行し、一定のRT以上のツイートIDのリストを返す
* ・APIで検索を実施
* ・検索結果の各ツイートデータを走査
* ・API検索結果のツイートオブジェクトをDB保存用のTweetモデルに変換
* ・配列に該当ツイートのIDを保存済ならスキップ(複数ユーザにRTされたツイートは複数件引っかかるため)
* ・アカウント名も検索の対象になるため、本文に検索キーワードが含まれていない場合はスキップ
* ・RT数が一定以上なら配列に追加
*
* @param {string} q 検索キーワード
* @returns {array} Tweetモデルデータの配列
*/
module.exports.getManyRTTweetsBySearch = async function(q) {
var tweets = [];
var savedTweetIds = [];
try {
// APIで対象キーワードの検索を実施
const searchResObj = await getSearchResultObj(q);
// 検索結果のツイートを走査
for (const twObj of searchResObj._realData.data) {
// API検索結果オブジェクトをTweetモデルに変換
const tw = _dbAccessor.getTweetModelFromTweetSearchObj(twObj);
// 配列に該当ツイートのIDを保存済ならスキップ
if (savedTweetIds.indexOf(tw.id_str_in_twitter) !== -1) {
continue;
// ツイート本文にキーワードが含まれていなければスキップ
} else if (tw.tweet_text.indexOf(q) === -1) {
continue;
}
// RT数が一定以上なら配列に追加
if (_constants.RETWEET_LEAST_RT <= tw.rt_count) {
tweets.push(tw);
savedTweetIds.push(tw.id_str_in_twitter);
}
}
} catch (error) {
_logger.error(error);
}
return tweets
}
//======================================================
// APIの検索結果オブジェクトを取得
//======================================================
/**
* APIの検索結果オブジェクトを取得
* ・定数の値に合わせて新着ツイート、または関連性の高いツイートを検索するかをセット
* ・24時間以内のツイートのみを100件取得
*
* @param {string} q 検索キーワード
* @returns {Object} API検索結果のTwitterObject
*/
async function getSearchResultObj(q) {
const SORT_ORDER_RECENCY = 'recency';
const SORT_ORDER_RELEVANCY = 'relevancy';
const SEARCH_COUNT = 100;
try {
// 自分のTwitterIDをセット
await setMyTwitterID();
// 新着ツイート、または関連性の高いツイートを検索するかをセット
var sortOrder = SORT_ORDER_RELEVANCY;
if (_constants.SEARCH_TWEET_BY_RECENCY) {
sortOrder = SORT_ORDER_RECENCY;
}
// 検索対象の開始時刻(24時間前)をセット
var startTime = new Date()
startTime.setHours(startTime.getHours() - _constants.SKIP_PAST_HOUR);
// 検索実行
const searchResObj = await _twClient.v2.search(q,
{'tweet.fields': ['public_metrics', 'referenced_tweets', 'created_at', 'source'],
'start_time': startTime.toISOString(),
'user.fields': ['public_metrics'],
'sort_order': sortOrder,
'max_results': SEARCH_COUNT,
'expansions': ['referenced_tweets.id']});
return searchResObj;
} catch (error) {
_logger.error(error);
}
}
トレンドからサッカージャンルのキーワードを取得する処理。APIv1を使用。
現在の日本のトレンドのワードのうち、自分のアカウントのTLでつぶやかれているものをサッカーネタとみなしてセットします。
//======================================================
//
// 4. トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
//
//======================================================
/**
* トレンドのキーワードのうち、ホームタイムラインでつぶやかれているキーワード一覧を取得
* ・日本のトレンドのキーワード一覧を取得
* ・ホームタイムラインのツイートに含まれているキーワード一覧を返す
*
* @return {array} キーワードの配列
*/
module.exports.getTrendKeywordsInHomeTimeLine = async function () {
htTrWords = [];
try {
// 日本のトレンドキーワード一覧をセット
const allTrWords = await getJPTrendKeywords();
// タイムラインからツイート取得
const ht = await _twClient.v1.homeTimeline();
// トレンドキーワードを走査
for (trWord of allTrWords) {
// 該当キーワードがタイムラインのツイートに含まれれば配列に追加
if (checkTargetKeywordExistInHomeTimeLine(ht, trWord)) {
htTrWords.push(trWord);
}
}
} catch (error) {
_logger.error(error);
}
return htTrWords;
}
//======================================================
// トレンドのキーワード一覧を取得
//======================================================
/**
* トレンドのキーワード一覧を取得
* ・日本のトレンドのキーワードを取得
*
* @return {array} キーワードの配列
*/
async function getJPTrendKeywords() {
trWords = [];
try {
const JAPAN_WOEID = 23424856;
// トレンド取得
const trVal = await _twClient.v1.trendsByPlace(JAPAN_WOEID);
for (tr of trVal[0].trends) {
trWords.push(tr.name);
}
} catch (error) {
_logger.error(error);
}
return trWords;
}
//======================================================
// 対象キーワードがホームタイムラインのツイート内に含まれるかを返す
//======================================================
/**
* 対象キーワードがホームタイムラインのツイート内に含まれるかを返す
* ・TLのツイートを走査
* ・フォローしているユーザがRTしたツイートはスキップ(対象ジャンル以外が含まれる可能性があるため)
* ・ツイート本文に該当ワードが含まれていればTrue
*
* @param {Object} ht ホームタイムラインオブジェクト
* @param {string} keyword 検索キーワード
* @return {bool}
*/
function checkTargetKeywordExistInHomeTimeLine(ht, keyword) {
try {
// タイムラインのツイートを走査
for (const twObj of ht.tweets) {
// RTされたツイートはスキップ
if (twObj.retweeted_status) {
continue;
}
// ツイート本文内に該当キーワードが含まれていれば
if (twObj.full_text.indexOf(keyword) !== -1) {
console.log('[トレンドが含まれるツイート]' + keyword);
console.log(twObj.full_text);
return true;
}
}
} catch (error) {
_logger.error(error);
}
return false;
}
コントローラ
上記を呼び出すコントローラを実装します。
//======================================================
//
// ツイート用コントローラモジュール
//
// [必要ライブラリ]
// ・log4js
//
// [索引]
// □ 1-1. タイムライン上の一定数以上のツイートをRT
// □ 1-2. トレンド内の対象ジャンルのキーワードを検索し、一定数以上のツイートをRT
// □ 1-3. 特定のキーワードを検索し、一定数以上のツイートをRT
//
//======================================================
//======================================================
// require設定
//======================================================
// 定数
const _constants = require('../etc/constants');
// log4js
const _log4js = require('log4js');
// Twitter操作用モデル
const _twAccessor = require('../models/twitterAPIAccessor');
// DB操作用
const _dbAccessor = require('../models/dbAccessor');
//======================================================
// 変数宣言
//======================================================
// ロギング用設定
_log4js.configure(_constants.LOG4JS_CONFIG_VALS_DICT);
// ロギング用
const _logger = _log4js.getLogger();
//======================================================
//
// 1-1. タイムライン上の一定数以上のツイートをRT
//
//======================================================
/**
* タイムライン上の一定数以上のツイートをRT
* ・タイムライン上から一定以上のRTのツイートを取得
* ・RT数の多いツイートのうち、RT対象のツイートをセット(DB未保存、投稿日時が一定時間以内)
* ・RT実行
* ・RTしたツイートをDB登録
*
* @param {Object} req
* @param {Object} res
*/
module.exports.retweetFromHomeTimeLine = async function(req, res) {
try {
// ホームタイムライン上から一定数以上のRTのツイートを取得
var manyRTTweets = await _twAccessor.getManyRTTweetsFromTimeLine();
console.log('TL内のRT数の多いツイート:' + manyRTTweets.length);
// RT対象のツイートをセット
const rtTargetTweets = await getRTTargetTweets(manyRTTweets);
console.log('RT対象のツイート:' + rtTargetTweets.length);
// RT実行
_twAccessor.retweetTargetTweets(rtTargetTweets);
// RTしたツイートをDB登録
_dbAccessor.saveTweetDatas(rtTargetTweets);
} catch (error) {
_logger.error(error);
}
// 結果を描画
res.send("done.");
}
//======================================================
// RT対象のツイートを返す
//======================================================
/**
* RT対象のツイートを配列で返す
* ・DB未保存(未RT)
* ・投稿日時が一定時間以内
* ・NGワードを含まない
*
* @param {array} tws
* @returns {array}
*/
async function getRTTargetTweets(tws) {
var rtTargetTweets = [];
try {
// ツイートを走査
for (tw of tws) {
// DB保存済ならスキップ
const isDBSaved = await _dbAccessor.isTargetTweetAlreadySaved(tw);
if (isDBSaved) {
continue;
}
// 投稿日時が一定時間以前ならスキップ
if (!checkPostedDateWithInTargetHours(tw)) {
continue;
}
// NGワードを含めばスキップ
if (checkTargetTweetContainsNGWord(tw)) {
continue;
}
// 配列に追加
rtTargetTweets.push(tw);
}
} catch (error) {
_logger.error(error);
}
return rtTargetTweets;
}
/**
* 投稿日時が一定時間以内かを返す
*
* @param {Object} tw
* @returns
*/
function checkPostedDateWithInTargetHours(tw) {
try {
// 24時間前をセット
var tdt = new Date();
tdt.setHours(tdt.getHours() - _constants.SKIP_PAST_HOUR);
// それ以降ならTrue
if (tdt <= tw.posted_date) {
return true
}
} catch (error) {
_logger.error(error);
}
return false;
}
/**
* 対象のツイートがNGワードを含むかを返す
* ・本文、またはクライアント名に含めばTrue
*
* @param {Object} tw
* @returns {Boolean}
*/
function checkTargetTweetContainsNGWord(tw) {
try {
// NGワードを走査
for (ngWord of _constants.RT_NG_KEYWORDS) {
if (tw.tweet_text.indexOf(ngWord) !== -1) {
return true;
}
if (tw.client_name.indexOf(ngWord) !== -1) {
return true;
}
}
} catch (error) {
_logger.error(error);
}
return false;
}
//======================================================
//
// 1-2. トレンド内の対象ジャンルのキーワードを検索し、一定数以上のツイートをRT
//
//======================================================
/**
* トレンド内の対象ジャンルのキーワードを検索し、一定数以上のツイートをRT
* ・日本のトレンドのキーワードのうち、TLのツイート内に含まれるキーワードをピックアップ
* ・それぞれを検索し、RT数の多いツイートを取得
* ・RT数の多いツイートのうち、RT対象をピックアップ(DB未保存、投稿日時が一定時間以内)
* ・RTしたツイートをDB登録
*
* @param {Object} req
* @param {Object} res
*/
module.exports.retweetFromTrendWord = async function(req, res) {
try {
// 日本のトレンドのキーワードのうち、TLのツイート内に含まれるキーワードをピックアップ
const trWords = await _twAccessor.getTrendKeywordsInHomeTimeLine();
// それぞれを検索し、RT数の多いツイートをRT
for (tWord of trWords) {
_logger.debug('トレンド内の対象ジャンル関連キーワード' + tWord);
// 検索結果からRTの多いツイートを取得
var manyRTTweets = await _twAccessor.getManyRTTweetsBySearch(tWord);
// 投稿日時をセット
manyRTTweets = await _twAccessor.setTweetPostedDates(manyRTTweets);
// RT対象のツイートをセット
const rtTargetTweets = await getRTTargetTweets(manyRTTweets);
// 0件ならスキップ
if (rtTargetTweets.length == 0) {
continue;
}
// ロギング
_logger.debug('[トレンドキーワード ' + tWord + ' から取得したRT対象のツイート] ' + rtTargetTweets.length + '件');
// RT実行
_twAccessor.retweetTargetTweets(rtTargetTweets);
// RTしたツイートをDB登録
_dbAccessor.saveTweetDatas(rtTargetTweets);
}
} catch (error) {
_logger.error(error);
}
// 結果を描画
res.send("done.");
}
//======================================================
//
// 1-3. 特定のキーワードを検索し、一定数以上のツイートをRT
//
//======================================================
/**
* 特定のキーワードを検索し、一定数以上のツイートをRT
* ・定数で設定されたキーワードを検索し、RT数の多いツイートをRT
* ・NGワードを含むツイートはRTしない
* ・RTしたツイートをDB登録
* ・RTしたツイートのうち、未フォローのユーザをDBに保存
*
* @param {Object} req
* @param {Object} res
*/
module.exports.retweetFromTargetSearchWords = async function(req, res) {
try {
// 対象のキーワードを走査
for (tWord of _constants.SEARCH_TARGET_KEYWORDS) {
// 対象のキーワードで検索し、RT数の多いツイートをセット
var manyRTTweets = await _twAccessor.getManyRTTweetsBySearch(tWord);
// 投稿日時をセット
manyRTTweets = await _twAccessor.setTweetPostedDates(manyRTTweets);
// RT対象のツイートをセット
const rtTargetTweets = await getRTTargetTweets(manyRTTweets);
// 0件ならスキップ
if (rtTargetTweets.length == 0) {
continue;
}
// ロギング
_logger.debug('[検索キーワード' + tWord + 'から取得したRT対象のツイート] ' + rtTargetTweets.length + '件');
// RT実行
_twAccessor.retweetTargetTweets(rtTargetTweets);
// RTしたツイートをDB登録
_dbAccessor.saveTweetDatas(rtTargetTweets);
}
} catch (error) {
_logger.error(error);
}
// 結果を描画
res.send("done.");
}
DB処理
DB関連のビジネスロジックを実装。
RTしたツイートのDBへの保存、TwiterAPIから取得したオブジェクトをPrismaのTweetモデルに変換する処理などを記述します。
//======================================================
//
// DB操作用モジュール
//
// [必要ライブラリ]
// ・prisma
// ・log4js
//
// [索引]
// □ 1-1. TwitterAPIから取得したTweetObjectをDB保存用のTweetモデルに変換
// □ 1-2. TwitterAPIから取得したTweet検索結果オブジェクトをDB保存用のTweetモデルに変換
// □ 2. 対象ツイートがDB保存済かを返す
// □ 3. 対象ツイートデータをDBに保存
//
//======================================================
//======================================================
// require設定
//======================================================
// 定数
const _constants = require('../etc/constants');
// log4js
const _log4js = require('log4js');
// prisma
const _prisma = require('@prisma/client');
//======================================================
// 変数宣言
//======================================================
// ロギング用設定
_log4js.configure(_constants.LOG4JS_CONFIG_VALS_DICT);
// ロギング用
const _logger = _log4js.getLogger();
// PrismaClient
const _prismaClient = new _prisma.PrismaClient();
//======================================================
//
// 1-1. TwitterAPIから取得したTweetObjectをDB保存用のTweetモデルに変換
//
//======================================================
/**
* APIから取得したTweetObjectをDB保存用のTweetモデルに変換
* ・RTの場合、RT数以外はretweeted_statusの値で各種値を上書き
*
* @param {Object} twObj
* @return {Object}
*/
module.exports.getTweetModelFromTweetObj = function(twObj) {
var d = {}
try {
// ツイートをセット
var st = twObj;
// RTの場合、retweeted_statusをセット
if (twObj.retweeted_status) {
st = twObj.retweeted_status;
}
d = {
'id_str_in_twitter': st.id_str,
'user_name': st.user.name,
'user_screen_name': st.user.screen_name,
'tweet_text': st.full_text,
'rt_count': twObj.retweet_count,
'client_name': st.source,
'posted_date': new Date(st.created_at)
}
} catch (error) {
_logger.error(error);
}
return d;
}
//======================================================
//
// 1-2. TwitterAPIから取得したTweet検索結果オブジェクトをDB保存用のTweetモデルに変換
//
//======================================================
/**
* APIのTweet検索結果オブジェクトをDB保存用のTweetモデルに変換
* ・RTの場合、IDをRT対象に書き換える
*
* @param {Object} sObj
* @return {Object}
*/
module.exports.getTweetModelFromTweetSearchObj = function(sObj) {
var d = {}
try {
d = {
'id_str_in_twitter': sObj.id,
'user_name': '',
'user_screen_name': '',
'tweet_text': sObj.text,
'client_name': sObj.source,
'rt_count': sObj.public_metrics.retweet_count,
'posted_date': new Date(sObj.created_at)
}
// RTはID書き換え
if (sObj['referenced_tweets']) {
d.id_str_in_twitter = sObj['referenced_tweets'][0]['id'];
}
} catch (error) {
_logger.error(error);
}
return d;
}
//======================================================
//
// 2. 対象ツイートがDB保存済かを返す
//
//======================================================
/**
* 対象ツイートがDB保存済かを返す
*
* @param {Object} tw
* @return {array}
*/
module.exports.isTargetTweetAlreadySaved = async function(tw) {
try {
// DBのデータを取得
const res = await _prismaClient.tweet.findFirst({
where: {
id_str_in_twitter: tw.id_str_in_twitter,
},
});
// 結果があればtrue;
if (res) {
return true;
}
} catch (error) {
_logger.error(error);
}
return false;
}
//======================================================
//
// 3. 対象ツイートデータをDBに保存
//
//======================================================
/**
* 対象ツイートデータをDBに保存
*
* @param {array} tModels
*/
module.exports.saveTweetDatas = async function(tws) {
try {
for (d of tws) {
await _prismaClient.tweet.create({
data: {
'id_str_in_twitter': d.id_str_in_twitter,
'user_name': d.user_name,
'user_screen_name': d.user_screen_name,
'tweet_text': d.tweet_text,
'rt_count': d.rt_count,
'client_name': d.client_name,
'posted_date': d.posted_date,
}
})
}
} catch (error) {
_logger.error(error);
}
}
定数の定義です。
アカウント固有の設定やAPIのキーは別ファイルappConfig.jsに記述しています。
//======================================================
//
// 定数定義用モジュール
// ・各Twitterアカウントで使い回せる定数を定義
// ・各アカウント固有の設定はapp_configに定義したものを読み込む
//
//======================================================
//======================================================
// require設定
//======================================================
// app_configからアカウント独自の定数を読込
var _app_config = require('./appConfig');
//======================================================
// リツイート設定
//======================================================
// この時間(h)より以前のツイートは無視
module.exports.SKIP_PAST_HOUR = 24;
// TLから1度に取得するツイートの数
module.exports.TWEET_GET_COUNT_FROM_TL = 200;
// この数以上のRT数でリツイート
module.exports.RETWEET_LEAST_RT = 100;
// 検索時、最新のツイートを検索するか false = 関連性の高いツイートを検索
module.exports.SEARCH_TWEET_BY_RECENCY = false;
//======================================================
// 検索キーワード
//======================================================
// 検索キーワード このキーワードで検索し、RT数の多いツイートをRT
module.exports.SEARCH_TARGET_KEYWORDS = _app_config.SEARCH_TARGET_KEYWORDS;
// RT時のNGワード このキーワードを含むツイートはRTしない
module.exports.RT_NG_KEYWORDS = _app_config.RT_NG_KEYWORDS;
//======================================================
// ツイッターアカウント
//======================================================
// アカウント名
module.exports.SCREEN_NAME = _app_config.TWITTER_SCREEN_NAME;
// アカウントID
module.exports.ACCOUNT_ID = _app_config.TWITTER_ACCOUNT_ID;
// Consumer Key
module.exports.CONSUMER_KEY = _app_config.CONSUMER_KEY;
// Consumer Secret
module.exports.CONSUMER_SECRET = _app_config.CONSUMER_SECRET;
// Access Token
module.exports.ACCESS_TOKEN_KEY = _app_config.ACCESS_TOKEN_KEY;
// Access Token Secret
module.exports.ACCESS_TOKEN_SECRET = _app_config.ACCESS_TOKEN_SECRET;
//======================================================
// ロギング用
//======================================================
// log4jsのオプション
module.exports.LOG4JS_CONFIG_VALS_DICT = {
appenders: {
console: {
type: 'console'
},
system: {
type: 'file',
'filename': './logs/log.txt',
'maxLogSize': 104857600,
'layout': {
'type': 'pattern',
'pattern': '%d [%p] %m'}
}
},
categories: {
default: {
appenders: [
'console',
'system'
],
level: 'debug' }
}
};
app.jsにはコントローラの3つの関数をルーティングに追加。
//======================================================
//
// 1. ルーティング設定
//
//======================================================
// タイムラインからのRT処理
app.get('/timeline', _twController.retweetFromHomeTimeLine);
// トレンドからのRT処理
app.get('/trend', _twController.retweetFromTrendWord);
// 検索からのRT処理
app.get('/search', _twController.retweetFromTargetSearchWords);
インフルエンサーをフォロー
最後にbotのアカウントでサッカー関係のインフルエンサーを数十人フォローしておきます。
以下の目的のために使用します。
- フォローしているユーザのツイートで多数RTされているものをRTする
- トレンドのキーワードのうち、フォローしているユーザがつぶやいているものをサッカーネタとみなす
定期的に実行
あとはタスクスケジューラやcronで定期的に上記3つのURLにアクセス。
curlを使うのが簡単ですね。
curl http://localhost:3000/timeline
他ジャンルへの対応
他ジャンル(野球、プログラミングなど)のツイートをRTするbotを作りたい場合、
アカウントをもう1つ作って該当ジャンルのインフルエンサーを何十人かフォローし、appConfigの検索キーワードを変えるだけで対応可能です。
割と汎用性が高いものが出来たかなと思います。