0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Node.js + ExpressでTwitterBot作成 #2 『実装』

Last updated at Posted at 2022-12-01

仕様の検討

作成する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数のものを取り出します。

各関数に渡すパラメータ、受け取る値は以下を参照ください。

twitterAPIAccessor.js
//======================================================
//
// 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前にセットしています。

twitterAPIAccessor.js
//======================================================
//
// 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以上のツイートを取得します。

twitterAPIAccessor.js
//======================================================
//
// 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でつぶやかれているものをサッカーネタとみなしてセットします。

twitterAPIAccessor.js
//======================================================
//
// 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;
}

コントローラ

上記を呼び出すコントローラを実装します。

tweetController.js
//======================================================
//
// ツイート用コントローラモジュール
//
// [必要ライブラリ]
//  ・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モデルに変換する処理などを記述します。

dbAccessor.js
//======================================================
//
// 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に記述しています。

constants.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つの関数をルーティングに追加。

app.js
//======================================================
//
// 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の検索キーワードを変えるだけで対応可能です。

割と汎用性が高いものが出来たかなと思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?