k47.cz
mastodon twitter RSS
bandcamp explorer

StripBot

StripBot je jednoduchý robot, který sleduje RSSka několika online stripů a komiksů a na všechny novinky upozorňuje na Twitteru.

StripBota jsem původně napsal v březnu 2009 v Groovy, po víc než roce jsem ho přepsal do Javy a konečně před několika měsíci ho přepsal do Scaly.

Tady jsou kompletní zdrojáky:

build.sbt

scalaVersion := "2.9.0-1"

libraryDependencies += "commons-codec" % "commons-codec" % "1.4"
libraryDependencies += "oauth.signpost" % "signpost-core" % "1.2.1.1"
libraryDependencies += "org.jdom" % "jdom" % "1.1"
libraryDependencies += "rome" % "rome" % "0.9"
libraryDependencies += "jtwitter" % "jtwitter" % "1.8.3" from "http://www.winterwell.com/software/jtwitter/jtwitter.jar"

stripbot.scala

// https://twitter.com/stripbot

package stripBot

import scala.actors.Actor
import scala.actors.Actor._

import java.net.{ URL, URLEncoder, MalformedURLException }
import java.io.{ FileReader, BufferedReader, FileWriter, IOException }
import java.util.Date

import com.sun.syndication.feed.synd.SyndEntry
import com.sun.syndication.io.{ SyndFeedInput, XmlReader }

import winterwell.jtwitter.{ Twitter, OAuthSignpostClient }


object Main extends App {
  val tweeter = new Tweeter("stripbot", Config.oauthKey, Config.oauthSecret)
  val rssFetcher = new Fetcher(Config.sources, tweeter)
  rssFetcher.start
  tweeter.start
}

object Config {
  val sources = Seq(
    StripSource("Bugemos",              "http://bugemos.com/?q=rss.xml"),
    StripSource("balónek strip",        "http://lubosbranda.blog.idnes.cz/rss/"),
    StripSource("bezejmenný hrdina",    "http://picasaweb.google.com/data/feed/base/user/marusjakub/albumid/5110845406354432785?alt=rss&kind=photo&hl=cs"),
    StripSource("ITBiz",                "http://www.itbiz.cz/taxonomy/term/25/0/feed"),
    StripSource("iDNES komix",          "http://servis.idnes.cz/rss.asp?c=komiksy"),
    StripSource("XKCD",                 "http://xkcd.com/rss.xml", "EN"),
    StripSource("Questionable Content", "http://www.questionablecontent.net/QCRSS.xml", "EN"),
    StripSource("Johny Wander",         "http://www.johnnywander.com/feed", "EN"),
    StripSource("Wasted Talent",        "http://feeds2.feedburner.com/WastedTalentRss", "EN"),
    StripSource("chainsawsuit",         "http://feeds.feedburner.com/Chainsawsuit", "EN"),
    StripSource("Cyanide & happyiness", "http://feeds.feedburner.com/Explosm", "EN", { e => e.getTitle.matches("""\d+\.\d+.\d+""") })
  )
  val oauthKey    = "---YOUR-OAUTH-KEY---"
  val oauthSecret = "---YOUR-OAUTH-SECRET---"
}


// --- main actors

class Fetcher(sources: Seq[StripSource], tweeter: Tweeter) extends Actor {

  val lastCheckFile = "lastCheck.txt"

  def checkRss(source: StripSource, lastCheck: Date) =
    for {
      e <- Util.readRssFromUrl(source.rssUrl)
      if source.filterFn(e)
      if e.getPublishedDate != null && e.getPublishedDate.after(lastCheck)
    } yield e

  def formatMessage(source: StripSource, entry: SyndEntry) =
    source.name+" - "+entry.getTitle.trim+" "+Util.shortenUrl(entry.getLink)+" "+source.lang+" #stripy"

  def getLastCheck: Date = {
    var line = Util readLine lastCheckFile
    if (line == null || line == "") new Date() else new Date(line.toLong)
  }

  def setLastCheck = Util.writeLine(lastCheckFile, "" + new Date().getTime)

  def act {
    println(sources.mkString("\n"))

    loop {
      var lastCheck = getLastCheck
      println("Last check: " + lastCheck)

      for {
        source <- sources
        entry  <- checkRss(source, lastCheck)
      } tweeter ! formatMessage(source, entry)

      setLastCheck
      Thread.sleep(1000 * 60 * 60) // 1 hour
    }
  }
}


class Tweeter(username: String, oauthKey: String, oauthSecret: String) extends Actor {

  val accessTokensFile = "accessTokens.txt"

  def connectToTwitter = {
    val tokens = Util readLine accessTokensFile

    val oauthClient =
    if (tokens.nonEmpty) {
      val t = tokens.split(" ")
      new OAuthSignpostClient(oauthKey, oauthSecret, t(0), t(1))

    } else {
      val oauthClient = new OAuthSignpostClient(oauthKey, oauthSecret, "oob")
      oauthClient.authorizeDesktop()
      val v = OAuthSignpostClient.askUser("Please enter the verification PIN from Twitter")
      oauthClient.setAuthorizationCode(v)
      Util.writeLine(accessTokensFile, oauthClient.getAccessToken.mkString(" "))
      oauthClient
    }

    new Twitter(username, oauthClient)
  }

  def act {
    val twitter = connectToTwitter
    loop {
      react {
        case m =>
          twitter.updateStatus(m.toString)
          println("tweeted: " + m.toString)
      }
    }
  }
}


// ----

case class StripSource (name: String, rssUrl: String, lang: String = "CZ", filterFn: SyndEntry => Boolean = x => true)

object Util {
  def readLine(f: String) = io.Source.fromFile(f).getLines.next
  def writeLine(f: String, data: String) = {
    val fw = new FileWriter(f)
    fw write data
    fw.close()
  }

  def readRssFromUrl(url: String): List[SyndEntry] = {
    try {
      val i = new SyndFeedInput().build(new XmlReader(new URL(url))).getEntries
      List(i.toArray(new Array[SyndEntry](0)) : _*)
    } catch {
      case _ => List()
    }
  }

  def shortenUrl(url: String) = {
    val line = io.Source.fromURL("https://url.k47.cz/api/get/?url=" + URLEncoder.encode(url, "UTF8")).getLines.next
    if (line startsWith "ERROR") throw new MalformedURLException
    line
  }
}
píše k47, ascii@k47.cz