20190530のC#に関する記事は5件です。

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

はじめに

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

環境

C#
CoreTweet

仕様

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

screenshot.1559216884.png

screenshot.1559216921.png

仕様の補足

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

実装

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

メイン呼び出し部分

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

            util.NameChangeAsync();
        }
    }
}

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

TweetUtillityクラス

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

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・・・定型文です。判定と名前部分の抜粋を正規表現でやっています。

トークン生成関数

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 + "トークンの作成に失敗しました。");
    }  
}

トークンを再利用するによると、

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

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

ツイート検索関数

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によると、名前の文字数の上限は50文字までらしいので、50文字以上の名前も排除しています。

メイン関数

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が実行されます(あまりよくないっぽいですねこれ)

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

拡張メソッド

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);
}

(使わんかったけど供養)

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(日本語)
[C#]CoreTweetでtwitterBot作ってみた
TwitterのAPI制限 [2018/08/26現在]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】LINEソーシャルログイン時のメールと名前のデコード方法

LINEログイン時、メールアドレスはJWTで帰ってくる。

JWTは、普通のjsonと違ってそのままでは中身が解読できないのでデコードしてやる必要があります。
LINE公式のドキュメントにはPythonしかなかったので、変換の部分だけ載せておきます。

デコードにライブラリ"IdentityModel.Tokens.Jwt"を使用するので、Nugetからインストールしておく必要があります

C#での簡易的なサンプル

payConvert
            //解説1
            Resp res = new Resp();
            res = JsonConvert.DeserializeObject<Resp>(response);
            response.Close();

            //解説2
            var tmp = res.id_token.Split('.');

            ////解説3
            JwtPayload a = JwtPayload.Base64UrlDeserialize(tmp[1]);

            //解説4
            Label6.InnerText = a["name"].ToString();  //名前
            Label7.InnerText = a["email"].ToString();  //メールアドレス
resp
    public class Resp
    {
        public string scope { get; set; }
        public string access_token { get; set; }
        public string token_type { get; set; }
        public string expires_in { get; set; }
        public string refresh_token { get; set; }
        public string id_token { get; set; }

    }

解説

  1. リクエストして帰ってきたJsonを変換してRespインスタンスに格納
  2. respのメンバのid_tokenにjwtが入ってるので、splitでヘッダー・ペイロード・署名の3つを分割
  3. ペイロードをデコードする
  4. キーを指定して値取得

これでメールアドレスと名前が取得できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

System.Data.SqlClient.SqlConnection のnewでハングする

現象

System.Data.SqlClient.SqlConnection のnewでハングする。
同じソースでも別の環境では正常動作する。ソースには問題なし。
環境依存か?

原因

WindowsのPerformance Counterが破損している場合に発生する。
".NET Data Provider for SqlServer"をPerformance Counterが読み込んでいるとハングする。

対策

".NET Data Provider for SqlServer"をPerformance Counterが読み込みを解除する。

unlodctr ".NET Data Provider for SqlServer"

参照元

https://stackoverflow.com/questions/8408236/calling-new-sqlconnection-hangs-program

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Autodesk Inventor API Hacking (BrowserNodeの複数選択)

0. はじめに

DrawingDocumentのBrowserNodeには、難があります。
これらNodeをmouseで選択した時、SelectSetに入るObjectの型はunknownなのです。
突然で「何を言っているんだ・・・」と思うかもしれませんが、DrawingView内の要素(線)をいじろうとすると、すぐにわかります。
手作業ではBrowserNode単位で選択 → 線種変更できるのですが、現状のAPIで自動化しようとすると、全DrawingCurveについて1つずつ処理しないといけないので、ものすごく遅くなります。

「線種変更は手作業で右クリックプロパティーするから、せめて条件に当てはまる項目を自動選択出来れば・・・」というのが、発端です。

1. BrowserNode.DoSelect()

普通に考えれば、

InventorApplication.ActiveDocument.SelectSet.SelectMultiple(collection);

すれば良いだけの話です。でも、このcollectionに入れる要素の型が公開されていないので、この方法では選択できません。

SelectSetに頼らない、もう1つの選択方法があります。それは、DoSelect()です。
BrowserNodesを辿り、目的のBrowserNodeを見つけてnode.DoSelect()すれば、選択できます。
しかし、この方法では、前回選択した内容がクリアされる == 常に最後の1つしか選択されないという欠点があります。

2. Control key

しかし、私は発見しました。keyboard(そう、物理キーボードです)のControl keyを押しっぱなしにしながら、DoSelect()を複数回呼び出すと、全てのObjectが選択されるのです。
これは、DoSelect()がまさにmouseの左クリックをエミュレートしているからです。(というか、実際は逆で、OnMouseClickがDoSelect()を呼んでいるのでしょう)

ここまで来れば、やることは1つです。あたかもControl keyが押されているかのごとく、振舞えば良いのです。

3. GetKeyboardState / SetKeyboardState

SendMessage, PostMessage, WH_KEYBOARD_LLなどと遠回りをしましたが、最終的にはGetKeyboardStateとSetKeyboardStateを使って、Control keyを押している振りをさせることに成功しました。

internal class NativeMethods
{
    [System.Runtime.InteropServices.DllImport("user32.dll")]
    public static extern bool GetKeyboardState(byte[] lpKeyState);
    [System.Runtime.InteropServices.DllImport("user32.dll")]
    public static extern bool SetKeyboardState(byte[] lpKeyState);
}
const int VK_CONTROL = 0x11;

void foo()
{
    byte[] keys = new byte[256];
    byte savedControlKey;

    // Controlキーを押しっぱなしにする
    NativeMethods.GetKeyboardState(keys);
    savedControlKey = keys[VK_CONTROL];
    keys[VK_CONTROL] = 0x80;
    NativeMethods.SetKeyboardState(keys);

    try
    {
        //
        //  ループか何かで、複数回BrowserNode.DoSelect()する
        //
    }
    finally
    {
        // Controlキーを復元する
        keys[VK_CONTROL] = savedControlKey;
        NativeMethods.SetKeyboardState(keys);
    }
}

実行時にユーザーがControl Keyを押している可能性も考慮して、変更前の状態を保管しておいて、作業終了後に元に戻しています。
まずはこの実装で問題ないと思いますが、

  • 処理時間が長い
  • 実行頻度が非常に高い (例えば、キーを1文字入力ごとに実行)

などの場合は、書き戻す前にもう一度GetkeyboardState()した方が良いかもしれません。
その場合、物理Control keyの状態が変化した場合はどうするんだ、と言われるとその通りで、厳密にはGetAsyncKeyState(VK_CONTROL)で状態を確認した方が良いのでしょう。
(GetAsyncKeyState()は、SetKeyboardState()の影響を受けないようです)

また別の問題として、DoSelect()するループ中に、物理Control keyをdown→upされると、状態が解除されてしまいます。

これらの問題を避けるには、DoSelect()の直前/直後で、Control keyを設定/解除すれば良いと思いますが、そこまでするかどうかは皆様の判断にお任せします。

4. DoSelect()を確実に実行するための補足 (19.06.01)

DrawingDocumentを開きたての状態で下層のnodeをDoSelect()しても、選択されないことがあります。遅延バインディングをしているのか、nodeを開かないと選択できないようです。一旦開けば、後は閉じても選択できます。
とりあえず、以下のような回避方法を考えました。

if (node.Parent is BrowserNode parentNode)
{
    // BrowserNodeを開く
    parentNode.Expanded = true;
}
node.DoSelect();

選択した後にnodeが(厳密には親nodeが)開きっぱなしになるのですが、私は自分用AddInなので、別に良しとしています。

parentNode.Expanded = falseしても、親の親は閉じません。
なので、厳密に実行前の状況を保管したいのなら、親を再帰的に辿りながら、nodeがkeyでnode.ExpandedがvalueのDictionaryを作り、最後にforeachで復帰すると良いでしょう。

99. 親の記事に戻る

Autodesk Inventor API Hacking (概略)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity2018へのアップデートでTextMeshProのエラー

発生し得るエラーは複数あります。
まずは公式が推奨する手順でアップデート作業をしましょう。

アップデート手順

ここを参照してアップデート作業をします。

アップデート前の準備

最も重要な注意点としては、アップデートを始める前に全てのSceneを閉じてPrefabやTextMeshProがSceneから読み込まれないようにしておく ことです。

空のSceneを開いて他のSceneを閉じ、プロジェクトを保存しましょう。

そしてプロジェクトのバックアップも忘れずに。

アップデート

自分は 2017.4.17f1 → 2018.4.0f1 とアップデートしました。

TextMeshProが重複してるエラー

Multiple plugins with the same name 'tmpro_plugin' (found at 'Packages/com.unity.textmeshpro/Plugins/64 Bit Plugins/TMPro_Plugin.bundle' and 'Assets/TextMesh Pro/Plugins/64 Bit Plugins/TMPro_Plugin.bundle'). That means one or more plugins are set to be compatible with Editor. Only one plugin at the time can be used by Editor. UnityEditor.EditorApplication:Internal_CallGlobalEventHandler()

2018.4で開くとTextMeshProが競合しているエラーが表示されます。
そこで、古いAsset配下のTextMeshProを削除します。

ここでの注意点は、フォントアセットやSpriteなどの自分で作ったリソースは退避 しておきましょう。

正しくエラーが解消されればそのまま使えます。

TMproが参照されないエラー

本来ならば Windowタブに TextMeshPro > Window - TextMeshPro - Project Files GUID Remapping Tool が表示をされ、このツールを使えば古いTextMeshProの参照が新しいものに入れ替えられるのですが、そもそもWindowタブにTextMeshProがなく、Consoleには次のエラーが表示されました。

The type or namespace name 'tmpro' could not be found

自分はここを見て、エディタをVS Codeに変更したら解消しました。

エディタの切り替えはPreferrences>External Script Editorから。
image.png

原因は完全には分かってませんが、VisualStudioがパッケージを上手く参照できずに発生してたエラーのようです。

改めてTextMeshPro > Window - TextMeshPro - Project Files GUID Remapping Tool を使ってTextMeshProの参照を直します。

その後、自分は再び使うエディタをVisualStudioに戻しました。
エラーは発生しませんでした。

VisualStudioのバージョンが異なったり、MacではなくWindowsだと発生しないか、復旧手順が異なるかもしれません。

自分の環境

MacOS10.14.4
Visual Studio 2019 for Mac
Unity2017.4
→Unity2018.4

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む