/*
SlackManager.java
Copyright (c) 2016 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.slackmessagehook.slack;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import com.codebutler.android_websockets.WebSocketClient;
import org.deviceconnect.android.deviceplugin.slackmessagehook.BuildConfig;
import org.deviceconnect.message.DConnectMessage;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
/**
* Slackの制御を管理するクラス.
*/
public class SlackManager {
//---------------------------------------------------------------------------------------
//region Declaration
/** シングルトンなManagerのインスタンス */
public static final SlackManager INSTANCE = new SlackManager();
/** TAG. */
private final static String TAG = "SlackManager";
/** SlackAPIベースURL */
private final static String BASE_URL = "https://slack.com/api/";
/** 接続状態(未接続) */
private static final int CONNECT_STATE_NONE = 0;
/** 接続状態(切断) */
private static final int CONNECT_STATE_DISCONNECTED = 1;
/** 接続状態(切断中) */
private static final int CONNECT_STATE_DISCONNECTING = 2;
/** 接続状態(接続中) */
private static final int CONNECT_STATE_CONNECTING = 3;
/** 接続状態(接続) */
private static final int CONNECT_STATE_CONNECTED = 4;
/** 接続状態 */
private int connectState = CONNECT_STATE_NONE; // 0:None 1:Disconnected 2:Disconnecting 3:Connecting 4:Connected
/** WebSocketのKeepAlive時間 */
private static final int KEEPALIVE_SPAN = 5000;
/** SlackManagerの基底Exception */
public abstract class SlackManagerException extends Exception {}
/** APITokenが不正 */
public class SlackAPITokenValueException extends SlackManagerException {}
/** 接続エラー */
public class SlackConnectionException extends SlackManagerException {}
/** Slack認証エラー */
public class SlackAuthException extends SlackManagerException {}
/** 未知のエラー */
public class SlackUnknownException extends SlackManagerException {}
/** WebSocket */
private WebSocketClient webSocket;
/** SlackBotのApiToken */
private String token = null;
/** 処理完了コールバック */
public interface FinishCallback<Result> {
/**
* 処理が完了した時に呼ばれます.
* @param result 結果
* @param error エラー
*/
void onFinish(Result result, Exception error);
}
/** 接続処理完了コールバック */
private FinishCallback<Void> connectionFinishCallback;
/** メッセージ送信処理完了コールバック */
private FinishCallback<String> sendMsgFinishCallback;
/** Slackイベントリスナー */
public interface SlackEventListener {
/**
* 接続イベント
*/
void OnConnect();
/**
* 切断イベント
*/
void OnConnectLost();
/**
* メッセージを受信したイベント.
* @param info メッセージ
*/
void OnReceiveSlackMessage(HistoryInfo info);
}
/** Slackイベントリスナー */
private ArrayList<SlackEventListener> slackEventListeners = new ArrayList<>();
/** Botの情報 */
public class BotInfo {
public String id;
public String name;
public String teamName;
public String teamDomain;
@Override
public String toString() {
return "BotInfo = {id: "+ id + ", name:" + name + ", teamName:" + teamName + "}";
}
}
/** Botの情報 */
private BotInfo botInfo = new BotInfo();
//endregion
//---------------------------------------------------------------------------------------
//region Init
/**
* 初期化。シングルトンのためにprivate.
*/
private SlackManager() {
}
/** Botの情報を取得 */
public BotInfo getBotInfo() {
return botInfo;
}
/**
* イベントリスナーを設定します.
* @param listener リスナー
*/
public void addSlackEventListener(SlackEventListener listener) {
slackEventListeners.add(listener);
}
/**
* イベントリスナーを解除します.
* @param listener リスナー
*/
public void removeSlackEventListener(SlackEventListener listener) {
slackEventListeners.remove(listener);
}
/**
* SlackBotのAPITokenを設定.
* @param apiToken APIToken
*/
public void setApiToken(final String apiToken, boolean needsConnect, final FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*setApiToken");
if (apiToken == null) {
if (callback != null) {
callback.onFinish(null, new SlackAPITokenValueException());
}
return;
}
// 不正文字列を入力できないようにencodeしておく
String newToken = null;
try {
newToken = URLEncoder.encode(apiToken,"utf-8");
} catch (UnsupportedEncodingException e) {
if (callback != null) {
callback.onFinish(null, new SlackAPITokenValueException());
}
return;
}
// トークンが変わらない場合は何もしない
if (newToken.equals(token)) {
if (callback != null) {
callback.onFinish(null, null);
}
return;
}
token = newToken;
// 接続
if (needsConnect) {
if (connectState > CONNECT_STATE_DISCONNECTING) {
// すでに接続中なら切断後に再接続
disconnect(new FinishCallback<Void>() {
@Override
public void onFinish(Void v,Exception error) {
connect(new FinishCallback<Void>() {
@Override
public void onFinish(Void aVoid, Exception error) {
if (callback != null) {
callback.onFinish(null, error);
}
}
});
}
});
} else {
connect(callback);
}
} else {
if (callback != null) {
callback.onFinish(null, null);
}
}
}
//endregion
//---------------------------------------------------------------------------------------
//region Connection
/**
* 接続中かを返します.
* @return 接続中ならtrue
*/
public boolean isConnected() {
return connectState == CONNECT_STATE_CONNECTED;
}
/**
* 切断中かを返します.
* @return 切断中ならtrue
*/
public boolean isDisconnecting() {
return (connectState > CONNECT_STATE_DISCONNECTING);
}
/**
* 接続.
*/
public void connect() {
connect(null);
}
/**
* 接続.
* @param callback 接続完了コールバック
*/
public void connect(FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*connect");
if (token == null) {
if (callback != null) {
callback.onFinish(null, new SlackAPITokenValueException());
}
return;
}
// 接続済み
if (connectState > CONNECT_STATE_DISCONNECTING) {
if (callback != null) {
callback.onFinish(null, null);
}
return;
}
connectState = CONNECT_STATE_CONNECTING;
if (callback != null) {
connectionFinishCallback = callback;
}
// 接続処理
new GetTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam("rtm.start", "&simple_latest=True&no_unreads=True") {
@Override
public void callBack(JSONObject json) {
// rtm.startに失敗
if (json == null) {
connectState = CONNECT_STATE_DISCONNECTED;
callConnectionFinishCallback(new SlackConnectionException(), null);
return;
}
// WebSocketに接続
if (BuildConfig.DEBUG) Log.d(TAG, json.toString());
// 接続
String jsonUrl;
try {
if (json.has("error")) {
connectState = CONNECT_STATE_DISCONNECTED;
String err = json.getString("error");
if (err.equals("invalid_auth") || err.equals("not_authed")) {
// Slack認証エラー
callConnectionFinishCallback(new SlackAuthException(), null);
} else {
// Slackサーバーエラー
callConnectionFinishCallback(new SlackConnectionException(), null);
}
return;
}
jsonUrl = json.getString("url");
if (BuildConfig.DEBUG) Log.d(TAG, "url:"+jsonUrl);
JSONObject selfJson = json.getJSONObject("self");
botInfo.id = selfJson.getString("id");
botInfo.name = selfJson.getString("name");
JSONObject teamJson = json.getJSONObject("team");
botInfo.teamName = teamJson.getString("name");
botInfo.teamDomain = teamJson.getString("domain");
if (BuildConfig.DEBUG) Log.d(TAG, "bot:" + botInfo.toString());
connectWebSocket(URI.create(jsonUrl));
} catch (JSONException e) {
Log.e(TAG, "error", e);
connectState = CONNECT_STATE_DISCONNECTED;
callConnectionFinishCallback(e, null);
}
}
});
}
/**
* 切断.
*/
@SuppressWarnings("unused")
public void disconnect() {
disconnect(null);
}
/**
* 切断.
* @param callback 切断完了コールバック
*/
public void disconnect(FinishCallback<Void> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*disconnect");
if (webSocket == null || connectState < CONNECT_STATE_CONNECTING) {
if (callback != null) {
callback.onFinish(null, null);
}
return;
}
connectState = CONNECT_STATE_DISCONNECTING;
connectionFinishCallback = callback;
webSocket.disconnect();
webSocket = null;
}
//endregion
//---------------------------------------------------------------------------------------
//region SendMessage
/**
* Slackにメッセージ送信.
* @param msg メッセージ
* @param channel チャンネル
* @param callback 完了コールバック
*/
public void sendMessage(String msg, String channel, final FinishCallback<String> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*sendMessage:" + msg);
if (connectState != CONNECT_STATE_CONNECTED) {
callSendMsgFinishCallback(null, new SlackConnectionException(), null);
return;
}
if (msg == null || channel == null) {
callSendMsgFinishCallback(null, new InvalidParameterException(), null);
return;
}
msg = escape(msg);
channel = escape(channel);
sendMsgFinishCallback = callback;
String data = "{\"type\": \"message\", \"channel\": \"" + channel + "\", \"text\": \"" + msg + "\"}";
webSocket.send(data);
}
/**
* Slackにメッセージ送信.
* @param msg メッセージ
* @param channel チャンネル
*/
public void sendMessage(String msg, String channel) {
sendMessage(msg, channel, null);
}
//endregion
//---------------------------------------------------------------------------------------
//region etc.
/**
* 文字列をエスケープ処理する
* @param str 文字列
* @return 処理後の文字列
*/
private String escape(String str) {
String ret = str.replace("\\", "\\\\");
return ret.replace("\"", "\\\"");
}
/**
* 接続完了コールバックを呼ぶ
* @param e 例外
*/
private void callConnectionFinishCallback(final Exception e, Handler handler) {
if (connectState == CONNECT_STATE_DISCONNECTED && handler != null) {
for (final SlackEventListener listener: slackEventListeners){
handler.post(new Runnable() {
public void run() {
listener.OnConnectLost();
}
});
}
}
if (connectionFinishCallback != null) {
final FinishCallback callback = connectionFinishCallback;
connectionFinishCallback = null;
if (handler == null) {
callback.onFinish(null, e);
} else {
handler.post(new Runnable() {
public void run() {
callback.onFinish(null, e);
}
});
}
}
}
/**
* 送信完了コールバックを呼ぶ
* @param text 送信text
* @param e 例外
*/
private void callSendMsgFinishCallback(String text, final Exception e, Handler handler) {
if (sendMsgFinishCallback != null) {
final FinishCallback callback = sendMsgFinishCallback;
sendMsgFinishCallback = null;
if (handler == null) {
callback.onFinish(null, e);
} else {
handler.post(new Runnable() {
public void run() {
callback.onFinish(null, e);
}
});
}
}
}
//endregion
//---------------------------------------------------------------------------------------
//region WebSocket
/**
* KeepAlive処理
* @param handler Handler
*/
private void keepAlive(final Handler handler) {
// keepalive
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (connectState == CONNECT_STATE_CONNECTED) {
webSocket.ping("ping");
handler.postDelayed(this, KEEPALIVE_SPAN);
}
}
}, KEEPALIVE_SPAN);
}
/**
* WebSocket接続
* @param uri 接続先
*/
private void connectWebSocket(final URI uri) {
final Handler handler = new Handler();
if (webSocket != null) {
webSocket.disconnect();
}
webSocket = new WebSocketClient(uri, new WebSocketClient.Listener() {
@Override
public void onConnect() {
if (BuildConfig.DEBUG) Log.d(TAG, "Connected!");
connectState = CONNECT_STATE_CONNECTED;
callConnectionFinishCallback(null, handler);
for (final SlackEventListener listener: slackEventListeners){
handler.post(new Runnable() {
public void run() {
listener.OnConnect();
}
});
}
// KeepAlive
keepAlive(handler);
}
@Override
public void onMessage(String message) {
if (BuildConfig.DEBUG) Log.d(TAG, String.format("Got string message! %s", message));
try {
JSONObject json = new JSONObject(message);
if (json.has("type")) {
String type = json.getString("type");
switch (type) {
// メッセージ受信時
case "message":
if (!json.has("subtype")) {
// subtypeが無いものが純粋なメッセージ
final HistoryInfo info = new HistoryInfo();
info.text = json.getString("text");
info.channel = json.getString("channel");
info.user = json.getString("user");
info.ts = json.getDouble("ts");
for (final SlackEventListener listener: slackEventListeners){
handler.post(new Runnable() {
public void run() {
listener.OnReceiveSlackMessage(info);
}
});
}
} else if (json.getString("subtype").equals("file_share")) {
// ファイルアップロード
final HistoryInfo info = new HistoryInfo();
info.channel = json.getString("channel");
info.user = json.getString("user");
info.ts = json.getDouble("ts");
if(json.has("file")) {
JSONObject file = json.getJSONObject("file");
info.mimetype = file.getString("mimetype");
info.file = file.getString("url_private");
if(file.has("initial_comment")) {
JSONObject comment = file.getJSONObject("initial_comment");
info.text = comment.getString("comment");
}
if (file.has("thumb_360")) {
info.thumb = file.getString("thumb_360");
}
if (file.has("thumb_360_h")) {
info.thumbHeight = file.getInt("thumb_360_h");
}
if (file.has("thumb_360_w")) {
info.thumbWidth = file.getInt("thumb_360_w");
}
}
for (final SlackEventListener listener: slackEventListeners){
handler.post(new Runnable() {
public void run() {
listener.OnReceiveSlackMessage(info);
}
});
}
}
break;
// エラー時
case "error":
callSendMsgFinishCallback(null, new SlackUnknownException(), handler);
break;
default:
}
}
if (json.has("ok")) {
if (json.getBoolean("ok")) {
callSendMsgFinishCallback(json.getString("text"), null, handler);
} else {
callSendMsgFinishCallback(null, new SlackUnknownException(), handler);
}
}
} catch (JSONException e) {
Log.e(TAG, e.getMessage());
}
}
@Override
public void onMessage(byte[] data) {
}
@Override
public void onDisconnect(int code, String reason) {
if (BuildConfig.DEBUG) Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason));
if (connectState == CONNECT_STATE_CONNECTED) {
// 接続中に急に切れたら再接続を試みる
connectState = CONNECT_STATE_CONNECTING;
retry(0, handler, new FinishCallback<Boolean>() {
@Override
public void onFinish(Boolean flg, Exception error) {
if (!flg) {
// 切断処理
connectState = CONNECT_STATE_DISCONNECTED;
callConnectionFinishCallback(null, handler);
}
}
});
} else {
// 切断処理
connectState = CONNECT_STATE_DISCONNECTED;
callConnectionFinishCallback(null, handler);
}
}
@Override
public void onError(Exception error) {
Log.e(TAG, "Error!", error);
if (connectState == CONNECT_STATE_CONNECTED) {
// 接続中に急に切れたら再接続を試みる
retry(0, handler, new FinishCallback<Boolean>() {
@Override
public void onFinish(Boolean flg, Exception error) {
if (!flg) {
// 切断処理
connectState = CONNECT_STATE_DISCONNECTED;
callConnectionFinishCallback(null, handler);
}
}
});
} else {
callConnectionFinishCallback(error, handler);
callSendMsgFinishCallback(null, error, handler);
}
}
}, null);
webSocket.connect();
}
/**
* 再接続処理
* @param handler Handler
* @return 再接続した場合はtrue
*/
private boolean retry(int retryCount, final Handler handler, final FinishCallback<Boolean> callback) {
int delay = 0;
switch (retryCount++) {
case 0:
delay = 1000; // 1s
break;
case 1:
delay = 5000; // 5s
break;
case 2:
delay = 30000; // 30s
break;
case 3:
delay = 60000; // 1m
break;
case 4:
delay = 3 * 60000; // 3m
break;
default:
// callback
callback.onFinish(false, null);
return false;
}
if (BuildConfig.DEBUG) Log.d(TAG, "retry:" + delay);
final int _retryCount = retryCount;
handler.postDelayed(new Runnable() {
public void run() {
// 再接続処理
new GetTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam("rtm.start", "&simple_latest=True&no_unreads=True") {
@Override
public void callBack(JSONObject json) {
// rtm.startに失敗
if (json == null) {
retry(_retryCount, handler, callback);
return;
}
// WebSocketに接続
if (BuildConfig.DEBUG) Log.d(TAG, json.toString());
// 接続
String jsonUrl;
try {
if (json.has("error")) {
retry(_retryCount, handler, callback);
return;
}
jsonUrl = json.getString("url");
if (BuildConfig.DEBUG) Log.d(TAG, "url:"+jsonUrl);
JSONObject selfJson = json.getJSONObject("self");
botInfo.id = selfJson.getString("id");
botInfo.name = selfJson.getString("name");
JSONObject teamJson = json.getJSONObject("team");
botInfo.teamName = teamJson.getString("name");
botInfo.teamDomain = teamJson.getString("domain");
if (BuildConfig.DEBUG) Log.d(TAG, "bot:" + botInfo.toString());
connectWebSocket(URI.create(jsonUrl));
// callback
callback.onFinish(true, null);
} catch (JSONException e) {
Log.e(TAG, "error", e);
retry(_retryCount, handler, callback);
}
}
});
}
}, delay);
return true;
}
//endregion
//---------------------------------------------------------------------------------------
//region History
/**
* Channelの履歴を取得
* @param callback 取得コールバック
*/
public void getHistory(String channel, Double latest, final FinishCallback<ArrayList<HistoryInfo>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*getHistory:" + channel);
String params = "&channel=" + channel;
if (latest != null) {
params += "&latest=" + String.format(Locale.ENGLISH, "%.2f", latest);
}
if (channel.startsWith("D")) {
getHistoryData("im.history", params, callback);
} else {
getHistoryData("channels.history", params, callback);
}
}
/**
* 受け渡し情報
*/
public static class HistoryInfo {
public String channel;
public String user;
public Double ts;
public String text;
public String name;
public String icon;
public String file;
public String mimetype;
public String thumb;
public int thumbHeight = 0;
public int thumbWidth = 0;
}
/**
* 履歴取得
* @param target ターゲットAPI
* @param params パラメータ
* @param callback 取得コールバック
*/
private void getHistoryData(String target, String params, final FinishCallback<ArrayList<HistoryInfo>> callback) {
if (connectState != CONNECT_STATE_CONNECTED) {
if (callback != null) {
callback.onFinish(null, new SlackConnectionException());
}
return;
}
new GetTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam(target, params) {
@Override
public void callBack(JSONObject json) {
if (BuildConfig.DEBUG) Log.d(TAG, json.toString());
try {
if (json.has("error")) {
connectState = CONNECT_STATE_DISCONNECTED;
String err = json.getString("error");
if (err.equals("invalid_auth") || err.equals("not_authed")) {
// Slack認証エラー
callConnectionFinishCallback(new SlackAuthException(), null);
} else {
// Slackサーバーエラー
callConnectionFinishCallback(new SlackConnectionException(), null);
}
return;
}
JSONArray jsonArray = json.getJSONArray("messages");
int length = jsonArray.length();
ArrayList<HistoryInfo> array = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
JSONObject obj = (JSONObject) jsonArray.get(i);
HistoryInfo info = new HistoryInfo();
String type = obj.getString("type");
if (BuildConfig.DEBUG) Log.d(TAG, "obj:" + obj.toString());
if (!"message".equals(type)) {
continue;
}
if (obj.has("user")) {
info.user = obj.getString("user");
}
if (obj.has("text")) {
info.text = obj.getString("text");
}
if (obj.has("ts")) {
info.ts = obj.getDouble("ts");
}
if (obj.has("file")) {
JSONObject file = obj.getJSONObject("file");
if (file.has("thumb_360")) {
info.thumb = file.getString("thumb_360");
}
if (file.has("thumb_360_h")) {
info.thumbHeight = file.getInt("thumb_360_h");
}
if (file.has("thumb_360_w")) {
info.thumbWidth = file.getInt("thumb_360_w");
}
if (file.has("initial_comment")) {
JSONObject comment = file.getJSONObject("initial_comment");
if (comment.has("comment")) {
info.text = comment.getString("comment");
}
} else {
if (info.thumb != null) {
info.text = null;
}
}
}
array.add(info);
}
if (callback != null) {
callback.onFinish(array, null);
}
} catch (JSONException e) {
Log.e(TAG, "error", e);
if (callback != null) {
callback.onFinish(null, e);
}
}
}
});
}
//endregion
//---------------------------------------------------------------------------------------
//region Channel/User/IM List
/**
* 受け渡し情報
*/
public class ListInfo {
public String id;
public String name;
public String icon;
}
/**
* Channel一覧とDM一覧を合成したものを取得
* @param callback 取得コールバック
*/
public void getAllChannelList(final FinishCallback<List<ListInfo>> callback) {
getAllChannelList(callback, null);
}
/**
* Channel一覧とDM一覧を合成したものを取得
* @param callback 取得コールバック
* @param handler Callbackを返すスレッド
*/
public void getAllChannelList(final FinishCallback<List<ListInfo>> callback, final Handler handler) {
new Thread() {
@Override
public void run() {
final CountDownLatch latch = new CountDownLatch(3);
final HashMap<String, ArrayList<ListInfo>> resMap = new HashMap<>();
final Exception[] err = new Exception[1];
// Channelリスト取得
getChannelList(new FinishCallback<ArrayList<ListInfo>>() {
@Override
public void onFinish(ArrayList<ListInfo> listInfos, Exception error) {
if (error == null) {
resMap.put("channel", listInfos);
} else {
err[0] = error;
Log.e("slack", "err", error);
}
latch.countDown();
}
});
// IMリスト取得
getIMList(new FinishCallback<ArrayList<ListInfo>>() {
@Override
public void onFinish(ArrayList<ListInfo> listInfos, Exception error) {
if (error == null) {
resMap.put("im", listInfos);
} else {
err[0] = error;
Log.e("slack", "err", error);
}
latch.countDown();
}
});
// ユーザーリスト取得
getUserList(new FinishCallback<ArrayList<ListInfo>>() {
@Override
public void onFinish(ArrayList<ListInfo> listInfos, Exception error) {
if (error == null) {
resMap.put("user", listInfos);
} else {
err[0] = error;
Log.e("slack", "err", error);
}
latch.countDown();
}
});
// 処理終了を待つ
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 各情報を詰め替え
ArrayList<ListInfo> channels = resMap.get("channel");
ArrayList<ListInfo> ims = resMap.get("im");
ArrayList<ListInfo> users = resMap.get("user");
if (channels != null && ims != null && users != null) {
final List<ListInfo> resList = new ArrayList<>();
// Channel
resList.addAll(channels);
// UserをHashMapへ
HashMap<String, ListInfo> userMap = new HashMap<>();
for (ListInfo info : users) {
userMap.put(info.id, info);
}
// IM
for (ListInfo info : ims) {
// UserIDからUserNameを取得
ListInfo user = userMap.get(info.name);
if (user != null) {
info.name = user.name;
info.icon = user.icon;
}
resList.add(info);
}
if (callback != null) {
if (handler != null) {
handler.post(new Runnable() {
public void run() {
callback.onFinish(resList, null);
}
});
} else {
callback.onFinish(resList, null);
}
}
} else {
if (callback != null) {
if (handler != null) {
handler.post(new Runnable() {
public void run() {
callback.onFinish(null, err[0]);
}
});
} else {
callback.onFinish(null, err[0]);
}
}
}
}
}.start();
}
/**
* Channel一覧を取得
* @param callback 取得コールバック
*/
public void getChannelList(final FinishCallback<ArrayList<ListInfo>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*getChannelList");
getList("channels.list", "&exclude_archived=1", "channels", callback);
}
/**
* IM一覧を取得
* @param callback 取得コールバック
*/
public void getIMList(final FinishCallback<ArrayList<ListInfo>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*getIMList");
getList("im.list", "", "ims",callback);
}
/**
* ユーザー一覧を取得
* @param callback 取得コールバック
*/
public void getUserList(final FinishCallback<ArrayList<ListInfo>> callback) {
if (BuildConfig.DEBUG) Log.d(TAG, "*getUserList");
getList("users.list", "", "members", callback);
}
/**
* 一覧取得ベース
* @param target ターゲットAPI
* @param params パラメータ
* @param listname リスト名
* @param callback 取得コールバック
*/
private void getList(String target, String params, final String listname, final FinishCallback<ArrayList<ListInfo>> callback) {
if (connectState != CONNECT_STATE_CONNECTED) {
if (callback != null) {
callback.onFinish(null, new SlackConnectionException());
}
return;
}
new GetTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam(target, params) {
@Override
public void callBack(JSONObject json) {
if (BuildConfig.DEBUG && json != null) Log.d(TAG, json.toString());
if (json == null) {
if (callback != null) {
// Slackサーバーエラー
callback.onFinish(null, new SlackConnectionException());
}
return;
}
try {
if (json.has("error") && callback != null) {
connectState = CONNECT_STATE_DISCONNECTED;
String err = json.getString("error");
if (err.equals("invalid_auth") || err.equals("not_authed")) {
// Slack認証エラー
callback.onFinish(null, new SlackAuthException());
} else {
// Slackサーバーエラー
callback.onFinish(null, new SlackConnectionException());
}
return;
}
JSONArray jsonArray = json.getJSONArray(listname);
int length = jsonArray.length();
ArrayList<ListInfo> array = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
JSONObject obj = (JSONObject) jsonArray.get(i);
ListInfo info = new ListInfo();
if (obj.has("id")) {
info.id = obj.getString("id");
}
if (obj.has("name")) {
info.name = obj.getString("name");
}
if (obj.has("user")) {
info.name = obj.getString("user");
}
/* RealNameいる?
if (obj.has("real_name")) {
String realName = obj.getString("real_name");
if (!realName.isEmpty()) {
info.name = realName;
}
}
*/
if (obj.has("profile")) {
JSONObject prof = obj.getJSONObject("profile");
info.icon = prof.getString("image_192");
}
array.add(info);
}
if (callback != null) {
callback.onFinish(array, null);
}
} catch (JSONException e) {
Log.e(TAG, "error", e);
if (callback != null) {
callback.onFinish(null, e);
}
}
}
});
}
//endregion
//---------------------------------------------------------------------------------------
//region GetTask
/**
* Get用Task用パラメータ
*/
private class TaskParam {
protected String target;
protected String params;
protected URL resource;
protected String origin;
public void callBack(JSONObject json) {}
public TaskParam(String target, String params) {
this.target = target;
this.params = params;
}
public TaskParam(String target, String params, URL resource, String origin) {
this.target = target;
this.params = params;
this.resource = resource;
this.origin = origin;
}
}
/**
* Get用Task
*/
private class GetTask extends AsyncTask<TaskParam, Void, JSONObject> {
/** パラメータ */
TaskParam param = null;
/**
* Background処理.
*
* @param params Params
*/
@Override
protected JSONObject doInBackground(TaskParam... params) {
HttpURLConnection con = null;
URL url;
InputStream stream = null;
JSONObject json = null;
param = params[0];
try {
// 接続
url = new URL(BASE_URL + param.target + "?token=" + token + param.params);
con = (HttpURLConnection)url.openConnection();
con.setRequestMethod("GET");
con.setInstanceFollowRedirects(false);
con.connect();
stream = con.getInputStream();
if (BuildConfig.DEBUG) Log.d(TAG, "url:"+url.toString());
// レスポンス取得
BufferedReader streamReader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null)
responseStrBuilder.append(inputStr);
// Json
json = new JSONObject(responseStrBuilder.toString());
} catch (IOException | JSONException e) {
Log.e(TAG, "error", e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.e(TAG, "stream close error", e);
}
}
if (con != null) {
con.disconnect();
}
}
return json;
}
@Override
protected void onPostExecute(JSONObject json) {
param.callBack(json);
}
}
//endregion
//---------------------------------------------------------------------------------------
//region FileUpload
/**
* Slackにファイルをアップロード.
* @param msg コメント
* @param channel チャンネル
* @param url リソースURL
* @param callback 終了コールバック
*/
public void uploadFile(String msg, String channel, URL url, String orign, final FinishCallback<JSONObject> callback) {
if (url == null || channel == null) {
if (callback != null) {
callback.onFinish(null, new InvalidParameterException());
}
return;
}
new UploadFileTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new TaskParam(channel, msg, url, orign) {
@Override
public void callBack(JSONObject json) {
if (callback != null) {
SlackManagerException exception = null;
if (json == null) {
exception = new SlackConnectionException();
} else {
try {
if (!json.getBoolean("ok")) {
exception = new SlackConnectionException();
}
} catch (JSONException e) {
exception = new SlackConnectionException();
}
}
callback.onFinish(json, exception);
}
}
});
}
/**
* Slackにファイルをアップロード.
* @param msg コメント
* @param channel チャンネル
* @param url リソースURL
*/
public void uploadFile(String msg, String channel, URL url, String orign) {
uploadFile(msg, channel, url, orign, null);
}
/**
* ファイルアップロード用Task
*/
private class UploadFileTask extends AsyncTask<TaskParam, Void, JSONObject> {
/** パラメータ */
TaskParam param = null;
/**
* Background処理.
*
* @param params Params
*/
@Override
protected JSONObject doInBackground(TaskParam... params) {
HttpURLConnection con1 = null;
HttpURLConnection con2 = null;
BufferedInputStream stream1 = null;
DataOutputStream stream2 = null;
BufferedReader streamReader = null;
JSONObject json = null;
param = params[0];
try {
// リソース取得
URL url = param.resource;
con1 = (HttpURLConnection)url.openConnection();
con1.setInstanceFollowRedirects(true);
con1.setRequestProperty(DConnectMessage.HEADER_GOTAPI_ORIGIN, param.origin);
con1.connect();
stream1 = new BufferedInputStream(con1.getInputStream());
// Slack接続
url = new URL(BASE_URL + "files.upload");
con2 = (HttpURLConnection)url.openConnection();
con2.setRequestMethod("POST");
con2.setInstanceFollowRedirects(false);
con2.setDoOutput(true);
con2.setRequestProperty("Accept-Charset", "UTF-8");
con2.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
BufferedOutputStream bo = new BufferedOutputStream(con2.getOutputStream());
stream2 = new DataOutputStream(bo);
// パラメータ設定
addDisposition(stream2, "token", token);
addDisposition(stream2, "channels", param.target);
if (param.params != null) {
addDisposition(stream2, "initial_comment", param.params);
}
addData(stream2, stream1, "file", param.resource.getFile());
addEnd(stream2);
stream2.flush();
// 接続
con2.connect();
// レスポンス取得
streamReader = new BufferedReader(new InputStreamReader(con2.getInputStream(), "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null)
responseStrBuilder.append(inputStr);
// Json
json = new JSONObject(responseStrBuilder.toString());
if (BuildConfig.DEBUG) Log.d(TAG, responseStrBuilder.toString());
} catch (IOException | JSONException e) {
Log.e(TAG, "error", e);
} finally {
if (streamReader != null) {
try {
streamReader.close();
} catch (IOException e) {
Log.e(TAG, "stream close error", e);
}
}
if (stream2 != null) {
try {
stream2.close();
} catch (IOException e) {
Log.e(TAG, "stream close error", e);
}
}
if (stream1 != null) {
try {
stream1.close();
} catch (IOException e) {
Log.e(TAG, "stream close error", e);
}
}
if (con2 != null) {
con2.disconnect();
}
if (con1 != null) {
con1.disconnect();
}
}
return json;
}
@Override
protected void onPostExecute(JSONObject json) {
param.callBack(json);
}
/** 区切り文字 */
private static final String BOUNDARY = "==================================";
/**
* パラメータ追加
* @param os OutputStream
* @param name 名前
* @param value 値
* @throws IOException 例外
*/
private void addDisposition(DataOutputStream os, String name, String value) throws IOException {
name = escape(name);
os.writeBytes("--" + BOUNDARY + "\r\n");
os.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
byte[] bytes = value.getBytes();
for (int i=0;i<bytes.length;i++){
os.writeByte(bytes[i]);
}
os.writeBytes("\r\n");
}
/**
* バイナリデータを追加
* @param os OutputStream
* @param is InputStream
* @param name 名前
* @param filename ファイル名
* @throws IOException 例外
*/
private void addData(DataOutputStream os, InputStream is, String name, String filename) throws IOException {
filename = escape(filename);
os.writeBytes("--" + BOUNDARY + "\r\n");
os.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n");
os.writeBytes("Content-Type: application/octet-stream\r\n\r\n");
// リソースを書き出し
int c;
while ((c = is.read()) >= 0) {
os.write(c);
}
os.writeBytes("\n");
}
/**
* 終了文字を追加
* @param os OutputStream
* @throws IOException 例外
*/
private void addEnd(DataOutputStream os) throws IOException {
os.writeBytes("--" + BOUNDARY + "--\r\n");
}
}
//endregion
//---------------------------------------------------------------------------------------
}