From f654fcbbdffe7f54a1cd504f6a76d9bcb6d35de2 Mon Sep 17 00:00:00 2001 From: "Tomachi [ICHIGO]" Date: Sun, 22 May 2022 06:37:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E5=87=A6=E7=90=86=E3=81=AE=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AA?= =?UTF-8?q?=E3=81=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pom.xml | 4 +- .../jdavcspeaker/Command/Cmd_Ignore.java | 5 +- .../jdavcspeaker/Command/Cmd_Summon.java | 1 - .../jdavcspeaker/Command/Cmd_Textimg.java | 4 +- .../jdavcspeaker/Event/Event_SpeakVCText.java | 482 ++---------------- .../jaoafa/jdavcspeaker/Lib/EmojiWrapper.java | 5 +- .../jaoafa/jdavcspeaker/Lib/LibIgnore.java | 34 +- .../com/jaoafa/jdavcspeaker/Lib/LibValue.java | 5 - .../jaoafa/jdavcspeaker/Lib/MsgFormatter.java | 2 +- .../jdavcspeaker/Lib/UserVoiceTextResult.java | 18 + .../jaoafa/jdavcspeaker/Lib/VisionAPI.java | 23 +- .../jaoafa/jdavcspeaker/Lib/VoiceText.java | 38 +- .../AttachmentsProcessor.java | 110 ++++ .../MessageProcessor/BaseProcessor.java | 16 + .../CreatedThreadProcessor.java | 30 ++ .../DefaultMessageProcessor.java | 412 +++++++++++++++ .../MessageProcessor/ProcessorType.java | 51 ++ .../MessageProcessor/StickersProcessor.java | 25 + .../jaoafa/jdavcspeaker/Player/TrackInfo.java | 6 +- 20 files changed, 773 insertions(+), 499 deletions(-) create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/Lib/UserVoiceTextResult.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/AttachmentsProcessor.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/BaseProcessor.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/CreatedThreadProcessor.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/DefaultMessageProcessor.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/ProcessorType.java create mode 100644 src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/StickersProcessor.java diff --git a/.gitignore b/.gitignore index 95d283a..9014055 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.idea/libraries /.idea/*.xml /.idea/.gitignore +/.idea/shelf *.iml diff --git a/pom.xml b/pom.xml index 5b5779b..2976bec 100644 --- a/pom.xml +++ b/pom.xml @@ -98,12 +98,12 @@ net.dv8tion JDA - 5.0.0-alpha.11 + 5.0.0-alpha.12 org.json json - 20210307 + 20220320 com.sedmelluq diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Ignore.java b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Ignore.java index 9e4c79c..abb398c 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Ignore.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Ignore.java @@ -4,7 +4,6 @@ import com.jaoafa.jdavcspeaker.Framework.Command.CmdSubstrate; import com.jaoafa.jdavcspeaker.Lib.LibEmbedColor; import com.jaoafa.jdavcspeaker.Lib.LibIgnore; -import com.jaoafa.jdavcspeaker.Lib.LibValue; import com.jaoafa.jdavcspeaker.Main; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; @@ -124,9 +123,9 @@ void list(SlashCommandInteractionEvent event) { String list; if (type.equals("contain")) { - list = String.join("\n", LibValue.ignoreContains); + list = String.join("\n", LibIgnore.contains); } else if (type.equals("equal")) { - list = String.join("\n", LibValue.ignoreEquals); + list = String.join("\n", LibIgnore.equals); } else { event.replyEmbeds(new EmbedBuilder() .setTitle(":x: 指定された type が正しくありません") diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Summon.java b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Summon.java index d857fba..380513c 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Summon.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Summon.java @@ -27,7 +27,6 @@ public void hooker(JDA jda, Guild guild, summon(guild, member, event); } - void summon(Guild guild, Member member, SlashCommandInteractionEvent event) { if (member == null || member.getVoiceState() == null) { event.replyEmbeds(new EmbedBuilder() diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Textimg.java b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Textimg.java index 7cc6283..229e107 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Textimg.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Command/Cmd_Textimg.java @@ -50,8 +50,8 @@ void generateTextImg(SlashCommandInteractionEvent event) { return; } String url = Main.getExistsOption(event, "messagelink").getAsString(); - Pattern msgUrlPattern = Pattern.compile("^https://discord\\.com/channels/([0-9]+)/([0-9]+)/([0-9]+)$"); - Pattern mediaUrlPattern = Pattern.compile("^https://cdn\\.discordapp\\.com/attachments/([0-9]+)/([0-9]+)/(.+)$"); + Pattern msgUrlPattern = Pattern.compile("^https://discord(?:app)?\\.com/channels/(\\d+)/(\\d+)/(\\d+)$"); + Pattern mediaUrlPattern = Pattern.compile("^https://cdn\\.discordapp\\.com/attachments/(\\d+)/(\\d+)/(.+)$"); String imageUrl; Matcher msgUrlMatcher = msgUrlPattern.matcher(url); diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Event/Event_SpeakVCText.java b/src/main/java/com/jaoafa/jdavcspeaker/Event/Event_SpeakVCText.java index a12f1fa..0d2dead 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Event/Event_SpeakVCText.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Event/Event_SpeakVCText.java @@ -1,41 +1,20 @@ package com.jaoafa.jdavcspeaker.Event; import com.jaoafa.jdavcspeaker.Lib.*; -import com.jaoafa.jdavcspeaker.Main; -import com.jaoafa.jdavcspeaker.Player.TrackInfo; +import com.jaoafa.jdavcspeaker.MessageProcessor.BaseProcessor; +import com.jaoafa.jdavcspeaker.MessageProcessor.ProcessorType; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.htmlparser.jericho.Source; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.json.JSONObject; -import java.io.IOException; -import java.util.Arrays; -import java.util.Comparator; +import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class Event_SpeakVCText extends ListenerAdapter { - final Pattern urlPattern = Pattern.compile("https?://\\S+", Pattern.CASE_INSENSITIVE); - final Pattern messageUrlPattern = Pattern.compile("^https://.*?discord(?:app)?\\.com/channels/([0-9]+)/([0-9]+)/([0-9]+)\\??(.*)$", Pattern.CASE_INSENSITIVE); - final Pattern eventDirectLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?discord(?:app)?\\.com/events/([0-9]+)/([0-9]+)$", Pattern.CASE_INSENSITIVE); - final Pattern eventInviteLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:discord(?:app)?\\.com/invite|discord\\.gg)/(\\w+)\\?event=([0-9]+)$", Pattern.CASE_INSENSITIVE); - final Pattern inviteLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:discord(?:app)?\\.com/invite|discord\\.gg)/(\\w+)$", Pattern.CASE_INSENSITIVE); - final Pattern tweetUrlPattern = Pattern.compile("^https://twitter\\.com/(\\w){1,15}/status/([0-9]+)\\??(.*)$", Pattern.CASE_INSENSITIVE); - final Pattern titlePattern = Pattern.compile("([^<]+)", Pattern.CASE_INSENSITIVE); - final Pattern spoilerPattern = Pattern.compile("\\|\\|.+\\|\\|"); - final Pattern channelReplyPattern = Pattern.compile("<#([0-9]+)>"); - @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { if (!event.isFromType(ChannelType.TEXT)) { @@ -56,8 +35,8 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { return; } - User user = member.getUser(); - if (user.isBot()) { + User user = event.getAuthor(); + if (user.getIdLong() == jda.getSelfUser().getIdLong()) { return; } @@ -65,9 +44,6 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { if (content.equals(".")) { return; // .のみは除外 } - if (content.startsWith("!")) { - return; // !から始まるコマンドと思われる文字列を除外 - } if (guild.getSelfMember().getVoiceState() == null || guild.getSelfMember().getVoiceState().getChannel() == null) { @@ -90,440 +66,48 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { } } - // ignore - boolean ignoreEquals = LibValue.ignoreEquals.contains(content); - boolean ignoreContain = LibValue.ignoreContains.stream().anyMatch(content::contains); - - if (ignoreEquals || ignoreContain) return; - - // 読み上げるメッセージの構築 - String speakContent = content; - - // Replace discord invite(include event) url - speakContent = replacerDiscordInviteLink(jda, guild, speakContent); - // Replace url - speakContent = replacerLink(jda, speakContent); - // Spoiler - speakContent = replacerSpoiler(speakContent); - // Thread reply - speakContent = replacerChannelThreadLink(jda, speakContent); - // Emphasize - boolean isEmphasize = isEmphasizeMessage(speakContent); - if (isEmphasize) { - speakContent = replacerEmphasizeMessage(speakContent); - } - - UserVoiceTextResult uvtr = getUserVoiceText(user); + UserVoiceTextResult uvtr = UserVoiceTextResult.getUserVoiceText(user); if (uvtr.isReset()) { message.reply("デフォルトパラメーターが不正であるため、リセットしました。").queue(); } - VoiceText vt = isEmphasize ? changeEmphasizeSpeed(uvtr.getVoiceText()) : uvtr.getVoiceText(); - vt.play(TrackInfo.SpeakFromType.RECEIVED_MESSAGE, message, speakContent); - - for (MessageSticker sticker : message.getStickers()) { - vt.play(TrackInfo.SpeakFromType.RECEIVED_MESSAGE, message, "スタンプ「" + sticker.getName() + "」が送信されました。"); - } - - // 画像等 - VisionAPI visionAPI = Main.getVisionAPI(); - if (visionAPI == null) { - message.getAttachments() - .forEach(attachment -> vt.play(TrackInfo.SpeakFromType.RECEIVED_FILE, message, "ファイル「" + attachment.getFileName() + "」が送信されました。")); - return; - } - LibFiles.VDirectory.VISION_API_TEMP.mkdirs(); - for (Message.Attachment attachment : message.getAttachments()) { - if (attachment.isSpoiler()) { - vt.play(TrackInfo.SpeakFromType.RECEIVED_FILE, message, "スポイラーファイルが送信されました。"); - return; - } - attachment - .downloadToFile(LibFiles.VDirectory.VISION_API_TEMP.getPath().resolve(attachment.getFileName()).toString()) - .thenAcceptAsync(file -> { - try { - List results = visionAPI.getImageLabelOrText(file); - boolean bool = file.delete(); - LibFlow flow = new LibFlow("SpeakVCText.VisionAPI"); - if (bool) { - flow.success("Temp attachment file have been delete successfully"); - } else { - flow.success("Temp attachment file have been delete failed"); - } - if (results == null) { - vt.play(TrackInfo.SpeakFromType.RECEIVED_FILE, message, "ファイル「%s」が送信されました。".formatted(attachment.getFileName())); - return; - } - - List sortedResults = results.stream() - .sorted(Comparator.comparing(VisionAPI.Result::getScore, Comparator.reverseOrder())) - .toList(); - String text = sortedResults.stream() - .filter(r -> r.getType() == VisionAPI.ResultType.TEXT_DETECTION) - .map(VisionAPI.Result::getDescription) - .findFirst() - .orElse(null); - - if (text != null) { - vt.play(TrackInfo.SpeakFromType.RECEIVED_IMAGE, message, "画像ファイル「%sを含む画像」が送信されました。".formatted(text.length() > 30 ? text.substring(0, 30) : text)); - - message.reply("```\n" + text.replaceAll("\n", " ") + "\n```").queue(); - } else { - vt.play(TrackInfo.SpeakFromType.RECEIVED_IMAGE, message, "画像ファイル「%s」が送信されました。".formatted(attachment.getFileName())); - } - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - } - - /** - * チャンネルIDをもとに、チャンネルまたはスレッドを取得します
- * VCSpeakerが参加しているテキストチャンネルとスレッドに対応しますが、アーカイブされているスレッドには対応していません。 - * - * @param jda JDA - * @param channelId チャンネルID (or スレッドID) - * - * @return チャンネル、見つからなければnull - */ - GuildMessageChannel getChannelOrThread(JDA jda, String channelId) { - TextChannel textChannel = jda.getTextChannelById(channelId); - if (textChannel != null) { - return textChannel; - } - return jda.getThreadChannelById(channelId); - } - - /** - * チャンネルIDをもとに、テキスト/ボイスチャンネルまたはスレッドを取得します
- * VCSpeakerが参加しているテキスト/ボイスチャンネルとスレッドに対応しますが、アーカイブされているスレッドには対応していません。 - * - * @param jda JDA - * @param channelId チャンネルID (or スレッドID) - * - * @return チャンネル、見つからなければnull - */ - Channel getTextVoiceChannelOrThread(JDA jda, String channelId) { - TextChannel textChannel = jda.getTextChannelById(channelId); - if (textChannel != null) { - return textChannel; - } - VoiceChannel voiceChannel = jda.getVoiceChannelById(channelId); - if (voiceChannel != null) { - return voiceChannel; - } - return jda.getThreadChannelById(channelId); - } - - String replacerLink(JDA jda, String content) { - Matcher m = urlPattern.matcher(content); - while (m.find()) { - String url = m.group(); - // Discordメッセージリンク - Matcher msgUrlMatcher = messageUrlPattern.matcher(url); - if (msgUrlMatcher.find()) { - String channelId = msgUrlMatcher.group(2); - String messageId = msgUrlMatcher.group(3); - - GuildMessageChannel channel = getChannelOrThread(jda, channelId); - if (channel == null) continue; - Message message = channel.retrieveMessageById(messageId).complete(); - if (message == null) continue; - - channel = message.getGuildChannel(); - String channelName = "チャンネル「" + channel.getName() + "」"; - if (channel instanceof ThreadChannel) { - channelName = "チャンネル「" + ((ThreadChannel) channel).getParentChannel().getName() + "」のスレッド「" + channel.getName() + "」"; - } - - content = content.replace(url, "%sが%sで送信したメッセージのリンク".formatted( - message.getAuthor().getAsTag(), - channelName - )); + List processorTypes = ProcessorType.getMatchProcessor(message); + for (BaseProcessor processor : getMessageProcessors()) { + if (!processorTypes.contains(processor.getType())) { continue; } - Matcher tweetUrlMatcher = tweetUrlPattern.matcher(url); - if (tweetUrlMatcher.find()) { - String screenName = tweetUrlMatcher.group(1); - String tweetId = tweetUrlMatcher.group(2); - - Tweet tweet = getTweet(screenName, tweetId); - if (tweet != null) { - System.out.println(tweet); - content = content.replace(url, "%sのツイート「%s」へのリンク".formatted( - EmojiWrapper.removeAllEmojis(tweet.authorName()), - tweet.plainText() - )); - continue; - } - } - - // GIFリンク - if (url.endsWith(".gif")) { - content = content.replace(url, "GIF画像へのリンク"); - continue; - } - - // Webページのタイトル取得 - String title = getTitle(url); - if (title != null) { - System.out.println("title: " + title); - if (title.length() >= 30) { - title = title.substring(0, 30) + "以下略"; - } - System.out.println("title 2: " + title); - content = content.replace(url, "Webページ「%s」へのリンク".formatted(title)); - } else { - content = content.replace(url, "Webページへのリンク"); - } - } - return content; - } - - String replacerChannelThreadLink(JDA jda, String content) { - return channelReplyPattern.matcher(content).replaceAll(result -> { - String channelId = result.group(1); - Channel channel = getTextVoiceChannelOrThread(jda, channelId); - if (channel == null) return "どこかのチャンネルへのリンク"; - String channelName = "チャンネル「" + channel.getName() + "」へのリンク"; - if (channel instanceof ThreadChannel) { - channelName = "チャンネル「" + ((ThreadChannel) channel).getParentChannel().getName() + "」のスレッド「" + channel.getName() + "」へのリンク"; - } - return channelName; - }); - } - - String replacerDiscordInviteLink(JDA jda, Guild sendFromGuild, String content) { - content = eventDirectLinkUrlPattern.matcher(content).replaceAll(result -> { - String guildId = result.group(1); - String eventId = result.group(2); - - Guild guild = jda.getGuildById(guildId); - if (guild == null) return "どこかのサーバのイベントへのリンク"; - - String eventName = getScheduledEventName(guildId, eventId); - if (eventName == null) return "サーバ「" + guild.getName() + "」のイベントへのリンク"; - - if (guild.getIdLong() == sendFromGuild.getIdLong()) { - return "イベント「" + eventName + "」へのリンク"; - } - return "サーバ「" + guild.getName() + "」のイベント「" + eventName + "」へのリンク"; - }); - - content = eventInviteLinkUrlPattern.matcher(content).replaceAll(result -> { - String inviteCode = result.group(1); - String eventId = result.group(2); - - DiscordInvite invite = getInvite(inviteCode, eventId); - if (invite == null) return "どこかのサーバのイベントへのリンク"; - if (invite.eventName() == null) return "サーバ「" + invite.guildName() + "」のイベントへのリンク"; - - if (invite.guildId().equals(sendFromGuild.getId())) { - return "イベント「" + invite.eventName() + "」へのリンク"; - } - return "サーバ「" + invite.guildName() + "」のイベント「" + invite.eventName() + "」へのリンク"; - }); - - content = inviteLinkUrlPattern.matcher(content).replaceAll(result -> { - String inviteCode = result.group(1); - - DiscordInvite invite = getInvite(inviteCode, null); - if (invite == null) return "どこかのサーバへの招待リンク"; - if (invite.channelName() == null) return "サーバ「" + invite.guildName() + "」への招待リンク"; - - if (invite.guildId().equals(sendFromGuild.getId())) { - return "チャンネル「" + invite.channelName() + "」への招待リンク"; - } - return "サーバ「" + invite.guildName() + "」のチャンネル「" + invite.channelName() + "」への招待リンク"; - }); - - return content; - } - - String replacerSpoiler(String content) { - return spoilerPattern.matcher(content).replaceAll(" ピー "); - } - - boolean isEmphasizeMessage(String content) { - return - content.matches("^\\*\\*(.[  ]){2,}.\\*\\*$") || // **あ い う え お** OR **あ い う え お** - content.matches("^(.[  ]){2,}.$") || // あ い う え お OR あ い う え お - content.matches("^\\*\\*(.+)\\*\\*$"); // **あああああああ** - } - - String replacerEmphasizeMessage(String content) { - for (String s : Arrays.asList("**", " ", " ")) { - content = content.replace(s, ""); - } - return content; - } - - VoiceText changeEmphasizeSpeed(VoiceText vt) { - try { - return vt.setSpeed(Math.max(vt.getSpeed() / 2, 50)); - } catch (VoiceText.WrongSpeedException e) { - return vt; - } - } - - @Nullable - String getTitle(String url) { - try { - OkHttpClient client = new OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - Request request = new Request.Builder().url(url).build(); - try (Response response = client.newCall(request).execute()) { - if (response.code() != 200 && response.code() != 302) { - return null; - } - ResponseBody body = response.body(); - if (body == null) { - return null; - } - Matcher m = titlePattern.matcher(body.string()); - return m.find() ? m.group(1) : null; - } - } catch (IOException e) { - return null; - } - } - - record Tweet(String authorName, String html, String plainText) { - } - - @Nullable - Tweet getTweet(String screenName, String tweetId) { - String url = "https://publish.twitter.com/oembed?url=https://twitter.com/%s/status/%s".formatted(screenName, tweetId); - try { - OkHttpClient client = new OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - Request request = new Request.Builder().url(url).build(); - try (Response response = client.newCall(request).execute()) { - if (response.code() != 200 && response.code() != 302) { - return null; - } - ResponseBody body = response.body(); - if (body == null) { - return null; - } - JSONObject json = new JSONObject(body.string()); - String html = json.getString("html"); - String authorName = json.getString("author_name"); - String plainText = new Source(html.replaceAll("(.*)", "")) - .getFirstElement("p") - .getRenderer() - .setMaxLineLength(Integer.MAX_VALUE) - .setNewLine(null) - .toString(); - System.out.println(plainText); - return new Tweet(authorName, html, plainText); - } - } catch (IOException e) { - return null; + processor.execute( + jda, + guild, + channel, + member, + message, + uvtr + ); } } - @Nullable - String getScheduledEventName(String guildId, String eventId) { - String url = "https://discord.com/api/guilds/%s/scheduled-events/%s".formatted(guildId, eventId); + List getMessageProcessors() { + List list = new ArrayList<>(); try { - OkHttpClient client = new OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - Request request = new Request.Builder() - .url(url) - .header("Authorization", "Bot " + Main.getDiscordToken()) - .build(); - try (Response response = client.newCall(request).execute()) { - if (response.code() != 200 && response.code() != 302) { - return null; + for (Class eventClass : new LibClassFinder().findClasses("com.jaoafa.jdavcspeaker.MessageProcessor")) { + if (eventClass.isInterface() || + !eventClass.getSimpleName().endsWith("Processor") + || eventClass.getEnclosingClass() != null + || eventClass.getName().contains("$")) { + continue; } - ResponseBody body = response.body(); - if (body == null) { - return null; + Object instance = ((Constructor) eventClass.getConstructor()).newInstance(); + if (!(instance instanceof BaseProcessor)) { + continue; } - JSONObject json = new JSONObject(body.string()); - return json.getString("name"); - } - } catch (IOException e) { - return null; - } - } - record DiscordInvite( - String code, - String guildId, - String guildName, - String channelId, - String channelName, - @Nullable String inviterId, - @Nullable String inviterName, - @Nullable String inviterDiscriminator, - @Nullable String eventName, - @Nullable String eventId - ) { - } - - @Nullable - DiscordInvite getInvite(String inviteCode, String eventId) { - String url = "https://discord.com/api/invites/%s".formatted(inviteCode); - if (eventId != null) { - url += "?guild_scheduled_event_id=%s".formatted(eventId); - } - try { - OkHttpClient client = new OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(); - Request request = new Request.Builder() - .url(url) - .build(); - try (Response response = client.newCall(request).execute()) { - if (response.code() != 200 && response.code() != 302) { - return null; - } - ResponseBody body = response.body(); - if (body == null) { - return null; - } - JSONObject json = new JSONObject(body.string()); - return new DiscordInvite( - json.getString("code"), - json.getJSONObject("guild").getString("id"), - json.getJSONObject("guild").getString("name"), - json.getJSONObject("channel").getString("id"), - json.getJSONObject("channel").getString("name"), - json.has("inviter") ? json.getJSONObject("inviter").getString("id") : null, - json.has("inviter") ? json.getJSONObject("inviter").getString("username") : null, - json.has("inviter") ? json.getJSONObject("inviter").getString("discriminator") : null, - json.has("guild_scheduled_event") ? json.getJSONObject("guild_scheduled_event").getString("name") : null, - json.has("guild_scheduled_event") ? json.getJSONObject("guild_scheduled_event").getString("id") : null - ); + list.add((BaseProcessor) eventClass.getConstructor().newInstance()); } - } catch (IOException e) { - return null; - } - } - - UserVoiceTextResult getUserVoiceText(User user) { - try { - return new UserVoiceTextResult(new VoiceText(user), false); - } catch (VoiceText.WrongException e) { - new DefaultParamsManager(user).setDefaultVoiceText(null); - return new UserVoiceTextResult(new VoiceText(), true); - } - } - - record UserVoiceTextResult(VoiceText vt, boolean isReset) { - public VoiceText getVoiceText() { - return vt; + } catch (Exception e) { + new LibReporter(null, e); } + return list; } } diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/EmojiWrapper.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/EmojiWrapper.java index ac1be37..46fdace 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/EmojiWrapper.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/EmojiWrapper.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.Objects; public class EmojiWrapper { /** @@ -17,7 +18,7 @@ public class EmojiWrapper { */ public static @NotNull String parseToAliases(@NotNull String input) { List rawEmojis = EmojiParser.extractEmojis(input); - for (Emoji emoji : rawEmojis.stream().map(EmojiManager::getByUnicode).toList()) { + for (Emoji emoji : rawEmojis.stream().map(EmojiManager::getByUnicode).filter(Objects::nonNull).toList()) { String alias = ":" + emoji.getAliases().get(0) + ":"; input = input.replaceAll(emoji.getUnicode(), alias); input = input.replaceAll(emoji.getTrimmedUnicode(), alias); @@ -34,7 +35,7 @@ public class EmojiWrapper { */ public static @NotNull String removeAllEmojis(@NotNull String input) { List rawEmojis = EmojiParser.extractEmojis(input); - for (Emoji emoji : rawEmojis.stream().map(EmojiManager::getByUnicode).toList()) { + for (Emoji emoji : rawEmojis.stream().map(EmojiManager::getByUnicode).filter(Objects::nonNull).toList()) { input = input.replaceAll(emoji.getUnicode(), ""); input = input.replaceAll(emoji.getTrimmedUnicode(), ""); } diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibIgnore.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibIgnore.java index 468e827..b614481 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibIgnore.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibIgnore.java @@ -2,8 +2,13 @@ import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; + public class LibIgnore { private static final LibFiles.VFile vFile = LibFiles.VFile.IGNORE; + public static final List contains = new ArrayList<>(); + public static final List equals = new ArrayList<>(); public static void fetchMap() { LibFlow ignoreFlow = new LibFlow("LibIgnore"); @@ -17,8 +22,8 @@ public static void fetchMap() { } } - LibValue.ignoreContains.clear(); - LibValue.ignoreEquals.clear(); + contains.clear(); + equals.clear(); JSONObject obj = vFile.readJSONObject(); if (obj == null) { @@ -27,38 +32,45 @@ public static void fetchMap() { } for (int i = 0; i < obj.getJSONArray("contain").length(); i++) { - LibValue.ignoreContains.add(obj.getJSONArray("contain").getString(i)); + contains.add(obj.getJSONArray("contain").getString(i)); } for (int i = 0; i < obj.getJSONArray("equal").length(); i++) { - LibValue.ignoreEquals.add(obj.getJSONArray("equal").getString(i)); + equals.add(obj.getJSONArray("equal").getString(i)); } - ignoreFlow.success("除外設定をロードしました(含む: %d / 一致: %d)。".formatted(LibValue.ignoreContains.size(), LibValue.ignoreEquals.size())); + ignoreFlow.success("除外設定をロードしました(含む: %d / 一致: %d)。".formatted(contains.size(), equals.size())); } public static void saveJson() { JSONObject obj = new JSONObject(); - obj.put("contain", LibValue.ignoreContains); - obj.put("equal", LibValue.ignoreEquals); + obj.put("contain", contains); + obj.put("equal", equals); vFile.write(obj); } public static void addToContainIgnore(String value) { - LibValue.ignoreContains.add(value); + contains.add(value); saveJson(); } public static void addToEqualIgnore(String value) { - LibValue.ignoreEquals.add(value); + equals.add(value); saveJson(); } public static void removeToContainIgnore(String value) { - LibValue.ignoreContains.remove(value); + contains.remove(value); saveJson(); } public static void removeToEqualIgnore(String value) { - LibValue.ignoreEquals.remove(value); + equals.remove(value); saveJson(); } + + public static boolean isIgnoreMessage(String content) { + boolean isEquals = equals.contains(content); + boolean isContain = contains.stream().anyMatch(content::contains); + + return isEquals || isContain; + } } \ No newline at end of file diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibValue.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibValue.java index d38b4c0..3efdb57 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibValue.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/LibValue.java @@ -3,12 +3,7 @@ import com.rollbar.notifier.Rollbar; import net.dv8tion.jda.api.JDA; -import java.util.ArrayList; -import java.util.List; - public class LibValue { - public static final List ignoreContains = new ArrayList<>(); - public static final List ignoreEquals = new ArrayList<>(); public static JDA jda; public static Rollbar rollbar = null; } diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/MsgFormatter.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/MsgFormatter.java index 2d3c1e6..1078417 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/MsgFormatter.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/MsgFormatter.java @@ -13,7 +13,7 @@ public static String format(String text) { text = EmojiWrapper.parseToAliases(text); // ReplaceCustomEmoji - String regex = ""; + String regex = ""; Pattern p = Pattern.compile(regex); Matcher m = p.matcher(text); while (m.find()) { diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/UserVoiceTextResult.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/UserVoiceTextResult.java new file mode 100644 index 0000000..fd23f6b --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/UserVoiceTextResult.java @@ -0,0 +1,18 @@ +package com.jaoafa.jdavcspeaker.Lib; + +import net.dv8tion.jda.api.entities.User; + +public record UserVoiceTextResult(VoiceText vt, boolean isReset) { + public VoiceText getVoiceText() { + return vt; + } + + public static UserVoiceTextResult getUserVoiceText(User user) { + try { + return new UserVoiceTextResult(new VoiceText(user), false); + } catch (VoiceText.WrongException e) { + new DefaultParamsManager(user).setDefaultVoiceText(null); + return new UserVoiceTextResult(new VoiceText(), true); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/VisionAPI.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/VisionAPI.java index 20595bf..a42e3c6 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/VisionAPI.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/VisionAPI.java @@ -187,10 +187,11 @@ public List loadCache(String hash) { } public void saveCache(String hash, List results, JSONObject raw_object) throws IOException { - Path file = LibFiles.VDirectory.VISION_API_CACHES.resolve(Path.of(hash)); + Path hashFileName = Path.of(hash); + Path file = LibFiles.VDirectory.VISION_API_CACHES.resolve(hashFileName); LibFiles.VDirectory.VISION_API_CACHES.mkdirs(); - Path file_result = LibFiles.VDirectory.VISION_API_RESULTS.resolve(Path.of(hash)); + Path file_result = LibFiles.VDirectory.VISION_API_RESULTS.resolve(hashFileName); LibFiles.VDirectory.VISION_API_RESULTS.mkdirs(); JSONArray array = new JSONArray(); @@ -215,7 +216,17 @@ public static List getSupportedContentType() { ); } - public boolean isCheckTarget(File file) { + public static List getSupportedFileExtensions() { + return List.of( + "jpg", + "jpeg", + "png", + "gif", + "bmp" + ); + } + + public static boolean isCheckTarget(File file) { try { String mime = getMimeType(file); return getSupportedContentType().contains(mime); @@ -224,7 +235,11 @@ public boolean isCheckTarget(File file) { } } - public String getMimeType(File file) throws IOException { + public static boolean isCheckTarget(String extension) { + return getSupportedFileExtensions().contains(extension); + } + + public static String getMimeType(File file) throws IOException { InputStream is = new BufferedInputStream(new FileInputStream(file)); return URLConnection.guessContentTypeFromStream(is); } diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Lib/VoiceText.java b/src/main/java/com/jaoafa/jdavcspeaker/Lib/VoiceText.java index 1514efb..d24b089 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Lib/VoiceText.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Lib/VoiceText.java @@ -26,6 +26,7 @@ public class VoiceText { Emotion emotion = null; EmotionLevel emotionLevel = EmotionLevel.NORMAL; int pitch = 100; + final LibFlow vtFlow = new LibFlow("VoiceText"); /** * Initialize Voice Text object @@ -247,14 +248,15 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String if (speakText.length() == 0) { return; } - System.out.printf("[VoiceText.play] %s by %s (%s)%n", message.getContentDisplay().length() >= 10 ? message.getContentDisplay().substring(0, 10) : message.getContentDisplay(), message.getAuthor().getAsTag(), speakFromType.name()); + + vtFlow.success("[VoiceText.play] %s by %s (%s)", message.getContentDisplay().length() >= 10 ? message.getContentDisplay().substring(0, 10) : message.getContentDisplay(), message.getAuthor().getAsTag(), speakFromType.name()); VoiceText vt; try { vt = parseMessage(speakText); } catch (WrongSpeakerException e) { - String allowParams = Arrays.stream(VoiceText.Speaker.values()) - .filter(s -> !s.equals(VoiceText.Speaker.__WRONG__)) + String allowParams = Arrays.stream(Speaker.values()) + .filter(s -> !s.equals(Speaker.__WRONG__)) .map(Enum::name) .collect(Collectors.joining("`, `")); message.replyEmbeds(new EmbedBuilder() @@ -271,8 +273,8 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String .build()).queue(); return; } catch (WrongEmotionException e) { - String allowParams = Arrays.stream(VoiceText.Emotion.values()) - .filter(s -> !s.equals(VoiceText.Emotion.__WRONG__)) + String allowParams = Arrays.stream(Emotion.values()) + .filter(s -> !s.equals(Emotion.__WRONG__)) .map(Enum::name) .collect(Collectors.joining("`, `")); message.replyEmbeds(new EmbedBuilder() @@ -282,8 +284,8 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String .build()).queue(); return; } catch (WrongEmotionLevelException e) { - String allowParams = Arrays.stream(VoiceText.EmotionLevel.values()) - .filter(s -> !s.equals(VoiceText.EmotionLevel.__WRONG__)) + String allowParams = Arrays.stream(EmotionLevel.values()) + .filter(s -> !s.equals(EmotionLevel.__WRONG__)) .map(Enum::name) .collect(Collectors.joining("`, `")); message.replyEmbeds(new EmbedBuilder() @@ -306,8 +308,6 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String emotionLevel = vt.getEmotionLevel(); pitch = vt.getPitch(); - System.out.println(this); - speakText = Main.getArgs().formatMessage .replace("{username}", message.getAuthor().getName()) .replace("{nickname}", message.getMember() != null && message.getMember().getNickname() != null ? message.getMember().getNickname() : message.getAuthor().getName()) @@ -320,12 +320,13 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String emotionLevel != null ? emotionLevel.name() : "null", pitch)); - if (LibFiles.VDirectory.VOICETEXT_CACHES.exists(Path.of("%s.mp3".formatted(hash)))) { + Path fileName = Path.of("%s.mp3".formatted(hash)); + if (LibFiles.VDirectory.VOICETEXT_CACHES.exists(fileName)) { filteringQueue(speakFromType, message); TrackInfo info = new TrackInfo(speakFromType, message); PlayerManager.getINSTANCE().loadAndPlay( info, - LibFiles.VDirectory.VOICETEXT_CACHES.resolve(Path.of("%s.mp3".formatted(hash))).toString() + LibFiles.VDirectory.VOICETEXT_CACHES.resolve(fileName).toString() ); return; } @@ -344,8 +345,8 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String .add("pitch", String.valueOf(pitch)) .add("format", "mp3"); if (emotion != null && emotionLevel != null) { - form = form.add("emotion", emotion.name().toLowerCase()); - form = form.add("emotion_level", String.valueOf(emotionLevel.getLevel())); + form.add("emotion", emotion.name().toLowerCase()); + form.add("emotion_level", String.valueOf(emotionLevel.getLevel())); } Request request = new Request.Builder() @@ -353,19 +354,20 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String .url("https://api.voicetext.jp/v1/tts") .header("Authorization", Credentials.basic(Main.getSpeakToken(), "")) .build(); + Path hashFileName = Path.of(hash); try (Response response = client.newCall(request).execute()) { ResponseBody body = response.body(); if (body == null) { - System.out.println("Warning: response.body() is null."); + vtFlow.error("Warning: response.body() is null."); return; } if (!response.isSuccessful()) { - System.out.println("Error: " + response.code()); - System.out.println(body.string()); + vtFlow.error("Error: " + response.code()); + vtFlow.error(body.string()); return; } System.setProperty("file.encoding", "UTF-8"); - Files.write(LibFiles.VDirectory.VOICETEXT_CACHES.resolve(Path.of(hash)), body.bytes()); + Files.write(LibFiles.VDirectory.VOICETEXT_CACHES.resolve(hashFileName), body.bytes()); } catch (IOException e) { e.printStackTrace(); return; @@ -375,7 +377,7 @@ public void play(TrackInfo.SpeakFromType speakFromType, Message message, String .queue(null, Throwable::printStackTrace); filteringQueue(speakFromType, message); TrackInfo info = new TrackInfo(speakFromType, message); - PlayerManager.getINSTANCE().loadAndPlay(info, LibFiles.VDirectory.VOICETEXT_CACHES.resolve(Path.of(hash)).toString()); + PlayerManager.getINSTANCE().loadAndPlay(info, LibFiles.VDirectory.VOICETEXT_CACHES.resolve(hashFileName).toString()); } catch (JSONException e) { e.printStackTrace(); } diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/AttachmentsProcessor.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/AttachmentsProcessor.java new file mode 100644 index 0000000..7fa8804 --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/AttachmentsProcessor.java @@ -0,0 +1,110 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import com.jaoafa.jdavcspeaker.Lib.*; +import com.jaoafa.jdavcspeaker.Main; +import com.jaoafa.jdavcspeaker.Player.TrackInfo; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + +/** + * 添付ファイルのメッセージプロセッサ + *

+ * ・拡張子で画像系ファイルであれば画像として扱う + *  ・その後、MimeTypeでも判定する + * ・拡張子で画像系ファイルでなければファイルとして扱う。この際はファイル名を読み上げる + */ +public class AttachmentsProcessor implements BaseProcessor { + @Override + public ProcessorType getType() { + return ProcessorType.ATTACHMENTS; + } + + @Override + public void execute(JDA jda, Guild guild, TextChannel channel, Member member, Message message, UserVoiceTextResult uvtr) { + for (Message.Attachment attachment : message.getAttachments()) { + // スポイラーファイルは無条件でスポイラーファイルとして読み上げ + if (attachment.isSpoiler()) { + uvtr.vt().play(TrackInfo.SpeakFromType.RECEIVED_FILE, message, "スポイラーファイルが送信されました。"); + return; + } + + // 画像か、それ以外で分岐する + if (VisionAPI.isCheckTarget(attachment.getFileExtension())) { + processImage(message, uvtr.vt(), attachment); + return; + } + + processFile(message, uvtr.vt(), attachment); + } + } + + void processImage(Message message, VoiceText vt, Message.Attachment attachment) { + VisionAPI visionAPI = Main.getVisionAPI(); + if (visionAPI == null) { + processFile(message, vt, attachment); + return; + } + LibFiles.VDirectory.VISION_API_TEMP.mkdirs(); + attachment + .getProxy() + .downloadToFile(LibFiles.VDirectory.VISION_API_TEMP.getPath().resolve(attachment.getFileName()).toFile()) + .thenAcceptAsync(file -> { + try { + List results = visionAPI.getImageLabelOrText(file); + boolean bool = file.delete(); + LibFlow flow = new LibFlow("SpeakVCText.VisionAPI"); + if (bool) { + flow.success("Temp attachment file have been delete successfully"); + } else { + flow.success("Temp attachment file have been delete failed"); + } + if (results == null) { + processFile(message, vt, attachment); + return; + } + + List sortedResults = results.stream() + .sorted(Comparator.comparing(VisionAPI.Result::getScore, Comparator.reverseOrder())) + .toList(); + String text = sortedResults.stream() + .filter(r -> r.getType() == VisionAPI.ResultType.TEXT_DETECTION) + .map(VisionAPI.Result::getDescription) + .findFirst() + .orElse(null); + + if (text != null) { + vt.play(TrackInfo.SpeakFromType.RECEIVED_IMAGE, message, "画像ファイル「%sを含む画像」が送信されました。".formatted(text.length() > 30 ? text.substring(0, 30) : text)); + + message + .getChannel() + .sendMessage("```\n" + safeSubstring(text.replaceAll("\n", " ")) + "\n```") + .reference(message) + .mentionRepliedUser(false) + .queue(); + } else { + vt.play(TrackInfo.SpeakFromType.RECEIVED_IMAGE, message, "画像ファイル「%s」が送信されました。".formatted(attachment.getFileName())); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + void processFile(Message message, VoiceText vt, Message.Attachment attachment) { + vt.play(TrackInfo.SpeakFromType.RECEIVED_FILE, message, "ファイル「%s」が送信されました。".formatted(attachment.getFileName())); + } + + String safeSubstring(String str) { + if (str.length() <= 1500) { + return str; + } + return str.substring(0, 1500 - 1); + } +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/BaseProcessor.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/BaseProcessor.java new file mode 100644 index 0000000..f90a945 --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/BaseProcessor.java @@ -0,0 +1,16 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import com.jaoafa.jdavcspeaker.Lib.UserVoiceTextResult; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; + +public interface BaseProcessor { + /** プロセッサの種別 */ + ProcessorType getType(); + + /** 処理関数 */ + void execute(JDA jda, Guild guild, TextChannel channel, Member member, Message message, UserVoiceTextResult uvtr); +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/CreatedThreadProcessor.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/CreatedThreadProcessor.java new file mode 100644 index 0000000..bb1c3bb --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/CreatedThreadProcessor.java @@ -0,0 +1,30 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import com.jaoafa.jdavcspeaker.Lib.UserVoiceTextResult; +import com.jaoafa.jdavcspeaker.Player.TrackInfo; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; + +/** + * スレッド作成メッセージプロセッサ + *

+ * ・スレッド作成時に自動投稿されるスレッド作成メッセージを処理する。 + */ +public class CreatedThreadProcessor implements BaseProcessor { + @Override + public ProcessorType getType() { + return ProcessorType.CREATED_THREAD; + } + + @Override + public void execute(JDA jda, Guild guild, TextChannel channel, Member member, Message message, UserVoiceTextResult uvtr) { + // 特定のメッセージからの派生としてスレッドを作成する場合は本プロセッサは動作しない可能性がある + + String threadName = message.getContentRaw(); // message.getStartedThread() が必ず null になるので、暫定的にこれで代用 + + uvtr.vt().play(TrackInfo.SpeakFromType.CREATED_THREAD, message, "%sがスレッド「%s」を開始しました。".formatted(member.getUser().getName(), threadName)); + } +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/DefaultMessageProcessor.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/DefaultMessageProcessor.java new file mode 100644 index 0000000..7b88c78 --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/DefaultMessageProcessor.java @@ -0,0 +1,412 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import com.jaoafa.jdavcspeaker.Lib.EmojiWrapper; +import com.jaoafa.jdavcspeaker.Lib.LibIgnore; +import com.jaoafa.jdavcspeaker.Lib.UserVoiceTextResult; +import com.jaoafa.jdavcspeaker.Lib.VoiceText; +import com.jaoafa.jdavcspeaker.Main; +import com.jaoafa.jdavcspeaker.Player.TrackInfo; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.htmlparser.jericho.Source; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 「ユーザーが送信したデフォルトメッセージ」のメッセージプロセッサ + *

+ * ・メッセージ本文自体を原則そのまま読み上げる。(エイリアス等は適用する) + * ・画像など添付ファイルは ProcessorType.ATTACHMENTS で処理する。 + */ +public class DefaultMessageProcessor implements BaseProcessor { + final Pattern urlPattern = Pattern.compile("https?://\\S+", Pattern.CASE_INSENSITIVE); + final Pattern messageUrlPattern = Pattern.compile("^https://.*?discord(?:app)?\\.com/channels/(\\d+)/(\\d+)/(\\d+)\\??(.*)$", Pattern.CASE_INSENSITIVE); + final Pattern eventDirectLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?discord(?:app)?\\.com/events/(\\d+)/(\\d+)$", Pattern.CASE_INSENSITIVE); + final Pattern eventInviteLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:discord(?:app)?\\.com/invite|discord\\.gg)/(\\w+)\\?event=(\\d+)$", Pattern.CASE_INSENSITIVE); + final Pattern inviteLinkUrlPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:discord(?:app)?\\.com/invite|discord\\.gg)/(\\w+)$", Pattern.CASE_INSENSITIVE); + final Pattern tweetUrlPattern = Pattern.compile("^https://twitter\\.com/(\\w){1,15}/status/(\\d+)\\??(.*)$", Pattern.CASE_INSENSITIVE); + final Pattern titlePattern = Pattern.compile("([^<]+)", Pattern.CASE_INSENSITIVE); + final Pattern spoilerPattern = Pattern.compile("\\|\\|.+\\|\\|"); + final Pattern channelReplyPattern = Pattern.compile("<#(\\d+)>"); + + @Override + public ProcessorType getType() { + return ProcessorType.DEFAULT; + } + + @Override + public void execute(JDA jda, Guild guild, TextChannel channel, Member member, Message message, UserVoiceTextResult uvtr) { + String speakContent = message.getContentDisplay(); + + if (LibIgnore.isIgnoreMessage(speakContent)) { + return; + } + + // Replace discord invite(include event) url + speakContent = replacerDiscordInviteLink(jda, guild, speakContent); + // Replace url + speakContent = replacerLink(jda, speakContent); + // Spoiler + speakContent = replacerSpoiler(speakContent); + // Thread reply + speakContent = replacerChannelThreadLink(jda, speakContent); + // Emphasize + boolean isEmphasize = isEmphasizeMessage(speakContent); + if (isEmphasize) { + speakContent = replacerEmphasizeMessage(speakContent); + } + + VoiceText vt = isEmphasize ? changeEmphasizeSpeed(uvtr.getVoiceText()) : uvtr.getVoiceText(); + vt.play( + TrackInfo.SpeakFromType.RECEIVED_MESSAGE, + message, + speakContent + ); + } + + /** + * チャンネルIDをもとに、チャンネルまたはスレッドを取得します
+ * VCSpeakerが参加しているテキストチャンネルとスレッドに対応しますが、アーカイブされているスレッドには対応していません。 + * + * @param jda JDA + * @param channelId チャンネルID (or スレッドID) + * + * @return チャンネル、見つからなければnull + */ + MessageChannel getChannelOrThread(JDA jda, String channelId) { + TextChannel textChannel = jda.getTextChannelById(channelId); + if (textChannel != null) { + return textChannel; + } + return jda.getThreadChannelById(channelId); + } + + /** + * チャンネルIDをもとに、テキスト/ボイスチャンネルまたはスレッドを取得します
+ * VCSpeakerが参加しているテキスト/ボイスチャンネルとスレッドに対応しますが、アーカイブされているスレッドには対応していません。 + * + * @param jda JDA + * @param channelId チャンネルID (or スレッドID) + * + * @return チャンネル、見つからなければnull + */ + Channel getTextVoiceChannelOrThread(JDA jda, String channelId) { + TextChannel textChannel = jda.getTextChannelById(channelId); + if (textChannel != null) { + return textChannel; + } + VoiceChannel voiceChannel = jda.getVoiceChannelById(channelId); + if (voiceChannel != null) { + return voiceChannel; + } + return jda.getThreadChannelById(channelId); + } + + String replacerLink(JDA jda, String content) { + Matcher m = urlPattern.matcher(content); + while (m.find()) { + String url = m.group(); + + // Discordメッセージリンク + Matcher msgUrlMatcher = messageUrlPattern.matcher(url); + if (msgUrlMatcher.find()) { + String channelId = msgUrlMatcher.group(2); + String messageId = msgUrlMatcher.group(3); + + MessageChannel channel = getChannelOrThread(jda, channelId); + if (channel == null) continue; + Message message = channel.retrieveMessageById(messageId).complete(); + if (message == null) continue; + + channel = message.getChannel(); + String channelName = "チャンネル「" + channel.getName() + "」"; + if (channel instanceof ThreadChannel) { + channelName = "チャンネル「%s」のスレッド「%s」".formatted(((ThreadChannel) channel).getParentChannel().getName(), channel.getName()); + } + + content = content.replace(url, "%sが%sで送信したメッセージのリンク".formatted( + message.getAuthor().getAsTag(), + channelName + )); + continue; + } + + Matcher tweetUrlMatcher = tweetUrlPattern.matcher(url); + if (tweetUrlMatcher.find()) { + String screenName = tweetUrlMatcher.group(1); + String tweetId = tweetUrlMatcher.group(2); + + Tweet tweet = getTweet(screenName, tweetId); + if (tweet != null) { + System.out.println(tweet); + content = content.replace(url, "%sのツイート「%s」へのリンク".formatted( + EmojiWrapper.removeAllEmojis(tweet.authorName()), + tweet.plainText() + )); + continue; + } + } + + // GIFリンク + if (url.endsWith(".gif")) { + content = content.replace(url, "GIF画像へのリンク"); + continue; + } + + // Webページのタイトル取得 + String title = getTitle(url); + if (title != null) { + System.out.println("title: " + title); + if (title.length() >= 30) { + title = title.substring(0, 30) + "以下略"; + } + System.out.println("title 2: " + title); + content = content.replace(url, "Webページ「%s」へのリンク".formatted(title)); + } else { + content = content.replace(url, "Webページへのリンク"); + } + } + return content; + } + + String replacerChannelThreadLink(JDA jda, String content) { + return channelReplyPattern.matcher(content).replaceAll(result -> { + String channelId = result.group(1); + Channel channel = getTextVoiceChannelOrThread(jda, channelId); + if (channel == null) return "どこかのチャンネルへのリンク"; + String channelName = "チャンネル「" + channel.getName() + "」へのリンク"; + if (channel instanceof ThreadChannel) { + channelName = "チャンネル「%s」のスレッド「%s」へのリンク".formatted(((ThreadChannel) channel).getParentChannel().getName(), channel.getName()); + } + return channelName; + }); + } + + String replacerDiscordInviteLink(JDA jda, Guild sendFromGuild, String content) { + content = eventDirectLinkUrlPattern.matcher(content).replaceAll(result -> { + String guildId = result.group(1); + String eventId = result.group(2); + + Guild guild = jda.getGuildById(guildId); + if (guild == null) return "どこかのサーバのイベントへのリンク"; + + String eventName = getScheduledEventName(guildId, eventId); + if (eventName == null) return "サーバ「%s」のイベントへのリンク".formatted(guild.getName()); + + if (guild.getIdLong() == sendFromGuild.getIdLong()) { + return "イベント「%s」へのリンク".formatted(eventName); + } + return "サーバ「%s」のイベント「%s」へのリンク".formatted(guild.getName(), eventName); + }); + + content = eventInviteLinkUrlPattern.matcher(content).replaceAll(result -> { + String inviteCode = result.group(1); + String eventId = result.group(2); + + DiscordInvite invite = getInvite(inviteCode, eventId); + if (invite == null) return "どこかのサーバのイベントへのリンク"; + if (invite.eventName() == null) return "サーバ「%s」のイベントへのリンク".formatted(invite.guildName()); + + if (invite.guildId().equals(sendFromGuild.getId())) { + return "イベント「%s」へのリンク".formatted(invite.eventName()); + } + return "サーバ「%s」のイベント「%s」へのリンク".formatted(invite.guildName(), invite.eventName()); + }); + + content = inviteLinkUrlPattern.matcher(content).replaceAll(result -> { + String inviteCode = result.group(1); + + DiscordInvite invite = getInvite(inviteCode, null); + if (invite == null) return "どこかのサーバへの招待リンク"; + if (invite.channelName() == null) return "サーバ「%s」への招待リンク".formatted(invite.guildName()); + + if (invite.guildId().equals(sendFromGuild.getId())) { + return "チャンネル「%s」への招待リンク".formatted(invite.channelName()); + } + return "サーバ「%s」のチャンネル「%s」への招待リンク".formatted(invite.guildName(), invite.channelName()); + }); + + return content; + } + + String replacerSpoiler(String content) { + return spoilerPattern.matcher(content).replaceAll(" ピー "); + } + + boolean isEmphasizeMessage(String content) { + return + content.matches("^\\*\\*(.[  ]){2,}.\\*\\*$") || // **あ い う え お** OR **あ い う え お** + content.matches("^(.[  ]){2,}.$") || // あ い う え お OR あ い う え お + content.matches("^\\*\\*(.+)\\*\\*$"); // **あああああああ** + } + + String replacerEmphasizeMessage(String content) { + for (String s : Arrays.asList("**", " ", " ")) { + content = content.replace(s, ""); + } + return content; + } + + VoiceText changeEmphasizeSpeed(VoiceText vt) { + try { + return vt.setSpeed(Math.max(vt.getSpeed() / 2, 50)); + } catch (VoiceText.WrongSpeedException e) { + return vt; + } + } + + @Nullable + String getTitle(String url) { + try { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + Request request = new Request.Builder().url(url).build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() != 200 && response.code() != 302) { + return null; + } + ResponseBody body = response.body(); + if (body == null) { + return null; + } + Matcher m = titlePattern.matcher(body.string()); + return m.find() ? m.group(1) : null; + } + } catch (IOException e) { + return null; + } + } + + record Tweet(String authorName, String html, String plainText) { + } + + @Nullable + Tweet getTweet(String screenName, String tweetId) { + String url = "https://publish.twitter.com/oembed?url=https://twitter.com/%s/status/%s".formatted(screenName, tweetId); + try { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + Request request = new Request.Builder().url(url).build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() != 200 && response.code() != 302) { + return null; + } + ResponseBody body = response.body(); + if (body == null) { + return null; + } + JSONObject json = new JSONObject(body.string()); + String html = json.getString("html"); + String authorName = json.getString("author_name"); + String plainText = new Source(html.replaceAll("(.*)", "")) + .getFirstElement("p") + .getRenderer() + .setMaxLineLength(Integer.MAX_VALUE) + .setNewLine(null) + .toString(); + System.out.println(plainText); + return new Tweet(authorName, html, plainText); + } + } catch (IOException e) { + return null; + } + } + + @Nullable + String getScheduledEventName(String guildId, String eventId) { + String url = "https://discord.com/api/guilds/%s/scheduled-events/%s".formatted(guildId, eventId); + try { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + Request request = new Request.Builder() + .url(url) + .header("Authorization", "Bot " + Main.getDiscordToken()) + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() != 200 && response.code() != 302) { + return null; + } + ResponseBody body = response.body(); + if (body == null) { + return null; + } + JSONObject json = new JSONObject(body.string()); + return json.getString("name"); + } + } catch (IOException e) { + return null; + } + } + + record DiscordInvite( + String code, + String guildId, + String guildName, + String channelId, + String channelName, + @Nullable String inviterId, + @Nullable String inviterName, + @Nullable String inviterDiscriminator, + @Nullable String eventName, + @Nullable String eventId + ) { + } + + @Nullable + DiscordInvite getInvite(String inviteCode, String eventId) { + String url = "https://discord.com/api/invites/%s".formatted(inviteCode); + if (eventId != null) { + url += "?guild_scheduled_event_id=%s".formatted(eventId); + } + try { + OkHttpClient client = new OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(); + Request request = new Request.Builder() + .url(url) + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() != 200 && response.code() != 302) { + return null; + } + ResponseBody body = response.body(); + if (body == null) { + return null; + } + JSONObject json = new JSONObject(body.string()); + return new DiscordInvite( + json.getString("code"), + json.getJSONObject("guild").getString("id"), + json.getJSONObject("guild").getString("name"), + json.getJSONObject("channel").getString("id"), + json.getJSONObject("channel").getString("name"), + json.has("inviter") ? json.getJSONObject("inviter").getString("id") : null, + json.has("inviter") ? json.getJSONObject("inviter").getString("username") : null, + json.has("inviter") ? json.getJSONObject("inviter").getString("discriminator") : null, + json.has("guild_scheduled_event") ? json.getJSONObject("guild_scheduled_event").getString("name") : null, + json.has("guild_scheduled_event") ? json.getJSONObject("guild_scheduled_event").getString("id") : null + ); + } + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/ProcessorType.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/ProcessorType.java new file mode 100644 index 0000000..7f2f77d --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/ProcessorType.java @@ -0,0 +1,51 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageType; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public enum ProcessorType { + /** ユーザーが送信したデフォルトメッセージ */ + DEFAULT((message) -> message.getType() == MessageType.DEFAULT && !message.getAuthor().isBot() && !message.getType().isSystem()), + /** Bot が送信したデフォルトメッセージ */ + BOT_DEFAULT((message) -> message.getType() == MessageType.DEFAULT && message.getAuthor().isBot() && !message.getType().isSystem()), + /** ユーザーがメッセージに対して返信したメッセージ (Embed メッセージを含む) */ + REPLY((message) -> message.getType() == MessageType.INLINE_REPLY && !message.getAuthor().isBot() && !message.getType().isSystem()), + /** Bot がメッセージに対して返信したメッセージ (Embed メッセージを含む) */ + BOT_REPLY((message) -> message.getType() == MessageType.INLINE_REPLY && message.getAuthor().isBot() && !message.getType().isSystem()), + /** Bot に対してのコマンドと思われるメッセージ */ + BOT_COMMAND((message) -> message.getType() == MessageType.DEFAULT && getBotCommandPrefix(message.getContentDisplay()) && !message.getType().isSystem()), + /** ユーザーが送信したEmbedメッセージ */ + EMBED((message) -> message.getType() == MessageType.DEFAULT && message.getEmbeds().size() > 0 && !message.getAuthor().isBot() && !message.getType().isSystem()), + /** Bot が送信したEmbedメッセージ */ + BOT_EMBED((message) -> message.getType() == MessageType.DEFAULT && message.getEmbeds().size() > 0 && message.getAuthor().isBot() && !message.getType().isSystem()), + /** スタンプ (Sticker) メッセージ */ + STICKERS((message) -> message.getType() == MessageType.DEFAULT && message.getStickers().size() > 0 && !message.getType().isSystem()), + /** 添付ファイル (メッセージ) */ + ATTACHMENTS((message) -> message.getType() == MessageType.DEFAULT && message.getAttachments().size() > 0 && !message.getType().isSystem()), + /** スレッド開始通知メッセージ (メッセージ派生かどうかを問わない) */ + CREATED_THREAD((message) -> message.getType() == MessageType.THREAD_CREATED), + /** スラッシュコマンドによるメッセージ */ + WITH_APPLICATION_COMMAND((message) -> message.getType() == MessageType.SLASH_COMMAND); + + private final Predicate predicate; + + ProcessorType(Predicate predicate) { + this.predicate = predicate; + } + + public boolean check(Message message) { + return predicate.test(message); + } + + public static List getMatchProcessor(Message message) { + return Stream.of(values()).filter(p -> p.check(message)).toList(); + } + + static boolean getBotCommandPrefix(String content) { + return Stream.of("/", "!").anyMatch(content::startsWith); + } +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/StickersProcessor.java b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/StickersProcessor.java new file mode 100644 index 0000000..315b9c5 --- /dev/null +++ b/src/main/java/com/jaoafa/jdavcspeaker/MessageProcessor/StickersProcessor.java @@ -0,0 +1,25 @@ +package com.jaoafa.jdavcspeaker.MessageProcessor; + +import com.jaoafa.jdavcspeaker.Lib.UserVoiceTextResult; +import com.jaoafa.jdavcspeaker.Player.TrackInfo; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; + +/** + * スタンプ (Sticker) プロセッサ + *

+ * ・「スタンプ XXX が送信されました。」と読み上げる + */ +public class StickersProcessor implements BaseProcessor { + @Override + public ProcessorType getType() { + return ProcessorType.STICKERS; + } + + @Override + public void execute(JDA jda, Guild guild, TextChannel channel, Member member, Message message, UserVoiceTextResult uvtr) { + for (MessageSticker sticker : message.getStickers()) { + uvtr.vt().play(TrackInfo.SpeakFromType.RECEIVED_STICKER, message, "スタンプ「%s」が送信されました。".formatted(sticker.getName())); + } + } +} diff --git a/src/main/java/com/jaoafa/jdavcspeaker/Player/TrackInfo.java b/src/main/java/com/jaoafa/jdavcspeaker/Player/TrackInfo.java index dfa5d05..0038518 100644 --- a/src/main/java/com/jaoafa/jdavcspeaker/Player/TrackInfo.java +++ b/src/main/java/com/jaoafa/jdavcspeaker/Player/TrackInfo.java @@ -36,6 +36,8 @@ public enum SpeakFromType { RECEIVED_FILE, /** 画像が送信された */ RECEIVED_IMAGE, + /** スタンプが送信された */ + RECEIVED_STICKER, /** VCにユーザーが参加した */ JOINED_VC, /** VCでユーザーが移動した */ @@ -47,6 +49,8 @@ public enum SpeakFromType { /** GoLiveを終了した */ ENDED_GOLIVE, /** VCタイトルを変えた */ - CHANGED_TITLE + CHANGED_TITLE, + /** スレッドを作成した */ + CREATED_THREAD } }