Compare commits

15 Commits

Author SHA1 Message Date
a06f9940ff #3 change sessions impl 2022-09-25 09:33:42 +03:00
8453ba42f8 up java-telegram-bot-api version 2022-09-24 14:15:22 +03:00
56a4d4957b change log level
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-23 15:17:24 +03:00
c62de38f17 #3 session API draft 2021-09-23 14:56:07 +03:00
05af2f05a4 [maven-release-plugin] prepare for next development iteration
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-17 21:32:41 +03:00
7270a34e97 [maven-release-plugin] prepare release stbf-parent-0.0.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-17 21:32:38 +03:00
a318034d77 add release plugin 2021-09-17 21:27:28 +03:00
5880739849 add release plugin 2021-09-17 21:07:07 +03:00
f2320ff2af #2 trim strings
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-14 23:47:04 +03:00
1765abbdfd add LICENSE 2021-09-14 23:04:15 +03:00
02c5ef32fa #1 temporary store fileId in RAM
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-12 21:04:55 +03:00
2103d03611 #1 initial sending media files support
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-12 01:35:23 +03:00
5bb9e3abab update README
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-11 20:40:20 +03:00
c5806b6b45 API improvements
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-10 19:19:50 +03:00
24b15ca4fe commands with callbackData support
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-09 23:55:14 +03:00
58 changed files with 1597 additions and 358 deletions

202
LICENSE.txt Normal file
View File

@@ -0,0 +1,202 @@
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 (c) 2021 Ruslan Penkrat <penkrat@gmail.com>
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.

135
pom.xml
View File

@@ -1,70 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<packaging>pom</packaging>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>stbf-parent</name>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.2-SNAPSHOT</version>
<name>stbf-parent</name>
<description>Simple Telegram Bot Facade</description>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version>
<slf4j.version>1.7.25</slf4j.version>
</properties>
<description>Simple Telegram Bot Facade</description>
<modules>
<module>stbf-api</module>
<module>stbf-pengrad</module>
<module>stbf-test</module>
<module>stbf-common</module>
<module>stbf-rubenlagus</module>
<module>stbf-templates</module>
<module>stbf-demo</module>
</modules>
<scm>
<connection>scm:git:https://git.penkrat.ru/ruslan/stbf.git</connection>
<developerConnection>scm:git:https://git.penkrat.ru/ruslan/stbf.git</developerConnection>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version>
<slf4j.version>1.7.25</slf4j.version>
</properties>
<distributionManagement>
<repository>
<id>${deployment.releases.id}</id>
<name>Releases</name>
<url>${deployment.releases.url}</url>
</repository>
<snapshotRepository>
<id>${deployment.snapshots.id}</id>
<name>Snapshots</name>
<url>${deployment.snapshots.url}</url>
</snapshotRepository>
<modules>
<module>stbf-api</module>
<module>stbf-pengrad</module>
<module>stbf-test</module>
<module>stbf-common</module>
<module>stbf-rubenlagus</module>
<module>stbf-templates</module>
<module>stbf-demo</module>
</modules>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0-M4</version>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>${deployment.releases.id}</id>
<name>Releases</name>
<url>${deployment.releases.url}</url>
</repository>
<snapshotRepository>
<id>${deployment.snapshots.id}</id>
<name>Snapshots</name>
<url>${deployment.snapshots.url}</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-api</artifactId>

View File

@@ -7,51 +7,51 @@ import lombok.ToString;
@Getter
@Builder
@ToString(of = "text")
@ToString(of = {"text", "inline"})
public class Action {
public static Action simple(String text) {
return builder().text(text).build();
}
public static Action simple(String text) {
return builder().text(text).build();
}
public static Action simple(String text, String cmd) {
validateCmd(cmd);
return builder().text(text).cmd(cmd).build();
}
public static Action simple(String text, String cmd) {
validateCmd(cmd);
return builder().text(text).cmd(cmd).build();
}
public static Action callback(String text, String callbackData) {
return builder().inline(true).text(text).callbackData(callbackData).build();
}
public static Action callback(String text, String callbackData) {
return builder().inline(true).text(text).callbackData(callbackData).build();
}
public static Action requestContact(String text) {
return builder().text(text).requestContact(true).build();
}
public static Action requestContact(String text) {
return builder().text(text).requestContact(true).build();
}
private boolean inline;
private boolean inline;
private String text;
private String text;
private String cmd;
private String cmd;
// in-line
private String callbackData;
private String url;
// in-line
private String callbackData;
private String url;
// keyboard
private boolean requestContact;
private boolean requestLocation;
// keyboard
private boolean requestContact;
private boolean requestLocation;
/**
* Text of the command, 1-32 characters. Can contain only lowercase English
* letters, digits and underscores.
*/
private static void validateCmd(@NonNull String cmd) {
if (cmd.length() > 32) {
throw new IllegalArgumentException("Max length - 32 characters");
}
if (!cmd.startsWith("/")) {
throw new IllegalArgumentException("Command must start / character");
}
// TODO validate lowercase etc
}
/**
* Text of the command, 1-32 characters. Can contain only lowercase English
* letters, digits and underscores.
*/
private static void validateCmd(@NonNull String cmd) {
if (cmd.length() > 32) {
throw new IllegalArgumentException("Max length - 32 characters");
}
if (!cmd.startsWith("/")) {
throw new IllegalArgumentException("Command must start / character");
}
// TODO validate lowercase etc
}
}

View File

@@ -1,6 +1,7 @@
package ru.penkrat.stbf.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.AccessLevel;
@@ -22,7 +23,7 @@ public class BotCommandChain implements CommandChain {
if (!commands.isEmpty()) {
try {
Command command = commands.get(0);
log.debug("Run command {}", command.getClass().getSimpleName());
log.debug("Run command {} ({})", command.getClass().getSimpleName(), command);
command.process(botRequest, botResponse,
new BotCommandChain(commands.subList(1, commands.size())));
} catch (Exception e) {
@@ -36,4 +37,9 @@ public class BotCommandChain implements CommandChain {
return this;
}
public BotCommandChain addAll(Collection<Command> cmds) {
commands.addAll(cmds);
return this;
}
}

View File

@@ -14,4 +14,5 @@ public interface BotRequest {
Long getChatId();
BotSession getSession();
}

View File

@@ -0,0 +1,9 @@
package ru.penkrat.stbf.api;
public interface BotSession {
Object getAttribute(String name);
void setAttribute(String name, Object value);
}

View File

@@ -0,0 +1,31 @@
package ru.penkrat.stbf.api;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import java.util.function.Consumer;
import java.util.function.Supplier;
@Getter
@Builder
@ToString(of = {"mediaType", "url"})
public class Media {
private MediaType mediaType;
private String url;
private String fileId;
private Supplier<byte[]> data;
private Integer duration;
private Integer width;
private Integer height;
private Consumer<String> fileIdConsumer;
}

View File

@@ -0,0 +1,9 @@
package ru.penkrat.stbf.api;
public enum MediaType {
ANIMATION,
AUDIO,
PHOTO,
VIDEO,
VOICE
}

View File

@@ -1,8 +1,20 @@
package ru.penkrat.stbf.api;
import java.util.Objects;
@FunctionalInterface
public interface RequestMatcher {
boolean match(BotRequest botRequest);
boolean match(BotRequest botRequest);
default RequestMatcher or(RequestMatcher other) {
Objects.requireNonNull(other);
return (botRequest) -> match(botRequest) || other.match(botRequest);
}
default RequestMatcher and(RequestMatcher other) {
Objects.requireNonNull(other);
return (botRequest) -> match(botRequest) && other.match(botRequest);
}
}

View File

@@ -2,13 +2,17 @@ package ru.penkrat.stbf.api;
public interface Screen {
String getText();
String getText();
default Keyboard getKeyboard() {
return null;
}
default Media getMedia() {
return null;
}
default ScreenProperties getScreenProperties() {
return ScreenProperties.DEFAULT;
}
default Keyboard getKeyboard() {
return null;
}
default ScreenProperties getScreenProperties() {
return ScreenProperties.DEFAULT;
}
}

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.api;
@FunctionalInterface
public interface SessionProvider {
BotSession get(Long chatId);
}

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-common</artifactId>
@@ -16,7 +14,7 @@
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>

View File

@@ -12,6 +12,10 @@ public abstract class AbstractActionCommand implements Command {
private RequestMatcher matcher;
public AbstractActionCommand(RequestMatcher matcher) {
this.matcher = matcher;
}
public AbstractActionCommand(Action action) {
matcher = RequestMatchers.action(action);
}

View File

@@ -0,0 +1,20 @@
package ru.penkrat.stbf.common.command;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.api.SessionProvider;
import ru.penkrat.stbf.common.session.SessionAwareBotRequest;
@RequiredArgsConstructor
public class SessionAwareCommandChain implements CommandChain {
private final CommandChain delegate;
private final SessionProvider sessionProvider;
@Override
public void processCommand(BotRequest botRequest, BotResponse botResponse) {
delegate.processCommand(new SessionAwareBotRequest(botRequest, sessionProvider), botResponse);
}
}

View File

@@ -1,5 +1,6 @@
package ru.penkrat.stbf.common.command;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
@@ -10,6 +11,7 @@ import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.common.screen.TextScreen;
import ru.penkrat.stbf.tools.RequestMatchers;
@RequiredArgsConstructor
public class SimpleCommand implements Command {
private final RequestMatcher matcher;

View File

@@ -0,0 +1,31 @@
package ru.penkrat.stbf.common.command;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.CommandChain;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPolledCommandChain implements CommandChain {
private final CommandChain delegate;
private final ExecutorService executor;
public ThreadPolledCommandChain(CommandChain delegate) {
this(delegate, 4);
}
public ThreadPolledCommandChain(CommandChain delegate, int nThreads) {
this.delegate = delegate;
this.executor = Executors.newFixedThreadPool(nThreads);
}
@Override
public void processCommand(BotRequest botRequest, BotResponse botResponse) {
executor.execute(() -> {
delegate.processCommand(botRequest, botResponse);
});
}
}

View File

@@ -0,0 +1,23 @@
package ru.penkrat.stbf.common.screen;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.Screen;
@Getter
@RequiredArgsConstructor
public class MediaScreen implements Screen {
private final String text;
private final Media media;
private final Keyboard keyboard;
public MediaScreen(String text, Media media) {
this(text, media, null);
}
}

View File

@@ -0,0 +1,85 @@
package ru.penkrat.stbf.common.screen;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.api.ScreenProperties;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
@Slf4j
@RequiredArgsConstructor
public class RamFileIdStorageMediaScreen implements Screen {
private static final Map<String, String> STORAGE = new ConcurrentHashMap<>();
private final Screen wrapped;
private Media wrappedMedia;
@Override
public Media getMedia() {
if (wrapped.getMedia() == null) {
return null;
}
if (wrappedMedia != null) {
return wrappedMedia;
}
final String url = wrapped.getMedia().getUrl();
wrappedMedia = Media.builder()
.data(getData(url))
.url(getUrl(url))
.fileId(STORAGE.get(url))
.fileIdConsumer(saveFileId(url))
.mediaType(wrapped.getMedia().getMediaType())
.duration(wrapped.getMedia().getDuration())
.height(wrapped.getMedia().getHeight())
.width(wrapped.getMedia().getWidth())
.duration(wrapped.getMedia().getDuration())
.build();
return wrappedMedia;
}
protected String getUrl(String url) {
return STORAGE.containsKey(url) ? null : url;
}
protected Supplier<byte[]> getData(String url) {
return null;
}
@Override
public String getText() {
return wrapped.getText();
}
@Override
public Keyboard getKeyboard() {
return wrapped.getKeyboard();
}
@Override
public ScreenProperties getScreenProperties() {
return wrapped.getScreenProperties();
}
private Consumer<String> saveFileId(String srcUrl) {
return fileId -> {
if (STORAGE.containsKey(srcUrl)) {
return;
}
log.debug("Store {} as {}", srcUrl, fileId);
STORAGE.put(srcUrl, fileId);
wrappedMedia = null;
};
}
}

View File

@@ -0,0 +1,18 @@
package ru.penkrat.stbf.common.session;
import ru.penkrat.stbf.api.BotSession;
import ru.penkrat.stbf.api.SessionProvider;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class InMemBotSessionProvider implements SessionProvider {
private static final Map<Long, BotSession> MAP = new ConcurrentHashMap<>();
@Override
public BotSession get(Long chatId) {
return MAP.computeIfAbsent(chatId, k -> new MapBackedBotSession(new ConcurrentHashMap<>()));
}
}

View File

@@ -0,0 +1,22 @@
package ru.penkrat.stbf.common.session;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotSession;
import java.util.Map;
@RequiredArgsConstructor
class MapBackedBotSession implements BotSession {
private final Map<String, Object> map;
@Override
public Object getAttribute(String name) {
return map.get(name);
}
@Override
public void setAttribute(String name, Object value) {
map.put(name, value);
}
}

View File

@@ -0,0 +1,49 @@
package ru.penkrat.stbf.common.session;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotSession;
import ru.penkrat.stbf.api.SessionProvider;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class SessionAwareBotRequest implements BotRequest {
private final BotRequest delegate;
private final SessionProvider sessionProvider;
private BotSession botSession;
@Override
public BotSession getSession() {
if (botSession == null) {
botSession = sessionProvider.get(getChatId());
Objects.requireNonNull(botSession, "Session can not be null");
}
return botSession;
}
public Optional<String> getMessageText() {
return this.delegate.getMessageText();
}
public Optional<String> getPhoneNumber() {
return this.delegate.getPhoneNumber();
}
public Optional<String> getCallbackData() {
return this.delegate.getCallbackData();
}
public Optional<String> getCallbackMessageText() {
return this.delegate.getCallbackMessageText();
}
public Long getChatId() {
return this.delegate.getChatId();
}
}

View File

@@ -4,17 +4,55 @@ import lombok.experimental.UtilityClass;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.RequestMatcher;
import java.util.Objects;
import java.util.regex.Pattern;
@UtilityClass
public class RequestMatchers {
public RequestMatcher action(Action action) {
return request -> request.getMessageText()
.filter(text -> matchText(action, text))
.isPresent();
}
public RequestMatcher action(Action action) {
return request -> request.getMessageText()
.filter(text -> matchText(action, text))
.isPresent();
}
private static boolean matchText(Action action, String inputText) {
return inputText.equalsIgnoreCase(action.getText())
|| (action.getCmd() != null && inputText.equalsIgnoreCase(action.getCmd()));
}
public RequestMatcher cmd(Action action) {
return request -> request.getMessageText()
.filter(Objects::nonNull)
.filter(data -> Objects.equals(action.getCmd(), data))
.isPresent();
}
public RequestMatcher text(String callbackData) {
return request -> request.getCallbackData()
.filter(Objects::nonNull)
.filter(data -> Objects.equals(callbackData, data))
.isPresent();
}
public RequestMatcher callbackData(String callbackData) {
return request -> request.getCallbackData()
.filter(Objects::nonNull)
.filter(data -> Objects.equals(callbackData, data))
.isPresent();
}
public RequestMatcher callbackDataStartsWith(String callbackDataPrefix) {
return request -> request.getCallbackData()
.filter(Objects::nonNull)
.filter(data -> data.startsWith(callbackDataPrefix))
.isPresent();
}
public RequestMatcher callbackDataRegexp(String callbackDataPrefix) {
return request -> request.getCallbackData()
.filter(Objects::nonNull)
.filter(Pattern.compile(callbackDataPrefix).asPredicate())
.isPresent();
}
private static boolean matchText(Action action, String inputText) {
return inputText.equalsIgnoreCase(action.getText())
|| (action.getCmd() != null && inputText.equalsIgnoreCase(action.getCmd()));
}
}

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<groupId>ru.penkrat.stbf</groupId>
@@ -35,6 +34,11 @@
<groupId>ru.penkrat.stbf</groupId>
<version>${project.version}</version>
</dependency>
<dependency>
<artifactId>stbf-common</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
@@ -108,8 +112,7 @@
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>ru.penkrat.stbf.demo.App</mainClass>
</transformer>
</transformers>

View File

@@ -5,64 +5,67 @@ import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import ru.penkrat.stbf.api.BotCommandChain;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.common.command.SessionAwareCommandChain;
import ru.penkrat.stbf.common.command.ThreadPolledCommandChain;
import ru.penkrat.stbf.common.session.InMemBotSessionProvider;
import ru.penkrat.stbf.impl.pengrad.PengradTelegramBot;
import ru.penkrat.stbf.templates.xml.XmlFlowResolver;
import java.util.concurrent.atomic.AtomicReference;
@CommandLine.Command(name = "java -jar stbf-demo.jar", mixinStandardHelpOptions = true, description = "Run bot",
version = "Simple Telegram bot framework Demo app v. 0.0.1")
version = "Simple Telegram bot framework Demo app v. 0.0.2")
public class App implements Runnable {
private static final Logger log = LoggerFactory.getLogger(App.class);
private static final Logger log = LoggerFactory.getLogger(App.class);
private static AtomicReference<PengradTelegramBot> botAtomicReference = new AtomicReference<>();
private static AtomicReference<PengradTelegramBot> botAtomicReference = new AtomicReference<>();
@CommandLine.Option(names = {"-f", "--file"}, description = "The file with config.")
private String flowFile = "classpath:/flow.xml";
@CommandLine.Option(names = {"-f", "--file"}, description = "The file with config.")
private String flowFile = "classpath:/flow.xml";
@CommandLine.Option(names = {"-t", "--token"}, required = true, description = "bot token 1234:abscdf...")
private String botToken = "";
@CommandLine.Option(names = {"-t", "--token"}, required = true, description = "bot token 1234:abscdf...")
private String botToken = "";
public static void main(String[] args) {
new CommandLine(new App()).execute(args);
}
public static void main(String[] args) {
new CommandLine(new App()).execute(args);
}
private BotCommandChain getCommandChain(String filename) {
XmlFlowResolver flow = new XmlFlowResolver(filename);
private CommandChain getCommandChain(String filename) {
XmlFlowResolver flow = new XmlFlowResolver(filename);
BotCommandChain chain = new BotCommandChain();
flow.getCommands().forEach(chain::add);
BotCommandChain chain = new BotCommandChain();
flow.getCommands().forEach(chain::add);
return chain;
}
return new ThreadPolledCommandChain(new SessionAwareCommandChain(chain, new InMemBotSessionProvider()));
}
private Runnable start(String token, CommandChain chain) {
return () -> botAtomicReference.set(new PengradTelegramBot(token, chain));
}
private Runnable start(String token, CommandChain chain) {
return () -> botAtomicReference.set(new PengradTelegramBot(token, chain));
}
private void onShutdown() {
try {
if (botAtomicReference.get() != null) {
botAtomicReference.get().close();
log.info("Bot finished.");
}
} catch (Exception e) {
log.error("Error:", e);
}
}
private void onShutdown() {
try {
if (botAtomicReference.get() != null) {
botAtomicReference.get().close();
log.info("Bot finished.");
}
} catch (Exception e) {
log.error("Error:", e);
}
}
@Override
public void run() {
Thread botThread = new Thread(start(botToken, getCommandChain(flowFile)));
botThread.setDaemon(false);
botThread.setName("stbf-bot-thread");
@Override
public void run() {
Thread botThread = new Thread(start(botToken, getCommandChain(flowFile)));
botThread.setDaemon(false);
botThread.setName("stbf-bot-thread");
Runtime.getRuntime().addShutdownHook(new Thread(this::onShutdown));
Runtime.getRuntime().addShutdownHook(new Thread(this::onShutdown));
log.info("Starting bot...");
botThread.start();
log.info("Bot started.");
log.info("Press Ctrl+C to exit.");
}
log.info("Starting bot...");
botThread.start();
log.info("Bot started.");
log.info("Press Ctrl+C to exit.");
}
}

View File

@@ -1,28 +1,114 @@
<flow>
<media>
<video id="40001" url="http://techslides.com/demos/sample-videos/small.mp4"/>
<photo id="40002" url="https://telegram.org/img/t_logo.png"/>
</media>
<actions>
<action id="10001" name="start-action" command="/start">Start</action>
<action id="10002" name="help-action" command="/help">Help</action>
<action id="10003" name="to-inline-action" command="/inline">Inline</action>
<action id="10004" name="inline1-action" callbackData="cmd:inline1">🔞 Inline button #1</action>
<action id="10005" name="inline2-action" callbackData="cmd:inline2">🐱 Inline button #2</action>
<action id="10006" name="url-action" url="https://git.penkrat.ru/ruslan/stbf">💻 Git repo</action>
<action id="10007" name="photo-action" callbackData="cmd:sendPhoto">🖼 My photo</action>
<action id="10008" name="video-action" callbackData="cmd:sendVideo">🎞 My video</action>
<action id="10009" name="to-inline-back-action" callbackData="cmd:inline">🔙 Back</action>
</actions>
<screens>
<screen id="20001" name="on-start-screen">
<text>This is demo bot</text>
<text>This is demo bot
use Help or /help to view help
</text>
<keyboard>
<row>
<button actionRef="help-action">Action.name</button>
</row>
</keyboard>
</screen>
<screen id="20001" name="on-help-screen">
<text>This is demo help</text>
<screen id="20002" name="on-help-screen">
<text>This is demo help
use /inline to switch to inline mode
</text>
<keyboard>
<row>
<button actionRef="help-action">Action.name</button>
</row>
</keyboard>
</screen>
<screen id="20003" name="inline-test-screen">
<text>This is inline buttons:</text>
<keyboard>
<row>
<button actionRef="inline1-action"></button>
<button actionRef="inline2-action"></button>
</row>
<row>
<button actionRef="photo-action"></button>
<button actionRef="video-action"></button>
</row>
<row>
<button actionRef="url-action"></button>
</row>
</keyboard>
</screen>
<screen id="20004" name="inline-test-1-screen">
<text>Inline Screen #1</text>
<keyboard>
<row>
<button actionRef="inline1-action"></button>
<button actionRef="inline2-action"></button>
</row>
<row>
<button actionRef="photo-action"></button>
<button actionRef="video-action"></button>
</row>
<row>
<button actionRef="url-action"></button>
</row>
</keyboard>
</screen>
<screen id="20005" name="inline-test-2-screen">
<text>Inline Screen #2
================</text>
<keyboard>
<row>
<button actionRef="inline1-action"></button>
<button actionRef="inline2-action"></button>
</row>
<row>
<button actionRef="photo-action"></button>
<button actionRef="video-action"></button>
</row>
<row>
<button actionRef="url-action"></button>
</row>
</keyboard>
</screen>
<screen id="20006" name="inline-photo-screen" mediaRef="40002">
<text>My photo</text>
<keyboard>
<row>
<button actionRef="to-inline-back-action"></button>
</row>
</keyboard>
</screen>
<screen id="20007" name="inline-video-screen" mediaRef="40001">
<text>My Video</text>
<keyboard>
<row>
<button actionRef="to-inline-back-action"></button>
</row>
</keyboard>
</screen>
</screens>
<commands>
<command actionRef="start-action" screenRef="on-start-screen" id="30001" name="startCommand"/>
<command actionRef="help-action" screenRef="on-help-screen" id="30002" name="helpCommand"/>
<command actionRef="to-inline-action" screenRef="inline-test-screen" id="30003" name="inlineTestCommand"/>
<command actionRef="to-inline-back-action" screenRef="inline-test-screen" replace="true" id="30004" name="inlineTestCommand"/>
<command actionRef="inline1-action" screenRef="inline-test-1-screen" edit="true" id="30005" name="inlineTest1Command"/>
<command actionRef="inline2-action" screenRef="inline-test-2-screen" edit="true" id="30006" name="inlineTest2Command"/>
<command actionRef="photo-action" screenRef="inline-photo-screen" replace="true" id="30005" name="photoCommand"/>
<command actionRef="video-action" screenRef="inline-video-screen" replace="true" id="30006" name="videoCommand"/>
</commands>
</flow>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-pengrad</artifactId>
@@ -18,12 +16,12 @@
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.pengrad</groupId>
<artifactId>java-telegram-bot-api</artifactId>
<version>[4.9.0,5.2.0]</version>
<version>6.2.0</version>
</dependency>
</dependencies>

View File

@@ -1,17 +1,17 @@
package ru.penkrat.stbf.impl.pengrad;
import static lombok.AccessLevel.PROTECTED;
import java.util.Optional;
import com.pengrad.telegrambot.model.CallbackQuery;
import com.pengrad.telegrambot.model.Contact;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotSession;
import java.util.Optional;
import static lombok.AccessLevel.PROTECTED;
@RequiredArgsConstructor
public class BotRequestImpl implements BotRequest {
@@ -58,4 +58,9 @@ public class BotRequestImpl implements BotRequest {
.orElseGet(() -> update.message()).chat().id();
}
@Override
public BotSession getSession() {
throw new UnsupportedOperationException("Session is not supported");
}
}

View File

@@ -2,11 +2,17 @@ package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.*;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup;
import com.pengrad.telegrambot.request.AbstractSendRequest;
import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.response.BaseResponse;
import com.pengrad.telegrambot.response.SendResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse;
@@ -22,20 +28,17 @@ public class BotResponseImpl implements BotResponse {
@Override
public void send(Screen screen) {
log.debug("Send message: \n============\n{}\n============", screen.getText().trim());
log.trace("Send trace: \n============\n{}\n============", screen.getText().trim());
SendMessage sendMessage = new SendMessage(chatId(), screen.getText().trim())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
AbstractSendRequest<? extends AbstractSendRequest> sendMessage = SendMethodUtils.createFromScreen(chatId(), screen);
if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
KeyboardButton[][] keyboard = kk.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
if (log.isTraceEnabled()) {
log.trace("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) {
@@ -43,11 +46,14 @@ public class BotResponseImpl implements BotResponse {
} else if (keyboard != null && keyboard.length > 0) {
sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard));
} else {
log.debug("No keyboard");
log.trace("No keyboard");
}
}
telegramBot.execute(sendMessage);
final SendResponse sendResponse = telegramBot.execute(sendMessage);
SendMethodUtils.processResponse(screen, sendResponse);
log.debug("Response message = {}", sendResponse.message().messageId());
}
@Override
@@ -73,18 +79,23 @@ public class BotResponseImpl implements BotResponse {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
if (log.isTraceEnabled()) {
log.trace("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) {
editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else {
log.debug("No keyboard");
log.trace("No keyboard");
}
}
telegramBot.execute(editMessage);
final BaseResponse response = telegramBot.execute(editMessage);
if (response.isOk()) {
log.debug("Response ok, edit message = {}", messageId());
} else {
log.debug("Response error code {}", response.errorCode());
}
}
@Override

View File

@@ -1,6 +1,6 @@
package ru.penkrat.stbf.impl.pengrad;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import lombok.Value;
@@ -58,7 +58,7 @@ class KeyboardImpl implements Keyboard {
return text.computeIfAbsent(btn, o -> {
try {
Field text = btn.getClass().getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
ReflectionHelper.makeAccessible(text);
return String.valueOf(text.get(btn));
} catch (NoSuchFieldException | IllegalAccessException e) {
return "*error*";

View File

@@ -0,0 +1,14 @@
package ru.penkrat.stbf.impl.pengrad;
import ru.penkrat.stbf.api.Keyboard;
class NoKeyboard implements Keyboard {
@Override
public String toString() {
return "EmptyKeyboard(pengrad)";
}
}

View File

@@ -2,149 +2,164 @@ package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
public class PengradKeyboardBuilder implements KeyboardBuilder {
private KeyboardButton[][] keyboard;
private KeyboardButton[][] keyboard;
private InlineKeyboardButton[][] inlineKeyboard;
private InlineKeyboardButton[][] inlineKeyboard;
private String keyboardStr = "";
private String keyboardStr = "";
public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build();
}
public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build();
}
@Override
public Keyboard build() {
return new KeyboardImpl(keyboard, inlineKeyboard);
}
@Override
public Keyboard build() {
if (keyboard != null) {
return new KeyboardImpl(keyboard, null);
}
if (inlineKeyboard != null) {
return new KeyboardImpl(null, inlineKeyboard);
}
return new NoKeyboard();
}
public KeyboardBuilder addGetContact(String text) {
put(new KeyboardButton(text).requestContact(true));
return self();
}
public KeyboardBuilder addGetContact(String text) {
put(new KeyboardButton(text).requestContact(true));
return self();
}
public KeyboardBuilder add(String text) {
put(new KeyboardButton(text));
return self();
}
public KeyboardBuilder add(String text) {
put(new KeyboardButton(text));
return self();
}
@Override
public KeyboardBuilder add(Action action) {
put(action);
return self();
}
@Override
public KeyboardBuilder add(Action action) {
put(action);
return self();
}
public KeyboardBuilder row(KeyboardButton... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
public KeyboardBuilder row(KeyboardButton... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder row(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder row(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
if (buttons.length > 0) {
if (buttons[0].isInline()) {
nextRowI();
} else {
nextRow();
}
}
return self();
}
@Override
public KeyboardBuilder column(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
if (buttons[i].isInline()) {
nextRowI();
} else {
nextRow();
}
}
return self();
}
@Override
public KeyboardBuilder column(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
if (buttons[i].isInline()) {
nextRowI();
} else {
nextRow();
}
}
return self();
}
public KeyboardBuilder addNl(String text) {
add(text);
nextRow();
return self();
}
public KeyboardBuilder addNl(String text) {
add(text);
nextRow();
return self();
}
public PengradKeyboardBuilder add(String text, String data) {
put(new InlineKeyboardButton(text).callbackData(data));
return self();
}
public PengradKeyboardBuilder add(String text, String data) {
put(new InlineKeyboardButton(text).callbackData(data));
return self();
}
public KeyboardBuilder addNl(String text, String data) {
add(text, data);
nextRowI();
return self();
}
public KeyboardBuilder addNl(String text, String data) {
add(text, data);
nextRowI();
return self();
}
private void nextRow() {
KeyboardButton[][] n = new KeyboardButton[keyboard.length + 1][];
System.arraycopy(keyboard, 0, n, 0, keyboard.length);
keyboard = n;
keyboard[keyboard.length - 1] = new KeyboardButton[] {};
}
private void nextRow() {
KeyboardButton[][] n = new KeyboardButton[keyboard.length + 1][];
System.arraycopy(keyboard, 0, n, 0, keyboard.length);
keyboard = n;
keyboard[keyboard.length - 1] = new KeyboardButton[]{};
}
private void nextRowI() {
InlineKeyboardButton[][] n = new InlineKeyboardButton[inlineKeyboard.length + 1][];
System.arraycopy(inlineKeyboard, 0, n, 0, inlineKeyboard.length);
inlineKeyboard = n;
inlineKeyboard[inlineKeyboard.length - 1] = new InlineKeyboardButton[] {};
}
private void nextRowI() {
InlineKeyboardButton[][] n = new InlineKeyboardButton[inlineKeyboard.length + 1][];
System.arraycopy(inlineKeyboard, 0, n, 0, inlineKeyboard.length);
inlineKeyboard = n;
inlineKeyboard[inlineKeyboard.length - 1] = new InlineKeyboardButton[]{};
}
protected PengradKeyboardBuilder self() {
return this;
}
protected PengradKeyboardBuilder self() {
return this;
}
private void put(Action action) {
if (action.isInline()) {
put(new InlineKeyboardButton(action.getText()).callbackData(action.getCallbackData()));
} else {
put(new KeyboardButton(action.getText()).requestContact(action.isRequestContact()));
}
}
private void put(Action action) {
if (action.isInline()) {
put(new InlineKeyboardButton(action.getText())
.callbackData(action.getCallbackData())
.url(action.getUrl()));
} else {
put(new KeyboardButton(action.getText())
.requestContact(action.isRequestContact())
.requestLocation(action.isRequestLocation()));
}
}
private void put(KeyboardButton btn) {
if (keyboard == null) {
keyboard = new KeyboardButton[][] {
new KeyboardButton[] { btn }
};
} else {
KeyboardButton[] k = keyboard[keyboard.length - 1];
KeyboardButton[] n = new KeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
keyboard[keyboard.length - 1] = n;
}
}
private void put(KeyboardButton btn) {
if (keyboard == null) {
keyboard = new KeyboardButton[][]{
new KeyboardButton[]{btn}
};
} else {
KeyboardButton[] k = keyboard[keyboard.length - 1];
KeyboardButton[] n = new KeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
keyboard[keyboard.length - 1] = n;
}
}
private void put(InlineKeyboardButton btn) {
if (inlineKeyboard == null) {
inlineKeyboard = new InlineKeyboardButton[][] {
new InlineKeyboardButton[] { btn }
};
} else {
InlineKeyboardButton[] k = inlineKeyboard[inlineKeyboard.length - 1];
InlineKeyboardButton[] n = new InlineKeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
inlineKeyboard[inlineKeyboard.length - 1] = n;
}
private void put(InlineKeyboardButton btn) {
if (inlineKeyboard == null) {
inlineKeyboard = new InlineKeyboardButton[][]{
new InlineKeyboardButton[]{btn}
};
} else {
InlineKeyboardButton[] k = inlineKeyboard[inlineKeyboard.length - 1];
InlineKeyboardButton[] n = new InlineKeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
inlineKeyboard[inlineKeyboard.length - 1] = n;
}
}
}
@Override
public KeyboardBuilder newInstance() {
return new PengradKeyboardBuilder();
}
@Override
public KeyboardBuilder newInstance() {
return new PengradKeyboardBuilder();
}
}

View File

@@ -2,11 +2,13 @@ package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.UpdatesListener;
import com.pengrad.telegrambot.model.CallbackQuery;
import com.pengrad.telegrambot.model.Update;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.CommandChain;
import java.util.Optional;
@Slf4j
public class PengradTelegramBot extends TelegramBot implements AutoCloseable {
@@ -15,6 +17,13 @@ public class PengradTelegramBot extends TelegramBot implements AutoCloseable {
this.setUpdatesListener(updates -> {
for (Update update : updates) {
try {
final Long chatId = Optional.of(update)
.map(Update::callbackQuery)
.map(CallbackQuery::message)
.orElseGet(() -> update.message()).chat().id();
log.debug("New message in chat {}", chatId);
commandChain.processCommand(
new BotRequestImpl(update),
new BotResponseImpl(update, this));

View File

@@ -0,0 +1,186 @@
package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.AbstractSendRequest;
import com.pengrad.telegrambot.request.SendAnimation;
import com.pengrad.telegrambot.request.SendAudio;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendPhoto;
import com.pengrad.telegrambot.request.SendVideo;
import com.pengrad.telegrambot.request.SendVoice;
import com.pengrad.telegrambot.response.SendResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.Screen;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@UtilityClass
class SendMethodUtils {
public AbstractSendRequest<? extends AbstractSendRequest> createFromScreen(@NonNull Object chatId, @NonNull Screen screen) {
if (isMedia(screen)) {
final Media media = screen.getMedia();
switch (media.getMediaType()) {
case ANIMATION:
return Setter.to(createSendAnimation(chatId, media))
.setNotNUll(SendAnimation::caption, screen.getText())
.setNotNUll(SendAnimation::width, media.getWidth())
.setNotNUll(SendAnimation::height, media.getHeight())
.setNotNUll(SendAnimation::duration, media.getDuration())
.set(SendAnimation::parseMode, screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2)
.get();
case AUDIO:
return Setter.to(createSendAudio(chatId, media))
.setNotNUll(SendAudio::caption, screen.getText())
.setNotNUll(SendAudio::duration, media.getDuration())
.set(SendAudio::parseMode, screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2)
.get();
case PHOTO:
return Setter.to(createSendPhoto(chatId, media))
.setNotNUll(SendPhoto::caption, screen.getText())
.set(SendPhoto::parseMode, screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2)
.get();
case VIDEO:
return Setter.to(createSendVideo(chatId, media))
.setNotNUll(SendVideo::caption, screen.getText())
.setNotNUll(SendVideo::width, media.getWidth())
.setNotNUll(SendVideo::height, media.getHeight())
.setNotNUll(SendVideo::duration, media.getDuration())
.set(SendVideo::parseMode, screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2)
.get();
case VOICE:
return Setter.to(createSendVoice(chatId, media))
.setNotNUll(SendVoice::caption, screen.getText())
.setNotNUll(SendVoice::duration, media.getDuration())
.set(SendVoice::parseMode, screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2)
.get();
default:
throw new IllegalStateException("Unsupported media type " + media.getMediaType());
}
}
return new SendMessage(chatId, screen.getText().trim())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
}
public static void processResponse(Screen screen, SendResponse sendResponse) {
if (isMedia(screen)) {
final Consumer<String> fileIdConsumer = screen.getMedia().getFileIdConsumer();
if (fileIdConsumer != null) {
final Message message = sendResponse.message();
if (message.animation() != null)
fileIdConsumer.accept(message.animation().fileId());
else if (message.audio() != null)
fileIdConsumer.accept(message.audio().fileId());
else if (message.photo() != null && message.photo().length > 0)
fileIdConsumer.accept(message.photo()[0].fileId());
else if (message.video() != null)
fileIdConsumer.accept(message.video().fileId());
else if (message.voice() != null)
fileIdConsumer.accept(message.voice().fileId());
}
}
}
@NotNull
private SendVoice createSendVoice(@NotNull Object chatId, Media media) {
if (media.getData() != null) {
return new SendVoice(chatId, media.getData().get());
}
if (media.getFileId() != null) {
return new SendVoice(chatId, media.getFileId());
}
return new SendVoice(chatId, media.getUrl());
}
@NotNull
private SendVideo createSendVideo(@NotNull Object chatId, Media media) {
if (media.getData() != null) {
return new SendVideo(chatId, media.getData().get());
}
if (media.getFileId() != null) {
return new SendVideo(chatId, media.getFileId());
}
return new SendVideo(chatId, media.getUrl());
}
@NotNull
private SendAudio createSendAudio(@NotNull Object chatId, Media media) {
if (media.getData() != null) {
return new SendAudio(chatId, media.getData().get());
}
if (media.getFileId() != null) {
return new SendAudio(chatId, media.getFileId());
}
return new SendAudio(chatId, media.getUrl());
}
@NotNull
private SendAnimation createSendAnimation(@NotNull Object chatId, Media media) {
if (media.getData() != null) {
return new SendAnimation(chatId, media.getData().get());
}
if (media.getFileId() != null) {
return new SendAnimation(chatId, media.getFileId());
}
return new SendAnimation(chatId, media.getUrl());
}
@NotNull
private SendPhoto createSendPhoto(@NotNull Object chatId, Media media) {
if (media.getData() != null) {
return new SendPhoto(chatId, media.getData().get());
}
if (media.getFileId() != null) {
return new SendPhoto(chatId, media.getFileId());
}
return new SendPhoto(chatId, media.getUrl());
}
private boolean isMedia(Screen screen) {
return screen.getMedia() != null;
}
@RequiredArgsConstructor(staticName = "to")
private static class Setter<T> {
private final T target;
<V> Setter<T> setNotNUll(BiFunction<T, V, T> setter, V value) {
if (value != null) {
setter.apply(target, value);
}
return this;
}
<V> Setter<T> set(BiFunction<T, V, T> setter, V value) {
setter.apply(target, value);
return this;
}
T get() {
return target;
}
}
}

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-rubenlagus</artifactId>
@@ -17,7 +15,7 @@
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.telegram</groupId>

View File

@@ -1,17 +1,17 @@
package ru.penkrat.stbf.impl.rubenlagus;
import static lombok.AccessLevel.PROTECTED;
import java.util.Optional;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Contact;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotSession;
import java.util.Optional;
import static lombok.AccessLevel.PROTECTED;
@RequiredArgsConstructor
class BotRequestImpl implements BotRequest {
@@ -52,7 +52,12 @@ class BotRequestImpl implements BotRequest {
@Override
public Long getChatId() {
return update.getMessage().getChatId();
return Utils.getChatId(update);
}
@Override
public BotSession getSession() {
throw new UnsupportedOperationException("Session is not supported");
}
}

View File

@@ -1,12 +1,11 @@
package ru.penkrat.stbf.impl.rubenlagus;
import lombok.extern.slf4j.Slf4j;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotCommandChain;
@Slf4j

View File

@@ -0,0 +1,21 @@
package ru.penkrat.stbf.impl.rubenlagus;
import lombok.experimental.UtilityClass;
import org.telegram.telegrambots.meta.api.objects.Update;
@UtilityClass
class Utils {
Long getChatId(Update update) {
if (update.getMessage() != null) {
return update.getMessage().getChatId();
}
if (update.getCallbackQuery() != null) {
update.getCallbackQuery().getMessage().getChatId();
}
throw new IllegalStateException("Can't extract chatId from 'Update'");
}
}

View File

@@ -7,12 +7,13 @@
<actions> </actions>
<screens> </screens>
<commands> </commands>
<media> </media>
</flow>
```
### Actions
Action - Это действие пользователя бота, которое бот должен обработать
Action - Это действие пользователя бота, которое бот должен обработать, а также соответсвующая кнопка или inline-кнопка.
```xml
<action id="10002" name="help-action" command="/help">Help</action>
@@ -22,14 +23,23 @@ Action - Это действие пользователя бота, которо
`command` - команда Telegram, должна начинаться со знака "/"
`requestContact="true|false"` - кнопка запроса контактных данных
`requestLocation="true|false"` - кнопка запроса метоположения
содержимое тега - вводимый текст, используется как экранная кнопка
`url` - ссылка для inline-кнопки
`callbackData` - callback данные для inline-кнопки
`callbackDataRegexp` - regexp для обработки callback программным способом
`callbackDataStartWith` - префикс, для обработки callback программным способом
### Screens
Screen - то, что бот ответит пользователю,
обычно текст и набор кнопок с действиями.
Screen - то, что бот ответит пользователю, обычно текст и набор кнопок с действиями. В качестве текста может задаваться шаблон, контекст передается при программной обработке при установленном `TemplateRenderer`
```xml
<screen id="20001" name="on-start-screen">
@@ -42,17 +52,61 @@ Screen - то, что бот ответит пользователю,
</screen>
```
`mediaRef` - ссылка на элемент медиа, который будет оправлен ботом
`text` - выводимый текст
`keyboard` - описание клавиатуры
### Keyboard
Описание клавиатуры
#### Row
Одна строка кнопок клавиатуры
#### Button
`if` - видимость кнопки, значение `true`, `false` или имя метода из контекста экрана (для программной обработки)
`actionRef` - `id` или `name` action, описанный в соответсвующей секции
Если кнопка не ссылается на action, то можно задать следующие свойства:
`url` - ссылка для inline-кнопки
`callbackData` - callback данные для inline-кнопки
`callbackDataRegexp` - regexp для обработки callback программным способом
`callbackDataStartWith` - префикс, для обработки callback программным способом
### Commands
Простая команда, при соверщении action будет выведен screen
Простая команда, при совершении `action` будет выведен `screen`. Используется для простых действий, не требующей программной логики.
```xml
<command actionRef="start-action" screenRef="on-start-screen" id="30001" name="startCommand"/>
```
`actionRef` - ссылка на action, может использоваться id или name
`screenRef` - ссылка на screen, может использоваться id или name
`edit` = `true|false`- исходное сообщение будет отредактировано (актуально для callback)
`replace` = `true|false`- исходное сообщение будет удалено, и отправлено новое (актуально для callback,
если меняется тип сообщения т.е. сообщение с фото, видео должно быть заменено на текстовое и наоборот)
### Media
Описывает медиа-ресурсы, доступные для отправки ботом
```xml
<media>
<video id="40001" url="https://example.com/video.mp4"/>
<photo id="40002" url="https://example.com//photo.png"/>
</media>
```

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-templates</artifactId>
<properties>

View File

@@ -1,8 +1,11 @@
package ru.penkrat.stbf.templates;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.RequestMatcher;
public interface ActionResolver {
Action getAction(String name);
RequestMatcher getMatcher(String actionName);
}

View File

@@ -0,0 +1,9 @@
package ru.penkrat.stbf.templates;
import ru.penkrat.stbf.api.Media;
public interface MediaResolver {
Media getMedia(String name);
}

View File

@@ -2,9 +2,14 @@ package ru.penkrat.stbf.templates.utils;
import lombok.experimental.UtilityClass;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@UtilityClass
public class StringUtils {
public static final String NL_DELIMITER = "\n";
public boolean isEmpty(String string) {
return string == null || string.isEmpty();
}
@@ -12,4 +17,13 @@ public class StringUtils {
public boolean isNotEmpty(String string) {
return !isEmpty(string);
}
public String ltrim(String text) {
if (text == null) {
return null;
}
return Stream.of(text.split(NL_DELIMITER))
.map(string -> string.trim())
.collect(Collectors.joining(NL_DELIMITER));
}
}

View File

@@ -24,6 +24,12 @@ class ActionItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String callbackData;
@JacksonXmlProperty(isAttribute = true)
private String callbackDataRegexp;
@JacksonXmlProperty(isAttribute = true)
private String callbackDataStartWith;
@JacksonXmlProperty(isAttribute = true)
private String url;
}

View File

@@ -21,4 +21,10 @@ public class CommandItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true, localName = "class")
private String clazz;
@JacksonXmlProperty(isAttribute = true)
private boolean edit;
@JacksonXmlProperty(isAttribute = true)
private boolean replace;
}

View File

@@ -1,8 +1,10 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.RequestMatcher;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.utils.StringUtils;
import ru.penkrat.stbf.tools.RequestMatchers;
class FlowActionResolverDelegate implements ActionResolver {
@@ -17,7 +19,7 @@ class FlowActionResolverDelegate implements ActionResolver {
final ActionItem actionItem = resolver.get(key);
boolean isInline = StringUtils.isNotEmpty(actionItem.getCallbackData())
&& StringUtils.isNotEmpty(actionItem.getUrl());
|| StringUtils.isNotEmpty(actionItem.getUrl());
if (isInline) {
if (StringUtils.isNotEmpty(actionItem.getCommand())) {
@@ -43,4 +45,21 @@ class FlowActionResolverDelegate implements ActionResolver {
.url(actionItem.getUrl())
.build();
}
@Override
public RequestMatcher getMatcher(String actionName) {
final ActionItem actionItem = resolver.get(actionName);
if (StringUtils.isNotEmpty(actionItem.getCallbackDataRegexp())) {
return RequestMatchers.callbackDataRegexp(actionItem.getCallbackDataRegexp());
}
if (StringUtils.isNotEmpty(actionItem.getCallbackDataStartWith())) {
return RequestMatchers.callbackDataStartsWith(actionItem.getCallbackDataStartWith());
}
if (StringUtils.isNotEmpty(actionItem.getCallbackData())) {
return RequestMatchers.callbackData(actionItem.getCallbackData());
}
return RequestMatchers.action(getAction(actionName));
}
}

View File

@@ -12,7 +12,6 @@ import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.CommandResolver;
import ru.penkrat.stbf.templates.ScreenResolver;
import ru.penkrat.stbf.templates.utils.StringUtils;
import ru.penkrat.stbf.tools.RequestMatchers;
import java.util.Collection;
import java.util.Collections;
@@ -51,31 +50,38 @@ public class FlowCommandResolverDelegate implements CommandResolver {
private Command createCommand(CommandItem commandItem) {
Action action = null;
RequestMatcher actionMatcher = null;
Screen screen = null;
Function<Object, Screen> screenFactory;
if (StringUtils.isNotEmpty(commandItem.getActionRef())) {
action = actionResolver.getAction(commandItem.getActionRef());
actionMatcher = actionResolver.getMatcher(commandItem.getActionRef());
}
if (StringUtils.isNotEmpty(commandItem.getScreenRef())) {
screen = screenResolver.getScreen(commandItem.getScreenRef());
screenFactory = screenResolver.getScreenFactory(commandItem.getScreenRef());
}
if (action != null && screen != null) {
return simpleCommand(action, screen, commandItem.getId(), commandItem.getName());
if (actionMatcher != null && screen != null) {
return simpleCommand(actionMatcher, screen, commandItem.isEdit(), commandItem.isReplace(), commandItem.getId(), commandItem.getName());
}
return null;
}
private Command simpleCommand(Action action, Screen screen, String id, String name) {
private Command simpleCommand(RequestMatcher matcher, Screen screen, boolean edit, boolean replace, String id, String name) {
return new Command() {
RequestMatcher matcher = RequestMatchers.action(action);
@Override
public void process(BotRequest botRequest, BotResponse botResponse, CommandChain chain) {
if (matcher.match(botRequest)) {
botResponse.send(screen);
if (replace) {
botResponse.deleteMessage();
}
if (edit && !replace) {
botResponse.edit(screen);
} else {
botResponse.send(screen);
}
}
chain.processCommand(botRequest, botResponse);
}

View File

@@ -0,0 +1,58 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.MediaType;
import ru.penkrat.stbf.templates.MediaResolver;
import java.util.Collection;
import java.util.Optional;
class FlowMediaResolverDelegate implements MediaResolver {
private final NamedItemResolver<MediaItem> videosResolver;
private final NamedItemResolver<MediaItem> photosResolver;
FlowMediaResolverDelegate(FlowRoot src) {
videosResolver = new NamedItemResolver<>(Traversal.traverse(src, FlowMediaResolverDelegate::videos));
photosResolver = new NamedItemResolver<>(Traversal.traverse(src, FlowMediaResolverDelegate::photos));
}
@Override
public Media getMedia(String name) {
Optional<Media> media = videosResolver.getOpt(name)
.map(FlowMediaResolverDelegate::toVideo);
if(media.isPresent()){
return media.get();
}
media = photosResolver.getOpt(name)
.map(FlowMediaResolverDelegate::toPhoto);
if(media.isPresent()){
return media.get();
}
throw new IllegalStateException("MediaElement not found by 'id' or 'name' " + name);
}
private static Media toVideo(MediaItem item) {
return Media.builder()
.mediaType(MediaType.VIDEO)
.url(item.getUrl())
.build();
}
private static Media toPhoto(MediaItem item) {
return Media.builder()
.mediaType(MediaType.PHOTO)
.url(item.getUrl())
.build();
}
private static Collection<MediaItem> videos(FlowRoot root) {
return root.getMedia().getVideos();
}
private static Collection<MediaItem> photos(FlowRoot root) {
return root.getMedia().getPhotos();
}
}

View File

@@ -31,6 +31,10 @@ class FlowRoot {
@JsonProperty("command")
private List<CommandItem> commands = new ArrayList<>();
@Getter
@JsonProperty("media")
private MediaRoot media = new MediaRoot();
@Getter
private transient List<FlowRoot> included = new ArrayList<>();
}

View File

@@ -6,10 +6,14 @@ import lombok.val;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.common.screen.MediaScreen;
import ru.penkrat.stbf.common.screen.RamFileIdStorageMediaScreen;
import ru.penkrat.stbf.common.screen.TextScreen;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.KeyboardProvider;
import ru.penkrat.stbf.templates.MediaResolver;
import ru.penkrat.stbf.templates.ScreenResolver;
import ru.penkrat.stbf.templates.TemplateRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils;
@@ -25,11 +29,14 @@ class FlowScreenResolverDelegate implements ScreenResolver {
private final ActionResolver actionResolver;
private final MediaResolver mediaResolver;
@Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver) {
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver, MediaResolver mediaResolver) {
this.actionResolver = actionResolver;
this.mediaResolver = mediaResolver;
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getScreens));
}
@@ -37,14 +44,35 @@ class FlowScreenResolverDelegate implements ScreenResolver {
public Screen getScreen(String name) {
ScreenItem item = resolver.get(name);
return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null));
String text = item.getText();
if (!item.isPreserve()) {
text = StringUtils.ltrim(text);
}
if (StringUtils.isNotEmpty(item.getMediaRef())) {
final Media media = mediaResolver.getMedia(item.getMediaRef());
final MediaScreen mediaScreen = new MediaScreen(text, media, buildKeyboard(item.getKeyboard(), null));
return new RamFileIdStorageMediaScreen(mediaScreen);
}
return new TextScreen(text, buildKeyboard(item.getKeyboard(), null));
}
@Override
public Screen getScreen(String name, Object context) {
ScreenItem item = resolver.get(name);
return new TextScreen(templateRenderer.render(item.getText(), context),
String text = item.getText();
if (!item.isPreserve()) {
text = StringUtils.ltrim(text);
}
if (StringUtils.isNotEmpty(item.getMediaRef())) {
final Media media = mediaResolver.getMedia(item.getMediaRef());
final MediaScreen mediaScreen = new MediaScreen(templateRenderer.render(text, context), media,
resolveKeyboard(item.getKeyboard(), context));
return new RamFileIdStorageMediaScreen(mediaScreen);
}
return new TextScreen(templateRenderer.render(text, context),
resolveKeyboard(item.getKeyboard(), context));
}
@@ -81,6 +109,8 @@ class FlowScreenResolverDelegate implements ScreenResolver {
.map(btn -> getAction(btn))
.collect(Collectors.toList());
log.info("Keyboard: {}", buttons);
builder.row(buttons.toArray(new Action[buttons.size()]));
}
@@ -97,6 +127,9 @@ class FlowScreenResolverDelegate implements ScreenResolver {
.text(btn.getText())
.requestContact(btn.isRequestContact())
.requestLocation(btn.isRequestLocation())
.url(btn.getUrl())
.callbackData(btn.getCallbackData())
.inline(StringUtils.isNotEmpty(btn.getUrl()) || StringUtils.isNotEmpty(btn.getCallbackData()))
.build();
}

View File

@@ -0,0 +1,14 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MediaItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String url;
}

View File

@@ -0,0 +1,21 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
public class MediaRoot {
@Getter
@JacksonXmlElementWrapper(useWrapping = false)
@JsonProperty("video")
private List<MediaItem> videos = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(useWrapping = false)
@JsonProperty("photo")
private List<MediaItem> photos = new ArrayList<>();
}

View File

@@ -5,6 +5,7 @@ import ru.penkrat.stbf.templates.utils.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
class NamedItemResolver<T extends NamedItem> {
@@ -39,4 +40,22 @@ class NamedItemResolver<T extends NamedItem> {
throw new IllegalStateException("Element not found by 'id' or 'name' " + key);
}
Optional<T> getOpt(String key) {
List<T> list = byId.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'id' " + key);
}
return Optional.of(list.get(0));
}
list = byName.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'name' " + key);
}
return Optional.of(list.get(0));
}
return Optional.empty();
}
}

View File

@@ -1,5 +1,6 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@@ -9,6 +10,12 @@ class ScreenItem extends NamedItem {
private String text;
@JacksonXmlProperty(isAttribute = true)
private String mediaRef;
private KeyboardWrapper keyboard = new KeyboardWrapper();
@JacksonXmlProperty(isAttribute = true)
private boolean preserve;
}

View File

@@ -3,6 +3,7 @@ package ru.penkrat.stbf.templates.xml;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.api.RequestMatcher;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.TemplateRenderer;
@@ -17,18 +18,21 @@ public class XmlFlowResolver implements FlowResolver {
private final FlowActionResolverDelegate actionDelegate;
private final FlowScreenResolverDelegate screenDelegate;
private final FlowCommandResolverDelegate commandResolver;
private final FlowMediaResolverDelegate mediaResolver;
public XmlFlowResolver(String filename) {
FlowRoot flow = reader.read(filename);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
mediaResolver = new FlowMediaResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this, mediaResolver);
commandResolver = new FlowCommandResolverDelegate(flow, actionDelegate, screenDelegate);
}
public XmlFlowResolver(InputStream is) {
FlowRoot flow = reader.read(is);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
mediaResolver = new FlowMediaResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this, mediaResolver);
commandResolver = new FlowCommandResolverDelegate(flow, actionDelegate, screenDelegate);
}
@@ -47,6 +51,11 @@ public class XmlFlowResolver implements FlowResolver {
return actionDelegate.getAction(name);
}
@Override
public RequestMatcher getMatcher(String actionName) {
return actionDelegate.getMatcher(actionName);
}
public void setTemplateRenderer(TemplateRenderer templateRenderer) {
screenDelegate.setTemplateRenderer(templateRenderer);
}

View File

@@ -2,6 +2,7 @@ package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.val;
import org.junit.Before;
import org.junit.Test;
import ru.penkrat.stbf.api.Command;
@@ -42,6 +43,7 @@ public class XmlWriterTest {
item2.setId("2");
item2.setName("screen-2");
item2.setText("Hello, {{ name }}");
item2.setMediaRef("12");
item2.getKeyboard().setFactoryMethod("getKeyboard");
@@ -58,6 +60,16 @@ public class XmlWriterTest {
commandItem.setClazz(Command.class.getCanonicalName());
root.getCommands().add(commandItem);
MediaItem video1 = new MediaItem();
video1.setId("11");
video1.setUrl("https://example.com/test.mpg");
MediaItem photo1 = new MediaItem();
photo1.setId("12");
photo1.setUrl("https://example.com/test.jpg");
root.getMedia().getVideos().add(video1);
root.getMedia().getPhotos().add(photo1);
String xml = mapper.writeValueAsString(root);
System.out.println(xml);

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</parent>
<artifactId>stbf-test</artifactId>
@@ -16,12 +14,12 @@
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -6,6 +6,7 @@ import java.util.Random;
import lombok.Builder;
import lombok.Getter;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotSession;
@Builder
class BotRequestImpl implements BotRequest {
@@ -14,6 +15,7 @@ class BotRequestImpl implements BotRequest {
private String phoneNumber;
private String callbackData;
private String callbackMessageText;
private BotSession session;
@Getter
@Builder.Default
@@ -39,4 +41,9 @@ class BotRequestImpl implements BotRequest {
return Optional.ofNullable(callbackMessageText);
}
@Override
public BotSession getSession() {
return session;
}
}