Twitterでコードをつぶやくと実行してくれるアプリを作った

#はじめに

本記事はクソアプリ2 Advent Calendar 12日目の記事です。
皆さんのクオリティの高いクソアプリを見て「来る場所間違えたかも…」と思っている所存です。
卒論の現実逃避としてササっと作ったものなので不備はありそうですが、温かい目で見てもらえると嬉しいです。


#概要
Twitterを見ていてふとコードが思いつくこと、ありませんか？
そんなときに、わざわざターミナルやテキストエディタを開いて…ってなると少し面倒かと思います。まじてや外出中に「この挙動を調べたいな」と思っても、街中でPCを開くことはおろかPC持ってないよ…となってしまうこともしばしば。

今回はそんな人のために「Twitterでコードをつぶやくと実行結果が返ってくるアプリ（？）」を作りました。

#完成形

TwitCodeRunnerと名付けました。
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240765/1c9319f0-74e3-cef8-9710-44580015ba1d.gif)

実行結果を自分のアカウントから呟かせたい場合はTwitterDeveloperの登録が必要となります。あと、Rubyが必要です。
詳しい仕様はREADMEを参照してください。
[Github](https://github.com/fyhcu/TwitCodeRunner)

タイムラインの取得開始は`ruby tcr-main.rb`でできます。TwitterのUserStreaming機能を用いているので、実行してからはプログラムを閉じない限りはリアルタイムで使えます。

また、たまに[僕のTwitterアカウント](https://twitter.com/_fuyok)でも運用しています。TwitterDeveloperに登録してない人は僕や他の人がプログラムを動かしているときに便乗しましょう。正直使い勝手が悪いですね…。（時間がなくてみんなが使えるようなシステムを組めませんでした、ごめんなさい！）

#おおまかな仕様
使用言語はRubyです。ツイートの読み込みや実行結果のつぶやくのはおなじみTwitterAPIを使用してます。肝心のコードの実行は[Paiza.IO](https://paiza.io/ja)のAPIを用いました。これは、ブラウザ上でコードを書いてすぐ実行できるサービスで、プログラミング初級者や環境構築前にサクッとプログラミングしたいと言う方なんかは触ったことがあるのではないでしょうか。

Paiza.IOは無料でAPIを公開しています。[APIを試せるページ](http://api.paiza.io/docs/swagger/#!/runners/Runners_get_status)があり、とても使いやすかったです。

対応言語もPaiza.IOに準じています。

- Bash
- C
- C#
- C++
- Clojure
- COBOL
- CoffeeScript
- D
- Elixir
- Erlang
- F#
- Go
- Haskell
- Java
- JavaScript
- Kotlin
- MySQL
- Perl
- PHP
- Python2
- Python3
- R
- Ruby
- Rust
- Scala
- Scheme
- Swift
- VB

Objective-Cだけは、Paiza.IO自体は対応してますが自分のアプリでは対応していません。

#何がクソなのか
多分「Twitterからプログラミングができる」という響きはめちゃくちゃ有用そうに聞こえます。（有用かどうかは個人の判断に任せます）開発を始める前も「天才か？」と自画自賛していました。ただ、いざできたものを見返すといくつか思うところがあります。

##有用性
個人の判断に任せると言いましたが、よくよく考えると本当に使い道なくね？と思ってしまいます。完成形のGIFはPCからTwitterを動かしているのですが、一応スマートフォンからもできることはテスト済みですので、Twitterから離れることなくプログラミングができます。ここがアピールポイントです。

##テストが間に合ってない
これはアプリ自体の仕様というよりは僕自身の問題なのですが、単純に28言語分のテストをしている時間や費やす労力がないです。なのでこれを利用しようとしてもどこかでエラーを起こすかもしれないです。Githubにコードを置いておくので、拡張したり利便性を求めてさらに何かしてみようという方はご自由にどうぞ。自己責任で！（そのまま使う分には許諾などいりませんが、機能拡張や多くの人に向けてのサービス化などがもしもありましたら一言くれると『使ってくれる人いるんだ…！』と思って嬉しくなります。）

##そもそも二番煎じ
paiza_run というTwitterからコードを実行できるサービスがありました。名前の通り、公式です。ただ、2019年11月現在このアカウントは凍結中でした。どうやらpaiza_runは[Quineでの無限再帰にハメられた挙句おもちゃにされた過去](https://togetter.com/li/1098185)があるようです。

##実行が遅い
外部APIを用いているためです。

#ここがすごい
では、paiza_runにはない新規性を出すとしたらどこか。

Twitterの1つの投稿に対しての文字数制限は140字で、これ以上長いコードは実行できなかったようです。（僕がpaiza_runを知ったのはこのアプリを開発し始めた後なので正確な機能はわかりません><）

しかし、TwitCodeRunnerは、自分に対してリプライする機能を用いて**文字数制限が無くなりました**。これについては後述します。
![ezgif.com-video-to-gif-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240765/e82db8ed-3ae3-326e-0523-113bda8a6274.gif)

#詳細な仕様

ここからは興味があるorマサカリを構えた人向けです。Rubyは全て独学で勉強しているので、しっかりできる人からしたら変なことをしているコードがあるかもしれません。「こうした方がいいよ！」などアドバイスを頂けたらと嬉しいです。（~~ただ、いじめるのはやめてください~~）

##実行の流れ

まず、TwitCodeRunnerは、`twitcoderunner (適当な文字列).(拡張子)`というツイートがトリガーとなって作動します。これが実行コマンドだと思ってください。拡張子の部分は、実行したい言語のものを入れてください（例えばRubyならrb、Clojureならcljなど）

`twitcoderunner (適当な文字列).(拡張子)`と入っているツイートを正規表現によって見つけ出すと、そのツイートのIDと**コードを書いた人のユーザーネーム**を取得します。取得はTwitterAPIの`statuses/filter`を用いてUserStreamでリアルタイムで頑張ってます。`statuses/filter`の仕様上、取得できるのは公開アカウントのツイートのみになります。基本一人で使用することを前提としての処理なのでこうしてますが、コードを書いた人と実行コマンドを呟いた人が違う場合はコードを書いた人に実行結果が届くので注意してください。しかも僕から直々にです。

実行コマンドの入ったツイートのIDを受け取ったあと、ソースコードを取得・再構築して`runner`という関数で動かします。

```ruby:main.rb

require_relative '../Privatekey/oauth_twitter'
require_relative 'runner'
require_relative 'apifunc'
require_relative 'sourcejoin'
require 'json'
require 'net/http'

consumer = OAuth::Consumer.new(
  @client.consumer_key,
  @client.consumer_secret,
)
endpoint = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)

filter_option = {
  track: "twitcoderunner"
 }

reg1 = /\Atwitcoderunner (.*)\.(.*)\Z/

@client_s.filter(filter_option) do |object|
  if reg1 =~ object.text
    lang = uselang($2)
    responce = endpoint.get("https://api.twitter.com/1.1/statuses/show/#{object.id.to_s}.json")
    result = JSON.parse(responce.body)
    finid = result["id_str"]
    name = result["in_reply_to_screen_name"]
    sourcetext = sourcejoinfirst(finid)
    source = charrefconversion(sourcetext)
    runner(source, lang, name)
  end
  puts "---------------------------------------------"
end
```

```ruby:runner.rb
require_relative '../Privatekey/oauth_twitter'
require_relative 'apifunc'
require 'json'
require 'net/http'

def runner(source,language,screen_name)
  if source == "error[NO TEXT]"
    puts "実行できるファイルがありません"
  elsif language == "none"
    puts "拡張子に対応した言語がありません"
  else
    uri = URI.parse('http://api.paiza.io/runners/create')
    param = {
      language: language,
      source_code: source,
      api_key: :guest
    }
    #------------------create code------------------
    responce = Net::HTTP.post_form(uri, param)
    create_result = JSON.parse(responce.body)
    cid = create_result["id"]
    sleep(2)

    #------------------show details------------------
    uri2 = URI.parse("http://api.paiza.io/runners/get_details?id=#{cid}&api_key=guest")
    responce = Net::HTTP.get_response(uri2)
    details_result = JSON.parse(responce.body)
    puts JSON.pretty_generate(details_result)
    if details_result["result"] == "success"
      text = "@#{screen_name}\n" + details_result["stdout"]
    elsif details_result["result"] == "failure"
      text = "@#{screen_name}\n" + details_result["stderr"]
    elsif details_result["result"].nil?
      text = "@#{screen_name}\n" + details_result["build_stderr"]
    else
      text = "@#{screen_name}\n" + "何らかのエラーが発生しました。"
    end
    if text.length > 140
      text = text[0...138] + "..."
    end
    @client.update text
  end
end

```
##ソースコードの取得・連結・再構築

実行したいコードを構成する関数は2つあります。

- `sourcejoinfirst`
- `sourcejoin`

言葉だけで説明するのがちょっと難しいので図とコードを載せます。簡単に言うと、「１つのツイートで完結するようなコード」は`sourcejoinfirst`だけでテキストを取得し、「何個のツイートにも及ぶコード」は`sourcejoinfirst`と`sourcejoin`を使って再帰でテキストを連結してコードを構成すると言う仕組みです。

![Untitled Diagram.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240765/7e69062e-2900-7096-2ba1-1d42fcf239e1.jpeg)

```ruby:sourcejoin.rb
require_relative '../Privatekey/oauth_twitter'
require_relative 'apifunc'
require 'json'
require 'oauth'

def sourcejoinfirst(tweet_id)
  info = show(tweet_id)
  if info["in_reply_to_status_id_str"].class == String
    final_info = show(info["in_reply_to_status_id_str"])
    source2 = final_info["full_text"]
    if final_info["in_reply_to_status_id_str"].class == String
      sourcejoin(source2, final_info["id_str"])
    else
      return source2
    end
  else
    return "error[NO TEXT]"
  end
end

def sourcejoin(text, id_str)
  source = text
  current_id = id_str
  current_info = show(current_id)

  if current_info["in_reply_to_status_id_str"].class == String
    before_id = current_info["in_reply_to_status_id_str"]
    before_info = show(before_id)
    source = before_info["full_text"] + "\n" + source
    if before_info["in_reply_to_status_id_str"].class == String
      sourcejoin(source, before_id)
    else
      return source
    end
  end
end
```

##その他やったこと
他にも、

- Twitterから取得したテキストは`&`,`<`,`>`が文字実体参照として表示されるのでそれを直すメソッド
- TwitterAPIのstatuses/show(id)を頻繁に使うのでちょっと簡単にしたメソッド

などを作りました。（大したものではないので省略）

#課題

今のところ見つけた不具合や直さなきゃいけないところです。テストはRubyでしかやってないのでまだまだ課題点は沢山あると思いますが…。

- インスタンス変数を使うときが不便。Rubyは、インスタンス変数は@から始めるのがルールですが、Twitterだと誰かに対するリプライになってしまう場合があります。それだけならまだいいのですが（よくはない）そのまま自分にリプライしてコードを書き続けていると、取得した後のテキストの先頭にいちいちユーザーIDが入ってしまいます。いわゆる「巻き込みリプ」がそのまま残ってしまう現象です。ツイートを行う際にメンションを外すことで回避できるはずなのですが、ちょっと面倒かなって思います。

- Twitterの自動URL短縮機能によって`(適当な文字列).(拡張子)`や`hoge.new`が勝手にリンクになってしまう。[これを直す方法は存在しない](https://help.twitter.com/ja/using-twitter/url-shortener)ため、頭を抱えているところです。~~Twitterこそクソアプリなのかもしれない~~ 少し確認したところ拡張子だけでも`.py`や`.java`をつけると文字が青くなり自動的にリンクになってしまいます。指定する拡張子ならこのアプリ独自のものを使用すればいいのですが（→そうしました。GithubのREADMEを参照）、コード内で青くなるような表現を探し出していちいち処理を書くのは大変そう…。コードを書くときは、文字が青くならないようにしましょうね、と言うしかないようです。

- もしこのプログラムを他の人が実行していたとして、正規表現で実行コマンドを拾っていたらいろんな人からリプライが来るのでは問題。（他の人に使われると言うことを考えていなかった…）

#最後に
まだまだ未熟な技術力ですが、やっぱり自分でこれ作りたい！って思ってモノ作るのって楽しいですね。

色んなことを知りたいので、気軽にコメントなどしていただけると嬉しいです！

こんなものも作ってます↓
[【TwitterAPI】「贅沢な名だねえ。今からお前の名前は○○だ。」【Ruby】](https://qiita.com/fyhcu/items/2a6c8b6612f3306e9040)

[自分のツイートを感情分析して機嫌を管理する](https://qiita.com/fyhcu/items/695fb341b4a47f58124d)

#参考にしたもの
- [paiza.IOのAPIを使って競プロっぽいアプリを作ってみた](https://qiita.com/M1YamA00/items/4d96f0df609ad246314f)
- [paiza.IO の API を使ってみた - ブログズミ](https://srz-zumix.blogspot.com/2015/01/paizaio-api.html)
- [POST statuses/filter — Twitter Developers](https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter)