Table of Contents

はじめに

twikit の C# 版は、Python 版と同じく API キー不要で Twitter / X の内部 API(Web 版が使っている GraphQL と v1.1 のエンドポイント)を呼び出します。ブラウザでログインした状態の Cookie を使って認証します。

動作環境

  • .NET 8 以降
  • 依存パッケージ: AngleSharp(HTML の解析)、Jint(ui_metrics の JavaScript 評価)

インストール

NuGet には公開していません。リポジトリを clone して、プロジェクト参照で使ってください。

git clone https://github.com/amanetoki7/twikit-dotnet.git
<ItemGroup>
  <ProjectReference Include="path/to/twikit-dotnet/src/Twikit/Twikit.csproj" />
</ItemGroup>

パッケージとして配布したい場合は dotnet pack src/Twikit/Twikit.csproj -c Release で .nupkg を作れます。

Important

パスワードではなく Cookie でログインしてください。 X は Client.LoginAsync() が使うオンボーディングフローを廃止しており、code 366, "flow name LoginFlow is currently not accessible" を返します。 パスワードログインはライブラリ側では直せません(LoginRetiredException が送出されます)。

  1. ブラウザで x.com にログインします。
  2. 開発者ツール(Application / Storage → Cookies)から auth_token と ct0 の値をコピーします。この 2 つだけで十分です。
using Twikit;

var client = new Client("ja");
client.SetCookies(new Dictionary<string, string>
{
    ["auth_token"] = "...",
    ["ct0"] = "...",
});
// または: client.LoadCookies("cookies.json");

if (!await client.IsLoggedInAsync())
    throw new Exception("Cookie が無効です。ブラウザから取り直してください。");

// 次回以降のために保存しておく
client.SaveCookies("cookies.json");

SetCookies には、ブラウザの拡張機能や Playwright が書き出す形式(name と value を持つオブジェクトの配列)も JsonNode として渡せます。

基本的な使い方

すべてのメソッドは非同期(Task / Task<T>)で、名前は ...Async で終わります。

// キーワードで最新のツイートを検索する
var tweets = await client.SearchTweetAsync("python", "Latest");
foreach (var tweet in tweets)
    Console.WriteLine($"{tweet.User?.Name}: {tweet.Text} ({tweet.CreatedAt})");

// 続きを取得する
var more = await tweets.NextAsync();

// ユーザーを取得してツイートを読む
var user = await client.GetUserByScreenNameAsync("example_user");
var userTweets = await user.GetTweetsAsync("Tweets");

// メディア付きツイートを投稿する
var mediaIds = new List<string>
{
    await client.UploadMediaAsync("media1.jpg"),
    await client.UploadMediaAsync("media2.jpg"),
};
await client.CreateTweetAsync("Example Tweet", mediaIds);

// DM を送る
await client.SendDmAsync("123456789", "Hello");

// トレンド
var trends = await client.GetTrendsAsync("trending");

ハイライト(Premium)

プロフィールの「ハイライト」タブは Premium 機能です。X はタイムラインやツイート取得にハイライト済みかどうかを 含めないため、Tweet.Highlighted は GetUserHighlightsTweetsAsync() で取得したツイートか、 IsHighlightedAsync() / HighlightAsync() / UnhighlightAsync() を呼んだあと以外は null(不明)です。

var tweet = await client.GetTweetByIdAsync("2103725182292820029");
Console.WriteLine(tweet.Highlighted);                 // null: タイムライン系の応答にはフラグが無い
Console.WriteLine(await tweet.IsHighlightedAsync());  // X に問い合わせる。tweet.Highlighted も更新される
await tweet.HighlightAsync();                         // 自分のツイートのみ。他人のものは TwitterException
await tweet.UnhighlightAsync();

var highlights = await client.GetUserHighlightsTweetsAsync(userId);   // 全件 Highlighted == true

ピン留め

プロフィールのピンは 1 件だけで、別のツイートをピン留めすると置き換わります。ピン留めできるのは自分のツイートだけで、 他人のツイートは NotFoundException になります。ピン留めしていないツイートの解除は何もせず成功します。 レート制限はヘッダーの値ではなく pin / unpin それぞれ 100 回なので、レート制限 の脚注も参照してください。

await client.PinTweetAsync("2103725182292820029");    // v1.1 account/pin_tweet.json(フォームの `id`)
await client.UnpinTweetAsync("2103725182292820029");

var tweet = await client.GetTweetByIdAsync("2103725182292820029");
await tweet.PinAsync();
await tweet.UnpinAsync();

Grok で翻訳する

「ポストを翻訳」ボタンと同じ api.x.com/2/grok/translation.json を呼びます。(ツイート, 言語) の組を初めて要求したときは X が翻訳をトークンごとにストリームし(0.5 秒ほどで完了)、以後は X のキャッシュから全文が 1 回で返ります (TweetTranslation.Cached)。dstLang を省略するとクライアントの言語の主タグ(ja-JP なら ja)になります。 X は言語コードを検証しないので、未知のコードでも Grok が推測した言語で返ってきます。レート制限は 15 分あたり 100 回です。

var translation = await client.TranslateTweetAsync("2114550616811094253", "ja");
Console.WriteLine(translation.Text);      // 私は完全にあなたの虜です。

var tweet = await client.GetTweetByIdAsync("2114547734862803416");
var english = await tweet.TranslateAsync("en");

クリエイター収益化の参加条件(Premium)

クリエイタースタジオの「オリジナルコンテンツ報酬」「サブスクリプション」ページにある参加条件の一覧と、 ログイン中のアカウントがどこまで満たしているかを取得します。X はこのページを GraphQL ではなく server-driven UI(/i/jfapi/...、text/plain の生バイナリ)で返すので、このライブラリがデコードして CreatorEligibility に組み立てます。

var rewards = await client.GetOriginalContentRewardsEligibilityAsync();
Console.WriteLine(rewards.Summary);        // 「…登録するには、次の条件を満たす必要があります:」
foreach (var r in rewards.Requirements)
    Console.WriteLine($"{(r.IsMet ? "OK" : "--")} {r.Text} {r.Value} {r.Progress:P0}");
// OK Premium、Premium+、またはPremium Businessに登録
// OK 18歳以上
// -- 認証済みフォロワーが500人以上いる 136 25%
// -- 過去90日間でホームタイムラインの認証済みインプレッションが50万回以上 26.9K 5%
Console.WriteLine(rewards.IsEligible);     // 全条件を満たしていれば true

var subscriptions = await client.GetCreatorSubscriptionsEligibilityAsync();
foreach (var r in subscriptions.Unmet) Console.WriteLine(r.Text);

Value と Progress は数値条件の行だけに入ります(X は進捗バーを 20 分割で丸めるので Progress は目安です)。 行に付く注記(「返信は含まれません」など)は Notes、ヘルプセンターのリンクは HelpUrl です。 他の Jetfuel ページは client.Jetfuel.GetAsync("creators/studio") で生の応答(ApiResponse.Bytes にバイナリ)を取得できます。

下書き

「未送信ポスト」→「下書き」の一覧・保存・削除・投稿です。保存するときは投稿時と同じ post_tweet_request(本文、メディア ID、 引用元 URL、返信先)を送ります。X に「下書きを投稿する」API は無く、Web クライアントは保存内容から通常の CreateTweet を 送ったあと DeleteDraftTweet を呼ぶので、PostDraftTweetAsync も同じ 2 リクエストを送ります。

var id = await client.CreateDraftTweetAsync("あとで書く", attachmentUrl: "https://x.com/user/status/1");
foreach (var d in await client.GetDraftTweetsAsync())       // 新しい順
    Console.WriteLine($"{d.Id} {d.Text} media={d.MediaIds.Count} quote={d.AttachmentUrl} reply={d.InReplyToTweetId}");
var tweet = await client.PostDraftTweetAsync(id);            // 投稿して下書きを削除(DraftTweet.PostAsync() も同じ)
await client.DeleteDraftTweetAsync(id);                      // まとめて消すなら DeleteDraftTweetsAsync(ids)

スレッド(thread_tweets を持つ下書き)の投稿は Web クライアントでの送り方が未確認のため、PostDraftTweetAsync は例外にします。

コミュニティノート

投稿に提案されたノートの一覧(表示前のものを含む)、協力者プロフィール、自分が書いたノート、x.com/i/communitynotes の 3 タブ、 ノートの評価です。評価の効果 RatingImpact と執筆の効果 WritingImpact は Web クライアントと同じ式 (successful.total − unsuccessful.helpful_count − 2 × unsuccessful.not_helpful_count、currently_rated_helpful − currently_rated_not_helpful)で計算します。

var me = await client.GetCommunityNotesProfileAsync();                 // 自分(alias を渡すと他人の公開統計)
Console.WriteLine($"{me.Alias} 評価の効果={me.RatingImpact} 執筆の効果={me.WritingImpact} 執筆解放まで={me.SuccessfulRatingNeededToEarnIn}");
var mine = await client.GetContributorCommunityNotesAsync();            // 自分が書いたノート(includeDrafts: true で下書きも)

var posts = await client.GetCommunityNotesTimelineAsync(CommunityNotesTimelineTab.NeedsYourHelp);   // New / RatedHelpful も同じ
foreach (var post in posts)
{
    Console.WriteLine($"{post.Id} {post.CommunityNote?.Text}");          // X がその投稿に対して選んだノート
    foreach (var note in await post.GetCommunityNotesAsync())             // 投稿の全ノート(評価できる形)
        if (note.RatingStatus == "NeedsMoreRatings")
            await note.RateAsync(CommunityNoteHelpfulness.Helpful, helpfulTags: new[] { CommunityNoteHelpfulTags.GoodSources });
}

await client.RateCommunityNoteAsync(noteId, tweetId, CommunityNoteHelpfulness.NotHelpful,
    notHelpfulTags: new[] { CommunityNoteNotHelpfulTags.NoSources, CommunityNoteNotHelpfulTags.OpinionSpeculation });

理由タグは Helpful / SomewhatHelpful のとき helpfulTags、NotHelpful / SomewhatHelpful のとき notHelpfulTags を Web のフォームが尋ねます。 不明なレベルやタグは ArgumentException です。needs_your_help タブは約 15 件の固定ピックで、3 タブともカーソル無しで終端します。

ページ送り(Result<T>)

一覧を返すメソッドは Result<T> を返します。IReadOnlyList<T> なので foreach やインデックスで読め、NextAsync() / PreviousAsync() で前後のページを取得できます。

Warning

NextCursor を自分でメソッドに渡し直さないでください。X は多くのエンドポイントで count を無視して 要求より多く返してくるため、余剰分は Result<T> の内部に蓄えられ、NextAsync() から順に返されます。 NextCursor はその余剰分を飛び越えた位置を指しているので、カーソルを渡して呼び直すと蓄えた分を読み飛ばします。

var page = await client.GetUserFollowersAsync(userId, 20);
while (page.Count > 0)          // カーソルではなく中身で終了判定する
{
    foreach (var follower in page) Console.WriteLine(follower.ScreenName);
    page = await page.NextAsync();
}

生のレスポンスを読む

各モデルは X から受け取った JSON を Data プロパティ(JsonObject)として保持しています。 モデルに無いフィールドは tweet.Data["legacy"]?["some_field"] のように直接読めます。 JsonExtensions には Str / Int / Bool / Sub / FindDict といった 「無いキーは null」で読むためのヘルパーがあります。

レート制限を見張る

直近のレスポンスに含まれる残りリクエスト数とリセット時刻は client.RateLimitRemaining と client.RateLimitReset で確認できます。詳しくは レート制限 を参照してください。

ハンドシェイクのやり直し

すべてのリクエストは X-Client-Transaction-Id ヘッダーを必要とし、そのキーは X が数日ごとに入れ替える webpack バンドルから取得します。長時間動かすプロセスで散発的な 404 が出始めたら client.RefreshTransaction() を呼んでハンドシェイクをやり直してください。SetCookies を呼んだときは自動でやり直されます。

プロキシと独自の HttpMessageHandler

var client = new Client("ja", proxy: "http://user:pass@127.0.0.1:8080");   // socks5:// も可

Python 版の impersonate=(curl_cffi によるブラウザ TLS フィンガープリントの偽装)に相当する仕組みは .NET 標準にはありません。必要であれば、コンストラクターの handler 引数に独自の HttpMessageHandler を渡してください。 Cookie とリダイレクトはライブラリ側で処理するので、ハンドラーの UseCookies と AllowAutoRedirect は無効になります (HttpClientHandler / SocketsHttpHandler なら自動で無効化します)。

サンプル

examples/Twikit.Examples に、 Python 版 twikit の examples/*.py に対応するサンプルがあります。

AUTH_TOKEN=... CT0=... dotnet run --project examples/Twikit.Examples -- basic