Browse Source
Allow sending image in dms
Allow sending image in dms
This commit streamlines the broadcast flow to handle both text and image messages in dms.legacy
Ammar Githam
4 years ago
6 changed files with 553 additions and 125 deletions
-
121app/src/main/java/awais/instagrabber/activities/directmessages/DirectMessageThread.java
-
167app/src/main/java/awais/instagrabber/asyncs/ImageUploader.java
-
84app/src/main/java/awais/instagrabber/asyncs/direct_messages/CommentAction.java
-
207app/src/main/java/awais/instagrabber/asyncs/direct_messages/DirectThreadBroadcaster.java
-
76app/src/main/java/awais/instagrabber/models/ImageUploadOptions.java
-
23app/src/main/java/awais/instagrabber/utils/Utils.java
@ -0,0 +1,167 @@ |
|||||
|
package awais.instagrabber.asyncs; |
||||
|
|
||||
|
import android.os.AsyncTask; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import org.json.JSONObject; |
||||
|
|
||||
|
import java.io.BufferedReader; |
||||
|
import java.io.IOException; |
||||
|
import java.io.InputStream; |
||||
|
import java.io.InputStreamReader; |
||||
|
import java.io.OutputStream; |
||||
|
import java.net.HttpURLConnection; |
||||
|
import java.net.URL; |
||||
|
import java.util.Date; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.Random; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import awais.instagrabber.models.ImageUploadOptions; |
||||
|
import awais.instagrabber.utils.Utils; |
||||
|
|
||||
|
public class ImageUploader extends AsyncTask<ImageUploadOptions, Void, ImageUploader.ImageUploadResponse> { |
||||
|
private static final String TAG = "ImageUploader"; |
||||
|
private static final long LOWER = 1000000000L; |
||||
|
private static final long UPPER = 9999999999L; |
||||
|
private OnImageUploadCompleteListener listener; |
||||
|
|
||||
|
protected ImageUploadResponse doInBackground(final ImageUploadOptions... imageUploadOptions) { |
||||
|
if (imageUploadOptions == null || imageUploadOptions.length == 0 || imageUploadOptions[0] == null) { |
||||
|
return null; |
||||
|
} |
||||
|
HttpURLConnection connection = null; |
||||
|
OutputStream out = null; |
||||
|
InputStream inputStream = null; |
||||
|
BufferedReader r = null; |
||||
|
try { |
||||
|
final ImageUploadOptions options = imageUploadOptions[0]; |
||||
|
final Map<String, String> headers = new HashMap<>(); |
||||
|
final String uploadId = String.valueOf(new Date().getTime()); |
||||
|
final long random = LOWER + new Random().nextLong() * (UPPER - LOWER + 1); |
||||
|
final String name = String.format("%s_0_%s", uploadId, random); |
||||
|
final String contentLength = String.valueOf(options.getContentLength()); |
||||
|
final String waterfallId = options.getWaterfallId() != null ? options.getWaterfallId() : UUID.randomUUID().toString(); |
||||
|
headers.put("X-Entity-Type", "image/jpeg"); |
||||
|
headers.put("Offset", "0"); |
||||
|
headers.put("X_FB_PHOTO_WATERFALL_ID", waterfallId); |
||||
|
headers.put("X-Instagram-Rupload-Params", new JSONObject(createPhotoRuploadParams(options, uploadId)).toString()); |
||||
|
headers.put("X-Entity-Name", name); |
||||
|
headers.put("X-Entity-Length", contentLength); |
||||
|
headers.put("Content-Type", "application/octet-stream"); |
||||
|
headers.put("Content-Length", contentLength); |
||||
|
headers.put("Accept-Encoding", "gzip"); |
||||
|
final String url = "https://www.instagram.com/rupload_igphoto/" + name + "/"; |
||||
|
connection = (HttpURLConnection) new URL(url).openConnection(); |
||||
|
connection.setRequestMethod("POST"); |
||||
|
connection.setUseCaches(false); |
||||
|
connection.setDoOutput(true); |
||||
|
Utils.setConnectionHeaders(connection, headers); |
||||
|
out = connection.getOutputStream(); |
||||
|
byte[] buffer = new byte[1024]; |
||||
|
int n; |
||||
|
inputStream = options.getInputStream(); |
||||
|
while (-1 != (n = inputStream.read(buffer))) { |
||||
|
out.write(buffer, 0, n); |
||||
|
} |
||||
|
out.flush(); |
||||
|
final int responseCode = connection.getResponseCode(); |
||||
|
Log.d(TAG, "response: " + responseCode); |
||||
|
if (responseCode != HttpURLConnection.HTTP_OK) { |
||||
|
return new ImageUploadResponse(responseCode, null); |
||||
|
} |
||||
|
r = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
||||
|
final StringBuilder builder = new StringBuilder(); |
||||
|
for (String line = r.readLine(); line != null; line = r.readLine()) { |
||||
|
if (builder.length() != 0) { |
||||
|
builder.append("\n"); |
||||
|
} |
||||
|
builder.append(line); |
||||
|
} |
||||
|
return new ImageUploadResponse(responseCode, new JSONObject(builder.toString())); |
||||
|
} catch (Exception ex) { |
||||
|
Log.e(TAG, "Image upload error:", ex); |
||||
|
} finally { |
||||
|
if (r != null) { |
||||
|
try { |
||||
|
r.close(); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
if (inputStream != null) { |
||||
|
try { |
||||
|
inputStream.close(); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
if (out != null) { |
||||
|
try { |
||||
|
out.close(); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
if (connection != null) { |
||||
|
connection.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onPostExecute(final ImageUploadResponse response) { |
||||
|
if (listener != null) { |
||||
|
listener.onImageUploadComplete(response); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Map<String, String> createPhotoRuploadParams(final ImageUploadOptions options, final String uploadId) { |
||||
|
final Map<String, Integer> retryContext = new HashMap<>(); |
||||
|
retryContext.put("num_step_auto_retry", 0); |
||||
|
retryContext.put("num_reupload", 0); |
||||
|
retryContext.put("num_step_manual_retry", 0); |
||||
|
final String retryContextString = new JSONObject(retryContext).toString(); |
||||
|
final Map<String, String> params = new HashMap<>(); |
||||
|
params.put("retry_context", retryContextString); |
||||
|
params.put("media_type", "1"); |
||||
|
params.put("upload_id", uploadId); |
||||
|
params.put("xsharing_user_ids", "[]"); |
||||
|
final Map<String, String> imageCompression = new HashMap<>(); |
||||
|
imageCompression.put("lib_name", "moz"); |
||||
|
imageCompression.put("lib_version", "3.1.m"); |
||||
|
imageCompression.put("quality", "80"); |
||||
|
params.put("image_compression", new JSONObject(imageCompression).toString()); |
||||
|
if (options.isSidecar()) { |
||||
|
params.put("is_sidecar", "1"); |
||||
|
} |
||||
|
return params; |
||||
|
} |
||||
|
|
||||
|
public void setOnTaskCompleteListener(final OnImageUploadCompleteListener listener) { |
||||
|
if (listener != null) { |
||||
|
this.listener = listener; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public interface OnImageUploadCompleteListener { |
||||
|
void onImageUploadComplete(ImageUploadResponse response); |
||||
|
} |
||||
|
|
||||
|
public static class ImageUploadResponse { |
||||
|
private int responseCode; |
||||
|
private JSONObject response; |
||||
|
|
||||
|
public ImageUploadResponse(int responseCode, JSONObject response) { |
||||
|
this.responseCode = responseCode; |
||||
|
this.response = response; |
||||
|
} |
||||
|
|
||||
|
public int getResponseCode() { |
||||
|
return responseCode; |
||||
|
} |
||||
|
|
||||
|
public JSONObject getResponse() { |
||||
|
return response; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,84 +0,0 @@ |
|||||
package awais.instagrabber.asyncs.direct_messages; |
|
||||
|
|
||||
import android.os.AsyncTask; |
|
||||
import android.util.Log; |
|
||||
|
|
||||
import java.io.DataOutputStream; |
|
||||
import java.net.HttpURLConnection; |
|
||||
import java.net.URL; |
|
||||
import java.net.URLEncoder; |
|
||||
import java.util.UUID; |
|
||||
|
|
||||
import awais.instagrabber.utils.Constants; |
|
||||
import awais.instagrabber.utils.Utils; |
|
||||
|
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|
||||
|
|
||||
public class CommentAction extends AsyncTask<Void, Void, Boolean> { |
|
||||
private final String text; |
|
||||
private final String threadId; |
|
||||
|
|
||||
private OnTaskCompleteListener listener; |
|
||||
|
|
||||
public CommentAction(String text, String threadId) { |
|
||||
this.text = text; |
|
||||
this.threadId = threadId; |
|
||||
} |
|
||||
|
|
||||
protected Boolean doInBackground(Void... lmao) { |
|
||||
boolean ok = false; |
|
||||
final String url2 = "https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/"; |
|
||||
final String cookie = settingsHelper.getString(Constants.COOKIE); |
|
||||
try { |
|
||||
final HttpURLConnection urlConnection2 = (HttpURLConnection) new URL(url2).openConnection(); |
|
||||
urlConnection2.setRequestMethod("POST"); |
|
||||
urlConnection2.setRequestProperty("User-Agent", Constants.I_USER_AGENT); |
|
||||
urlConnection2.setUseCaches(false); |
|
||||
final String commentText = URLEncoder.encode(text, "UTF-8") |
|
||||
.replaceAll("\\+", "%20").replaceAll("\\%21", "!").replaceAll("\\%27", "'") |
|
||||
.replaceAll("\\%28", "(").replaceAll("\\%29", ")").replaceAll("\\%7E", "~"); |
|
||||
final String cc = UUID.randomUUID().toString(); |
|
||||
final String urlParameters2 = Utils.sign("{\"_csrftoken\":\"" + cookie.split("csrftoken=")[1].split(";")[0] |
|
||||
+ "\",\"_uid\":\"" + Utils.getUserIdFromCookie(cookie) |
|
||||
+ "\",\"__uuid\":\"" + settingsHelper.getString(Constants.DEVICE_UUID) |
|
||||
+ "\",\"client_context\":\"" + cc |
|
||||
+ "\",\"mutation_token\":\"" + cc |
|
||||
+ "\",\"text\":\"" + commentText |
|
||||
+ "\",\"thread_ids\":\"[" + threadId |
|
||||
+ "]\",\"action\":\"send_item\"}"); |
|
||||
urlConnection2.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); |
|
||||
urlConnection2.setRequestProperty("Content-Length", "" + Integer.toString(urlParameters2.getBytes().length)); |
|
||||
urlConnection2.setDoOutput(true); |
|
||||
DataOutputStream wr2 = new DataOutputStream(urlConnection2.getOutputStream()); |
|
||||
wr2.writeBytes(urlParameters2); |
|
||||
wr2.flush(); |
|
||||
wr2.close(); |
|
||||
urlConnection2.connect(); |
|
||||
Log.d("austin_debug", urlConnection2.getResponseCode() + " " + urlParameters2 + " " + cookie); |
|
||||
if (urlConnection2.getResponseCode() == HttpURLConnection.HTTP_OK) { |
|
||||
ok = true; |
|
||||
} |
|
||||
urlConnection2.disconnect(); |
|
||||
} catch (Throwable ex) { |
|
||||
Log.e("austin_debug", "dm send: " + ex); |
|
||||
} |
|
||||
return ok; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected void onPostExecute(final Boolean result) { |
|
||||
if (listener != null) { |
|
||||
listener.onTaskComplete(result); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void setOnTaskCompleteListener(final OnTaskCompleteListener listener) { |
|
||||
if (listener != null) { |
|
||||
this.listener = listener; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public interface OnTaskCompleteListener { |
|
||||
void onTaskComplete(boolean ok); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,207 @@ |
|||||
|
package awais.instagrabber.asyncs.direct_messages; |
||||
|
|
||||
|
import android.os.AsyncTask; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import org.json.JSONObject; |
||||
|
|
||||
|
import java.io.BufferedReader; |
||||
|
import java.io.DataOutputStream; |
||||
|
import java.io.IOException; |
||||
|
import java.io.InputStreamReader; |
||||
|
import java.io.UnsupportedEncodingException; |
||||
|
import java.net.HttpURLConnection; |
||||
|
import java.net.URL; |
||||
|
import java.net.URLEncoder; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import awais.instagrabber.utils.Constants; |
||||
|
import awais.instagrabber.utils.Utils; |
||||
|
|
||||
|
import static awais.instagrabber.utils.Utils.settingsHelper; |
||||
|
|
||||
|
public class DirectThreadBroadcaster extends AsyncTask<DirectThreadBroadcaster.BroadcastOptions, Void, DirectThreadBroadcaster.DirectThreadBroadcastResponse> { |
||||
|
private static final String TAG = "DirectThreadBroadcaster"; |
||||
|
|
||||
|
private final String threadId; |
||||
|
|
||||
|
private OnBroadcastCompleteListener listener; |
||||
|
|
||||
|
public DirectThreadBroadcaster(String threadId) { |
||||
|
this.threadId = threadId; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected DirectThreadBroadcastResponse doInBackground(final BroadcastOptions... broadcastOptionsArray) { |
||||
|
if (broadcastOptionsArray == null || broadcastOptionsArray.length == 0 || broadcastOptionsArray[0] == null) { |
||||
|
return null; |
||||
|
} |
||||
|
final BroadcastOptions broadcastOptions = broadcastOptionsArray[0]; |
||||
|
final String cookie = settingsHelper.getString(Constants.COOKIE); |
||||
|
final String cc = UUID.randomUUID().toString(); |
||||
|
final Map<String, String> form = new HashMap<>(); |
||||
|
form.put("_csrftoken", cookie.split("csrftoken=")[1].split(";")[0]); |
||||
|
form.put("_uid", Utils.getUserIdFromCookie(cookie)); |
||||
|
form.put("__uuid", settingsHelper.getString(Constants.DEVICE_UUID)); |
||||
|
form.put("client_context", cc); |
||||
|
form.put("mutation_token", cc); |
||||
|
form.putAll(broadcastOptions.getFormMap()); |
||||
|
form.put("thread_ids", String.format("[%s]", threadId)); |
||||
|
form.put("action", "send_item"); |
||||
|
final String message = new JSONObject(form).toString(); |
||||
|
final String content = Utils.sign(message); |
||||
|
final String url = "https://i.instagram.com/api/v1/direct_v2/threads/broadcast/" + broadcastOptions.getItemType().getValue() + "/"; |
||||
|
HttpURLConnection connection = null; |
||||
|
DataOutputStream outputStream = null; |
||||
|
BufferedReader r = null; |
||||
|
try { |
||||
|
connection = (HttpURLConnection) new URL(url).openConnection(); |
||||
|
connection.setRequestMethod("POST"); |
||||
|
connection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); |
||||
|
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); |
||||
|
if (content != null) { |
||||
|
connection.setRequestProperty("Content-Length", "" + content.getBytes().length); |
||||
|
} |
||||
|
connection.setUseCaches(false); |
||||
|
connection.setDoOutput(true); |
||||
|
outputStream = new DataOutputStream(connection.getOutputStream()); |
||||
|
outputStream.writeBytes(content); |
||||
|
outputStream.flush(); |
||||
|
final int responseCode = connection.getResponseCode(); |
||||
|
if (responseCode != HttpURLConnection.HTTP_OK) { |
||||
|
Log.d(TAG, responseCode + ": " + content + ": " + cookie); |
||||
|
return new DirectThreadBroadcastResponse(responseCode, null); |
||||
|
} |
||||
|
r = new BufferedReader(new InputStreamReader(connection.getInputStream())); |
||||
|
final StringBuilder builder = new StringBuilder(); |
||||
|
for (String line = r.readLine(); line != null; line = r.readLine()) { |
||||
|
if (builder.length() != 0) { |
||||
|
builder.append("\n"); |
||||
|
} |
||||
|
builder.append(line); |
||||
|
} |
||||
|
return new DirectThreadBroadcastResponse(responseCode, new JSONObject(builder.toString())); |
||||
|
} catch (Exception e) { |
||||
|
Log.e(TAG, "Error", e); |
||||
|
} finally { |
||||
|
if (r != null) { |
||||
|
try { |
||||
|
r.close(); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
if (outputStream != null) { |
||||
|
try { |
||||
|
outputStream.close(); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
if (connection != null) { |
||||
|
connection.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onPostExecute(final DirectThreadBroadcastResponse result) { |
||||
|
if (listener != null) { |
||||
|
listener.onTaskComplete(result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setOnTaskCompleteListener(final OnBroadcastCompleteListener listener) { |
||||
|
if (listener != null) { |
||||
|
this.listener = listener; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public interface OnBroadcastCompleteListener { |
||||
|
void onTaskComplete(DirectThreadBroadcastResponse response); |
||||
|
} |
||||
|
|
||||
|
public enum ItemType { |
||||
|
TEXT("text"), |
||||
|
IMAGE("configure_photo"); |
||||
|
|
||||
|
private final String value; |
||||
|
|
||||
|
ItemType(final String value) { |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
public String getValue() { |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static abstract class BroadcastOptions { |
||||
|
private final ItemType itemType; |
||||
|
|
||||
|
public BroadcastOptions(final ItemType itemType) { |
||||
|
this.itemType = itemType; |
||||
|
} |
||||
|
|
||||
|
public ItemType getItemType() { |
||||
|
return itemType; |
||||
|
} |
||||
|
|
||||
|
abstract Map<String, String> getFormMap(); |
||||
|
} |
||||
|
|
||||
|
public static class TextBroadcastOptions extends BroadcastOptions { |
||||
|
private final String text; |
||||
|
|
||||
|
public TextBroadcastOptions(String text) throws UnsupportedEncodingException { |
||||
|
super(ItemType.TEXT); |
||||
|
this.text = URLEncoder.encode(text, "UTF-8") |
||||
|
.replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'") |
||||
|
.replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
Map<String, String> getFormMap() { |
||||
|
return Collections.singletonMap("text", text); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class ImageBroadcastOptions extends BroadcastOptions { |
||||
|
final boolean allowFullAspectRatio; |
||||
|
final String uploadId; |
||||
|
|
||||
|
public ImageBroadcastOptions(final boolean allowFullAspectRatio, final String uploadId) { |
||||
|
super(ItemType.IMAGE); |
||||
|
this.allowFullAspectRatio = allowFullAspectRatio; |
||||
|
this.uploadId = uploadId; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
Map<String, String> getFormMap() { |
||||
|
final Map<String, String> form = new HashMap<>(); |
||||
|
form.put("allow_full_aspect_ratio", String.valueOf(allowFullAspectRatio)); |
||||
|
form.put("upload_id", uploadId); |
||||
|
return form; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static class DirectThreadBroadcastResponse { |
||||
|
private int responseCode; |
||||
|
private JSONObject response; |
||||
|
|
||||
|
public DirectThreadBroadcastResponse(int responseCode, JSONObject response) { |
||||
|
this.responseCode = responseCode; |
||||
|
this.response = response; |
||||
|
} |
||||
|
|
||||
|
public int getResponseCode() { |
||||
|
return responseCode; |
||||
|
} |
||||
|
|
||||
|
public JSONObject getResponse() { |
||||
|
return response; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,76 @@ |
|||||
|
package awais.instagrabber.models; |
||||
|
|
||||
|
import java.io.InputStream; |
||||
|
|
||||
|
public class ImageUploadOptions { |
||||
|
private InputStream inputStream; |
||||
|
private long contentLength; |
||||
|
private boolean isSidecar; |
||||
|
private String waterfallId; |
||||
|
|
||||
|
public static class Builder { |
||||
|
private InputStream inputStream; |
||||
|
private long contentLength; |
||||
|
private boolean isSidecar; |
||||
|
private String waterfallId; |
||||
|
|
||||
|
public Builder(final InputStream inputStream, final long contentLength) { |
||||
|
this.inputStream = inputStream; |
||||
|
this.contentLength = contentLength; |
||||
|
} |
||||
|
|
||||
|
public Builder setInputStream(final InputStream inputStream) { |
||||
|
this.inputStream = inputStream; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public Builder setContentLength(final long contentLength) { |
||||
|
this.contentLength = contentLength; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public Builder setIsSidecar(final boolean isSidecar) { |
||||
|
this.isSidecar = isSidecar; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public Builder setWaterfallId(final String waterfallId) { |
||||
|
this.waterfallId = waterfallId; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
public ImageUploadOptions build() { |
||||
|
return new ImageUploadOptions(inputStream, contentLength, isSidecar, waterfallId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static Builder builder(final InputStream inputStream, final long contentLength) { |
||||
|
return new Builder(inputStream, contentLength); |
||||
|
} |
||||
|
|
||||
|
private ImageUploadOptions(final InputStream inputStream, |
||||
|
final long contentLength, |
||||
|
final boolean isSidecar, |
||||
|
final String waterfallId) { |
||||
|
this.inputStream = inputStream; |
||||
|
this.contentLength = contentLength; |
||||
|
this.isSidecar = isSidecar; |
||||
|
this.waterfallId = waterfallId; |
||||
|
} |
||||
|
|
||||
|
public InputStream getInputStream() { |
||||
|
return inputStream; |
||||
|
} |
||||
|
|
||||
|
public long getContentLength() { |
||||
|
return contentLength; |
||||
|
} |
||||
|
|
||||
|
public boolean isSidecar() { |
||||
|
return isSidecar; |
||||
|
} |
||||
|
|
||||
|
public String getWaterfallId() { |
||||
|
return waterfallId; |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue