Compare commits

...

17 Commits

Author SHA1 Message Date
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
932da0549a add readme
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-05 13:03:47 +03:00
a18abc41d4 update .gitignore 2021-09-05 12:24:11 +03:00
d25eac9366 add demo app
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-05 12:20:05 +03:00
e5a9c82ff3 initial commands support
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-05 08:33:26 +03:00
d47e43edb1 support included configurations
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-04 23:58:22 +03:00
c930070b9c add flow: actions + screens 2021-08-30 23:34:57 +03:00
81cac78737 implement toString for keyboard 2021-08-30 23:23:49 +03:00
64 changed files with 2617 additions and 654 deletions

4
.gitignore vendored
View File

@@ -3,6 +3,9 @@ target/
!**/src/main/**/target/ !**/src/main/**/target/
!**/src/test/**/target/ !**/src/test/**/target/
# maven shade plugin
dependency-reduced-pom.xml
### STS ### ### STS ###
.apt_generated .apt_generated
.classpath .classpath
@@ -17,3 +20,4 @@ target/
*.iws *.iws
*.iml *.iml
*.ipr *.ipr

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.

22
pom.xml
View File

@@ -6,10 +6,24 @@
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
<name>stbf-parent</name> <name>stbf-parent</name>
<description>Simple Telegram Bot Facade</description> <description>Simple Telegram Bot Facade</description>
<scm>
<connection>scm:git:https://git.penkrat.ru/ruslan/stbf.git</connection>
<developerConnection>scm:git:https://git.penkrat.ru/ruslan/stbf.git</developerConnection>
<tag>stbf-parent-0.0.1</tag>
</scm>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version> <lombok.version>1.18.6</lombok.version>
@@ -23,6 +37,7 @@
<module>stbf-common</module> <module>stbf-common</module>
<module>stbf-rubenlagus</module> <module>stbf-rubenlagus</module>
<module>stbf-templates</module> <module>stbf-templates</module>
<module>stbf-demo</module>
</modules> </modules>
<dependencies> <dependencies>
@@ -50,6 +65,11 @@
<target>${java.version}</target> <target>${java.version}</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0-M4</version>
</plugin>
</plugins> </plugins>
</build> </build>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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>
<parent> <parent>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-api</artifactId> <artifactId>stbf-api</artifactId>

View File

@@ -7,7 +7,7 @@ import lombok.ToString;
@Getter @Getter
@Builder @Builder
@ToString(of = "text") @ToString(of = {"text", "inline"})
public class Action { public class Action {
public static Action simple(String text) { public static Action simple(String text) {

View File

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

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; package ru.penkrat.stbf.api;
import java.util.Objects;
@FunctionalInterface @FunctionalInterface
public interface RequestMatcher { 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

@@ -4,6 +4,10 @@ public interface Screen {
String getText(); String getText();
default Media getMedia() {
return null;
}
default Keyboard getKeyboard() { default Keyboard getKeyboard() {
return null; return null;
} }

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-common</artifactId> <artifactId>stbf-common</artifactId>
@@ -16,7 +14,7 @@
<dependency> <dependency>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId> <artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>

View File

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

View File

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

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

@@ -4,6 +4,9 @@ import lombok.experimental.UtilityClass;
import ru.penkrat.stbf.api.Action; import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.RequestMatcher; import ru.penkrat.stbf.api.RequestMatcher;
import java.util.Objects;
import java.util.regex.Pattern;
@UtilityClass @UtilityClass
public class RequestMatchers { public class RequestMatchers {
@@ -13,6 +16,41 @@ public class RequestMatchers {
.isPresent(); .isPresent();
} }
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) { private static boolean matchText(Action action, String inputText) {
return inputText.equalsIgnoreCase(action.getText()) return inputText.equalsIgnoreCase(action.getText())
|| (action.getCmd() != null && inputText.equalsIgnoreCase(action.getCmd())); || (action.getCmd() != null && inputText.equalsIgnoreCase(action.getCmd()));

56
stbf-demo/README.MD Normal file
View File

@@ -0,0 +1,56 @@
## Демо приложение
Позволяет запустить простого бота, который будет отвечать на заранее определенные команды,
без программирования логики.
### Запуск
Собрать fat-jar
```shell
mvn package
```
Запустить
```shell
java -jar stbf-demo.jar -t <bot:token> -f <полный путь к конфигу>
```
### Файл конфигурации
Если запустить без указания файла конфигурации бота, то будет использована следующая:
```xml
<flow>
<actions>
<action id="10001" name="start-action" command="/start">Start</action>
<action id="10002" name="help-action" command="/help">Help</action>
</actions>
<screens>
<screen id="20001" name="on-start-screen">
<text>This is demo bot</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>
<keyboard>
<row>
<button actionRef="help-action">Action.name</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"/>
</commands>
</flow>
```
Ее можно использовать как базу для разработки своей.

125
stbf-demo/pom.xml Normal file
View File

@@ -0,0 +1,125 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1</version>
</parent>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-demo</artifactId>
<name>stbf-demo</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.jar.plugin.version>3.0.2</maven.jar.plugin.version>
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
<junit.version>4.12</junit.version>
<assertj.version>3.9.0</assertj.version>
<mockito.version>2.13.0</mockito.version>
<jmustache.version>1.14</jmustache.version>
</properties>
<dependencies>
<dependency>
<artifactId>stbf-templates</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>${project.version}</version>
</dependency>
<dependency>
<artifactId>stbf-pengrad</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.6.1</version>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
<version>${jmustache.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- annotationProcessorPaths requires maven-compiler-plugin version 3.5 or higher -->
<version>${maven-compiler-plugin-version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>4.6.1</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>ru.penkrat.stbf.demo.App</mainClass>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>junit:junit</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,68 @@
package ru.penkrat.stbf.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import ru.penkrat.stbf.api.BotCommandChain;
import ru.penkrat.stbf.api.CommandChain;
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")
public class App implements Runnable {
private static final Logger log = LoggerFactory.getLogger(App.class);
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 = {"-t", "--token"}, required = true, description = "bot token 1234:abscdf...")
private String botToken = "";
public static void main(String[] args) {
new CommandLine(new App()).execute(args);
}
private BotCommandChain getCommandChain(String filename) {
XmlFlowResolver flow = new XmlFlowResolver(filename);
BotCommandChain chain = new BotCommandChain();
flow.getCommands().forEach(chain::add);
return 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);
}
}
@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));
log.info("Starting bot...");
botThread.start();
log.info("Bot started.");
log.info("Press Ctrl+C to exit.");
}
}

View File

@@ -0,0 +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
use Help or /help to view help
</text>
<keyboard>
<row>
<button actionRef="help-action">Action.name</button>
</row>
</keyboard>
</screen>
<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

@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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>
<parent> <parent>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-pengrad</artifactId> <artifactId>stbf-pengrad</artifactId>
@@ -18,7 +16,7 @@
<dependency> <dependency>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId> <artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.pengrad</groupId> <groupId>com.github.pengrad</groupId>

View File

@@ -1,10 +1,5 @@
package ru.penkrat.stbf.impl.pengrad; package ru.penkrat.stbf.impl.pengrad;
import java.lang.reflect.Field;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.pengrad.telegrambot.TelegramBot; import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
@@ -12,13 +7,12 @@ import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
import com.pengrad.telegrambot.model.request.KeyboardButton; import com.pengrad.telegrambot.model.request.KeyboardButton;
import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup; import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup;
import com.pengrad.telegrambot.request.AbstractSendRequest;
import com.pengrad.telegrambot.request.DeleteMessage; import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendDocument; import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.response.SendResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse; import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Screen; import ru.penkrat.stbf.api.Screen;
@@ -35,28 +29,28 @@ public class BotResponseImpl implements BotResponse {
public void send(Screen screen) { public void send(Screen screen) {
log.debug("Send message: \n============\n{}\n============", screen.getText().trim()); log.debug("Send message: \n============\n{}\n============", screen.getText().trim());
SendMessage sendMessage = new SendMessage(chatId(), screen.getText().trim()) AbstractSendRequest<? extends AbstractSendRequest> sendMessage = SendMethodUtils.createFromScreen(chatId(), screen);
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
if (screen.getKeyboard() instanceof KeyboardImpl) { if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard(); KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
KeyboardButton[][] keyboard = kk.getKeyboard(); KeyboardButton[][] keyboard = kk.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard(); InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) { if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
sendMessage = sendMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard)); sendMessage = sendMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else if (keyboard != null && keyboard.length > 0) { } else if (keyboard != null && keyboard.length > 0) {
logKeyboard(keyboard);
sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard)); sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard));
} else { } else {
log.debug("No keyboard"); log.debug("No keyboard");
} }
} }
telegramBot.execute(sendMessage); final SendResponse sendResponse = telegramBot.execute(sendMessage);
SendMethodUtils.processResponse(screen, sendResponse);
} }
@Override @Override
@@ -82,8 +76,11 @@ public class BotResponseImpl implements BotResponse {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard(); KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard(); InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (log.isDebugEnabled()) {
log.debug("{}", kk.toFriendlyString());
}
if (inlineKeyboard != null && inlineKeyboard.length > 0) { if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard)); editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else { } else {
log.debug("No keyboard"); log.debug("No keyboard");
@@ -112,40 +109,4 @@ public class BotResponseImpl implements BotResponse {
return update.message().messageId(); return update.message().messageId();
} }
private void logKeyboard(KeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Keyboard: {}", row);
}
}
}
}
private void logKeyboard(InlineKeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Inline keyboard: {}", row);
}
}
}
}
@SneakyThrows
private String getText(KeyboardButton btn) {
Field text = KeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
@SneakyThrows
private String getText(InlineKeyboardButton btn) {
Field text = InlineKeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
} }

View File

@@ -1,11 +1,17 @@
package ru.penkrat.stbf.impl.pengrad; package ru.penkrat.stbf.impl.pengrad;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton; import com.pengrad.telegrambot.model.request.KeyboardButton;
import lombok.Value; import lombok.Value;
import ru.penkrat.stbf.api.Keyboard; import ru.penkrat.stbf.api.Keyboard;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Value @Value
class KeyboardImpl implements Keyboard { class KeyboardImpl implements Keyboard {
@@ -13,4 +19,50 @@ class KeyboardImpl implements Keyboard {
private InlineKeyboardButton[][] inlineKeyboard; private InlineKeyboardButton[][] inlineKeyboard;
private Map<Object, String> text = new HashMap<>();
@Override
public String toString() {
int k = keyboard != null ? keyboard.length : 0;
int kn = k > 0 ? keyboard[0].length : 0;
int i = inlineKeyboard != null ? inlineKeyboard.length : 0;
int in = i > 0 ? inlineKeyboard[0].length : 0;
return "KeyboardImpl(pengrad) [keyboard=" + k + "x" + kn
+ ", inline=" + i + "x" + in + "]";
}
String toFriendlyString() {
StringBuilder sb = new StringBuilder();
if (keyboard != null) {
sb.append("Keyboard:\n");
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
sb.append(row).append("\n");
}
}
}
if (inlineKeyboard != null) {
sb.append("Inline keyboard:\n");
for (int i = 0; i < inlineKeyboard.length; i++) {
if (inlineKeyboard[i].length > 0) {
String row = Stream.of(inlineKeyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
sb.append(row).append("\n");
}
}
}
return sb.toString();
}
private String getText(Object btn) {
return text.computeIfAbsent(btn, o -> {
try {
Field text = btn.getClass().getDeclaredField("text");
ReflectionAccessor.getInstance().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,7 +2,6 @@ package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton; import com.pengrad.telegrambot.model.request.KeyboardButton;
import ru.penkrat.stbf.api.Action; import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard; import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder; import ru.penkrat.stbf.api.KeyboardBuilder;
@@ -13,13 +12,21 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
private InlineKeyboardButton[][] inlineKeyboard; private InlineKeyboardButton[][] inlineKeyboard;
private String keyboardStr = "";
public static Keyboard singleKey(Action action) { public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build(); return KeyboardBuilder.newKeyboard().add(action).build();
} }
@Override @Override
public Keyboard build() { public Keyboard build() {
return new KeyboardImpl(keyboard, inlineKeyboard); if (keyboard != null) {
return new KeyboardImpl(keyboard, null);
}
if (inlineKeyboard != null) {
return new KeyboardImpl(null, inlineKeyboard);
}
return new NoKeyboard();
} }
public KeyboardBuilder addGetContact(String text) { public KeyboardBuilder addGetContact(String text) {
@@ -51,7 +58,13 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
for (int i = 0; i < buttons.length; i++) { for (int i = 0; i < buttons.length; i++) {
put(buttons[i]); put(buttons[i]);
} }
if (buttons.length > 0) {
if (buttons[0].isInline()) {
nextRowI();
} else {
nextRow(); nextRow();
}
}
return self(); return self();
} }
@@ -89,14 +102,14 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
KeyboardButton[][] n = new KeyboardButton[keyboard.length + 1][]; KeyboardButton[][] n = new KeyboardButton[keyboard.length + 1][];
System.arraycopy(keyboard, 0, n, 0, keyboard.length); System.arraycopy(keyboard, 0, n, 0, keyboard.length);
keyboard = n; keyboard = n;
keyboard[keyboard.length - 1] = new KeyboardButton[] {}; keyboard[keyboard.length - 1] = new KeyboardButton[]{};
} }
private void nextRowI() { private void nextRowI() {
InlineKeyboardButton[][] n = new InlineKeyboardButton[inlineKeyboard.length + 1][]; InlineKeyboardButton[][] n = new InlineKeyboardButton[inlineKeyboard.length + 1][];
System.arraycopy(inlineKeyboard, 0, n, 0, inlineKeyboard.length); System.arraycopy(inlineKeyboard, 0, n, 0, inlineKeyboard.length);
inlineKeyboard = n; inlineKeyboard = n;
inlineKeyboard[inlineKeyboard.length - 1] = new InlineKeyboardButton[] {}; inlineKeyboard[inlineKeyboard.length - 1] = new InlineKeyboardButton[]{};
} }
protected PengradKeyboardBuilder self() { protected PengradKeyboardBuilder self() {
@@ -105,16 +118,20 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
private void put(Action action) { private void put(Action action) {
if (action.isInline()) { if (action.isInline()) {
put(new InlineKeyboardButton(action.getText()).callbackData(action.getCallbackData())); put(new InlineKeyboardButton(action.getText())
.callbackData(action.getCallbackData())
.url(action.getUrl()));
} else { } else {
put(new KeyboardButton(action.getText()).requestContact(action.isRequestContact())); put(new KeyboardButton(action.getText())
.requestContact(action.isRequestContact())
.requestLocation(action.isRequestLocation()));
} }
} }
private void put(KeyboardButton btn) { private void put(KeyboardButton btn) {
if (keyboard == null) { if (keyboard == null) {
keyboard = new KeyboardButton[][] { keyboard = new KeyboardButton[][]{
new KeyboardButton[] { btn } new KeyboardButton[]{btn}
}; };
} else { } else {
KeyboardButton[] k = keyboard[keyboard.length - 1]; KeyboardButton[] k = keyboard[keyboard.length - 1];
@@ -123,13 +140,12 @@ public class PengradKeyboardBuilder implements KeyboardBuilder {
n[k.length] = btn; n[k.length] = btn;
keyboard[keyboard.length - 1] = n; keyboard[keyboard.length - 1] = n;
} }
} }
private void put(InlineKeyboardButton btn) { private void put(InlineKeyboardButton btn) {
if (inlineKeyboard == null) { if (inlineKeyboard == null) {
inlineKeyboard = new InlineKeyboardButton[][] { inlineKeyboard = new InlineKeyboardButton[][]{
new InlineKeyboardButton[] { btn } new InlineKeyboardButton[]{btn}
}; };
} else { } else {
InlineKeyboardButton[] k = inlineKeyboard[inlineKeyboard.length - 1]; InlineKeyboardButton[] k = inlineKeyboard[inlineKeyboard.length - 1];

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"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-rubenlagus</artifactId> <artifactId>stbf-rubenlagus</artifactId>
@@ -17,7 +15,7 @@
<dependency> <dependency>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId> <artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>

112
stbf-templates/README.MD Normal file
View File

@@ -0,0 +1,112 @@
## Поддержка шаблонов и конфигурации
### Основные секции
```xml
<flow>
<actions> </actions>
<screens> </screens>
<commands> </commands>
<media> </media>
</flow>
```
### Actions
Action - Это действие пользователя бота, которое бот должен обработать, а также соответсвующая кнопка или inline-кнопка.
```xml
<action id="10002" name="help-action" command="/help">Help</action>
```
`id`, `name` - идентификаторы узла (экшена)
`command` - команда Telegram, должна начинаться со знака "/"
`requestContact="true|false"` - кнопка запроса контактных данных
`requestLocation="true|false"` - кнопка запроса метоположения
содержимое тега - вводимый текст, используется как экранная кнопка
`url` - ссылка для inline-кнопки
`callbackData` - callback данные для inline-кнопки
`callbackDataRegexp` - regexp для обработки callback программным способом
`callbackDataStartWith` - префикс, для обработки callback программным способом
### Screens
Screen - то, что бот ответит пользователю, обычно текст и набор кнопок с действиями. В качестве текста может задаваться шаблон, контекст передается при программной обработке при установленном `TemplateRenderer`
```xml
<screen id="20001" name="on-start-screen">
<text>This is demo bot</text>
<keyboard>
<row>
<button actionRef="help-action">Action.name</button>
</row>
</keyboard>
</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`. Используется для простых действий, не требующей программной логики.
```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"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-templates</artifactId> <artifactId>stbf-templates</artifactId>
<properties> <properties>

View File

@@ -0,0 +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,11 @@
package ru.penkrat.stbf.templates;
import ru.penkrat.stbf.api.Command;
import java.util.Collection;
public interface CommandResolver {
Collection<Command> getCommands();
}

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

@@ -4,36 +4,48 @@ import lombok.experimental.UtilityClass;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Function;
@UtilityClass @UtilityClass
public class ReflectionUtils { public class ReflectionUtils {
public <T> T getMethodResult(Object context, String methodName, Class<T> clazz) { public <T> T getMethodResult(Object context, String methodName, Class<T> clazz) {
try { return getMethod(context.getClass(), methodName)
Method method = context.getClass().getMethod(methodName); .map(invoker(context, clazz))
method.setAccessible(true); .orElse(null);
Object result = method.invoke(context);
if (result != null && clazz.isAssignableFrom(result.getClass())) {
return (T) result;
}
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
private Optional<Method> getMethod(Class<?> clazz, String methodName) {
try {
Method method = clazz.getMethod(methodName);
method.setAccessible(true);
return Optional.of(method);
} catch (NoSuchMethodException e) {
// try find as private
}
try {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return Optional.of(method);
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}
private <T> Function<Method, T> invoker(Object target, Class<T> resultClass) {
return (method) -> {
Object result = null;
try {
result = method.invoke(target);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
if (result != null && resultClass.isAssignableFrom(result.getClass())) {
return (T) result;
}
return null; return null;
};
} }
} }

View File

@@ -0,0 +1,29 @@
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();
}
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

@@ -0,0 +1,35 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
class ActionItem extends NamedItem {
@JacksonXmlText
private String text;
@JacksonXmlProperty(isAttribute = true)
private String command;
@JacksonXmlProperty(isAttribute = true)
private boolean requestContact;
@JacksonXmlProperty(isAttribute = true)
private boolean requestLocation;
@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

@@ -1,10 +1,8 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -13,7 +11,10 @@ import lombok.Setter;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@JacksonXmlRootElement(localName = "button") @JacksonXmlRootElement(localName = "button")
class Button { class Button extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String actionRef;
@JacksonXmlText @JacksonXmlText
private String text; private String text;

View File

@@ -0,0 +1,30 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@JacksonXmlRootElement(localName = "command")
public class CommandItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String actionRef;
@JacksonXmlProperty(isAttribute = true)
private String screenRef;
@JacksonXmlProperty(isAttribute = true, localName = "class")
private String clazz;
@JacksonXmlProperty(isAttribute = true)
private boolean edit;
@JacksonXmlProperty(isAttribute = true)
private boolean replace;
}

View File

@@ -0,0 +1,65 @@
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 {
private final NamedItemResolver<ActionItem> resolver;
FlowActionResolverDelegate(FlowRoot src) {
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getActions));
}
@Override
public Action getAction(String key) {
final ActionItem actionItem = resolver.get(key);
boolean isInline = StringUtils.isNotEmpty(actionItem.getCallbackData())
|| StringUtils.isNotEmpty(actionItem.getUrl());
if (isInline) {
if (StringUtils.isNotEmpty(actionItem.getCommand())) {
throw new IllegalArgumentException("'command' is not allowed for inline button");
}
if (actionItem.isRequestContact()) {
throw new IllegalArgumentException("'requestContact' is not allowed for inline button");
}
if (actionItem.isRequestLocation()) {
throw new IllegalArgumentException("'requestLocation' is not allowed for inline button");
}
}
return Action.builder()
.text(actionItem.getText())
// btn only
.cmd(actionItem.getCommand())
.requestContact(actionItem.isRequestContact())
.requestLocation(actionItem.isRequestLocation())
// inline btn only
.inline(isInline)
.callbackData(actionItem.getCallbackData())
.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

@@ -0,0 +1,98 @@
package ru.penkrat.stbf.templates.xml;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.api.RequestMatcher;
import ru.penkrat.stbf.api.Screen;
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 java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public class FlowCommandResolverDelegate implements CommandResolver {
private final FlowRoot root;
private final ActionResolver actionResolver;
private final ScreenResolver screenResolver;
private boolean resolved;
private Collection<Command> commands;
@Override
public Collection<Command> getCommands() {
resolve();
return commands;
}
private void resolve() {
if (!resolved) {
Collection<Command> parsedCommands = Traversal.traverse(root, FlowRoot::getCommands)
.stream()
.map(this::createCommand)
.filter(Objects::nonNull)
.collect(Collectors.toList());
commands = Collections.unmodifiableCollection(parsedCommands);
resolved = true;
}
}
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 (actionMatcher != null && screen != null) {
return simpleCommand(actionMatcher, screen, commandItem.isEdit(), commandItem.isReplace(), commandItem.getId(), commandItem.getName());
}
return null;
}
private Command simpleCommand(RequestMatcher matcher, Screen screen, boolean edit, boolean replace, String id, String name) {
return new Command() {
@Override
public void process(BotRequest botRequest, BotResponse botResponse, CommandChain chain) {
if (matcher.match(botRequest)) {
if (replace) {
botResponse.deleteMessage();
}
if (edit && !replace) {
botResponse.edit(screen);
} else {
botResponse.send(screen);
}
}
chain.processCommand(botRequest, botResponse);
}
@Override
public String toString() {
return "Command {" +
"id=" + id +
", name=" + name +
'}';
}
};
}
}

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

@@ -0,0 +1,68 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
class FlowParser {
private final XmlMapper mapper = new XmlMapper();
private String basePath = "";
public FlowRoot read(File file) throws IOException {
return read(new FileInputStream(file));
}
public FlowRoot read(@NonNull String fileName) {
if (fileName.startsWith("classpath:")) {
String fn = fileName.replaceFirst("^classpath:", "");
basePath = "classpath:" + Paths.get(fileName).getParent().toString();
return read(FlowParser.class.getResourceAsStream(fn));
} else {
basePath = Paths.get(fileName).getParent().toString();
try {
return read(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Can't load file " + fileName, e);
}
}
}
public FlowRoot read(InputStream is) {
FlowRoot flow = readFlow(is);
includes(flow);
return flow;
}
private void includes(FlowRoot src) {
final List<FlowRoot> flowRoots = src.getIncludes().stream()
.map(IncludeItem::getFile)
.map(fileName -> readFlow(basePath, fileName))
.filter(Objects::nonNull)
.peek(this::includes)
.collect(Collectors.toList());
src.getIncluded().addAll(flowRoots);
}
private FlowRoot readFlow(InputStream is) {
try {
return mapper.readValue(is, FlowRoot.class);
} catch (IOException e) {
throw new RuntimeException("Can't load resource", e);
}
}
private FlowRoot readFlow(String basePath, String file) {
return readFlow(FlowParser.class.getResourceAsStream(file));
}
}

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.templates.xml;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.CommandResolver;
import ru.penkrat.stbf.templates.ScreenResolver;
public interface FlowResolver extends ScreenResolver, ActionResolver, CommandResolver {
}

View File

@@ -0,0 +1,40 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@JacksonXmlRootElement(localName = "flow")
class FlowRoot {
@Getter
@JacksonXmlElementWrapper(localName = "actions")
@JsonProperty("action")
private List<ActionItem> actions = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(localName = "screens")
@JsonProperty("screen")
private List<ScreenItem> screens = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(useWrapping = false)
@JsonProperty("include")
private List<IncludeItem> includes = new ArrayList<>();
@Getter
@JacksonXmlElementWrapper(localName = "commands")
@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

@@ -0,0 +1,153 @@
package ru.penkrat.stbf.templates.xml;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
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;
import ru.penkrat.stbf.templates.utils.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
class FlowScreenResolverDelegate implements ScreenResolver {
private final NamedItemResolver<ScreenItem> resolver;
private final ActionResolver actionResolver;
private final MediaResolver mediaResolver;
@Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver, MediaResolver mediaResolver) {
this.actionResolver = actionResolver;
this.mediaResolver = mediaResolver;
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getScreens));
}
@Override
public Screen getScreen(String name) {
ScreenItem item = resolver.get(name);
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);
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));
}
private Keyboard resolveKeyboard(KeyboardWrapper wrapper, Object context) {
if (wrapper == null)
return null;
if (context instanceof KeyboardProvider) {
final Keyboard keyboard = ((KeyboardProvider) context).getKeyboard();
if (keyboard == null) {
log.warn("Method 'getKeyboard' returns NULL value!", wrapper.getFactoryMethod());
}
return keyboard;
}
if (wrapper.getFactoryMethod() != null && !wrapper.getFactoryMethod().isEmpty() && context != null) {
val keyboard = ReflectionUtils.getMethodResult(context, wrapper.getFactoryMethod(), Keyboard.class);
if (keyboard == null) {
log.warn("Method '{}' returns NULL value!", wrapper.getFactoryMethod());
}
return keyboard;
}
return buildKeyboard(wrapper, context);
}
private Keyboard buildKeyboard(KeyboardWrapper wrapper, Object context) {
Keyboard keyboard = null;
if (wrapper.isNotEmpty()) {
KeyboardBuilder builder = KeyboardBuilder.newKeyboard();
for (ButtonsRow row : wrapper.getRows()) {
List<Action> buttons = row.getButtons().stream()
.filter(btn -> checkIfCondition(context, btn.getIfCondition()))
.map(btn -> getAction(btn))
.collect(Collectors.toList());
log.info("Keyboard: {}", buttons);
builder.row(buttons.toArray(new Action[buttons.size()]));
}
keyboard = builder.build();
}
return keyboard;
}
private Action getAction(Button btn) {
if (StringUtils.isNotEmpty(btn.getActionRef())) {
return actionResolver.getAction(btn.getActionRef());
}
return Action.builder()
.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();
}
private boolean checkIfCondition(Object context, String ifCondition) {
if (ifCondition == null || ifCondition.isEmpty()) {
return true;
}
if (context == null) {
return false;
}
if ("true".equalsIgnoreCase(ifCondition)) {
return true;
}
if ("false".equalsIgnoreCase(ifCondition)) {
return false;
}
return ReflectionUtils.getMethodResult(context, ifCondition, Boolean.class);
}
}

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.templates.xml;
import lombok.Data;
@Data
class IncludeItem {
private String file;
}

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

@@ -0,0 +1,19 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
abstract class NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String name;
@JacksonXmlProperty(isAttribute = true)
private String ref;
}

View File

@@ -0,0 +1,61 @@
package ru.penkrat.stbf.templates.xml;
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> {
private final Map<String, List<T>> byId;
private final Map<String, List<T>> byName;
NamedItemResolver(Collection<T> src) {
byId = src.stream()
.filter(item -> StringUtils.isNotEmpty(item.getId()))
.collect(Collectors.groupingBy(NamedItem::getId));
byName = src.stream()
.filter(item -> StringUtils.isNotEmpty(item.getName()))
.collect(Collectors.groupingBy(NamedItem::getName));
}
public T get(String key) {
List<T> list = byId.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'id' " + key);
}
return list.get(0);
}
list = byName.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'name' " + key);
}
return list.get(0);
}
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,22 +1,21 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
class ScreenItem { class ScreenItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String name;
private String text; private String text;
@JacksonXmlProperty(isAttribute = true)
private String mediaRef;
private KeyboardWrapper keyboard = new KeyboardWrapper(); private KeyboardWrapper keyboard = new KeyboardWrapper();
@JacksonXmlProperty(isAttribute = true)
private boolean preserve;
} }

View File

@@ -1,20 +0,0 @@
package ru.penkrat.stbf.templates.xml;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.Getter;
@JacksonXmlRootElement(localName = "screens")
class Screens {
@Getter
@JacksonXmlElementWrapper(useWrapping = false, localName = "screen")
@JsonProperty("screen")
private List<ScreenItem> screens = new ArrayList<>();
}

View File

@@ -0,0 +1,24 @@
package ru.penkrat.stbf.templates.xml;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
@UtilityClass
class Traversal {
public <T> Collection<T> traverse(FlowRoot flow, Function<FlowRoot, Collection<T>> getter) {
final ArrayList<T> target = new ArrayList<>();
traverse(flow, getter, target);
return target;
}
private <T> void traverse(FlowRoot flow, Function<FlowRoot, Collection<T>> getter, Collection<T> target) {
Collection<T> c = getter.apply(flow);
target.addAll(c);
flow.getIncluded().forEach(f -> traverse(f, getter, target));
}
}

View File

@@ -0,0 +1,67 @@
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;
import java.io.InputStream;
import java.util.Collection;
@Slf4j
public class XmlFlowResolver implements FlowResolver {
private final FlowParser reader = new FlowParser();
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);
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);
mediaResolver = new FlowMediaResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this, mediaResolver);
commandResolver = new FlowCommandResolverDelegate(flow, actionDelegate, screenDelegate);
}
@Override
public Screen getScreen(String name) {
return screenDelegate.getScreen(name);
}
@Override
public Screen getScreen(String name, Object context) {
return screenDelegate.getScreen(name, context);
}
@Override
public Action getAction(String name) {
return actionDelegate.getAction(name);
}
@Override
public RequestMatcher getMatcher(String actionName) {
return actionDelegate.getMatcher(actionName);
}
public void setTemplateRenderer(TemplateRenderer templateRenderer) {
screenDelegate.setTemplateRenderer(templateRenderer);
}
@Override
public Collection<Command> getCommands() {
return commandResolver.getCommands();
}
}

View File

@@ -1,130 +0,0 @@
package ru.penkrat.stbf.templates.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
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.Screen;
import ru.penkrat.stbf.common.screen.TextScreen;
import ru.penkrat.stbf.templates.ScreenResolver;
import ru.penkrat.stbf.templates.TemplateRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class XmlScreenResolver implements ScreenResolver {
private final XmlMapper mapper = new XmlMapper();
private final Map<String, List<ScreenItem>> byId;
private final Map<String, List<ScreenItem>> byName;
@Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
public XmlScreenResolver(InputStream is) throws IOException {
Screens screens = mapper.readValue(is, Screens.class);
byId = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getId));
byName = screens.getScreens().stream()
.collect(Collectors.groupingBy(ScreenItem::getName));
}
@Override
public Screen getScreen(String name) {
ScreenItem item = get(name);
return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null));
}
@Override
public Screen getScreen(String name, Object context) {
ScreenItem item = get(name);
return new TextScreen(templateRenderer.render(item.getText(), context),
resolveKeyboard(item.getKeyboard(), context));
}
private Keyboard resolveKeyboard(KeyboardWrapper wrapper, Object context) {
if (wrapper == null)
return null;
if (wrapper.getFactoryMethod() != null && !wrapper.getFactoryMethod().isEmpty() && context != null) {
val keyboard = ReflectionUtils.getMethodResult(context, wrapper.getFactoryMethod(), Keyboard.class);
if (keyboard == null) {
log.warn("Method '{}' returns NULL value!", wrapper.getFactoryMethod());
}
return keyboard;
}
return buildKeyboard(wrapper, context);
}
private Keyboard buildKeyboard(KeyboardWrapper wrapper, Object context) {
Keyboard keyboard = null;
if (wrapper.isNotEmpty()) {
KeyboardBuilder builder = KeyboardBuilder.newKeyboard();
for (ButtonsRow row : wrapper.getRows()) {
List<Action> buttons = row.getButtons().stream()
.filter(btn -> checkIfCondition(context, btn.getIfCondition()))
.map(btn -> Action.builder()
.text(btn.getText())
.requestContact(btn.isRequestContact())
.requestLocation(btn.isRequestLocation())
.build())
.collect(Collectors.toList());
builder.row(buttons.toArray(new Action[buttons.size()]));
}
keyboard = builder.build();
}
return keyboard;
}
private boolean checkIfCondition(Object context, String ifCondition) {
if (ifCondition == null || ifCondition.isEmpty()) {
return true;
}
if (context == null) {
return false;
}
if ("true".equalsIgnoreCase(ifCondition)) {
return true;
}
if ("false".equalsIgnoreCase(ifCondition)) {
return false;
}
return ReflectionUtils.getMethodResult(context, ifCondition, Boolean.class);
}
private ScreenItem get(String key) {
List<ScreenItem> list = byId.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'id' " + key);
}
return list.get(0);
}
list = byName.get(key);
if (list != null) {
if (list.size() > 1) {
throw new IllegalStateException("Non unique 'name' " + key);
}
return list.get(0);
}
throw new IllegalStateException("Screen not found by 'id' or 'name' " + key);
}
}

View File

@@ -0,0 +1,18 @@
package ru.penkrat.stbf.templates.xml;
import org.junit.Test;
import ru.penkrat.stbf.api.Command;
import static org.assertj.core.api.Assertions.assertThat;
public class FlowCommandsTest {
@Test
public void testReadCommands() {
XmlFlowResolver resolver = new XmlFlowResolver("classpath:/ru/penkrat/stbf/templates/xml/flow.xml");
assertThat(resolver.getCommands()).hasSize(1);
final Command command = resolver.getCommands().iterator().next();
assertThat(command.toString()).contains("startCommand");
}
}

View File

@@ -0,0 +1,112 @@
package ru.penkrat.stbf.templates.xml;
import org.junit.Before;
import org.junit.Test;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.impl.MustacheRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
public class XmlFlowResolverTest {
@Before
public void setUp() throws Exception {
}
@Test
public void testRead() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("screens.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
Screen screen1 = resolver.getScreen("screen-1");
assertThat(screen1).isNotNull();
assertThat(screen1.getText()).isEqualTo("Test text");
assertThat(screen1.getKeyboard()).isNotNull();
String keyboard = ReflectionUtils.getMethodResult(screen1.getKeyboard(), "toFriendlyString", String.class);
assertThat(keyboard).contains("Send phone");
}
@Test
public void testReadWithTemplates() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("screens.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
resolver.setTemplateRenderer(new MustacheRenderer());
Keyboard keyboard = KeyboardBuilder.newKeyboard().build();
Screen screen2 = resolver.getScreen("screen-2", new Object() {
@SuppressWarnings("unused")
public String name = "Tester";
@SuppressWarnings("unused")
public Keyboard getKeyboard() {
return keyboard;
}
});
assertThat(screen2).isNotNull();
assertThat(screen2.getText()).isEqualTo("Hello, Tester");
assertThat(screen2.getKeyboard()).isEqualTo(keyboard);
}
@Test
public void testReadWithInclude() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
assertThat(resolver.getScreen("screen-1")).isNotNull();
}
@Test
public void testReadWithIncludeWithInclude() throws Exception {
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
XmlFlowResolver resolver = new XmlFlowResolver(xmlUnderTest);
assertThat(resolver.getAction("4001")).isNotNull();
}
@Test
public void testReadFile() throws Exception {
Path tmp = Files.createTempFile("test-flow", ".xml");
OutputStream fos = new FileOutputStream(tmp.toFile());
InputStream xmlUnderTest = XmlFlowResolverTest.class.getResourceAsStream("flow.xml");
int b = xmlUnderTest.read();
while (b >= 0) {
fos.write(b);
b = xmlUnderTest.read();
}
fos.close();
XmlFlowResolver resolver = new XmlFlowResolver(tmp.toFile().getAbsolutePath());
assertThat(resolver.getScreen("screen-1")).isNotNull();
assertThat(resolver.getAction("3001")).isNotNull();
Files.deleteIfExists(tmp);
}
@Test
public void testReadClasspath() throws Exception {
XmlFlowResolver resolver = new XmlFlowResolver("classpath:/ru/penkrat/stbf/templates/xml/flow.xml");
assertThat(resolver.getScreen("screen-1")).isNotNull();
assertThat(resolver.getAction("3001")).isNotNull();
}
}

View File

@@ -1,59 +0,0 @@
package ru.penkrat.stbf.templates.xml;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.InputStream;
import org.junit.Before;
import org.junit.Test;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.templates.impl.MustacheRenderer;
public class XmlScreenResolverTest {
@Before
public void setUp() throws Exception {
}
@Test
public void testRead() throws Exception {
InputStream xmlUnderTest = XmlScreenResolverTest.class.getResourceAsStream("screens.xml");
XmlScreenResolver resolver = new XmlScreenResolver(xmlUnderTest);
Screen screen1 = resolver.getScreen("screen-1");
assertThat(screen1).isNotNull();
assertThat(screen1.getText()).isEqualTo("Test text");
assertThat(screen1.getKeyboard()).isNotNull();
}
@Test
public void testReadWithTemplates() throws Exception {
InputStream xmlUnderTest = XmlScreenResolverTest.class.getResourceAsStream("screens.xml");
XmlScreenResolver resolver = new XmlScreenResolver(xmlUnderTest);
resolver.setTemplateRenderer(new MustacheRenderer());
Keyboard keyboard = KeyboardBuilder.newKeyboard().build();
Screen screen2 = resolver.getScreen("screen-2", new Object() {
@SuppressWarnings("unused")
public String name = "Tester";
@SuppressWarnings("unused")
public Keyboard getKeyboard() {
return keyboard;
}
});
assertThat(screen2).isNotNull();
assertThat(screen2.getText()).isEqualTo("Hello, Tester");
assertThat(screen2.getKeyboard()).isEqualTo(keyboard);
}
}

View File

@@ -1,15 +1,11 @@
package ru.penkrat.stbf.templates.xml; package ru.penkrat.stbf.templates.xml;
import org.junit.Before;
import org.junit.Test;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.val;
import ru.penkrat.stbf.templates.xml.Button; import org.junit.Before;
import ru.penkrat.stbf.templates.xml.ButtonsRow; import org.junit.Test;
import ru.penkrat.stbf.templates.xml.ScreenItem; import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.templates.xml.Screens;
public class XmlWriterTest { public class XmlWriterTest {
@@ -18,11 +14,12 @@ public class XmlWriterTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
} }
@Test @Test
public void testWrite() throws Exception { public void testWrite() throws Exception {
Screens root = new Screens(); FlowRoot root = new FlowRoot();
ScreenItem item1 = new ScreenItem(); ScreenItem item1 = new ScreenItem();
item1.setId("1"); item1.setId("1");
@@ -46,12 +43,33 @@ public class XmlWriterTest {
item2.setId("2"); item2.setId("2");
item2.setName("screen-2"); item2.setName("screen-2");
item2.setText("Hello, {{ name }}"); item2.setText("Hello, {{ name }}");
item2.setMediaRef("12");
item2.getKeyboard().setFactoryMethod("getKeyboard"); item2.getKeyboard().setFactoryMethod("getKeyboard");
root.getScreens().add(item1); root.getScreens().add(item1);
root.getScreens().add(item2); root.getScreens().add(item2);
root.getActions().add(new ActionItem());
root.getIncludes().add(new IncludeItem());
final CommandItem commandItem = new CommandItem();
commandItem.setActionRef("action-1");
commandItem.setScreenRef("screen-1");
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); String xml = mapper.writeValueAsString(root);
System.out.println(xml); System.out.println(xml);

View File

@@ -0,0 +1,23 @@
<flow>
<include file="screens.xml"/>
<include file="/test/a.xml"/>
<actions>
<action id="2001" name="start" command="/start">Start</action>
</actions>
<screens>
<screen id="5001" name="screen-5001">
<text>Test text</text>
<keyboard>
<row>
<button>Btn1</button>
<button>Btn2</button>
</row>
</keyboard>
</screen>
</screens>
<commands>
<command actionRef="2001" screenRef="5001" id="5002" name="startCommand"/>
</commands>
</flow>

View File

@@ -1,10 +1,15 @@
<screens> <flow>
<actions>
<action id="1001" requestContact="true">Send phone</action>
</actions>
<screens>
<screen id="1" name="screen-1"> <screen id="1" name="screen-1">
<text>Test text</text> <text>Test text</text>
<keyboard> <keyboard>
<row> <row>
<button if="false">Btn1</button> <button if="false">Btn1</button>
<button>Btn2</button> <button>Btn2</button>
<button actionRef="1001">Action.name</button>
</row> </row>
<row> <row>
<button>Btn1</button> <button>Btn1</button>
@@ -16,4 +21,5 @@
<text>Hello, {{ name }}</text> <text>Hello, {{ name }}</text>
<keyboard factoryMethod="getKeyboard"/> <keyboard factoryMethod="getKeyboard"/>
</screen> </screen>
</screens> </screens>
</flow>

View File

@@ -0,0 +1,6 @@
<flow>
<include file="/test/b/b.xml"/>
<actions>
<action id="3001">A action</action>
</actions>
</flow>

View File

@@ -0,0 +1,5 @@
<flow>
<actions>
<action id="4001">B action</action>
</actions>
</flow>

View File

@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <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">
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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>stbf-parent</artifactId> <artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<artifactId>stbf-test</artifactId> <artifactId>stbf-test</artifactId>
@@ -16,12 +14,12 @@
<dependency> <dependency>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId> <artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ru.penkrat.stbf</groupId> <groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-common</artifactId> <artifactId>stbf-common</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>