Java
jersey
mastodon

JavaでMastodonのTootとStreamingAPIを試した

More than 1 year has passed since last update.


やってみたこと


  • MastodonもTwitterみたいに自分のコードからTootしたりできるらしい

  • PythonとかRubyとかはライブラリがあるらしいけどJavaのが見当たらない

  • やっつけでやってみたらTootと連合TLのStreamingできた


コード

JerseyなるRESTなAPIをいじるのに楽なライブラリがあるらしいのでそれを使用。

正直手探りで適当にくっつけ合わせたので詳細はよくわかっていないままいじっています。

接続instanceは mstdn.jp 、予めclient id/secretと自分用accesstokenをMastodon.pyにお世話になって用意しておいています。

(認証用コード書くの面倒だったので…いつか試します)

Streaming接続やToot自体はaccesstokenさえあればできるようです。

import java.awt.Container;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

import org.glassfish.jersey.media.sse.EventInput;
import org.glassfish.jersey.media.sse.InboundEvent;
import org.glassfish.jersey.media.sse.SseFeature;

public class MainWindow extends JFrame{
private static final String APP_NAME = "TestMastodonClient";
private static final int APP_WIDTH = 640;
private static final int APP_HEIGHT = 480;

// とりあえずmstdn.jpでやりました、他のinstanceでやるときは適宜変更
private static final String HOST_MASTODON = "https://mstdn.jp";

// 事前にclient id/secret と accesstoken を取得しておく(自分はMastodon.pyでやりました)
private static final String MASTODON_ACCESSTOKEN_TOKEN = "ここに自分で取得したAccessTokenを入れる";

JComboBox<String> visibilityComboBox; // 投稿の公開範囲
JTextField tootField;
JButton tootButton;

Client client;

public MainWindow() {
setTitle(APP_NAME);
setSize(APP_WIDTH, APP_HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// TLSv1.2 を使わないとStreamingに接続できなかったので変更
System.out.println("HTTPS:" + System.getProperty("https.protocols"));
System.setProperty("https.protocols", "TLSv1.2");
System.out.println("HTTPS:" + System.getProperty("https.protocols"));

client = ClientBuilder.newBuilder().register(SseFeature.class).build();

Container container = getContentPane();
JPanel parentPanel = new JPanel();
container.add(parentPanel);

visibilityComboBox = new JComboBox<String>();
visibilityComboBox.addItem("public"); //公開(連合TLに出てくる)
visibilityComboBox.addItem("unlisted");//公開(連合TLには出てこない)
visibilityComboBox.addItem("private"); //非公開(自分とフォロワしか見られない)
visibilityComboBox.addItem("direct"); //わからん(TwitterのDM的な?)
parentPanel.add(visibilityComboBox);

tootField = new JTextField(10); //やっつけToot文字列入力用
parentPanel.add(tootField);

tootButton = new JButton("Toot");
tootButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
String msg = tootField.getText();
System.out.println("Toot: " + msg);

// AccessTokenを仕込んだheaderを作るっぽい
MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
headers.putSingle("Authorization", "Bearer " + MASTODON_ACCESSTOKEN_TOKEN);

// statusに本文、visibilityに投稿の公開範囲を仕込む
Entity<Form> entity = Entity.entity(new Form().param("status", msg)
.param("visibility",
(String) visibilityComboBox.getSelectedItem()),
MediaType.APPLICATION_FORM_URLENCODED_TYPE);

// 投げる
String result = client.target(HOST_MASTODON)
.path("/api/v1/statuses")
.request()
.headers(headers)
.post(entity, String.class);

System.out.println("----------実行結果----------");
System.out.println(result);

//tootField.setText(""); //Toot後に入力欄を空にしたかったらやっておく
}
});
parentPanel.add(tootButton);

// こんなんでいいのかわからないけどとりあえず別スレッドでStreaming拾い
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("----------Streaming Thread----------");

// AccessTokenを仕込む
MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
headers.putSingle("Authorization", "Bearer " + MASTODON_ACCESSTOKEN_TOKEN);
headers.putSingle("Content-Type", "application/json; charset=UTF-8");

WebTarget target = client.target(HOST_MASTODON);

// 連合TLに接続
EventInput eventInput = target.path("/api/v1/streaming/public")
.request()
.headers(headers)
.get(EventInput.class);

// ひたすら受信されつづけるのをコンソールに流すだけ
// JSONまんまな文字列が出て来るのであとでパースしなきゃいけない
while (!eventInput.isClosed()) {
final InboundEvent inboundEvent = eventInput.read();
if (inboundEvent == null) {
// connection has been closed
System.out.println("----------End of Streaming Thread----------");
break;
}
System.out.println("----------Response----------");
System.out.println(inboundEvent.getName() + "; ");
System.out.println(inboundEvent.readData(String.class));
}
}
}).start();
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new MainWindow().setVisible(true);
}
});
}
}

連合TLを受信するとものすごい勢いで流れていくのでHomeTLとかで試した方が確認とかしやすいかもしれません。

追記:上記コード最後らへんのThread内でwhileしてStreamingのJSON拾い続けてるところ、15.5.2. Asynchronous SSE processing with EventSourceにあるようにonEventとして定義できるようなのでそっちの方がよさそうです

いろいろ探してやってみた結果、EventSourceはリクエストヘッダ付きの接続がそのまんまではできないっぽいので上記Whileからイベント発生させるコード書くか、EventSourceにどうにかしてAccessTokenが付いたヘッダを載せるコードを書くかで対応することになりそうです