diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2c7d170 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/maven-docker-publish.yml b/.github/workflows/maven-docker-publish.yml new file mode 100644 index 0000000..b80b849 --- /dev/null +++ b/.github/workflows/maven-docker-publish.yml @@ -0,0 +1,73 @@ +# This workflow will build a Docker Container using Maven and then publish it to Dockerhub when change was made. + +name: Maven Build and Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v2.4.0 + + - name: Docker meta + id: meta + if: github.event_name != 'pull_request' + uses: docker/metadata-action@v3.6.2 + with: + images: patbaumgartner/lovebox-telegram-sender + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker images + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v2.7.0 + with: + push: true + tags: patbaumgartner/run:base-cnb + file: Dockerfile.base-cnb + + - name: Set up JDK + uses: actions/setup-java@v2.5.0 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build with Maven + run: mvn clean verify --batch-mode --no-transfer-progress + + - name: Build Docker Container with Maven and Publish to Docker Hub + if: github.event_name != 'pull_request' + run: | + mvn spring-boot:build-image \ + --batch-mode --no-transfer-progress \ + -Dspring-boot.build-image.imageName=${{ steps.meta.outputs.tags }} \ + -Dspring-boot.build-image.runImage=patbaumgartner/run:base-cnb \ + -Dspring-boot.build-image.publish=true \ + -DCI_REGISTRY=https://index.docker.io/v1/ \ + -DCI_REGISTRY_USER=${{ secrets.DOCKER_USERNAME }} \ + -DCI_REGISTRY_PASSWORD=${{ secrets.DOCKER_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bb0087 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +application-local.properties + +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Docker ### +.env \ No newline at end of file diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..a9f1ef8 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/Dockerfile.base-cnb b/Dockerfile.base-cnb new file mode 100644 index 0000000..bf2fb74 --- /dev/null +++ b/Dockerfile.base-cnb @@ -0,0 +1,13 @@ +FROM paketobuildpacks/run:base-cnb + +USER root + +## Idea from: https://blog.adoptopenjdk.net/2021/01/prerequisites-for-font-support-in-adoptopenjdk/ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libfreetype6 \ + fontconfig \ + fonts-dejavu \ + && rm -rf /var/lib/apt/lists/* + +USER cnb diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..123faf9 --- /dev/null +++ b/HELP.md @@ -0,0 +1,19 @@ +# Getting Started + +### Reference Documentation + +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.6.2/maven-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.6.2/maven-plugin/reference/html/#build-image) +* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.6.2/reference/htmlsingle/#using-boot-devtools) +* [OpenFeign](https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/) +* [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.6.2/reference/htmlsingle/#configuration-metadata-annotation-processor) + +### Additional Links + +These additional references should also help you: + +* [Declarative REST calls with Spring Cloud OpenFeign sample](https://github.com/spring-cloud-samples/feign-eureka) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100755 index 0000000..9891964 --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ +![maven-docker-publish](https://github.com/patbaumgartner/lovebox-telegram-sender/actions/workflows/maven-docker-publish.yml/badge.svg) + +# Lovebox Telegram Sender + +The app allows to send messages via Telegram Bot to a single Lovebox instance. Text messages and photos with captions +are supported. Other message types (e.g. Stickers, Audio, etc.) will lead to a default message. + +_**Known Issues:** Currently Emojis can not be drawn on a Graphics2d canvas. Therefore Emojis are translated to a text +representation._ + +## Application Setup + +To set up the app a few ids and values need to be retrieved on the Lovebox API. The following curl commands help to find +the needed data on an existing account. Make sure you have set up your account via the Android or iOS app already. + +### Login with Password + +Login with password to retrieve the authorization token for the following me request. + +```bash +curl --location --request POST 'https://app-api.loveboxlove.com/v1/auth/loginWithPassword' \ +--header 'accept: application/json' \ +--header 'content-type: application/json' \ +--header 'host: app-api.loveboxlove.com' \ +--data-raw '{ + "email": "my@email.com", + "password": "mySecret" +}' +``` + +```json +{ + "_id": "42c61f261f399d0016350b7f", + "firstName": "FirstName", + "email": "my@email.com", + "token": "exJhbGcpOiJIVzI1NipsInR5cCI6IkpXVDj9.eyJ1c2xySwFiOiI2MWM5LWYzNjFhMzk5YzAwMTYzNTBhN2YiLCJpYXQiFjE22DAVNmQwNTL9.qlsvp_roqCu4MFwBMwNZu2eyImFGjogvNeR4tkoTLPe" +} +``` + +### Me Request with the Authorization Token + +Use the token above to make the "Me Request" to find for the configuration needed details. + +```bash +curl --location --request POST 'https://app-api.loveboxlove.com/v1/graphql' \ +--header 'authorization: Bearer exJhbGcpOiJIVzI1NipsInR5cCI6IkpXVDj9.eyJ1c2xySwFiOiI2MWM5LWYzNjFhMzk5YzAwMTYzNTBhN2YiLCJpYXQiFjE22DAVNmQwNTL9.qlsvp_roqCu4MFwBMwNZu2eyImFGjogvNeR4tkoTLPe' \ +--header 'content-type: application/json' \ +--header 'host: app-api.loveboxlove.com' \ +--data-raw '{ + "operationName": null, + "variables": {}, + "query": "{\n me {\n _id\n firstName\n email\n boxes {\n _id\n color\n signature\n lovePercentage\n nickname\n notifications {\n disableUntil\n messageRead\n heartReceived\n __typename\n }\n admin {\n _id\n firstName\n email\n __typename\n }\n privacyPolicy\n pairingCode\n isConnected\n isAdmin\n hardware\n hasColor\n hasColorBackup\n connectionDate\n __typename\n }\n relations {\n _id\n name\n relationType\n picture\n color\n streak\n boxId\n loveGoal\n streakDeadline\n reminders {\n day\n meridiem\n number\n weekday\n time\n __typename\n }\n specialDates {\n _id\n name\n date\n dateType\n __typename\n }\n addresses {\n firstname\n lastname\n streetAddress\n zipCode\n city\n country\n state\n __typename\n }\n __typename\n }\n roles\n device {\n _id\n appVersion\n os\n __typename\n }\n profile\n reminder\n premium\n beta\n fcmToken\n language\n loveCoins\n __typename\n }\n}\n" +}' +``` + +```json +{ + "data": { + "me": { + "_id": "42c61f261f399d0016350b7f", + "firstName": "FirstName", + "email": "me@email.com", + "boxes": [ + { + // lovebox.box-id + "_id": "417a114e58e15a0214cf3612", + "color": "#8A64FF", + // lovebox.signature + "signature": "Signature", + "lovePercentage": 100, + "nickname": "Nickname", + "notifications": { + // ... + }, + "admin": { + "_id": "61c61ecc71010a00161789f2", + "firstName": null, + "email": "significant-other@email.com", + "__typename": "User" + }, + "privacyPolicy": "ADMIN_AND_ME", + "pairingCode": "BECF-RBMA", + "isConnected": true, + "isAdmin": false, + "hardware": "C2", + "hasColor": true, + "hasColorBackup": null, + "connectionDate": "2021-12-24T19:27:35.123Z", + "__typename": "BoxSettings" + } + ], + "relations": [ + { + // lovebox.relation-id + "_id": "33c67a2127d7be09142f4326", + "name": "Nickname", + "relationType": "other", + "picture": null, + "color": "#3399FF", + "streak": 5, + // lovebox.box-id + "boxId": "417a114e58e15a0214cf3612", + "loveGoal": "Daily", + "streakDeadline": "2021-12-29T22:59:59.999Z", + "reminders": [], + "addresses": [], + "__typename": "Relation" + } + ], + "roles": [], + "device": { + // lovebox.device-id + "_id": "42fab8322d8cec91", + "appVersion": "5.4.9", + "os": "android", + "__typename": "Device" + }, + "profile": { + // ... + }, + "reminder": null, + "premium": 0, + "beta": 0, + "fcmToken": "edMLqMyMrpoGig9ZHdFdvH:AdA91bGGwx3UEuYTdhYOWDIdwlm2b23B9Jjin3MCGbi7CmUSpCVHFlorfryygi5QUBQMUVUiGsDJIE3RliENFmsuWrOnf4cBba-mNT5032NoKlo9AdPU5YhuCOR0KIdAbCokR42Hru", + "language": "en", + "loveCoins": 10, + "__typename": "User" + } + } +} +``` + +### Setting up a Telegram Bot + +To create a chatbot on Telegram, you need to contact the [@BotFather](https://telegram.me/BotFather), which is a bot +used to create other bots. + +The command you need is `/newbot` which leads to the next steps to create your bot. Follow the instructions and get the +bot `username`, and `token`. + +### Adjusting SpringBoot's application.properties + +Running the app from the source needs adjustments according to your settings. Adjusting the `application.properties` +in the sources or passing them as Java options or CLI arguments to the app. + +```properties +# Lovebox Login +lovebox.enabled=true +lovebox.email=me@email.com +lovebox.password=mySecret +# Lovebox Setting +lovebox.signature=Signature +lovebox.device-id=42fab8322d8cec91 +lovebox.relation-id=33c67a2127d7be09142f4326 +lovebox.box-id=417a114e58e15a0214cf3612 +# Telegram Bot Settings +bot.username=Lovebox_bot +bot.token=4072971853:ABEojZ42uNA6YYn_c7DF8RH0UOorqXuveSQ +``` + +### Setting Environment Variables e.g. for Docker + +The folling snippet can be passed as `.env` and read by the `docker-compose.yml` or used to be passed directly to the +`docker run` command. + +```bash +# Lovebox Login +LOVEBOX_ENABLED=true +LOVEBOX_EMAIL="me@email.com" +LOVEBOX_PASSWORD="mySecret" +# Lovebox Setting +LOVEBOX_SIGNATURE="Signature" +LOVEBOX_DEVICE_ID="42fab8322d8cec91" +LOVEBOX_RELATION_ID="33c67a2127d7be09142f4326" +LOVEBOX_BOX_ID="417a114e58e15a0214cf3612" +# Telegram Bot Settings +BOT_USERNAME="Lovebox_bot" +BOT_TOKEN="4072971853:ABEojZ42uNA6YYn_c7DF8RH0UOorqXuveSQ" +``` + +## Building the Docker Container + +### Building the Docker Container Locally + +```bash +mvn spring-boot:build-image \ + --batch-mode \ + --no-transfer-progress \ + -Dspring-boot.build-image.imageName='patbaumgartner/${project.artifactId}:${project.version}' +``` + +### Building the Docker Container and Pushing to Docker Hub + +```bash +mvn spring-boot:build-image \ + --batch-mode \ + --no-transfer-progress \ + -Dspring-boot.build-image.imageName='patbaumgartner/${project.artifactId}:${project.version}' \ + -Dspring-boot.build-image.publish=true \ + -DCI_REGISTRY=https://index.docker.io/v1/ \ + -DCI_REGISTRY_USER=${DCI_REGISTRY_USER} \ + -DCI_REGISTRY_PASSWORD=${DCI_REGISTRY_PASSWORD} +``` + +### Fixing Known Issues with Missing Fonts + +Since the app uses fonts, we need to make sure that fonts are part of the docker container. The containers produced +above throw and exception when using them +`java.lang.NullPointerException: Cannot load from short array because "sun.awt.FontConfiguration.head" is null` + +[Andreas Ahlensdorf](https://github.com/aahlenst) describes nicely the font problem in his blog +post [Prerequisites for Font Support in AdoptOpenJDK +](https://blog.adoptopenjdk.net/2021/01/prerequisites-for-font-support-in-adoptopenjdk/). + +After more research, it seems that the only solution to add fonts to the buildpack base image is to create a OCI run +image by extending the base one. See the `Dockerfile.base-cnb` file how a patch with the additional font packages might +look like. + +Build the `runImage` locally with the following command. + +```bash +docker build --no-cache -f Dockerfile.base-cnb -t patbaumgartner/run:base-cnb . +``` + +Since we run the pull policy in the `mvn spring-boot:build-image` command with IF_NOT_PRESENT, we need to make sure that +the newest version of the builder is locally available. + +``` bash +docker pull paketobuildpacks/builder:base +``` + +Finally, passing to the `spring-boot-maven-plugin` the `runImage` to build the docker container containing the fonts. + +```bash +mvn spring-boot:build-image \ + --batch-mode \ + --no-transfer-progress \ + -Dspring-boot.build-image.imageName='patbaumgartner/${project.artifactId}:${project.version}' \ + -Dspring-boot.build-image.runImage=patbaumgartner/run:base-cnb \ + -Dspring-boot.build-image.pullPolicy=IF_NOT_PRESENT +``` + +## Credits + +Reverse engineering (unpinning certificates) was done with [APKLab](https://github.com/APKLab/APKLab) and +the [Lovebox APK](https://www.apkmonk.com/app/love.lovebox.loveboxapp/) provided by [apkmonk](https://www.apkmonk.com). +Postman was used to capture the REST calls from the mobile app. The +article [Capturing Http Requests](https://learning.postman.com/docs/sending-requests/capturing-request-data/capturing-http-requests/) +covers everything needed. After a Postman +update [new certs](https://learning.postman.com/docs/sending-requests/capturing-request-data/capturing-http-requests/#troubleshooting-certificate-issues) +need to be installed. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1dca903 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.8" + +services: + lovebox-telegram-sender: + image: patbaumgartner/lovebox-telegram-sender:0.0.1-SNAPSHOT + container_name: lovebox-telegram-sender + env_file: + - ./.env \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0d7702e --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.2 + + + + com.patbaumgartner + lovebox-telegram-sender + 0.0.1-SNAPSHOT + Lovebox Telegram Sender + Lovebox Telegram Sender + + + + + + + + 6.0 + 4.2 + 17 + 2021.0.0 + 5.5.0 + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-json + + + com.google.code.gson + gson + + + org.telegram + telegrambots-spring-boot-starter + ${telegrambots-spring-boot-starter.version} + + + org.imgscalr + imgscalr-lib + ${imgscalr.version} + + + com.kcthota + emoji4j + ${emoji4j.version} + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.skyscreamer + jsonassert + + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + ${CI_REGISTRY_USER} + ${CI_REGISTRY_PASSWORD} + ${CI_REGISTRY} + + + + + + + + diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplication.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplication.java new file mode 100644 index 0000000..8c5859e --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplication.java @@ -0,0 +1,21 @@ +package com.patbaumgartner.lovebox.telegram.sender; + +import com.patbaumgartner.lovebox.telegram.sender.rest.clients.LoveboxRestClientProperties; +import com.patbaumgartner.lovebox.telegram.sender.telegram.LoveboxBotProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableFeignClients +@EnableScheduling +@SpringBootApplication +@EnableConfigurationProperties({LoveboxRestClientProperties.class, LoveboxBotProperties.class}) +public class LoveboxTelegramSenderApplication { + + public static void main(String[] args) { + SpringApplication.run(LoveboxTelegramSenderApplication.class, args); + } + +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailRequestBody.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailRequestBody.java new file mode 100644 index 0000000..f1e7c95 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailRequestBody.java @@ -0,0 +1,5 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +public record CheckEmailRequestBody(String email) { + +} \ No newline at end of file diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailResponseBody.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailResponseBody.java new file mode 100644 index 0000000..5b403e1 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/CheckEmailResponseBody.java @@ -0,0 +1,5 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +public record CheckEmailResponseBody(Boolean existingUser, String firstName) { + +} \ No newline at end of file diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/GraphqlRequestBody.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/GraphqlRequestBody.java new file mode 100644 index 0000000..5f8543c --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/GraphqlRequestBody.java @@ -0,0 +1,5 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +public record GraphqlRequestBody(String operationName, Object variables, String query) { + +} \ No newline at end of file diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordResponseBody.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordResponseBody.java new file mode 100644 index 0000000..574412d --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordResponseBody.java @@ -0,0 +1,8 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LoginWithPasswordResponseBody(@JsonProperty("_id") String id, String firstName, + String email, String token) { + +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordlRequestBody.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordlRequestBody.java new file mode 100644 index 0000000..744d7d8 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoginWithPasswordlRequestBody.java @@ -0,0 +1,5 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +public record LoginWithPasswordlRequestBody(String email, String password) { + +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClient.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClient.java new file mode 100644 index 0000000..c0463fe --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClient.java @@ -0,0 +1,19 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "LoveboxApi", url = "https://app-api.loveboxlove.com") +public interface LoveboxRestClient { + + @PostMapping(path = "/v1/auth/checkEmail") + ResponseEntity checkEmail(CheckEmailRequestBody request); + + @PostMapping(path = "/v1/auth/loginWithPassword") + ResponseEntity loginWithPassword(LoginWithPasswordlRequestBody request); + + @PostMapping(path = "/v1/graphql") + ResponseEntity graphql(@RequestHeader("authorization") String authorization, GraphqlRequestBody request); +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClientProperties.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClientProperties.java new file mode 100644 index 0000000..415d90b --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/rest/clients/LoveboxRestClientProperties.java @@ -0,0 +1,26 @@ +package com.patbaumgartner.lovebox.telegram.sender.rest.clients; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "lovebox") +public class LoveboxRestClientProperties { + + /* Lovebox sender enabled */ + private boolean enabled = true; + + /* Email used to login into App */ + private String email; + /* Password used to login into App */ + private String password; + + /* Mobile device id for logged in user */ + private String deviceId; + /* Box id for message receiver */ + private String boxId; + /* Signature of message sender */ + private String signature; + /* Recipient relation id */ + private String relationId; +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/ImageService.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/ImageService.java new file mode 100644 index 0000000..39e6f58 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/ImageService.java @@ -0,0 +1,135 @@ +package com.patbaumgartner.lovebox.telegram.sender.services; + +import com.patbaumgartner.lovebox.telegram.sender.utils.Pair; +import emoji4j.EmojiUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.imgscalr.Scalr; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Base64; +import java.util.Random; + +@Slf4j +@Component +public record ImageService(ResourceLoader resourceLoader) { + + public static final int DISPLAY_WIDTH = 1280; + public static final int DISPLAY_HEIGHT = 960; + public static final int BORDER_WIDTH = 20; + public static final int INITIAL_FONT_SIZE = 18; + public static final String FONT_NAME = "DejaVu Sans Mono"; + + @SneakyThrows + public Pair resizeImageToPair(File file, String text) { + BufferedImage originalImage = ImageIO.read(file); + BufferedImage resizedImage = + Scalr.resize( + originalImage, + Scalr.Method.AUTOMATIC, + Scalr.Mode.AUTOMATIC, + DISPLAY_WIDTH, + DISPLAY_HEIGHT, + Scalr.OP_ANTIALIAS); + + BufferedImage image = new BufferedImage(DISPLAY_WIDTH, DISPLAY_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + graphics.setColor(Color.black); + graphics.fillRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); + + int x = (DISPLAY_WIDTH - resizedImage.getWidth()) / 2; + int y = (DISPLAY_HEIGHT - resizedImage.getHeight()) / 2; + + graphics.drawImage(resizedImage, x, y, null); + + if (text != null) { + drawCenteredMessage(graphics, text); + } + + graphics.dispose(); + + return constructImagePair(image); + } + + @SneakyThrows + public Pair createTextImageToPair(String message) { + BufferedImage image = new BufferedImage(DISPLAY_WIDTH, DISPLAY_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + + // Reading message and fixing emojis + String text = message; + try { + text = EmojiUtils.shortCodify(message); + } catch (Exception | Error e) { + // Suppress exception + log.error("Exception occurred: {}", e.getMessage(), e); + } + + // Set background color + Random rnd = new Random(); + int red = rnd.nextInt(256); + int green = rnd.nextInt(256); + int blue = rnd.nextInt(256); + Color color = new Color(red, green, blue); + graphics.setColor(color); + graphics.fillRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); + + if (text != null) { + drawCenteredMessage(graphics, text); + } + + graphics.dispose(); + + return constructImagePair(image); + } + + @SneakyThrows + public Pair createFixedImageToPair() { + Resource resource = resourceLoader.getResource("lovebox.jpeg"); + Image image = ImageIO.read(resource.getInputStream()); + image = image.getScaledInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, Image.SCALE_SMOOTH); + BufferedImage bufferedImage = new BufferedImage(DISPLAY_WIDTH, DISPLAY_HEIGHT, BufferedImage.TYPE_INT_ARGB); + + Graphics2D graphics = bufferedImage.createGraphics(); + graphics.drawImage(image, 0, 0, null); + + graphics.dispose(); + + return constructImagePair(bufferedImage); + } + + protected void drawCenteredMessage(Graphics2D graphics, String text) { + // Calculate max font + graphics.setColor(Color.white); + Font font = new Font(FONT_NAME, Font.BOLD, INITIAL_FONT_SIZE); + graphics.setFont(font); + FontMetrics initialFm = graphics.getFontMetrics(); + + int stringWidth = initialFm.stringWidth(text) + 2 * BORDER_WIDTH; + double widthRatio = (double) DISPLAY_WIDTH / (double) stringWidth; + + int newFontSize = (int) (initialFm.getFont().getSize() * widthRatio); + int fontSizeToUse = Math.min(newFontSize, DISPLAY_HEIGHT); + graphics.setFont(new Font(initialFm.getFont().getName(), Font.PLAIN, fontSizeToUse)); + + // Draw centered string + FontMetrics fm = graphics.getFontMetrics(); + int x = (DISPLAY_WIDTH - fm.stringWidth(text)) / 2; + int y = (fm.getAscent() + (DISPLAY_HEIGHT - (fm.getAscent() + fm.getDescent())) / 2); + graphics.drawString(text, x, y); + } + + protected Pair constructImagePair(BufferedImage image) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ImageIO.write(image, "png", output); + String base64Image = Base64.getEncoder().encodeToString(output.toByteArray()); + + return new Pair("data:image/png;base64," + base64Image, new ByteArrayInputStream(output.toByteArray())); + } +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/LoveboxService.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/LoveboxService.java new file mode 100644 index 0000000..b10d855 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/services/LoveboxService.java @@ -0,0 +1,201 @@ +package com.patbaumgartner.lovebox.telegram.sender.services; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.patbaumgartner.lovebox.telegram.sender.rest.clients.*; +import com.patbaumgartner.lovebox.telegram.sender.utils.Pair; +import com.patbaumgartner.lovebox.telegram.sender.utils.Tripple; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public record LoveboxService( + LoveboxRestClientProperties restClientProperties, + LoveboxRestClient restClient) { + + public void checkIfUserExists() { + // Check email user + if (restClientProperties.isEnabled()) { + ResponseEntity checkEmailResponse = restClient.checkEmail(new CheckEmailRequestBody(restClientProperties.getEmail())); + log.debug("CheckEmail response: {}", checkEmailResponse); + CheckEmailResponseBody checkEmailResponseBody = checkEmailResponse.getBody(); + if (!checkEmailResponseBody.existingUser()) { + throw new IllegalStateException(String.format("User %s does not exists!", restClientProperties.getEmail())); + } + } + } + + public void simulateDeviceQueries() { + if (restClientProperties.isEnabled()) { + String token = loginAndResolveToken(); + + // Me query + String meQuery = "{\n me {\n _id\n firstName\n email\n boxes {\n _id\n color\n signature\n lovePercentage\n nickname\n notifications {\n disableUntil\n messageRead\n heartReceived\n __typename\n }\n admin {\n _id\n firstName\n email\n __typename\n }\n privacyPolicy\n pairingCode\n isConnected\n isAdmin\n hardware\n hasColor\n hasColorBackup\n connectionDate\n __typename\n }\n relations {\n _id\n name\n relationType\n picture\n color\n streak\n boxId\n loveGoal\n streakDeadline\n reminders {\n day\n meridiem\n number\n weekday\n time\n __typename\n }\n specialDates {\n _id\n name\n date\n dateType\n __typename\n }\n addresses {\n firstname\n lastname\n streetAddress\n zipCode\n city\n country\n state\n __typename\n }\n __typename\n }\n roles\n device {\n _id\n appVersion\n os\n __typename\n }\n profile\n reminder\n premium\n beta\n fcmToken\n language\n loveCoins\n __typename\n }\n}\n"; + GraphqlRequestBody meGraphqlRequestBody = new GraphqlRequestBody(null, null, meQuery); + ResponseEntity meResponse = restClient.graphql("Bearer " + token, meGraphqlRequestBody); + log.debug("Me response: {}", meResponse); + + // Set device query + String setDeviceQuery = "mutation setDevice($deviceId: String!, $deviceParams: JSON) {\n setDevice(deviceId: $deviceId, deviceParams: $deviceParams) {\n _id\n __typename\n }\n}\n"; + Map setDeviceVariables = new HashMap<>(); + setDeviceVariables.put("deviceId", restClientProperties.getDeviceId()); + Map deviceParams = new HashMap<>(); + deviceParams.put("os", "android"); + deviceParams.put("appVersion", "5.4.9"); + deviceParams.put("model", "Nokia 7.2"); + deviceParams.put("Nokia", "Nokia"); + deviceParams.put("osVersion", "10"); + deviceParams.put("hasNotch", false); + deviceParams.put("deviceType", "Handset"); + setDeviceVariables.put("deviceParams", deviceParams); + + GraphqlRequestBody setDeviceGraphqlRequestBody = new GraphqlRequestBody("setDevice", setDeviceVariables, setDeviceQuery); + ResponseEntity setDeviceResponse = restClient.graphql("Bearer " + token, setDeviceGraphqlRequestBody); + log.debug("Set device response: {}", setDeviceResponse); + + // setBoxSignature + String setBoxSignatureQuery = "mutation setBoxSignature($boxId: String, $signature: String) {\n setBoxSignature(boxId: $boxId, signature: $signature)\n}\n"; + Map setBoxSignatureVariables = new HashMap<>(); + setBoxSignatureVariables.put("boxId", restClientProperties.getBoxId()); + setBoxSignatureVariables.put("signature", restClientProperties.getSignature()); + GraphqlRequestBody setBoxSignatureGraphqlRequestBody = new GraphqlRequestBody("setBoxSignature", setBoxSignatureVariables, setBoxSignatureQuery); + ResponseEntity setBoxSignatureResponse = restClient.graphql("Bearer " + token, setBoxSignatureGraphqlRequestBody); + log.debug("Set box signature response: {}", setBoxSignatureResponse); + } + } + + public String loginAndResolveToken() { + // Login with password and get token + if (restClientProperties.isEnabled()) { + ResponseEntity loginWithPasswordResponse = restClient.loginWithPassword(new LoginWithPasswordlRequestBody(restClientProperties.getEmail(), restClientProperties.getPassword())); + log.debug("Login with password response: {}", loginWithPasswordResponse); + return loginWithPasswordResponse.getBody().token(); + } + return null; + } + + @SneakyThrows + public Tripple sendImageMessage(String imageAsBase64) { + if (restClientProperties.isEnabled()) { + String token = loginAndResolveToken(); + + String sendPixNoteQuery = "mutation sendPixNote($channel: ChannelsTypes, $appVersion: String, $postcardStripePaymentId: String, $postcardAddress: JSON, $postcardSettings: JSON, $postcardScheduledDate: Date, $postcardText: String, $recipientRelationId: String, $base64: String, $recipient: String, $date: Date, $options: JSON, $contentType: [String], $timezone: Int, $promotionCode: String) {\n sendPixNote(channel: $channel, appVersion: $appVersion, postcardStripePaymentId: $postcardStripePaymentId, postcardAddress: $postcardAddress, postcardSettings: $postcardSettings, postcardScheduledDate: $postcardScheduledDate, postcardText: $postcardText, recipientRelationId: $recipientRelationId, base64: $base64, recipient: $recipient, date: $date, contentType: $contentType, timezone: $timezone, options: $options, promotionCode: $promotionCode) {\n _id\n channel\n type\n recipient\n postcardStripePayment\n postcardAddress {\n firstname\n lastname\n country\n state\n streetAddress\n city\n zipCode\n __typename\n }\n postcardSettings {\n color\n fontFamily\n fontSize\n __typename\n }\n recipientRelation\n postcardText\n url\n date\n status {\n label\n __typename\n }\n statusList {\n label\n date\n __typename\n }\n senderUser {\n _id\n firstName\n email\n __typename\n }\n privacyPolicy\n addedLoveCoins\n __typename\n }\n}\n"; + Map sendPixNoteVariables = new HashMap<>(); + sendPixNoteVariables.put("channel", "LOVEBOX"); + sendPixNoteVariables.put("base64", imageAsBase64); + sendPixNoteVariables.put("recipient", restClientProperties.getBoxId()); + sendPixNoteVariables.put("recipientRelationId", restClientProperties.getRelationId()); + sendPixNoteVariables.put("contentType", new Object[]{}); + Map options = new HashMap<>(); + options.put("framesBase64", null); + options.put("deviceId", restClientProperties.getDeviceId()); + options.put("privacyPolicy", "ADMIN_AND_ME"); + options.put("templateId", null); + sendPixNoteVariables.put("options", options); + sendPixNoteVariables.put("timezone", 60); + sendPixNoteVariables.put("appVersion", "5.4.9"); + GraphqlRequestBody sendPixNoteGraphqlRequestBody = new GraphqlRequestBody("sendPixNote", sendPixNoteVariables, sendPixNoteQuery); + ResponseEntity sendPixNoteResponse = restClient.graphql("Bearer " + token, sendPixNoteGraphqlRequestBody); + log.debug("Send pix note response: {}", sendPixNoteResponse); + + JsonElement jsonRoot = JsonParser.parseString(sendPixNoteResponse.getBody()); + JsonObject sendPixNote = jsonRoot.getAsJsonObject() + .get("data").getAsJsonObject() + .get("sendPixNote").getAsJsonObject(); + JsonObject stati = sendPixNote + .get("statusList").getAsJsonArray().get(0).getAsJsonObject(); + + String id = sendPixNote.get("_id").getAsString(); + String status = stati.get("label").getAsString(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + LocalDateTime sentTime = LocalDateTime.parse(stati.get("date").getAsString(), formatter); + + return new Tripple<>(id, sentTime, status); + } + return null; + } + + @SneakyThrows + public boolean receiveWaterfallOfHearts() { + boolean getHeartsRain2 = false; + + if (restClientProperties.isEnabled()) { + String token = loginAndResolveToken(); + + // Get hearts rain + String getHeartsRain2Query = "query getHeartsRain2($boxesIds: [String]) {\n getHeartsRain2(boxesIds: $boxesIds)\n}\n"; + Map getHeartsRain2QueryVariables = new HashMap<>(); + List boxesIds = new ArrayList<>(); + boxesIds.add(restClientProperties.getBoxId()); + getHeartsRain2QueryVariables.put("boxesIds", boxesIds); + + GraphqlRequestBody getHeartsRain2GraphqlRequestBody = new GraphqlRequestBody("getHeartsRain2", getHeartsRain2QueryVariables, getHeartsRain2Query); + ResponseEntity getHeartsRain2Response = restClient.graphql("Bearer " + token, getHeartsRain2GraphqlRequestBody); + log.debug("Get hearts rain 2 response: {}", getHeartsRain2Response); + + JsonElement jsonRoot = JsonParser.parseString(getHeartsRain2Response.getBody()); + JsonObject data = jsonRoot.getAsJsonObject().get("data").getAsJsonObject(); + JsonElement getHeartsRain2String = data.get("getHeartsRain2"); + + if (!getHeartsRain2String.isJsonNull()) { + getHeartsRain2 = getHeartsRain2String.getAsBoolean(); + } + + if (getHeartsRain2) { + // (Re)Set hearts rain to false + String setHeartsRainQuery = "mutation setHeartsRain($heartsRain: Boolean) {\n setHeartsRain(heartsRain: $heartsRain)\n}\n"; + Map setHeartsRainQueryVariables = new HashMap<>(); + setHeartsRainQueryVariables.put("heartsRain", false); + + GraphqlRequestBody setHeartsRainGraphqlRequestBody = new GraphqlRequestBody("setHeartsRain", setHeartsRainQueryVariables, setHeartsRainQuery); + ResponseEntity setHeartsRainResponse = restClient.graphql("Bearer " + token, setHeartsRainGraphqlRequestBody); + log.debug("Set hearts rain response: {}", setHeartsRainResponse); + } + } + return getHeartsRain2; + } + + @SneakyThrows + public List> getMessagesByBox() { + List> messageStatus = new ArrayList<>(); + + if (restClientProperties.isEnabled()) { + String token = loginAndResolveToken(); + + // Get hearts rain + String getMessagesByBoxQuery = "query getMessagesByBox($boxId: String, $relationId: String, $messagesShown: Int!) {\n getMessagesByBox(boxId: $boxId, relationId: $relationId, messagesShown: $messagesShown) {\n _id\n channel\n content\n type\n recipient\n date\n status {\n label\n __typename\n }\n statusList {\n label\n date\n __typename\n }\n drawing {\n base64\n rotate\n __typename\n }\n base64\n bytes\n premium\n textOnly\n textCentered\n gifId\n url\n urlId\n frames\n senderUser {\n _id\n firstName\n email\n __typename\n }\n privacyPolicy\n postcardAddress {\n firstname\n lastname\n streetAddress\n zipCode\n city\n country\n state\n __typename\n }\n postcardSettings {\n color\n fontFamily\n fontSize\n __typename\n }\n postcardScheduledDate\n estimatedArrivalDate\n __typename\n }\n}\n"; + Map getMessagesByBoxQueryVariables = new HashMap<>(); + getMessagesByBoxQueryVariables.put("relationId", restClientProperties.getRelationId()); + getMessagesByBoxQueryVariables.put("messagesShown", 0); + + GraphqlRequestBody getMessagesByBoxGraphqlRequestBody = new GraphqlRequestBody("getMessagesByBox", getMessagesByBoxQueryVariables, getMessagesByBoxQuery); + ResponseEntity getMessagesByBoxResponse = restClient.graphql("Bearer " + token, getMessagesByBoxGraphqlRequestBody); + log.debug("Get messages by box response: {}", getMessagesByBoxResponse); + + JsonElement jsonRoot = JsonParser.parseString(getMessagesByBoxResponse.getBody()); + JsonArray getMessagesByBox = jsonRoot.getAsJsonObject() + .get("data").getAsJsonObject() + .get("getMessagesByBox").getAsJsonArray(); + + getMessagesByBox.forEach(m -> { + JsonObject message = m.getAsJsonObject(); + String id = message.get("_id").getAsString(); + String status = message.get("status").getAsJsonObject().get("label").getAsString(); + messageStatus.add(new Pair(id, status)); + }); + } + return messageStatus; + } +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBot.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBot.java new file mode 100644 index 0000000..253642e --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBot.java @@ -0,0 +1,195 @@ +package com.patbaumgartner.lovebox.telegram.sender.telegram; + +import com.patbaumgartner.lovebox.telegram.sender.services.ImageService; +import com.patbaumgartner.lovebox.telegram.sender.services.LoveboxService; +import com.patbaumgartner.lovebox.telegram.sender.utils.Pair; +import com.patbaumgartner.lovebox.telegram.sender.utils.Tripple; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.methods.GetFile; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.methods.send.SendPhoto; +import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageCaption; +import org.telegram.telegrambots.meta.api.objects.InputFile; +import org.telegram.telegrambots.meta.api.objects.Message; +import org.telegram.telegrambots.meta.api.objects.PhotoSize; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import java.io.File; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LoveboxBot extends TelegramLongPollingBot { + + private final LoveboxBotProperties botProperties; + private final ImageService imageService; + private final LoveboxService loveboxService; + + private final ConcurrentHashMap loveboxMessageStore = new ConcurrentHashMap<>(); + private final ConcurrentHashMap telegramMessageStore = new ConcurrentHashMap<>(); + + private long chatId = 0; + + @Scheduled(fixedRate = 10_000) + public void readMessageBox() { + List> messages = loveboxService.getMessagesByBox(); + messages.stream().forEach(p -> { + loveboxMessageStore.computeIfPresent(p.left(), (key, value) -> { + if (!value.equals(p.right())) { + Message message = telegramMessageStore.get(p.left()); + if (message != null) { + updatePhotoMessageCaption(message, p.right()); + } + } + return value; + }); + loveboxMessageStore.put(p.left(), p.right()); + }); + } + + @Scheduled(fixedRate = 10_000) + public void receiveWaterfallOfHearts() { + if (chatId > 0 && loveboxService.receiveWaterfallOfHearts()) { + sendTextMessage(chatId, "You received a waterfall of hearts! ❤❤❤"); + } + } + + @Override + public void onUpdateReceived(Update update) { + if (update.hasMessage()) { + + // Retrieve Message + Message message = update.getMessage(); + if (chatId <= 0) { + chatId = message.getChat().getId(); + } + + String text = null; + Pair imagePair = null; + + // Create Lovebox Image + try { + if (message.hasPhoto()) { + File file = downloadImageFromPhotoMessage(message); + text = message.getCaption(); + imagePair = imageService.resizeImageToPair(file, text); + } + + if (message.hasText()) { + text = message.getText(); + imagePair = imageService.createTextImageToPair(text); + } + + // Set default message + if (imagePair == null) { + imagePair = imageService.createFixedImageToPair(); + } + } catch (RuntimeException e) { + // Suppress exception + log.error("Exception occurred: {}", e.getMessage(), e); + } + + Tripple statusTripple = loveboxService.sendImageMessage(imagePair.left()); + loveboxMessageStore.put(statusTripple.left(), statusTripple.right()); + + // Send/respond Message + Message sentMessage = sendPhotoMessage(chatId, text, imagePair, statusTripple); + telegramMessageStore.put(statusTripple.left(), sentMessage); + } + } + + protected File downloadImageFromPhotoMessage(Message message) { + List photoSizes = message.getPhoto(); + PhotoSize photoSize = photoSizes.get(photoSizes.size() - 1); + + GetFile getFile = new GetFile(); + getFile.setFileId(photoSize.getFileId()); + try { + String filePath = execute(getFile).getFilePath(); + File file = downloadFile(filePath); + log.debug("Download photo \"{}\" from {}", photoSize.getFileId(), filePath); + return file; + } catch (TelegramApiException | RuntimeException e) { + log.error("Failed to download photo \"{}\" due to error: {}", photoSize.getFileId(), e.getMessage()); + } + return null; + } + + protected void sendTextMessage(long chatId, String text) { + SendMessage message = new SendMessage(); + message.setChatId(String.valueOf(chatId)); + message.setText(text); + try { + execute(message); + log.debug("Sent message \"{}\" to {}", text, chatId); + } catch (TelegramApiException | RuntimeException e) { + log.error("Failed to send message \"{}\" to {} due to error: {}", text, chatId, e.getMessage()); + } + } + + protected Message sendPhotoMessage(long chatId, String text, Pair imagePair, Tripple statusTripple) { + SendPhoto message = new SendPhoto(); + message.setChatId(String.valueOf(chatId)); + message.setPhoto(new InputFile(imagePair.right(), "image.png")); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + String formattedDateTime = ZonedDateTime.of(statusTripple.middle(), ZoneId.of("Europe/London")) + .format(formatter); + String caption = String.format("Message with text: \"%s\" status: [%s].\n%s", + text != null ? text : "", + statusTripple.right(), + formattedDateTime); + message.setCaption(caption); + + Message sentMessage = null; + try { + sentMessage = execute(message); + log.debug("Sent message \"{}\" to {}", text, chatId); + } catch (TelegramApiException | RuntimeException e) { + log.error("Failed to send message \"{}\" to {} due to error: {}", text, chatId, e.getMessage()); + } + return sentMessage; + } + + protected void updatePhotoMessageCaption(Message message, String status) { + String text = message.getCaption().replaceAll("\\[.*\\]\\.", "[" + status + "]."); + String chatId = String.valueOf(message.getChatId()); + EditMessageCaption editMessage = EditMessageCaption.builder() + .messageId(message.getMessageId()) + .chatId(chatId) + .caption(text).build(); + try { + execute(editMessage); + log.debug("Sent message \"{}\" to {}", text, chatId); + } catch (TelegramApiException | RuntimeException e) { + log.error("Failed to send message \"{}\" to {} due to error: {}", text, chatId, e.getMessage()); + } + } + + @Override + public void onRegister() { + log.info("Registering TelegramBot with Username: {}", getBotUsername()); + } + + @Override + public String getBotUsername() { + return botProperties.getUsername(); + } + + @Override + public String getBotToken() { + return botProperties.getToken(); + } +} \ No newline at end of file diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBotProperties.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBotProperties.java new file mode 100644 index 0000000..a6f19e0 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/telegram/LoveboxBotProperties.java @@ -0,0 +1,14 @@ +package com.patbaumgartner.lovebox.telegram.sender.telegram; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "bot") +public class LoveboxBotProperties { + + /* Telegram username. */ + private String username; + /* Telegram token. */ + private String token; +} diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Pair.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Pair.java new file mode 100644 index 0000000..ba48554 --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Pair.java @@ -0,0 +1,4 @@ +package com.patbaumgartner.lovebox.telegram.sender.utils; + +public record Pair(A left, B right) { +} \ No newline at end of file diff --git a/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Tripple.java b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Tripple.java new file mode 100644 index 0000000..d92c4fa --- /dev/null +++ b/src/main/java/com/patbaumgartner/lovebox/telegram/sender/utils/Tripple.java @@ -0,0 +1,4 @@ +package com.patbaumgartner.lovebox.telegram.sender.utils; + +public record Tripple(A left, B middle, C right) { +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a44e5e4 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,14 @@ +# Application Settings +logging.level.com.patbaumgartner=debug +# Lovebox Login +lovebox.enabled=true +lovebox.email=me@email.com +lovebox.password=mySecret +# Lovebox Setting +lovebox.signature=Signature +lovebox.device-id=42fab8322d8cec91 +lovebox.relation-id=33c67a2127d7be09142f4326 +lovebox.box-id=417a114e58e15a0214cf3612 +# Telegram Bot Settings +bot.username=Lovebox_bot +bot.token=4072971853:ABEojZ42uNA6YYn_c7DF8RH0UOorqXuveSQ \ No newline at end of file diff --git a/src/main/resources/lovebox.jpeg b/src/main/resources/lovebox.jpeg new file mode 100644 index 0000000..1a8f393 Binary files /dev/null and b/src/main/resources/lovebox.jpeg differ diff --git a/src/test/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplicationTests.java b/src/test/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplicationTests.java new file mode 100644 index 0000000..352d4d1 --- /dev/null +++ b/src/test/java/com/patbaumgartner/lovebox/telegram/sender/LoveboxTelegramSenderApplicationTests.java @@ -0,0 +1,17 @@ +package com.patbaumgartner.lovebox.telegram.sender; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest +@TestPropertySource( + properties = {"lovebox.enabled=false", "telegrambots.enabled=false"} +) +class LoveboxTelegramSenderApplicationTests { + + @Test + void contextLoads() { + } + +}