Skip to content

Commit

Permalink
add uploading and pulling images
Browse files Browse the repository at this point in the history
  • Loading branch information
rukins committed Apr 3, 2023
1 parent 6763d58 commit 4e62ebf
Show file tree
Hide file tree
Showing 8 changed files with 361 additions and 9 deletions.
10 changes: 10 additions & 0 deletions src/main/java/io/github/rukins/gkeepapi/GKeepAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import io.github.rukins.gkeepapi.client.GKeepClientWrapper;
import io.github.rukins.gkeepapi.model.gkeep.NodeRequest;
import io.github.rukins.gkeepapi.model.gkeep.NodeResponse;
import io.github.rukins.gkeepapi.model.gkeep.node.blob.blobobject.ImageBlob;
import io.github.rukins.gkeepapi.model.gkeep.node.nodeobject.Node;
import io.github.rukins.gkeepapi.model.gkeep.userinfo.UserInfo;
import io.github.rukins.gkeepapi.model.image.ImageData;
import io.github.rukins.gkeepapi.utils.NodeUtils;
import io.github.rukins.gpsoauth.exception.AuthError;

Expand Down Expand Up @@ -63,6 +65,14 @@ public NodeResponse changes() throws AuthError {
return changes(NodeRequest.withDefaultValues().build());
}

public ImageBlob uploadImage(byte[] imageBytes, String blobServerId, String nodeServerId) throws AuthError {
return client.uploadImage(imageBytes, blobServerId, nodeServerId, client.getUploadId(blobServerId, nodeServerId));
}

public ImageData getImageData(String blobServerId, String nodeServerId) throws AuthError {
return client.getImageData(blobServerId, nodeServerId);
}

public String getCurrentVersion() {
return currentVersion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@
import io.github.rukins.gkeepapi.model.gkeep.NodeResponse;

public interface GKeepClient {
String URL = "https://notes-pa.googleapis.com";

@RequestLine("POST /notes/v1/changes")
@Headers({
"Content-Type: application/json; charset=UTF-8", "Host: notes-pa.googleapis.com",
"Accept-Encoding: gzip", "Content-Encoding: gzip", "Connection: Keep-Alive",
"Content-Type: application/json; charset=UTF-8",
"Connection: Keep-Alive",
"Authorization: OAuth {access-token}"
})
NodeResponse changes(NodeRequest body, @Param("access-token") String accessToken);

@RequestLine("POST /notes/v1/getFamilyInfo")
@Headers({
"Content-Type: application/json; charset=UTF-8", "Host: notes-pa.googleapis.com",
"Accept-Encoding: gzip", "Content-Encoding: gzip", "Connection: Keep-Alive",
"Connection: Keep-Alive",
"Authorization: OAuth {access-token}"
})
NodeResponse getFamilyInfo(@Param("access-token") String accessToken);
Expand Down
133 changes: 128 additions & 5 deletions src/main/java/io/github/rukins/gkeepapi/client/GKeepClientWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,45 @@
import com.google.gson.Gson;
import feign.Feign;
import feign.FeignException;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import io.github.rukins.gkeepapi.config.GsonConfig;
import io.github.rukins.gkeepapi.model.gkeep.NodeRequest;
import io.github.rukins.gkeepapi.model.gkeep.NodeResponse;
import io.github.rukins.gkeepapi.model.gkeep.node.blob.MimeType;
import io.github.rukins.gkeepapi.model.gkeep.node.blob.blobobject.ImageBlob;
import io.github.rukins.gkeepapi.model.image.ImageData;
import io.github.rukins.gpsoauth.Auth;
import io.github.rukins.gpsoauth.exception.AuthError;
import io.github.rukins.gpsoauth.model.AccessTokenRequestParams;

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class GKeepClientWrapper {
private final Gson gson = GsonConfig.gson();

private final GKeepClient client = Feign.builder()
.decoder(new GsonDecoder(gson))
.encoder(new GsonEncoder(gson))
.target(GKeepClient.class, "https://notes-pa.googleapis.com");
.target(GKeepClient.class, GKeepClient.URL);

private final GKeepUploadMediaClient uploadMediaClient = Feign.builder()
.decoder(new GsonDecoder(gson))
.encoder(new FileEncoder())
.target(GKeepUploadMediaClient.class, GKeepUploadMediaClient.URL);

private final GKeepMediaClient mediaClient = Feign.builder()
.target(GKeepMediaClient.class, GKeepMediaClient.URL);

private final Auth auth = new Auth();

Expand All @@ -31,17 +54,97 @@ public GKeepClientWrapper(String masterToken) {
}

public NodeResponse changes(NodeRequest body) throws AuthError {
NodeResponse response;
NodeResponse nodeResponse;

try {
response = client.changes(body, accessToken);
nodeResponse = client.changes(body, accessToken);
} catch (FeignException.Unauthorized unauthorized) {
updateAccessToken();

nodeResponse = client.changes(body, accessToken);
}

return nodeResponse;
}

public String getUploadId(String blobServerId, String nodeServerId) throws AuthError {
final String UPLOAD_ID_HEADER = "X-GUploader-UploadID";

Map<String, Collection<String>> headers;

try (Response response = uploadMediaClient.uploadMedia(blobServerId, nodeServerId, accessToken)) {
if (response.status() == 401) {
throw new FeignException.Unauthorized(
response.reason(), response.request(),
response.body().asInputStream().readAllBytes(), response.headers()
);
}

headers = response.headers();
} catch (FeignException.Unauthorized unauthorized) {
updateAccessToken();

response = client.changes(body, accessToken);
Response response = uploadMediaClient.uploadMedia(blobServerId, nodeServerId, accessToken);

headers = response.headers();

response.close();
} catch (IOException e) {
throw new RuntimeException(e);
}

return response;
return headers.containsKey(UPLOAD_ID_HEADER)
? (String) headers.get(UPLOAD_ID_HEADER).toArray()[0]
: null;
}

public ImageBlob uploadImage(byte[] imageBytes, String blobServerId, String nodeServerId, String uploadId) {
return uploadMediaClient.uploadMedia(imageBytes, blobServerId, nodeServerId, uploadId);
}

public ImageData getImageData(String blobServerId, String nodeServerId) throws AuthError {
final String CONTENT_TYPE_HEADER = "Content-Type";
final String CONTENT_DISPOSITION_HEADER = "Content-Disposition";

byte[] imageBytes;
Map<String, Collection<String>> headers;

try(Response response = mediaClient.media(blobServerId, nodeServerId, accessToken)) {
if (response.status() == 401) {
throw new FeignException.Unauthorized(
response.reason(), response.request(),
response.body().asInputStream().readAllBytes(), response.headers()
);
}

imageBytes = response.body().asInputStream().readAllBytes();
headers = response.headers();
} catch (FeignException.Unauthorized unauthorized) {
updateAccessToken();

Response response = mediaClient.media(blobServerId, nodeServerId, accessToken);

try {
imageBytes = response.body().asInputStream().readAllBytes();
} catch (IOException e) {
throw new RuntimeException(e);
}
headers = response.headers();

response.close();
} catch (IOException e) {
throw new RuntimeException(e);
}

return headers.containsKey(CONTENT_TYPE_HEADER) && headers.containsKey(CONTENT_DISPOSITION_HEADER)
? new ImageData(
imageBytes,
getFileNameFromContentDispositionHeader(
((String) headers.get(CONTENT_DISPOSITION_HEADER).toArray()[0])
),
MimeType.getByValue((String) headers.get(CONTENT_TYPE_HEADER).toArray()[0])
)
: null;
}

private void updateAccessToken() throws AuthError {
Expand All @@ -54,4 +157,24 @@ private void updateAccessToken() throws AuthError {

accessToken = auth.getAccessToken(accessTokenRequestParams).getAccessToken();
}

private String getFileNameFromContentDispositionHeader(String contentDispositionHeaderData) {
Pattern pattern = Pattern.compile("filename=\"(?<name>.+)\"");

Matcher matcher = pattern.matcher(contentDispositionHeaderData);

if (matcher.find()) {
return matcher.group("name");
}

return null;
}

private static class FileEncoder implements Encoder {
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (object instanceof Map && ((Map<?, ?>) object).containsKey("file")) {
template.body((byte[]) ((Map<?, ?>) object).get("file"), StandardCharsets.UTF_8);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.rukins.gkeepapi.client;

import feign.Headers;
import feign.Param;
import feign.RequestLine;
import feign.Response;

public interface GKeepMediaClient {
String URL = "https://keep.google.com";

// it automatically redirects to https://lh3.googleusercontent.com/keep-bbsk/...
// (that is located in Location header of the response) and returns image bytes
@RequestLine("GET /media/v2/{node-serverId}/{blob-serverId}") // ?accept=audio/3gpp,audio/amr-wb,image/gif,image/jpg,image/jpeg,image/png&sz=2148
@Headers({
"Authorization: OAuth {access-token}"
})
Response media(
@Param("blob-serverId") String blobServerId,
@Param("node-serverId") String nodeServerId,
@Param("access-token") String accessToken
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.rukins.gkeepapi.client;

import feign.Headers;
import feign.Param;
import feign.RequestLine;
import feign.Response;
import io.github.rukins.gkeepapi.model.gkeep.node.blob.blobobject.ImageBlob;

public interface GKeepUploadMediaClient {
String URL = "https://notes-pa.googleapis.com";

@RequestLine("POST /upload/notes/v1/media/{blob-serverId}?noteId={node-serverId}&uploadType=resumable")
@Headers({
"Connection: Keep-Alive",
"Authorization: OAuth {access-token}"
})
Response uploadMedia(
@Param("blob-serverId") String blobServerId,
@Param("node-serverId") String nodeServerId,
@Param("access-token") String accessToken
);

@RequestLine("PUT /upload/notes/v1/media/{blob-serverId}?noteId={node-serverId}&uploadType=resumable&upload_id={upload_id}")
@Headers({
"Connection: Keep-Alive"
})
ImageBlob uploadMedia(
@Param("file") byte[] imageBytes,
@Param("blob-serverId") String blobServerId,
@Param("node-serverId") String nodeServerId,
@Param("upload_id") String uploadId
);
}
76 changes: 76 additions & 0 deletions src/main/java/io/github/rukins/gkeepapi/model/image/ImageData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.github.rukins.gkeepapi.model.image;

import io.github.rukins.gkeepapi.model.gkeep.node.blob.MimeType;

public class ImageData {
private byte[] bytes;
private Integer byteSize;
private String fileName;
private MimeType mimeType;
private ImageSize imageSize;

public ImageData(byte[] bytes, String fileName, MimeType mimeType) {
this.bytes = bytes;
this.byteSize = bytes.length;
this.fileName = fileName;
this.mimeType = mimeType;
}

public ImageData(byte[] bytes, String fileName, MimeType mimeType, ImageSize imageSize) {
this.bytes = bytes;
this.byteSize = bytes.length;
this.fileName = fileName;
this.mimeType = mimeType;
this.imageSize = imageSize;
}

public byte[] getBytes() {
return bytes;
}

public void setBytes(byte[] bytes) {
this.bytes = bytes;
}

public Integer getByteSize() {
return byteSize;
}

public void setByteSize(Integer byteSize) {
this.byteSize = byteSize;
}

public String getFileName() {
return fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}

public MimeType getMimeType() {
return mimeType;
}

public void setMimeType(MimeType mimeType) {
this.mimeType = mimeType;
}

public ImageSize getImageSize() {
return imageSize;
}

public void setImageSize(ImageSize imageSize) {
this.imageSize = imageSize;
}

@Override
public String toString() {
return "ImageData{" +
"byteSize=" + byteSize +
", fileName='" + fileName + '\'' +
", mimeType='" + mimeType + '\'' +
", imageSize=" + imageSize +
'}';
}
}
35 changes: 35 additions & 0 deletions src/main/java/io/github/rukins/gkeepapi/model/image/ImageSize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.rukins.gkeepapi.model.image;

public class ImageSize {
private Integer width;
private Integer height;

public ImageSize(Integer width, Integer height) {
this.width = width;
this.height = height;
}

public Integer getWidth() {
return width;
}

public void setWidth(Integer width) {
this.width = width;
}

public Integer getHeight() {
return height;
}

public void setHeight(Integer height) {
this.height = height;
}

@Override
public String toString() {
return "ImageSize{" +
"width=" + width +
", height=" + height +
'}';
}
}
Loading

0 comments on commit 4e62ebf

Please sign in to comment.