-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[AB-xxx] adding bonk image generation command
fixing offline streamer handling not being able to handle streamer without current sessions
- Loading branch information
Showing
9 changed files
with
272 additions
and
4 deletions.
There are no files selected for viewing
147 changes: 147 additions & 0 deletions
147
...age-generation-impl/src/main/java/dev/sheldan/abstracto/imagegeneration/command/Bonk.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package dev.sheldan.abstracto.imagegeneration.command; | ||
|
||
import dev.sheldan.abstracto.core.command.UtilityModuleDefinition; | ||
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; | ||
import dev.sheldan.abstracto.core.command.config.CommandConfiguration; | ||
import dev.sheldan.abstracto.core.command.config.HelpInfo; | ||
import dev.sheldan.abstracto.core.command.config.Parameter; | ||
import dev.sheldan.abstracto.core.command.execution.CommandContext; | ||
import dev.sheldan.abstracto.core.command.execution.CommandResult; | ||
import dev.sheldan.abstracto.core.config.FeatureDefinition; | ||
import dev.sheldan.abstracto.core.interaction.InteractionService; | ||
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; | ||
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; | ||
import dev.sheldan.abstracto.core.service.ChannelService; | ||
import dev.sheldan.abstracto.core.templating.model.AttachedFile; | ||
import dev.sheldan.abstracto.core.templating.model.MessageToSend; | ||
import dev.sheldan.abstracto.core.templating.service.TemplateService; | ||
import dev.sheldan.abstracto.core.utils.FileService; | ||
import dev.sheldan.abstracto.core.utils.FutureUtils; | ||
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition; | ||
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames; | ||
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService; | ||
import net.dv8tion.jda.api.entities.Member; | ||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.File; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.CompletableFuture; | ||
|
||
@Component | ||
public class Bonk extends AbstractConditionableCommand { | ||
public static final String MEMBER_PARAMETER_KEY = "member"; | ||
|
||
@Autowired | ||
private ImageGenerationService imageGenerationService; | ||
|
||
@Autowired | ||
private TemplateService templateService; | ||
|
||
@Autowired | ||
private ChannelService channelService; | ||
|
||
@Autowired | ||
private FileService fileService; | ||
|
||
@Autowired | ||
private InteractionService interactionService; | ||
|
||
@Autowired | ||
private SlashCommandParameterService slashCommandParameterService; | ||
|
||
private static final String BONK_EMBED_TEMPLATE_KEY = "bonk_response"; | ||
|
||
@Value("${abstracto.feature.imagegeneration.bonk.imagesize}") | ||
private Integer imageSize; | ||
|
||
@Override | ||
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) { | ||
Member member; | ||
List<Object> parameters = commandContext.getParameters().getParameters(); | ||
if(parameters.isEmpty()) { | ||
member = commandContext.getAuthor(); | ||
} else { | ||
member = (Member) parameters.get(0); | ||
} | ||
File bonkGifFile = imageGenerationService.getBonkGif(member.getEffectiveAvatar().getUrl(imageSize)); | ||
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object()); | ||
// template support does not support binary files | ||
AttachedFile file = AttachedFile | ||
.builder() | ||
.file(bonkGifFile) | ||
.fileName("bonk.gif") | ||
.build(); | ||
messageToSend.getAttachedFiles().add(file); | ||
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel())) | ||
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile())) | ||
.thenApply(unused -> CommandResult.fromIgnored()); | ||
} | ||
|
||
@Override | ||
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) { | ||
event.deferReply().queue(); | ||
Member targetMember; | ||
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) { | ||
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class); | ||
} else { | ||
targetMember = event.getMember(); | ||
} | ||
File bonkGifFile = imageGenerationService.getBonkGif(targetMember.getEffectiveAvatar().getUrl(imageSize)); | ||
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object()); | ||
// template support does not support binary files | ||
AttachedFile file = AttachedFile | ||
.builder() | ||
.file(bonkGifFile) | ||
.fileName("bonk.gif") | ||
.build(); | ||
messageToSend.getAttachedFiles().add(file); | ||
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook())) | ||
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile())) | ||
.thenApply(unused -> CommandResult.fromIgnored()); | ||
} | ||
|
||
@Override | ||
public CommandConfiguration getConfiguration() { | ||
List<Parameter> parameters = new ArrayList<>(); | ||
Parameter memberParameter = Parameter | ||
.builder() | ||
.name(MEMBER_PARAMETER_KEY) | ||
.type(Member.class) | ||
.templated(true) | ||
.optional(true) | ||
.build(); | ||
parameters.add(memberParameter); | ||
HelpInfo helpInfo = HelpInfo | ||
.builder() | ||
.templated(true) | ||
.build(); | ||
|
||
SlashCommandConfig slashCommandConfig = SlashCommandConfig | ||
.builder() | ||
.enabled(true) | ||
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION) | ||
.groupName("memes") | ||
.commandName("bonk") | ||
.build(); | ||
|
||
return CommandConfiguration.builder() | ||
.name("bonk") | ||
.module(UtilityModuleDefinition.UTILITY) | ||
.templated(true) | ||
.supportsEmbedException(true) | ||
.async(true) | ||
.slashCommandConfig(slashCommandConfig) | ||
.parameters(parameters) | ||
.help(helpInfo) | ||
.build(); | ||
} | ||
|
||
@Override | ||
public FeatureDefinition getFeature() { | ||
return ImageGenerationFeatureDefinition.IMAGE_GENERATION; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
from __main__ import app | ||
|
||
from PIL import Image, ImageOps | ||
from flask import request | ||
import requests | ||
import validators | ||
import logging | ||
import io | ||
|
||
from utils import flask_utils | ||
|
||
|
||
bonk_angles = [40, 45, -5, 0, 5, 45, -40, 45, -30, -5, 50, 40, 5, 0, -45, 15, 5, 40, 0, -45, 5, -40, 60, -50, -40] | ||
allowed_content_length = 4_000_000 | ||
allowed_image_formats = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'] | ||
gif_speedup_factor = 2 | ||
|
||
|
||
def get_avatar_height_factor(angle): | ||
return max(0.1, angle * -1 / 45) | ||
|
||
@app.route('/memes/bonk/file.gif') # to directly embed, discord _requires_ this file ending, it seems | ||
def bonk_animated(): | ||
url = request.args.get('url') | ||
if not validators.url(url): | ||
return 'no valid url', 400 | ||
session = requests.Session() | ||
response = session.head(url) | ||
content_type = response.headers['content-type'] | ||
|
||
if content_type not in allowed_image_formats: | ||
return f'Incorrect image type {content_type}', 400 | ||
|
||
actual_content_length = int(response.headers['content-length']) | ||
if actual_content_length > allowed_content_length: | ||
return f'Image too large {actual_content_length}', 400 | ||
|
||
image_file = requests.get(url, stream=True) | ||
input_image = Image.open(io.BytesIO(image_file.content)) | ||
original_input_image = input_image | ||
old_width, old_height = input_image.size | ||
with Image.open('resources/img/newspaper.png') as newspaper_image: | ||
newspaper_image = newspaper_image.convert('RGBA') | ||
newspaper_width, newspaper_height = newspaper_image.size | ||
newspaper_ratio = old_width / newspaper_width | ||
desired_new_newspaper_width = int(newspaper_width * newspaper_ratio) | ||
desired_newspaper_height = newspaper_height | ||
if newspaper_ratio > 1: | ||
newspaper_image = newspaper_image.resize((desired_new_newspaper_width, desired_newspaper_height)) | ||
else: | ||
newspaper_image = ImageOps.contain(newspaper_image, (desired_new_newspaper_width, desired_newspaper_height)) | ||
new_newspaper_width, new_newspaper_height = newspaper_image.size | ||
new_total_height = new_newspaper_height | ||
if content_type == 'image/gif': | ||
logging.info(f'Rendering bonk for gif.') | ||
frame_count = original_input_image.n_frames | ||
old_frames = [] | ||
for frame_index in range(frame_count): | ||
input_image.seek(frame_index) | ||
frame = input_image.convert('RGBA') | ||
old_frames.append(frame) | ||
frames = [] | ||
current_factor = 1 | ||
for index, old_frame in enumerate(old_frames): | ||
angle = bonk_angles[index % len(bonk_angles)] | ||
frame = Image.new('RGBA', (old_width, new_total_height), (0, 0, 0, 0)) | ||
current_factor *= (1 - get_avatar_height_factor(angle)) | ||
current_factor += 0.2 | ||
current_factor = min(1, current_factor) | ||
avatar_height_factor = current_factor | ||
target_height = int(max(1, old_height / 2 * avatar_height_factor)) | ||
old_frame = old_frame.resize((int(old_width / 2), target_height)) | ||
target_position = int(old_height / 2 + (1 - avatar_height_factor) * old_height / 2) | ||
frame.paste(old_frame, (int(old_width / 2), target_position), old_frame) | ||
rotated_news_paper = newspaper_image.rotate(angle, center=(0, new_newspaper_height)) | ||
frame.paste(rotated_news_paper, (0, 0), rotated_news_paper) | ||
frames.append(frame) | ||
return flask_utils.serve_pil_gif_image(frames, (int(original_input_image.info['duration']) / gif_speedup_factor)) | ||
else: | ||
|
||
frames = [] | ||
logging.info(f'Rendering bonk for static image.') | ||
input_image = input_image.convert('RGBA') | ||
current_factor = 1 | ||
for angle in bonk_angles: | ||
frame = Image.new('RGBA', (old_width, old_height), (0, 0, 0, 0)) | ||
current_factor *= (1 - get_avatar_height_factor(angle)) | ||
current_factor += 0.2 | ||
current_factor = min(1, current_factor) | ||
avatar_height_factor = current_factor | ||
target_height = int(max(1, old_height / 2 * avatar_height_factor)) | ||
frame_input_image = input_image.resize((int(old_width / 2), target_height)) | ||
target_position = int(old_height / 2 + (1 - avatar_height_factor) * old_height / 2) | ||
frame.paste(frame_input_image, (int(old_width / 2), target_position), frame_input_image) | ||
rotated_news_paper = newspaper_image.rotate(angle, center=(0, new_newspaper_height)) | ||
frame.paste(rotated_news_paper, (0, 0), rotated_news_paper) | ||
frames.append(frame) | ||
return flask_utils.serve_pil_gif_image(frames, 50) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.