#1 initial sending media files support
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-09-12 01:35:23 +03:00
parent 5bb9e3abab
commit 2103d03611
20 changed files with 369 additions and 25 deletions

View File

@@ -0,0 +1,28 @@
package ru.penkrat.stbf.api;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
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;
}

View File

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

View File

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

View File

@@ -0,0 +1,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

@@ -1,11 +1,18 @@
<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="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">
@@ -35,6 +42,10 @@
<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>
@@ -47,6 +58,10 @@
<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>
@@ -59,17 +74,40 @@
<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="inline1-action" screenRef="inline-test-1-screen" edit="true" id="30004" name="inlineTest1Command"/>
<command actionRef="inline2-action" screenRef="inline-test-2-screen" edit="true" id="30005" name="inlineTest2Command"/>
<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

@@ -7,10 +7,10 @@ import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup;
import com.pengrad.telegrambot.request.AbstractSendRequest;
import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse;
@@ -28,10 +28,7 @@ public class BotResponseImpl implements BotResponse {
public void send(Screen screen) {
log.debug("Send message: \n============\n{}\n============", screen.getText().trim());
SendMessage sendMessage = new SendMessage(chatId(), screen.getText().trim())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
AbstractSendRequest<? extends AbstractSendRequest> sendMessage= SendMethodUtils.createFromScreen(chatId(), screen);
if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();

View File

@@ -0,0 +1,58 @@
package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.AbstractSendRequest;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendPhoto;
import com.pengrad.telegrambot.request.SendVideo;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import ru.penkrat.stbf.api.Media;
import ru.penkrat.stbf.api.Screen;
import java.util.function.Function;
@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 PHOTO:
final SendPhoto sendPhoto = new SendPhoto(chatId, media.getUrl());
apply(sendPhoto, sendPhoto::caption, screen.getText());
sendPhoto.parseMode(screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2);
return sendPhoto;
case VIDEO:
final SendVideo sendVideo = new SendVideo(chatId, media.getUrl());
apply(sendVideo, sendVideo::caption, screen.getText());
apply(sendVideo, sendVideo::width, media.getWidth());
apply(sendVideo, sendVideo::height, media.getHeight());
apply(sendVideo, sendVideo::duration, media.getDuration());
sendVideo.parseMode(screen.getScreenProperties().isParseModeHtml()
? ParseMode.HTML
: ParseMode.MarkdownV2);
return sendVideo;
}
}
return new SendMessage(chatId, screen.getText().trim())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
}
private boolean isMedia(Screen screen) {
return screen.getMedia() != null;
}
private <T, V> T apply(T target, Function<V, T> setter, V value) {
if (value != null) {
setter.apply(value);
}
return target;
}
}

View File

@@ -7,6 +7,7 @@
<actions> </actions>
<screens> </screens>
<commands> </commands>
<media> </media>
</flow>
```
@@ -51,6 +52,8 @@ Screen - то, что бот ответит пользователю, обычн
</screen>
```
`mediaRef` - ссылка на элемент медиа, который будет оправлен ботом
`text` - выводимый текст
`keyboard` - описание клавиатуры
@@ -65,7 +68,7 @@ Screen - то, что бот ответит пользователю, обычн
#### Button
`if` - видомость кнопки, значение `true`, `false` или имя метода из контекста экрана (для программной обработки)
`if` - видимость кнопки, значение `true`, `false` или имя метода из контекста экрана (для программной обработки)
`actionRef` - `id` или `name` action, описанный в соответсвующей секции
@@ -89,4 +92,21 @@ Screen - то, что бот ответит пользователю, обычн
```
`actionRef` - ссылка на action, может использоваться id или name
`screenRef` - ссылка на screen, может использоваться 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

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

View File

@@ -24,4 +24,7 @@ public class CommandItem extends NamedItem {
@JacksonXmlProperty(isAttribute = true)
private boolean edit;
@JacksonXmlProperty(isAttribute = true)
private boolean replace;
}

View File

@@ -62,19 +62,22 @@ public class FlowCommandResolverDelegate implements CommandResolver {
screenFactory = screenResolver.getScreenFactory(commandItem.getScreenRef());
}
if (actionMatcher != null && screen != null) {
return simpleCommand(actionMatcher, screen, commandItem.isEdit(), commandItem.getId(), commandItem.getName());
return simpleCommand(actionMatcher, screen, commandItem.isEdit(), commandItem.isReplace(), commandItem.getId(), commandItem.getName());
}
return null;
}
private Command simpleCommand(RequestMatcher matcher, Screen screen, boolean edit, String id, String name) {
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 (edit) {
if (replace) {
botResponse.deleteMessage();
}
if (edit && !replace) {
botResponse.edit(screen);
} else {
botResponse.send(screen);

View File

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

View File

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

View File

@@ -6,10 +6,13 @@ 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.TextScreen;
import ru.penkrat.stbf.templates.ActionResolver;
import ru.penkrat.stbf.templates.KeyboardProvider;
import ru.penkrat.stbf.templates.MediaResolver;
import ru.penkrat.stbf.templates.ScreenResolver;
import ru.penkrat.stbf.templates.TemplateRenderer;
import ru.penkrat.stbf.templates.utils.ReflectionUtils;
@@ -25,11 +28,14 @@ class FlowScreenResolverDelegate implements ScreenResolver {
private final ActionResolver actionResolver;
private final MediaResolver mediaResolver;
@Setter
private TemplateRenderer templateRenderer = new NoopTemplateRenderer();
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver) {
FlowScreenResolverDelegate(FlowRoot src, ActionResolver actionResolver, MediaResolver mediaResolver) {
this.actionResolver = actionResolver;
this.mediaResolver = mediaResolver;
resolver = new NamedItemResolver<>(Traversal.traverse(src, FlowRoot::getScreens));
}
@@ -37,6 +43,11 @@ class FlowScreenResolverDelegate implements ScreenResolver {
public Screen getScreen(String name) {
ScreenItem item = resolver.get(name);
if (StringUtils.isNotEmpty(item.getMediaRef())) {
final Media media = mediaResolver.getMedia(item.getMediaRef());
return new MediaScreen(item.getText(), media, buildKeyboard(item.getKeyboard(), null));
}
return new TextScreen(item.getText(), buildKeyboard(item.getKeyboard(), null));
}
@@ -44,6 +55,12 @@ class FlowScreenResolverDelegate implements ScreenResolver {
public Screen getScreen(String name, Object context) {
ScreenItem item = resolver.get(name);
if (StringUtils.isNotEmpty(item.getMediaRef())) {
final Media media = mediaResolver.getMedia(item.getMediaRef());
return new MediaScreen(templateRenderer.render(item.getText(), context), media,
resolveKeyboard(item.getKeyboard(), context));
}
return new TextScreen(templateRenderer.render(item.getText(), context),
resolveKeyboard(item.getKeyboard(), context));
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,18 +18,21 @@ public class XmlFlowResolver implements FlowResolver {
private final FlowActionResolverDelegate actionDelegate;
private final FlowScreenResolverDelegate screenDelegate;
private final FlowCommandResolverDelegate commandResolver;
private final FlowMediaResolverDelegate mediaResolver;
public XmlFlowResolver(String filename) {
FlowRoot flow = reader.read(filename);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
mediaResolver = new FlowMediaResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this, mediaResolver);
commandResolver = new FlowCommandResolverDelegate(flow, actionDelegate, screenDelegate);
}
public XmlFlowResolver(InputStream is) {
FlowRoot flow = reader.read(is);
actionDelegate = new FlowActionResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this);
mediaResolver = new FlowMediaResolverDelegate(flow);
screenDelegate = new FlowScreenResolverDelegate(flow, this, mediaResolver);
commandResolver = new FlowCommandResolverDelegate(flow, actionDelegate, screenDelegate);
}

View File

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