import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathFactory; import org.pircbotx.Channel; import org.pircbotx.PircBotX; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import pl.shockah.FileLine; import pl.shockah.HTTPQuery; import pl.shockah.StringTools; import pl.shockah.shocky.Module; import pl.shockah.shocky.Shocky; import pl.shockah.shocky.Utils; import pl.shockah.shocky.cmds.Command; import pl.shockah.shocky.cmds.CommandCallback; import pl.shockah.shocky.cmds.Parameters; public class ModuleRSS extends Module { private static final String[] rssFormats = new String[] { "EEE, dd MMM yyyy HH:mm:ss z", "EEE, dd MMM yyyy HH:mm:ss Z"}; private static final String[] atomFormats = new String[] { "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss"}; protected Command cmd; protected List<Feed> feeds = Collections.synchronizedList(new LinkedList<Feed>()); private Timer timer; public String name() {return "rss";} public void onEnable(File dir) { Command.addCommands(this, cmd = new CmdRSS()); synchronized (feeds) { feeds.clear(); ArrayList<String> lines = FileLine.read(new File(dir,"rss.cfg")); int n = 4; for (int i = 0; i < lines.size(); i += n) { try { URL url = new URL(lines.get(i)); long time = Long.parseLong(lines.get(i+1)); Date date = time <= 0 ? null : new Date(time); long interval; String[] channels; try { interval = Long.parseLong(lines.get(i+2)); channels = lines.get(i+3).split(" "); } catch (NumberFormatException e) { n = 3; interval = (5*60*1000); channels = lines.get(i+2).split(" "); } feeds.add(new Feed(url,interval,date,channels)); } catch (MalformedURLException e) { continue; } } } startUpdater(); } public void onDisable() { Command.removeCommands(cmd); stopUpdater(); } public void onDataSave(File dir) { ArrayList<String> lines = new ArrayList<String>(); synchronized (feeds) { for (Feed feed : feeds) { lines.add(feed.getURL().toString()); lines.add(Long.toString((feed.getDate()!=null)?feed.getDate().getTime():0L)); lines.add(Long.toString(feed.getInterval())); lines.add(StringTools.implode(feed.channels.toArray(new String[feed.channels.size()])," ")); } } FileLine.write(new File(dir,"rss.cfg"),lines); } public void stopUpdater() { if (timer == null) return; timer.cancel(); timer = null; } public void startUpdater() { if (timer != null) stopUpdater(); timer = new Timer(true); synchronized (feeds) { for (Feed feed : feeds) timer.scheduleAtFixedRate(feed, 0, feed.getInterval()); } } protected class Feed extends TimerTask { private final URL url; private final long interval; private Date lastDate; public ArrayList<String> channels = new ArrayList<String>(); public Feed(URL url, long interval, Date date, String... channels) { this.url = url; this.interval = interval; this.channels.addAll(Arrays.asList(channels)); lastDate = date; } public boolean equals(Object o) { if (o == null) return false; if (o instanceof Feed) return ((Feed)o).getURL().equals(getURL()); return false; } public int hashCode() {return url.hashCode();} public URL getURL() {return url;} public long getInterval() {return interval;} public Date getDate() {return lastDate;} public Date parseAtomDate(String s) { Date d = parseDate(atomFormats,s.substring(0,19)); if (d == null) return null; try { Calendar c = Calendar.getInstance(); c.setTimeZone(TimeZone.getTimeZone("GMT")); c.setTime(d); s = s.substring(19); long t = c.getTime().getTime(); int sign = s.charAt(0) == '+' ? 1 : -1; t += sign*Integer.parseInt(s.substring(1,3))*(60*60*1000); t += sign*Integer.parseInt(s.substring(4,6))*(60*1000); return new Date(t); } catch (Exception e) {e.printStackTrace();} return null; } public Date parseDate(String[] formats, String s) { for (int i=0; i < formats.length; ++i) { DateFormat sdf = new SimpleDateFormat(formats[i]); try { return sdf.parse(s); } catch (ParseException e) {} } return null; } @Override public void run() { HTTPQuery q = new HTTPQuery(url); Document xBase; try { q.connect(true,false); q.setUserAgentFirefox(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); xBase = builder.parse(q.getConnection().getInputStream()); } catch (Exception e1) { e1.printStackTrace(); this.cancel(); synchronized (feeds) { feeds.remove(this); } return; } finally { q.close(); } List<FeedEntry> ret = new LinkedList<FeedEntry>(); Date newest = null; try { XPathFactory xpathFactory = XPathFactory.newInstance(); XPath xpath = xpathFactory.newXPath(); XPathExpression xTitle = xpath.compile("./title"); NodeList feeds = (NodeList) xpath.evaluate("//feed/entry", xBase, XPathConstants.NODESET); if (feeds.getLength() > 0) { XPathExpression xHref = xpath.compile("./link/@href"); XPathExpression xDateAtom = xpath.compile("./updated|./published"); String channel = xpath.evaluate("//feed/title", xBase); for (int i = 0; i < feeds.getLength(); ++i) { Node xEntry = feeds.item(i); Date entryDate; String entryTitle = xTitle.evaluate(xEntry); String entryLink = xHref.evaluate(xEntry); String date = xDateAtom.evaluate(xEntry); if (date != null) { entryDate = parseDate(atomFormats, date); if (entryDate == null) entryDate = parseAtomDate(date); } else continue; if (newest == null || entryDate.after(newest)) newest = entryDate; if (lastDate == null) continue; if (entryDate.after(lastDate)) ret.add(new FeedEntry(channel,entryTitle,entryLink,entryDate)); } } else { feeds = (NodeList) xpath.evaluate("//rss/channel/item", xBase, XPathConstants.NODESET); if (feeds.getLength() > 0) { XPathExpression xLink = xpath.compile("./link"); XPathExpression xDateRSS = xpath.compile("./pubDate|./dc:date"); String channel = xpath.evaluate("//rss/channel/title", xBase); for (int i = 0; i < feeds.getLength(); ++i) { Node xEntry = feeds.item(i); String entryTitle = xTitle.evaluate(xEntry); String entryLink = xLink.evaluate(xEntry); String date = xDateRSS.evaluate(xEntry); if (date == null) continue; Date entryDate = parseDate(rssFormats, date); if (entryDate == null) continue; if (newest == null || entryDate.after(newest)) newest = entryDate; if (lastDate == null) continue; if (entryDate.after(lastDate)) ret.add(new FeedEntry(channel,entryTitle,entryLink,entryDate)); } } } if (newest != null) lastDate = newest; if (!ret.isEmpty()) { Collections.sort(ret); newEntries(this,ret); } } catch(Exception e) { e.printStackTrace(); lastDate = new Date(); } } } protected class FeedEntry implements Comparable<FeedEntry> { private String channel, title, url; private Date date; private boolean needsShorten = true; public FeedEntry(String channel, String title, String url, Date date) { this.channel = channel; this.title = title; this.url = url; this.date = date; } public String toString() { if (needsShorten) { needsShorten = false; if (title != null && !title.isEmpty()) title = Utils.shortenAllUrls(StringTools.unescapeHTML(title)); url = Utils.shortenUrl(url); } StringBuilder sb = new StringBuilder(); if (channel != null && !channel.isEmpty()) sb.append(channel).append(' '); sb.append(Utils.timeAgo(date)).append(" | "); if (title != null && !title.isEmpty()) sb.append(title).append(" | "); sb.append(url); return sb.toString(); } public int compareTo(FeedEntry entry) { return date.compareTo(entry.date); } } public class CmdRSS extends Command { public String command() {return "rss";} public String help(Parameters params) { StringBuilder sb = new StringBuilder(); if (params.type == EType.Channel) { sb.append("rss [channel] - list feeds\n"); sb.append("[r:op] rss add [interval] [channel] {url} - add a new feed\n"); sb.append("[r:op] rss remove [channel] {url} - remove a feed"); } else { sb.append("rss {channel} - list feeds\n"); sb.append("[r:op] rss add {channel} {url} - add a new feed\n"); sb.append("[r:op] rss remove {channel} {url} - remove a feed"); } return sb.toString(); } public void doCommand(Parameters params, CommandCallback callback) { String action = null; URL url = null; long interval = (5*60*1000); Channel c = null; callback.type = EType.Notice; try { if (params.type == EType.Channel) { if (params.tokenCount == 0) { c = params.channel; } else if (params.tokenCount == 1) { c = Shocky.getChannel(params.nextParam()); } else if (params.tokenCount == 2) { action = params.nextParam(); c = params.channel; url = new URL(params.nextParam()); } else if (params.tokenCount == 3) { action = params.nextParam(); String s = params.nextParam(); if (s.startsWith("#")) c = Shocky.getChannel(s); else { interval = Utils.parseInterval(s); c = params.channel; } url = new URL(params.nextParam()); } else if (params.tokenCount == 4) { action = params.nextParam(); interval = Utils.parseInterval(params.nextParam()); c = Shocky.getChannel(params.nextParam()); url = new URL(params.nextParam()); } if (c == null) { callback.append("No such channel"); return; } } else { if (params.tokenCount == 1) { c = Shocky.getChannel(params.nextParam()); } else if (params.tokenCount == 3) { action = params.nextParam(); c = Shocky.getChannel(params.nextParam()); url = new URL(params.nextParam()); } if (c == null) { callback.append("No such channel"); return; } } } catch (MalformedURLException e) { callback.append("Malformed URL"); return; } if (action != null) params.checkOp(); synchronized (feeds) { if (action == null) { StringBuffer sb = new StringBuffer(); for (Feed feed : feeds) { if (!feed.channels.contains(c.getName())) continue; if (sb.length() != 0) sb.append("\n"); sb.append(feed.getURL()).append(' ').append(Utils.timeAgo(feed.getInterval()/1000)); } callback.append(sb); } else if (action.equals("add")) { for (Feed feed : feeds) { if (feed.getURL().equals(url)) { if (feed.channels.contains(c.getName())) { callback.append("Feed already on channel's list"); } else { feed.channels.add(c.getName()); callback.append("Added"); } return; } } if (interval < 300) interval = 300; interval*=1000; Feed feed = new Feed(url,interval,null,c.getName()); feeds.add(feed); timer.scheduleAtFixedRate(feed, 0, interval); callback.append("Added"); } else if (action.equals("remove")) { Iterator<Feed> iter = feeds.iterator(); while (iter.hasNext()) { Feed feed = iter.next(); if (feed.getURL().equals(url)) { if (feed.channels.contains(c.getName())) { feed.channels.remove(feed.channels.indexOf(c.getName())); if (feed.channels.isEmpty()) { iter.remove(); feed.cancel(); } callback.append("Removed"); } else callback.append("Feed isn't on channel's list"); return; } } callback.append("Feed isn't on channel's list"); } } } } public void newEntries(Feed feed, Iterable<FeedEntry> entries) throws InterruptedException { for (String s : feed.channels) { Channel channel = Shocky.getChannel(s); if (channel == null) continue; PircBotX bot = channel.getBot(); int i = 0; for (FeedEntry entry : entries) { Shocky.sendChannel(bot,channel,Utils.mungeAllNicks(channel,0,"RSS: "+entry)); Thread.sleep(2000); if (++i > 5) break; } } } }