From 8d2648a8952400fd5992c08123551a72ade7eb64 Mon Sep 17 00:00:00 2001 From: thmsdy Date: Wed, 20 Aug 2025 23:06:58 -0500 Subject: [PATCH] Added YouTube RSS feed monitoring --- pom.xml | 5 + src/main/java/com/fpghoti/biscuit/Main.java | 5 +- .../commands/discord/AddYTFeedCommand.java | 52 ++++++ .../fpghoti/biscuit/config/BiscuitConfig.java | 2 +- .../fpghoti/biscuit/guild/BiscuitGuild.java | 118 +++++++++++++- .../java/com/fpghoti/biscuit/rss/YTEntry.java | 67 ++++++++ .../java/com/fpghoti/biscuit/rss/YTFeed.java | 143 +++++++++++++++++ .../com/fpghoti/biscuit/rss/YTFeedConfig.java | 149 ++++++++++++++++++ .../java/com/fpghoti/biscuit/util/Util.java | 11 ++ src/main/resources/logback.xml | 2 + 10 files changed, 550 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/fpghoti/biscuit/commands/discord/AddYTFeedCommand.java create mode 100644 src/main/java/com/fpghoti/biscuit/rss/YTEntry.java create mode 100644 src/main/java/com/fpghoti/biscuit/rss/YTFeed.java create mode 100644 src/main/java/com/fpghoti/biscuit/rss/YTFeedConfig.java diff --git a/pom.xml b/pom.xml index a5ea503..57e1fbf 100644 --- a/pom.xml +++ b/pom.xml @@ -151,5 +151,10 @@ pf4j 3.13.0 + + xom + xom + 1.3.9 + \ No newline at end of file diff --git a/src/main/java/com/fpghoti/biscuit/Main.java b/src/main/java/com/fpghoti/biscuit/Main.java index 3d730f4..ddf52c6 100644 --- a/src/main/java/com/fpghoti/biscuit/Main.java +++ b/src/main/java/com/fpghoti/biscuit/Main.java @@ -14,6 +14,7 @@ import com.fpghoti.biscuit.commands.console.GuildSayCommand; import com.fpghoti.biscuit.commands.console.SayCommand; import com.fpghoti.biscuit.commands.console.ShutdownConsoleCommand; import com.fpghoti.biscuit.commands.discord.AddCommand; +import com.fpghoti.biscuit.commands.discord.AddYTFeedCommand; import com.fpghoti.biscuit.commands.discord.ChanIDCommand; import com.fpghoti.biscuit.commands.discord.DivideCommand; import com.fpghoti.biscuit.commands.discord.GetConfigCommand; @@ -100,6 +101,7 @@ public class Main { private static Cage cage; private static File pluginsDir; public static File audioDir; + //public static File ytFeedDir; private static PluginController pluginController; private static AudioPlayerManager playerManager; @@ -131,9 +133,7 @@ public class Main { playerManager = new DefaultAudioPlayerManager(); AudioSourceManagers.registerLocalSource(playerManager); YoutubeAudioSourceManager ytSourceManager = new dev.lavalink.youtube.YoutubeAudioSourceManager(); - //LocalAudioSourceManager localSourceManager = new LocalAudioSourceManager(); playerManager.registerSourceManager(ytSourceManager); - //playerManager.registerSourceManager(localSourceManager); AudioSourceManagers.registerRemoteSources(playerManager, com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager.class); @@ -175,6 +175,7 @@ public class Main { CommandManager.addCommand(new ToggleRoleCommand()); CommandManager.addCommand(new SquareRootCommand()); CommandManager.addCommand(new AddCommand()); + CommandManager.addCommand(new AddYTFeedCommand()); CommandManager.addCommand(new SubtractCommand()); CommandManager.addCommand(new MultiplyCommand()); CommandManager.addCommand(new DivideCommand()); diff --git a/src/main/java/com/fpghoti/biscuit/commands/discord/AddYTFeedCommand.java b/src/main/java/com/fpghoti/biscuit/commands/discord/AddYTFeedCommand.java new file mode 100644 index 0000000..e04cf16 --- /dev/null +++ b/src/main/java/com/fpghoti/biscuit/commands/discord/AddYTFeedCommand.java @@ -0,0 +1,52 @@ +package com.fpghoti.biscuit.commands.discord; + +import com.fpghoti.biscuit.Main; +import com.fpghoti.biscuit.commands.base.ClientCommand; +import com.fpghoti.biscuit.guild.BiscuitGuild; +import com.fpghoti.biscuit.rest.MessageText; +import com.fpghoti.biscuit.util.PermUtil; +import com.fpghoti.biscuit.util.Util; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +//import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; + +public class AddYTFeedCommand extends ClientCommand{ + + public AddYTFeedCommand() { + name = "Add YouTube Feed"; + description = "Sets bot to post YouTube uploads for a specified YouTube channel RSS feed inside the text channel the command was run in"; + usage = Main.getMainBiscuit().getProperties().getCommandSignifier() + "addytfeed "; + minArgs = 1; + maxArgs = 1000000; + identifiers.add("addytfeed"); + } + + @Override + public void execute(String[] args, MessageReceivedEvent event) { + System.out.println("0"); + BiscuitGuild b = BiscuitGuild.getBiscuitGuild(event.getGuild()); + b.log(event.getAuthor().getName() + " issued a command: -addytfeed " + args[0]); + System.out.println("A"); + if((PermUtil.isAdmin(event.getMember()))) { + System.out.println("B"); + Guild g = event.getGuild(); + TextChannel c = event.getChannel().asTextChannel(); + String alias = args[0]; + String channelURL = args[1]; + String message = Util.getArgsMessage(2, args); + System.out.println("C"); + boolean success = b.addYoutubeFeed(alias, c, channelURL, message); + System.out.println("D"); + if(success) { + MessageText.send(event.getChannel().asTextChannel(), "YouTube Feed has been created."); + }else { + MessageText.send(event.getChannel().asTextChannel(), "There was an error creating the YouTube feed."); + } + }else { + MessageText.send(event.getChannel().asTextChannel(), "You do not have permission to create a YouTube feed."); + } + } + +} diff --git a/src/main/java/com/fpghoti/biscuit/config/BiscuitConfig.java b/src/main/java/com/fpghoti/biscuit/config/BiscuitConfig.java index c6ccda9..590cfcf 100644 --- a/src/main/java/com/fpghoti/biscuit/config/BiscuitConfig.java +++ b/src/main/java/com/fpghoti/biscuit/config/BiscuitConfig.java @@ -152,7 +152,7 @@ public class BiscuitConfig { //Add properties to appear in both types of configs here added = addProperty("ChatLog", "true", prop, added, silent); - added = addProperty("AllowSpamPunish", "true", prop, added, silent); + added = addProperty("AllowSpamPunish", "false", prop, added, silent); added = addProperty("Channels-To-Not-Chatlog", "ignore_me,ignore_me2,ignore_me3", prop, added, silent); added = addProperty("NaughtyList", "piff,word123,another1", prop, added, silent); added = addProperty("Restrict-Cmd-Channels", "false", prop, added, silent); diff --git a/src/main/java/com/fpghoti/biscuit/guild/BiscuitGuild.java b/src/main/java/com/fpghoti/biscuit/guild/BiscuitGuild.java index b5129d7..1dfed5b 100644 --- a/src/main/java/com/fpghoti/biscuit/guild/BiscuitGuild.java +++ b/src/main/java/com/fpghoti/biscuit/guild/BiscuitGuild.java @@ -1,11 +1,18 @@ package com.fpghoti.biscuit.guild; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Properties; import java.util.Timer; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import com.fpghoti.biscuit.Main; import com.fpghoti.biscuit.PluginCore; @@ -15,6 +22,8 @@ import com.fpghoti.biscuit.config.BiscuitProperties; import com.fpghoti.biscuit.logging.BColor; import com.fpghoti.biscuit.logging.BiscuitLogger; import com.fpghoti.biscuit.rest.MessageText; +import com.fpghoti.biscuit.rss.YTFeed; +import com.fpghoti.biscuit.rss.YTFeedConfig; import com.fpghoti.biscuit.timer.BiscuitTimer; import com.fpghoti.biscuit.timer.task.ChatCountTimer; import com.fpghoti.biscuit.timer.task.DecrementTimer; @@ -29,7 +38,6 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Invite; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; -//import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.User; @@ -83,9 +91,11 @@ public class BiscuitGuild { private Timer timer; private List timers; private File captchaDir; + private File ytFeedDir; //private Cage cage; private Guild guild; private HashMap inviteUses; + private HashMap ytfeeds; private BiscuitConfig config; private BiscuitProperties properties; private GuildMessageStore messageStore; @@ -106,6 +116,7 @@ public class BiscuitGuild { this.properties = new BiscuitProperties(this); this.rolequeue = new HashMap(); this.player = Main.getPlayerManager().createPlayer(); + this.ytfeeds = new HashMap(); scheduler = new AudioScheduler(this); player.addListener(scheduler); @@ -115,9 +126,19 @@ public class BiscuitGuild { if(!Main.isPlugin) { captchaDir = new File("captcha"); captchaDir.mkdir(); + if(guild != null) { + File f = new File("ytchannels"); + f.mkdir(); + ytFeedDir = new File(f, guild.getId()); + ytFeedDir.mkdir(); + } }else { captchaDir = new File(PluginCore.plugin.getDataFolder(), "captcha"); captchaDir.mkdir(); + if(guild != null) { + ytFeedDir = new File(PluginCore.plugin.getDataFolder(), "ytchannels\\" + guild.getId()); + ytFeedDir.mkdirs(); + } } if(isMain) { wipeCaptchaDir(); @@ -131,6 +152,20 @@ public class BiscuitGuild { }); } } + + if(guild != null) { + loadYoutubeFeeds(); + Runnable post = () -> { + try { + log("Updating Youtube feeds..."); + postYoutubeFeeds(); + } catch (IOException e) { + e.printStackTrace(); + } + }; + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(post, 0, 1, TimeUnit.MINUTES); + } } public BiscuitConfig getConfig() { @@ -161,6 +196,87 @@ public class BiscuitGuild { return scheduler; } + public YTFeedConfig loadYouTubeFeedConfig(String alias) { + alias = alias.toLowerCase(); + String channelID = "null"; + String youTubeChannelURL = "null"; + String message = "null"; + String lastVideo = "null"; + + Properties prop = new Properties(); + InputStream input = null; + + File config = new File(ytFeedDir, alias); + + if(!config.exists()) { + logger.error("Could not locate YouTube feed config."); + return null; + } + + try { + input = new FileInputStream(config); + prop.load(input); + channelID = prop.getProperty("TextChannelID"); + youTubeChannelURL = prop.getProperty("YouTubeChannelURL"); + message = prop.getProperty("Message"); + lastVideo = prop.getProperty("LastVideo"); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + TextChannel textChannel = jda.getTextChannelById(channelID); + if(textChannel == null) { + logger.error("Error retrieving Text Channel from YouTube feed file."); + return null; + } + YTFeed feed = new YTFeed(alias, textChannel, youTubeChannelURL, message); + feed.setLastVideo(lastVideo); + return new YTFeedConfig(this, feed); + } + + public void loadYoutubeFeeds() { + ytfeeds.clear(); + File[] files = ytFeedDir.listFiles(); + for(File f : files) { + String alias = f.getName().toLowerCase(); + ytfeeds.put(alias, loadYouTubeFeedConfig(alias)); + } + } + + public boolean addYoutubeFeed(String alias, TextChannel channel, String channelURL, String message) { + alias = alias.toLowerCase(); + if(guild == null || ytfeeds.containsKey(alias)) { + return false; + } + YTFeed feed = new YTFeed(alias, channel, channelURL, message); + //Generate a file for the feed + new YTFeedConfig(this, feed); + //Loads all feeds from files into hash map + loadYoutubeFeeds(); + return true; + } + + public void postYoutubeFeeds() throws IOException { + for(String s : ytfeeds.keySet()) { + YTFeedConfig config = ytfeeds.get(s); + YTFeed feed = config.getFeed(); + feed.post(); + config.setLastPosted(feed.getLastVideo()); + } + } + + public File getYTFeedDir() { + return ytFeedDir; + } + public void log(String message) { if(properties == null) { logger.info(message); diff --git a/src/main/java/com/fpghoti/biscuit/rss/YTEntry.java b/src/main/java/com/fpghoti/biscuit/rss/YTEntry.java new file mode 100644 index 0000000..acfabeb --- /dev/null +++ b/src/main/java/com/fpghoti/biscuit/rss/YTEntry.java @@ -0,0 +1,67 @@ +package com.fpghoti.biscuit.rss; + +import java.awt.Color; + +import com.fpghoti.biscuit.util.Util; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; + +public class YTEntry { + + private String id; + private String title; + private String author; + private String description; + private String thumbnail; + + public YTEntry(String id, String title, String author, String description, String thumbnail) { + this.id = id; + this.title = title; + this.author = author; + this.description = description; + this.thumbnail = thumbnail; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getAuthor() { + return author; + } + + public String getDescription() { + return description; + } + + public String getThumbnail() { + return thumbnail; + } + + public String getURL() { + return "https://www.youtube.com/watch?v=" + id; + } + + public MessageEmbed getEmbedMessage() { + EmbedBuilder embed = new EmbedBuilder(); + embed.setTitle(title, getURL()); + embed.setColor(Color.RED); + + String descPreview = description.substring(0, Math.min(description.length(), 200)); + if(descPreview.length() < description.length()) { + descPreview += "..."; + } + + embed.setDescription(descPreview); + embed.setAuthor(author, null, null); + embed.setThumbnail(thumbnail); + return embed.build(); + } + +} diff --git a/src/main/java/com/fpghoti/biscuit/rss/YTFeed.java b/src/main/java/com/fpghoti/biscuit/rss/YTFeed.java new file mode 100644 index 0000000..aec8b34 --- /dev/null +++ b/src/main/java/com/fpghoti/biscuit/rss/YTFeed.java @@ -0,0 +1,143 @@ +package com.fpghoti.biscuit.rss; + +import java.awt.Color; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fpghoti.biscuit.guild.BiscuitGuild; +import com.fpghoti.biscuit.rest.MessageText; + +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import nu.xom.Builder; +import nu.xom.Document; +import nu.xom.Element; +import nu.xom.Elements; +import nu.xom.ParsingException; + +public class YTFeed { + + private BiscuitGuild guild; + private String alias; + private TextChannel channel; + private String channelURL; + private String message; + private String lastVideo; + + public YTFeed(String alias, TextChannel channel, String channelURL, String message) { + this.guild = BiscuitGuild.getBiscuitGuild(channel.getGuild()); + this.alias = alias; + this.channel = channel; + this.channelURL = channelURL; + this.message = message; + lastVideo = ""; + } + + public String getAlias() { + return alias; + } + + public TextChannel getTextChannel() { + return channel; + } + + public String getChannelURL() { + return channelURL; + } + + public String getMesage() { + return message; + } + + public String getLastVideo() { + return lastVideo; + } + + public void setLastVideo(String link) { + lastVideo = link; + } + + public void post(){ + List ytentries = getEntries(); + int index = 0; + int lastVidIndex = -1; + for(YTEntry entry : ytentries) { + String link = entry.getURL(); + if(link.equals(lastVideo)) { + lastVidIndex = index; + } + index++; + } + index = 0; + for(YTEntry entry : ytentries) { + if(index > lastVidIndex) { + String link = entry.getURL(); + lastVideo = link; + MessageText.send(channel, message); + MessageText.send(channel, entry.getEmbedMessage()); + } + index++; + } + } + + public List getEntries() { + List ytentries = new ArrayList(); + + Builder builder = new Builder(); + Document doc = null; + try { + doc = builder.build(channelURL); + } catch (ParsingException | IOException e) { + guild.error("Unable to parse feed: " + channelURL); + MessageText.send(channel, "Unable to parse feed: " + channelURL); + } + + Element rss = doc.getRootElement(); + Elements entries = rss.getChildElements(); + for(Element entry : entries){ + if(entry.getLocalName().equals("entry")) { + + String id = ""; + String title = ""; + String author = ""; + String description = ""; + String thumbnail = ""; + + Elements values = entry.getChildElements(); + for(Element value : values){ + if(value.getLocalName().equals("videoId")) { + id = value.getValue(); + } + if(value.getLocalName().equals("title")) { + title = value.getValue(); + } + if(value.getLocalName().equals("author")) { + Elements subvalues = value.getChildElements(); + for(Element subvalue : subvalues) { + if(subvalue.getLocalName().equals("name")) { + author = subvalue.getValue(); + } + } + } + if(value.getLocalName().equals("group")) { + Elements subvalues = value.getChildElements(); + for(Element subvalue : subvalues) { + if(subvalue.getLocalName().equals("description")) { + description = subvalue.getValue(); + } + if(subvalue.getLocalName().equals("thumbnail")) { + thumbnail = subvalue.getAttributeValue("url"); + } + } + } + } + + ytentries.add(new YTEntry(id, title, author, description, thumbnail)); + } + } + + return ytentries.reversed(); + + } + +} diff --git a/src/main/java/com/fpghoti/biscuit/rss/YTFeedConfig.java b/src/main/java/com/fpghoti/biscuit/rss/YTFeedConfig.java new file mode 100644 index 0000000..7c9f252 --- /dev/null +++ b/src/main/java/com/fpghoti/biscuit/rss/YTFeedConfig.java @@ -0,0 +1,149 @@ +package com.fpghoti.biscuit.rss; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.configuration2.PropertiesConfigurationLayout; +import org.apache.commons.configuration2.ex.ConfigurationException; + +import com.fpghoti.biscuit.guild.BiscuitGuild; + +public class YTFeedConfig { + + private BiscuitGuild biscuit; + private YTFeed feed; + private File config; + + public YTFeedConfig(BiscuitGuild b, YTFeed feed) { + this.biscuit = b; + this.feed = feed; + generateConfig(); + } + + public BiscuitGuild getGuild() { + return biscuit; + } + + public YTFeed getFeed() { + return feed; + } + + public void generateConfig() { + + String name = feed.getAlias(); + + File config = new File(biscuit.getYTFeedDir(), name); + + if (!config.exists()) { + try { + config.createNewFile(); + updateConfig(config); + } catch (Exception e) { + e.printStackTrace(); + } + }else { + updateConfig(config); + } + this.config = config; + } + + + public void setLastPosted(String link) { + PropertiesConfiguration prop = new PropertiesConfiguration(); + PropertiesConfigurationLayout layout = new PropertiesConfigurationLayout(); + prop.setLayout(layout); + try { + layout.load(prop, new FileReader(config)); + FileWriter fw = new FileWriter(config); + prop.setProperty("LastVideo", link); + feed.setLastVideo(link); + layout.save(prop, fw); + } catch (ConfigurationException e) { + e.printStackTrace(); + biscuit.error("There was an issue updating a YouTube feed file."); + return; + } catch (IOException e) { + e.printStackTrace(); + biscuit.error("There was an issue updating a YouTube feed file."); + return; + } + } + + private void updateConfig(File config) { + PropertiesConfiguration prop = new PropertiesConfiguration(); + PropertiesConfigurationLayout layout = new PropertiesConfigurationLayout(); + prop.setLayout(layout); + try { + layout.load(prop, new FileReader(config)); + FileWriter fw = new FileWriter(config); + addNewOptions(prop); + layout.save(prop, fw); + } catch (ConfigurationException e) { + e.printStackTrace(); + biscuit.error("There was an issue preparing a YouTube feed config for updates."); + return; + } catch (IOException e) { + e.printStackTrace(); + biscuit.error("There was an issue preparing a YouTube feed config for updates."); + return; + } + } + + private void addNewOptions(PropertiesConfiguration prop) { + if(feed == null) { + return; + } + + addProperty("Alias", feed.getAlias(), prop); + addProperty("TextChannelID", feed.getTextChannel().getId(), prop); + addProperty("YouTubeChannelURL", feed.getChannelURL(), prop); + addProperty("Message", feed.getMesage(), prop); + addProperty("LastVideo", "", prop); + } + + private void addProperty(String key, String value, PropertiesConfiguration prop) { + if(prop.getProperty(key) == null) { + prop.addProperty(key, value); + } + } + + public String getFromConfig(String property){ + + String setting = ""; + + Properties prop = new Properties(); + InputStream input = null; + + File config = new File(biscuit.getYTFeedDir(), feed.getAlias()); + + if(!config.exists()) { + return ""; + } + + try { + input = new FileInputStream(config); + prop.load(input); + setting = prop.getProperty(property); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return setting; + + } + +} diff --git a/src/main/java/com/fpghoti/biscuit/util/Util.java b/src/main/java/com/fpghoti/biscuit/util/Util.java index c2854ac..d4b31ba 100644 --- a/src/main/java/com/fpghoti/biscuit/util/Util.java +++ b/src/main/java/com/fpghoti/biscuit/util/Util.java @@ -83,5 +83,16 @@ public class Util { String last = s[s.length - 1]; return last; } + + public static String getArgsMessage(int startAt, String[] args) { + String message = ""; + for(int i = startAt; i < args.length; i++) { + message = message + args[i] + " "; + } + if (message.length() > 0) { + message = message.substring(0, message.length() - 1); + } + return message; + } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 0f89553..5b184f5 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -38,4 +38,6 @@ + + \ No newline at end of file