みんな僕のTwitterの名前を自由に変えていいよ 

# はじめに
昔、特定の文字列を含んだツイートを送るとその相手のTwitterの名前を変えられるといったサービスがありました。
名前も覚えてないし何年も前ですし、もうサービスは終了してるかもしれません(ユーザーストリームの廃止とかありましたし)
なので今回は自分で作ってみることにしました。

# 環境
C#
[CoreTweet](https://github.com/CoreTweet/CoreTweet)

# 仕様
①1分ごとに自分宛てのツイートを検索する
②『お前の名前は「???」だ』という定型文を含み、かつ10分前までで、かつ未ふぁぼのツイートに絞る
③絞ったツイートの中からランダムで採用
④そのツイートをふぁぼり、名前を変える。報告ツイートもする。

###例
![screenshot.1559216884.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/38e03e93-378e-84fd-8ed0-f04e456959d9.png)

   ↓

![screenshot.1559216921.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260914/ef9bdd98-fbf8-b48c-43f2-dc4a054a938e.png)


## 仕様の補足
①の一分毎に検索、というのは、TwitterAPI取得の制限によるものです。検索は15分に15回制限があるので1分毎に検索をかけます。
②の未ふぁぼのツイートに絞るのにも理由があります。同じツイートを何度も採用するのはよくないということから、④の段階でふぁぼります。
採用したツイートはふぁぼってあるので②の段階で排除されるわけです。
10分以内、というのは、めちゃめちゃ前のツイートを拾うのを防止してます。
③のランダムというのは、対象のツイートが複数あった場合、何を選べば良いかわからなかったのでとりあえずランダムにしてます。

#実装
実装はC#のCoreTweetを使いました。
C#Twitterライブラリで一番情報量が多そうだったので。

# メイン呼び出し部分

```C#:Program.cs
namespace TwitterNameBot {
    class Program {
        static void Main(string[] args) {
            var util = new TweetUtillity(args[0],args[1]);

            util.NameChangeAsync();
        }
    }
}
```
とりあえず、Debugモードでも動けばいいということで、コマンドライン引数からAPIKeyを入力するようにしました。コードに直接書くのは嫌だったので。

# TweetUtillityクラス

## インスタンス変数とコンストラクタ

```C#:TweetUtillity.cs
private string keyWord;
private const string expression = "お前の名前は「(?<name>.*)」だ$";
private readonly string ApiKey;
private readonly string ApiSecret;

public TweetUtillity(string apiKey,string apiSecret) {
    this.ApiKey = apiKey;
    this.ApiSecret = apiSecret;
}
```
keyWord・・・検索するワードです。今回はログインした自分のIDです。
expressionn・・・定型文です。判定と名前部分の抜粋を正規表現でやっています。

## トークン生成関数

```C#:TweetUtillity.cs
private Tokens GetAccessToken() {
    var accesTokenPath = "AccesToken.txt";
    var secretTokenPath = "AccesTokenSecret.txt";
    Tokens token;
    try {
        if (!File.Exists(accesTokenPath) || !File.Exists(secretTokenPath)) { // 初めてのログインの場合
            var session = OAuth.Authorize(ApiKey, ApiSecret);
            Process.Start(session.AuthorizeUri.AbsoluteUri); //ブラウザを開いてPINコードを取得
            var pinCode = Console.ReadLine(); //ここでPINコードを入力
            token = session.GetTokens(pinCode); //PINコードからトークンを取得

            File.WriteAllText(accesTokenPath, token.AccessToken); //アクセスキーをファイルをファイルに書き出す
            File.WriteAllText(secretTokenPath, token.AccessTokenSecret); //アクセスキーをファイルをファイルに書き出す

            keyWord = token.ScreenName; //自分のID(ScreenName)を検索ワードとして登録
            Console.WriteLine(keyWord);
        } else {
            var accesToken = File.ReadAllText(accesTokenPath);  //アクセスキーをファイルから取得
            var secretToken = File.ReadAllText(secretTokenPath); //アクセスキーをファイルから取得

            token = Tokens.Create(ApiKey, ApiSecret, accesToken, secretToken);
            var user =token.Account.VerifyCredentials(); //ユーザ情報を取得
            keyWord = user.ScreenName;
            Console.WriteLine($"{keyWord}");
        }
        
        return token;
    } catch (Exception e) {
        throw new Exception(e + "トークンの作成に失敗しました。");
    }  
}
```
[トークンを再利用する](https://github.com/CoreTweet/CoreTweet/wiki/%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%86%8D%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)によると、

>また、 tokens.UserId と tokens.ScreenName は Tokens.Create を使った場合、自動で取得されることはありません。 Account.VerifyCredentials で取得してください。

ということなので、アクセスキーがあるときとないときで方法を分けています。
また、このアプリはセキュリティうんぬんは考えていないので、安易にファイルに保存するという方法を取っています。

## ツイート検索関数

```C#:TweetUtillity.cs
 private Dictionary<Status,string> GetTargetTweet(Tokens tokens) {
     var result = tokens.Search.Tweets(count => 50, q => "to:" + keyWord); //自分のIDで50件検索
     
     var tweetTable = new Dictionary<Status, string>();

     foreach (var tweet in result) {
         var text = tweet.Text;
         if (!(tweet.CreatedAt.UtcDateTime - DateTime.UtcNow > new TimeSpan(0, -10, 0))) { //10分以内じゃないツイートは排除
             continue;
         }
         
         if((bool)tokens.Statuses.Show(id => tweet.Id).IsFavorited) { continue; } //ふぁぼ済みだったら排除

         Regex reg = new Regex(expression);
         Match match = reg.Match(text);
         if (match.Success == true) {
             var matchText = match.Groups["name"].Value;
             if(matchText.Length > 50 || matchText.Length == 0) { continue; }
             tweetTable.Add(tweet, matchText);
         }
     }
     return tweetTable;
 }
```

採用したツイートを後でふぁぼる、という仕様上、ツイートと正規表現で抜いた文字をセットにしてDictionaryで返しています(まあ分ければいいんですけど)
この関数は10分以上前のツイート、ふぁぼ済みのツイートを排除しています。
[公式Twitter](https://twitter.com/TwitterSupport/status/928777729180254208)によると、名前の文字数の上限は50文字までらしいので、50文字以上の名前も排除しています。

## メイン関数

```C#:TweetUtillity.cs
public void NameChangeAsync() {
    var tokens = GetAccessToken();
    while (true) {

        var tweetTable = GetTargetTweet(tokens); // 定型文のツイートを検索して取得

        var tweets = tweetTable
                        .GetKeys()
                        .ToList();

        if (!tweets.Any()) { //ツイートがなかったら
            //Console.WriteLine("対象ツイートないよーーーん");
            Thread.Sleep(60 * 1000);
            continue;
        }
        var tweet = tweets.Random();  //ランダム

        try {
            var name = tweetTable[tweet];
            tokens.Favorites.Create(id => tweet.Id); //対象ツイートをふぁぼる
            tokens.Account.UpdateProfile(name: name); //名前を変える
            tokens.Statuses.Update(status => $"私の名前は「{name}」です。#NameChangeBot"); //名前変更の報告ツイート
            Console.WriteLine("NewName:" + name);
            Thread.Sleep(60 * 1000);
        } catch (Exception e) {
            throw new Exception(e + "名前変更に失敗しました。");
        }

    }
}
```
メイン関数です。Thread.Sleepメソッドを使っているので、60秒ごとにwhileが実行されます(あまりよくないっぽいですねこれ)

ふぁぼったり名前変えたりツイートしたりします。

## 拡張メソッド
```C#:Extension.cs
//DictionaryのKeyをまとめてIEnumerableにして返す
public static IEnumerable<TKey> GetKeys<TKey, TValue>(this Dictionary<TKey, TValue> self) {
    foreach (var item in self.Keys) {
        yield return item;
    }
}

//ランダムな要素を返す
public static T Random<T>(this IEnumerable<T> self) {
    if (!self.Any()) {
        return default;
    }
    Random random = new Random();
    var idx = random.Next(0, self.Count());
    return self.ElementAt(idx);
}
```
(使わんかったけど供養)

```C#:Extension.cs
//ふぁぼされてないツイートだけ抽出
public static IEnumerable<Status> GetNotFavoTweets(this IEnumerable<Status> tweets,Tokens token) =>
            tweets.Where(tweet => (bool)!token.Statuses.Show(id => tweet.Id).IsFavorited);
```
これはメイン関数で使ってたのですが、ツイート検索関数でふぁぼされてるかを個別に調べたので使いませんでした。
それにしても、tweet.IsFavoritedの中にふぁぼ済みかそうじゃないかが入ってないとかどんな罠ですか……なんのためのプロパティなんですかこれ……なにがあってもFalseしか返ってこなくて、この方法にたどりつくまで時間がかかりました……

# できたー！
というわけで、このプログラムが起動してる間は一分毎に検索、ツイートを選別し名前を変えられるようになりました。
サーバーレスでここまでできるとすごいですね。自分はPCをつけっぱにして出かけたり寝たりするので割とずっと動いてます。
実質サーバーみたいなものですね……

#おわりに
最後まで記事を見てくださりありがとうございました。
ノリから始まり、一晩で作ったものですが、Qiitaに書いて！という要望で記事にさせていただきました。
CoreTweetの記事を無限に読み漁ってたので、みなさんに感謝です。

#参考
[Home(日本語)](https://github.com/CoreTweet/CoreTweet/wiki/Home(%E6%97%A5%E6%9C%AC%E8%AA%9E))
[[C#]CoreTweetでtwitterBot作ってみた](https://qiita.com/SoreKiita/items/e272c01e5b431ae7bd6b)
[TwitterのAPI制限 [2018/08/26現在]](https://qiita.com/mpyw/items/32d44a063389236c0a65)