package jp.juggler.subwaytooter.api; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import org.json.JSONArray; import org.json.JSONObject; import jp.juggler.subwaytooter.App1; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.R; import jp.juggler.subwaytooter.util.Utils; import jp.juggler.subwaytooter.table.ClientInfo; import okhttp3.Call; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class TootApiClient { private static final LogCategory log = new LogCategory( "TootApiClient" ); private static final OkHttpClient ok_http_client = App1.ok_http_client; public interface Callback { boolean isApiCancelled(); void publishApiProgress( String s ); } private final Context context; public final Callback callback; public TootApiClient( @NonNull Context context, @NonNull Callback callback ){ this.context = context; this.callback = callback; } // インスタンスのホスト名 public String instance; // アカウント追加時に使用する public void setInstance( String instance ){ this.instance = instance; } // アカウントがある場合に使用する public SavedAccount account; public void setAccount( SavedAccount account ){ this.instance = account.host; this.account = account; } public boolean isCancelled(){ return callback.isApiCancelled(); } public static final MediaType MEDIA_TYPE_FORM_URL_ENCODED = MediaType.parse( "application/x-www-form-urlencoded" ); public TootApiResult request( String path ){ return request( path, new Request.Builder() ); } public TootApiResult request( String path, Request.Builder request_builder ){ log.d( "request: %s", path ); TootApiResult result = request_sub( path, request_builder ); if( result != null && result.error != null ){ log.d( "error: %s", result.error ); } return result; } private static final String KEY_AUTH_VERSION = "SubwayTooterAuthVersion"; private static final int AUTH_VERSION = 1; private static final String REDIRECT_URL = "subwaytooter://oauth"; private TootApiResult request_sub( String path, Request.Builder request_builder ){ if( account == null ){ return new TootApiResult( "account is null" ); } JSONObject token_info = account.token_info; // if( token_info != null && token_info.optInt( KEY_AUTH_VERSION, 0 ) < AUTH_VERSION ){ // // このトークンは形式が古くて使えないよ // token_info = null; // } if( callback.isApiCancelled() ) return null; if( token_info == null ){ // アクセストークンの更新が必要 return new TootApiResult( context.getString( R.string.login_required ) ); } // アクセストークンを使ってAPIを呼び出す callback.publishApiProgress( context.getString( R.string.request_api, path ) ); Request request = request_builder .url( "https://" + instance + path ) .header( "Authorization", "Bearer " + Utils.optStringX( token_info, "access_token" ) ) .build(); Call call = ok_http_client.newCall( request ); Response response; try{ response = call.execute(); }catch( Throwable ex ){ return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); } if( callback.isApiCancelled() ) return null; // TODO: アクセストークンが無効な場合はどうなる? if( ! response.isSuccessful() ){ return new TootApiResult( context.getString( R.string.network_error_arg, response ) ); } try{ String json = response.body().string(); if( TextUtils.isEmpty( json ) || json.startsWith( "<" ) ){ return new TootApiResult( context.getString( R.string.response_not_json ) + "\n" + json ); }else if( json.startsWith( "[" ) ){ JSONArray array = new JSONArray( json ); return new TootApiResult( response, token_info, json, array ); }else{ JSONObject object = new JSONObject( json ); String error = Utils.optStringX( object, "error" ); if( ! TextUtils.isEmpty( error ) ){ return new TootApiResult( context.getString( R.string.api_error, error ) ); } return new TootApiResult( response, token_info, json, object ); } }catch( Throwable ex ){ ex.printStackTrace(); return new TootApiResult( Utils.formatError( ex, "API data error" ) ); } } public TootApiResult authorize1(){ JSONObject client_info = null; for( ; ; ){ if( callback.isApiCancelled() ) return null; if( client_info == null ){ // DBからまず探す client_info = ClientInfo.load( instance ); if( client_info != null && client_info.optInt( KEY_AUTH_VERSION, 0 ) < AUTH_VERSION ){ // このトークンは形式が古くて使えないよ client_info = null; } if( client_info != null ) continue; callback.publishApiProgress( context.getString( R.string.register_app_to_server, instance ) ); // OAuth2 クライアント登録 String client_name = "SubwayTooter"; // + UUID.randomUUID().toString(); Request request = new Request.Builder() .url( "https://" + instance + "/api/v1/apps" ) .post( RequestBody.create( MEDIA_TYPE_FORM_URL_ENCODED , "client_name=" + Uri.encode( client_name ) + "&redirect_uris=" + Uri.encode( REDIRECT_URL ) + "&scopes=read write follow" ) ) .build(); Call call = ok_http_client.newCall( request ); Response response; try{ response = call.execute(); }catch( Throwable ex ){ return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); } if( callback.isApiCancelled() ) return null; if( ! response.isSuccessful() ){ return new TootApiResult( context.getString( R.string.network_error_arg, response ) ); } try{ String json = response.body().string(); if( TextUtils.isEmpty( json ) || json.startsWith( "<" ) ){ return new TootApiResult( context.getString( R.string.response_not_json ) + "\n" + json ); } // {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"} client_info = new JSONObject( json ); String error = Utils.optStringX( client_info, "error" ); if( ! TextUtils.isEmpty( error ) ){ return new TootApiResult( context.getString( R.string.api_error, error ) ); } client_info.put( KEY_AUTH_VERSION, AUTH_VERSION ); ClientInfo.save( instance, client_info.toString() ); continue; }catch( Throwable ex ){ ex.printStackTrace(); return new TootApiResult( Utils.formatError( ex, "API data error" ) ); } } // 認証ページURLを作る final String browser_url = "https://" + instance + "/oauth/authorize" + "?client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) ) // この段階では要らない + "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) ) + "&response_type=code" + "&redirect_uri=" + Uri.encode( REDIRECT_URL ) + "&scope=read write follow" + "&scopes=read write follow" + "&state=" + ( account != null ? "db:" + account.db_id : "host:" + instance ) + "&grant_type=authorization_code" // + "&username=" + Uri.encode( user_mail ) // + "&password=" + Uri.encode( password ) + "&approval_prompt=force" // +"&access_type=offline" ; // APIリクエストは失敗?する // URLをエラーとして返す return new TootApiResult( browser_url ); } } public TootApiResult authorize2( String code ){ JSONObject client_info = ClientInfo.load( instance ); if( client_info != null && client_info.optInt( KEY_AUTH_VERSION, 0 ) < AUTH_VERSION ){ client_info = null; } if( client_info == null ){ return new TootApiResult( "missing client id" ); } // コードを使ってトークンを取得する callback.publishApiProgress( context.getString( R.string.request_access_token ) ); JSONObject token_info; String post_content = "grant_type=authorization_code" + "&code=" + Uri.encode( code ) + "&client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) ) + "&redirect_uri=" + Uri.encode( REDIRECT_URL ) + "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) ) + "&scope=read write follow" + "&scopes=read write follow"; Request request = new Request.Builder() .url( "https://" + instance + "/oauth/token" ) .post( RequestBody.create( MEDIA_TYPE_FORM_URL_ENCODED, post_content ) ) .build(); Call call = ok_http_client.newCall( request ); Response response; try{ response = call.execute(); }catch( Throwable ex ){ return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); } if( callback.isApiCancelled() ) return null; if( ! response.isSuccessful() ){ return new TootApiResult( context.getString( R.string.network_error_arg, response ) ); } try{ String json = response.body().string(); // {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641} if( TextUtils.isEmpty( json ) || json.charAt( 0 ) == '<' ){ return new TootApiResult( context.getString( R.string.login_failed ) ); } token_info = new JSONObject( json ); String error = Utils.optStringX( client_info, "error" ); if( ! TextUtils.isEmpty( error ) ){ return new TootApiResult( context.getString( R.string.api_error, error ) ); } token_info.put( KEY_AUTH_VERSION, AUTH_VERSION ); }catch( Throwable ex ){ ex.printStackTrace(); return new TootApiResult( Utils.formatError( ex, "API data error" ) ); } // 認証されたアカウントのユーザ名を取得する String path = "/api/v1/accounts/verify_credentials"; callback.publishApiProgress( context.getString( R.string.request_api, path ) ); request = new Request.Builder() .url( "https://" + instance + path ) .header( "Authorization", "Bearer " + Utils.optStringX( token_info, "access_token" ) ) .build(); call = ok_http_client.newCall( request ); try{ response = call.execute(); }catch( Throwable ex ){ return new TootApiResult( Utils.formatError( ex, context.getResources(), R.string.network_error ) ); } if( callback.isApiCancelled() ) return null; if( ! response.isSuccessful() ){ return new TootApiResult( context.getString( R.string.network_error_arg, response ) ); } try{ String json = response.body().string(); if( TextUtils.isEmpty( json ) || json.startsWith( "<" ) ){ return new TootApiResult( context.getString( R.string.response_not_json ) + "\n" + json ); }else if( json.startsWith( "[" ) ){ JSONArray array = new JSONArray( json ); return new TootApiResult( response, token_info, json, array ); }else{ JSONObject object = new JSONObject( json ); String error = Utils.optStringX( object, "error" ); if( ! TextUtils.isEmpty( error ) ){ return new TootApiResult( context.getString( R.string.api_error, error ) ); } return new TootApiResult( response, token_info, json, object ); } }catch( Throwable ex ){ ex.printStackTrace(); return new TootApiResult( Utils.formatError( ex, "API data error" ) ); } } }