18
5

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.

nemAdvent Calendar 2021

Day 15

TwitterAPI V2で引用RTを検索しSymbolアドレス,TwitterID,本文を抜き出す

Last updated at Posted at 2021-12-22

#XYM配布イベントを行うのは大変
最近TwitterではXYM配布イベントが流行っています.私も以前配布イベントを行ったことがあるのですが次のような問題に直面しました.
・TwitterアプリではすべてのRT履歴を見ることができない
・数が多い
・当選報告をする際フォローされているのにDMが送れない
私の場合はアドレスを引用RTで記載してもらわなかったので当選報告が非常に大変でした(結局メンションで代用した)
規模にもよりますが,正直Twitterアプリを利用して公正な抽選を行うのは結構厳しいです.あたれらが利用されている理由がよくわかりますね..

#TwitterAPIでこの問題を解決しよう
APIの申請や制限が結構厳しいといった問題はありますが,これらの問題はTwitterAPIを利用することで大体解決します.そこで今回は以前抽選の際に使用した引用RT検索プログラムにアドレス制限の確認,アドレスの形式チェックを加えてより便利な検索プログラムに改良してみました.

#データを取得する
#TwitterAPIを取得する
自力でプログラムを動かそうとする場合TwitterAPIのアクセス権を取得する必要があります.ここは長くなってしまうので割愛します.
##クエリを作る
まず,引用されるツイートのIDを取得します.引用されるツイートを選択し,URL末尾の数字をコピーしてください

この場合'1471969461267996672'となります
その後.ツイートのIDとユーザー名を使って検索するためのクエリを作成します.
-from:で自分のツイートを除外.is:quoteで引用RTを指定,-is:retweetでRTを除外しています.

const targetTweetId = `1471969461267996672`;
const targetAccountId = 'monakaJP';

const query = targetTwwetId + ' -from:' + targetAccountId + ' is:quote -is:retweet';

その後,TwitterAPIのbearerTokenを利用してヘッダを作り,検索を行います.100件を超える場合はnext_tokenがmetaに入っているのでこれを指定して再検索します.

const bearerToken = '******';
const headers = { 'Authorization': 'Bearer ' + bearerToken };

(async()=>{
    var res = await searchTweet(headers, query);
    var tweetData = res.data;
    while(true){
        if (typeof res.meta?.next_token !== 'undefined') {
            res = await searchTweet(headers, query, res.meta.next_token);
            tweetData = tweetData.concat(res.data);
        } else {
            break;
        }
    }
})();
async function searchTweet(headers,query,next_token = ''){
    const searchEndpoint = 'https://api.twitter.com/2/tweets/search/recent';
    var params = { 'query': query, 'tweet.fields': 'author_id', 'max_results': 100};
    if(next_token!=='')params["next_token"] = next_token;
    const res = await needle('get', searchEndpoint, params, {headers});
    if (res.statusCode ===200) {
        return res.body;
    } else {
        console.log(res);
        throw new Error('Unsuccessful request');
    }
}

##ユーザー名を取得する
取得した引用RTからユーザーIDを取得しユーザー名を取得します

async function searchUserNames(headers, idArray) {
    const searchEndpoint = 'https://api.twitter.com/2/users';
    var params = { 'ids':idArray.join(',') };
    const res = await needle('get', searchEndpoint, params, { headers });
    if (res.statusCode === 200) {
        return res.body;
    } else {
        console.log(res);
        throw new Error('Unsuccessful request');
    }
}

##アドレスを抜き出す
SymbolメインネットのアドレスはNからはじまる仕様になっています.そこで,ハイフンなどの不要な文字列を本文から除去した後Nから始まる文字列をツイート本文から検索し,39文字を抜き出します.その後アドレスが正しいかチェックを行います.本当は残高確認などでもう少し確認した方が良いのかもしれないのですが,Symbolはアドレスにチェックデジットが含まれているので誤判定はあまり起きないと思います.

const searchStr = /[N]+/g;
        var res;
        var address = '';
        const text = tweetData[i].text.slice(0, tweetData[i].text.lastIndexOf('http')).replace(/\n/g, '').replace(/,/g, '').replace(/-/g, '');
        while (res = searchStr.exec(text)) {
            var str = text.slice(res.index, res.index + 39);
            if (xym.Address.isValidRawAddress(str)) {
                address = str;
                break;
            } else {
                console.log("invalid:",str);
            }
        }

#CSVにして保存する
このままだと見づらいのでCSVに保存しておきます.おそらくエクセルで見たい場合が多いと思うのでbomを付けて保存します.

    var fileStr = '\ufeff' + 'username,address,text\n' + csvStr.join('\n');
    fs.writeFile('quote_accountData.csv', fileStr, 'utf8', (err, data) => {
        if (err) console.log(err);
        else console.log("write end");
    });
})();

こんな感じになります(自分の引用RTなのでアドレスを検出できず,空欄になっています)
キャプチャ.JPG

#全文

const needle = require('needle');
const fs = require('fs');
const xym = require('symbol-sdk');

const bearerToken = '*************************';

const targetTwwetId = `*******`;//引用されるツイートのID
const targetAccountId = '*****';//ツイート主

const query = targetTwwetId + ' -from:' + targetAccountId + ' is:quote -is:retweet';

const headers = { 'Authorization': 'Bearer ' + bearerToken };
(async()=>{
    var res = await searchTweet(headers, query);
    var tweetData = res.data;
    var users = [];
    var csvStr = [];
    while(true){
        if (typeof res.meta?.next_token !== 'undefined') {
            res = await searchTweet(headers, query, res.meta.next_token);
            tweetData = tweetData.concat(res.data);
        } else {
            break;
        }
    }
    //console.log(tweetData);
    var ids = [];
    for (i = 0; i < tweetData.length; i++)ids.push(tweetData[i].author_id);
    for(i=0;i<Math.ceil(tweetData.length/100);i++)users = users.concat((await searchUserNames(headers,ids.slice(i*100,(i+1)*100))).data);
    console.log(users.length);
    
    for(i=0;i<users.length;i++){
        const searchStr = /[N]+/g;
        var res;
        var address = '';
        const text = tweetData[i].text.slice(0, tweetData[i].text.lastIndexOf('http')).replace(/\n/g, '').replace(/,/g, '').replace(/-/g, '');
        while (res = searchStr.exec(text)) {
            var str = text.slice(res.index, res.index + 39);
            if (xym.Address.isValidRawAddress(str)) {
                address = str;
                break;
            } else {
                console.log("invalid:",str);
            }
        }
        csvStr.push([users[i].username,address,text]);
    }

    var fileStr = "username,address,te\n";
    for (var i = 0; i < csvStr.length; i++)fileStr += csvStr[i].join() + "\n";
    fs.writeFile('quote_accountData.csv', fileStr, 'utf8', (err, data) => {
        if (err) console.log(err);
        else console.log("write end");
    });
})();

async function searchTweet(headers,query,next_token = ''){
    const searchEndpoint = 'https://api.twitter.com/2/tweets/search/recent';
    var params = { 'query': query, 'tweet.fields': 'author_id', 'max_results': 100};
    if(next_token!=='')params["next_token"] = next_token;
    const res = await needle('get', searchEndpoint, params, {headers});
    if (res.statusCode ===200) {
        return res.body;
    } else {
        console.log(res);
        throw new Error('Unsuccessful request');
    }
}

async function searchUserNames(headers, idArray) {
    const searchEndpoint = 'https://api.twitter.com/2/users';
    var params = { 'ids':idArray.join(',') };
    const res = await needle('get', searchEndpoint, params, { headers });
    if (res.statusCode === 200) {
        return res.body;
    } else {
        console.log(res);
        throw new Error('Unsuccessful request');
    }
}

あとはエクセルなどで閲覧すれば大丈夫です
アドレス制限の確認は行っていないので送金の際は別途アドレスが使えるか確認する必要があります.
#その他
・TwitterAPI無料版の仕様上,ツイートしてから7日間のものしか検索できません.Twitterのキャンペーンは必ず7日以内を締め切りにしましょう
・抽選機能をつければアグリゲートでの自動配布まで一括実行できたりもすると思います.
・アプリにすればTwitterAPIを取得しなくても実行可能なので今の皆さんの抽選キャンペーンのお役に立てそうなのですが多分間に合いません.ごめんなさい...

18
5
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
18
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?