大掃除(twitter)

大掃除シーズンですね。家の大掃除はもちろんですが、今年はネットの大掃除もしてみようかと思い、過去のtweetを削除した。

これがtwitterの制約で結構難しい。難しいはちょっと違うか。やることはAPIたたくだけなんだけどそこにたどり着くまでが長い。だから面倒ってのが妥当かもしれない。

twitterの仕様

Webやアプリからtweetの削除はできる。が、1個ずつしかできない。2009年から使っているのでそれなりの量なのでそれをポチポチしたくない。

そして、自分のプロフィールからたどれるtweetは、2014? 2015?以降の直近3200tweetに限られているらしく、Webやアプリから消すためにたどり着くのは非常に困難。

さらに、APIでユーザIDをキーにして取得できるtweetもプロフィールと同様の3200件に限られる。

アーカイブを申請する

EUでGDPRが施工されてから、自身のアーカイブデータをダウンロードできるようになっている。 自身の設定からアーカイブを申請すると2日後ぐらいにはダウンロードできるようになった。

アーカイブはzipでまとまっててダウンロードするとこんな感じのフォルダ構成になっている。

Folder PATH listing for volume Local Disk
Volume serial number is 70EC-48A8
C:.
│  tree.txt
│  tweets.html
│  Your archive.html
│  
├─assets
│  │...
└─data
    │  account-creation-ip.js
    │  account-label.js
    │  account-suspension.js
    │  account-timezone.js
    │  account.js
    │  ad-engagements.js
    │  ad-free-article-visits.js
    │  ad-impressions.js
    │  ad-mobile-conversions-attributed.js
    │  ad-mobile-conversions-unattributed.js
    │  ad-online-conversions-attributed.js
    │  ad-online-conversions-unattributed.js
    │  ageinfo.js
    │  app.js
    │  block.js
    │  branch-links.js
    │  catalog-item.js
    │  commerce-catalog.js
    │  community-note-rating.js
    │  community-note-tombstone.js
    │  community-note.js
    │  community-tweet.js
    │  connected-application.js
    │  contact.js
    │  deleted-tweet-headers.js
    │  deleted-tweets.js
    │  device-token.js
    │  direct-message-group-headers.js
    │  direct-message-headers.js
    │  direct-message-mute.js
    │  direct-messages-group.js
    │  direct-messages.js
    │  email-address-change.js
    │  follower.js
    │  following.js
    │  ip-audit.js
    │  like.js
    │  lists-created.js
    │  lists-member.js
    │  lists-subscribed.js
    │  manifest.js
    │  moment.js
    │  mute.js
    │  ni-devices.js
    │  periscope-account-information.js
    │  periscope-ban-information.js
    │  periscope-broadcast-metadata.js
    │  periscope-comments-made-by-user.js
    │  periscope-expired-broadcasts.js
    │  periscope-followers.js
    │  periscope-profile-description.js
    │  personalization.js
    │  phone-number.js
    │  product-drop.js
    │  product-set.js
    │  professional-data.js
    │  profile.js
    │  protected-history.js
    │  README.txt
    │  reply-prompt.js
    │  saved-search.js
    │  screen-name-change.js
    │  shop-module.js
    │  shopify-account.js
    │  smartblock.js
    │  spaces-metadata.js
    │  sso.js
    │  tweet-headers.js
    │  tweetdeck.js
    │  tweets.js    ############### 今ままでのツイートデータ #################
    │  twitter-article-metadata.js
    │  twitter-article.js
    │  twitter-circle-member.js
    │  twitter-circle-tweet.js
    │  twitter-circle.js
    │  twitter-shop.js
    │  user-link-clicks.js
    │  verified.js
    │  
 ...

Your archive.htmlをブラウザで開くとWeb版と同様のインターフェースで今までつぶやきが見れる。

tweetデータはdata\tweet.jsに入っているのでここからIDを特定できる。

後は削除APIを呼び出せば削除できる。

APIが使えるようになるまでが長い

twitterのAPIはおいそれと使えない。まずはDeveloper登録が必要で、developer.twitter.comで登録する。この時にAPIの使用用途を説明する必要がある。

登録するがアクティベーションメールがなかなか来ず1週間程度たった後に届いた。そのあとさらに、メールで使用用途を聞かれるので丁寧に対応する必要がある。Devポータルの使用用途は英語が言語指定になっていたのでメールの回答も英語で書いたら差し戻された。メール文面は日本語だったので使用用途も日本語で書いたら通った。

ここでやっとAPIが試せるようになる。今回は自分のツイートが削除できればいいのでここで終わりだが、公開アプリとかになるとさらに審査が必要だ。巷のTwitter連携アプリを作ってる皆々様はすごいなあ。

Powerhellで消してく。

後はAPIを呼ぶだけ。呼ぶだけなんだけど流量制限がある。50回/15分なので18秒に1tweetしか消すことができない。全部消すのに2日間かかった。

$ErrorActionPreference = "Stop"

$lessThan = [DateTimeOffset]::Parse("2017-01-01 00:00:00");
$tweetJs = "path\to\the\archived\data\tweets.js";
$oauthConsumerKey = "{Consumer API Key}";
$oauthConsumerSecret = "{Consumer API Secret}";
$oauthToken = "{Authentication Access Token}";
$oauthTokenSecret = "{Authentication Access Secret}";


$oauthNonce = "CyLqATRO30R";
$oauthSignatureMethod = "HMAC-SHA1";
$oauthTimestamp = [DateTimeOffset]::Now.ToUnixTimeSeconds().ToString();
$oauthVersion = "1.0";


Add-Type -AssemblyName System.Security

function Get-HMAC($Key, $BaseString){

    $KeyByte = [System.Text.Encoding]::UTF8.GetBytes($Key)

    $BaseByte = [System.Text.Encoding]::UTF8.GetBytes($BaseString)

    $HMAC = New-Object System.Security.Cryptography.HMACSHA1

    $HMAC.key = $KeyByte

    $HMACBytes = $HMAC.ComputeHash($BaseByte)

    $HMAC.Dispose()

    $HMACString = [System.Convert]::ToBase64String($HMACBytes)

    return $HMACString
}

function Delete-Tweet($id) {
    $method = "DELETE";
    $ep = "https://api.twitter.com/2/tweets/$id";
    $parameters = [string]::Join('&', @(
        "oauth_consumer_key=$([System.Uri]::EscapeDataString($oauthConsumerKey))";
        "oauth_nonce=$([System.Uri]::EscapeDataString($oauthNonce))";
        "oauth_signature_method=$([System.Uri]::EscapeDataString($oauthSignatureMethod))";
        "oauth_timestamp=$([System.Uri]::EscapeDataString($oauthTimestamp))";
        "oauth_token=$([System.Uri]::EscapeDataString($oauthToken))";
        "oauth_version=$([System.Uri]::EscapeDataString($oauthVersion))";
    ));

    $payload = [string]::Join('&', @(
        [System.Uri]::EscapeDataString($method);
        [System.Uri]::EscapeDataString($ep);
        [System.Uri]::EscapeDataString($parameters);
    ));
    
    $signKey = $oauthConsumerSecret + "&" + $oauthTokenSecret;

    $sign = Get-HMAC -Key $signKey -BaseString $payload 

    # Write-Output $sign

    $signature = "oauth_consumer_key=`"$oauthConsumerKey`", oauth_token=`"$oauthToken`", oauth_signature_method=`"$oauthSignatureMethod`", oauth_timestamp=`"$oauthTimestamp`", oauth_nonce=`"$oauthNonce`", oauth_version=`"$oauthVersion`", oauth_signature=`"$([System.Uri]::EscapeDataString($sign))`""

    Invoke-RestMethod -Method $method -Uri $ep -Headers @{ Authorization = "OAuth $signature" } 
}


function Parse-DateTimeOffset($str) {
    return [DateTimeOffset]::ParseExact($str, "ddd MMM dd HH:mm:ss zzzz yyyy", [CultureInfo]::InvariantCulture);
}

$tweets = (Get-Content $tweetJs -Raw).Replace("window.YTD.tweets.part0 = ", "") | ConvertFrom-Json

$ids = $tweets | ?{ (Parse-DateTimeOffset $_.tweet.created_at) -lt $lessThan} | %{ $_.tweet.id }

Write-Output "削除対象 $(($ids | measure).Count)"

foreach ($id in $ids) {
    Write-Output $id
    Delete-Tweet -id $id
    sleep 20
}

所感

IT系のコミュニティがSlackやDiscordに移るようになって久しいけど、APIの手軽さとか流量制限周りがこれだとそりゃそうなるよなって感じがした。