diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009,  Tom Lokhorst
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,4 @@
+#! /usr/bin/env runhaskell
+
+> import Distribution.Simple
+> main = defaultMain
diff --git a/feed2twitter.cabal b/feed2twitter.cabal
new file mode 100644
--- /dev/null
+++ b/feed2twitter.cabal
@@ -0,0 +1,47 @@
+name:                feed2twitter
+version:             0.2.0
+synopsis:            Send posts from a feed to Twitter
+description:         Reads feeds and tweets each post to a Twitter account.
+                     This is both a library and a simple executable build on top
+                     of it.
+                     .
+                     The feed is read only once. To keep updating, call this 
+                     program/library every few minutes.
+                     A local cache of earlier tweets is kept in a file to make
+                     sure no duplicates are sent.
+                     .
+                     To build your own program on top of this library use the
+                     `atom2twitter` or `rss2twitter` functions. If you need
+                     access to the complete feed instead of just individual
+                     individual items, use the `feed2twitter` function.
+                     .
+                     See the `hackage2twitter` program for an example of how to
+                     use this library.
+                     .
+                     The executable can be used as such:
+                     .
+                     > $ feed2twitter http://example.com/feed.rss username password cache-file 50 [--debug-mode]
+license:             BSD3
+license-file:        LICENSE
+author:              Tom Lokhorst
+maintainer:          Tom Lokhorst <tom@lokhorst.eu>
+homepage:            http://github.com/tomlokhorst/feed2twitter
+stability:           Experimental
+category:            Web
+build-type:          Simple
+cabal-version:       >= 1.6
+
+library
+  build-depends:     base >= 4,
+                     bytestring >= 0.9.1.4,
+                     download-curl >= 0.1.1,
+                     feed >= 0.3.6,
+                     hs-twitter >= 0.2.5
+  hs-source-dirs:    src
+  exposed-modules:   Web.Feed2Twitter
+  other-modules:     Web.Feed2Twitter.Twitter
+
+executable feed2twitter
+  main-is:           Main.hs
+  hs-source-dirs:    src
+
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,99 @@
+-------------------------------------------------------------------------------
+-- |
+-- Module      : Main
+-- Copyright   : (c) 2009, Tom Lokhorst
+-- License     : BSD3
+--
+-- Maintainer  : Tom Lokhorst <tom@lokhorst.eu>
+-- Stability   : Experimental
+--
+-- Main module for `feed2twitter` executable.
+-- Contains generic Atom entry and RSS item to Tweet map functions.
+--
+-------------------------------------------------------------------------------
+module Main where
+
+import Web.Feed2Twitter
+
+import Text.Atom.Feed
+import Text.RSS.Syntax
+
+import Data.List
+import System.Console.GetOpt
+import System.Environment
+import System.IO
+
+main :: IO ()
+main = do
+  args <- getArgs
+  (cfg, rest) <- processArgs defaultConfig options header args
+  if length rest /= 5
+   then putStrLn $ usageInfo header options
+   else do
+        let cfg' = cfg { feedUrl   = rest !! 0
+                       , username  = rest !! 1
+                       , password  = rest !! 2
+                       , cacheFile = rest !! 3
+                       , cacheSize = read $ rest !! 4
+                       }
+        item2twitter cfg' item2tweet
+  where
+    header = "Usage: feed2twitter FEED-URL USERNAME PASSWORD CACHE-FILE"
+              ++ " CACHE-SIZE [OPTIONS...], with the following options:"
+
+
+item2tweet :: Either Entry RSSItem -> Tweet
+item2tweet (Left entry) =
+  if null (entryLinks entry)
+  then trunc4tweet s
+  else trunc4url s ++ linkHref (head (entryLinks entry))
+  where
+    title = showTextContent $ entryTitle entry
+    s = case (entryAuthors entry, entrySummary entry) of
+          ([], Nothing) -> title
+          (as, Nothing) -> title ++ " by " ++ intercalate ", " (map personName as)
+          ([], Just s ) -> title ++ ": " ++ showTextContent s
+          (as, Just s ) -> title ++ " by " ++ intercalate ", " (map personName as)
+                             ++ ": " ++ showTextContent s
+item2tweet (Right item) = maybe (trunc4tweet s) (trunc4url s ++) (rssItemLink item)
+  where
+    -- Stupid optional fields...
+    s = case (rssItemTitle item, rssItemAuthor item, rssItemDescription item) of
+          (Nothing   , Nothing  , Nothing  ) -> ""
+          (Just title, Nothing  , Nothing  ) -> title
+          (Nothing   , Just auth, Nothing  ) -> auth ++ " posted."
+          (Just title, Just auth, Nothing  ) -> title ++ " by " ++ auth
+          (Nothing   , Nothing  , Just desc) -> desc
+          (Just title, Nothing  , Just desc) -> (title ++ " " ++ desc)
+          (Nothing   , Just auth, Just desc) -> auth ++ ": " ++ desc
+          (Just title, Just auth, Just desc) -> title ++ " by " ++ auth ++ ": " ++ desc
+
+showTextContent :: TextContent -> String
+showTextContent (TextString s)  = s
+showTextContent (HTMLString s)  = s -- Todo: make better
+showTextContent (XHTMLString e) = show e -- Todo: also make better
+
+defaultConfig :: Config
+defaultConfig = Config
+  { feedUrl = ""
+  , username = ""
+  , password = ""
+  , cacheFile = ""
+  , cacheSize = 0
+  , debugMode = False
+  }
+
+options :: [OptDescr (Config -> Config)]
+options =
+  [ Option ['d'] ["debug-mode"] (NoArg (\c -> c { debugMode = True })) "Debug mode, send tweets to stdout."
+  -- , Option ['V'] ["version"] (NoArg (\c -> c { showVersion = True })) "Show program version."
+  ]
+
+-- Seems like this function should already exist somewhere in a package.
+processArgs :: a -> [OptDescr (a -> a)] -> String -> [String] -> IO (a, [String])
+processArgs defaultConfig options header args =
+  case getOpt Permute options args of
+    (oargs, nonopts, []    ) -> return (foldl (flip ($)) defaultConfig oargs, nonopts)
+    (_    , _      , errors) -> ioError $ userError $ (concat errors) ++ usageInfo header options
+
+
diff --git a/src/Web/Feed2Twitter.hs b/src/Web/Feed2Twitter.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Feed2Twitter.hs
@@ -0,0 +1,181 @@
+-------------------------------------------------------------------------------
+-- |
+-- Module      : Web.Feed2Twitter
+-- Copyright   : (c) 2009, Tom Lokhorst
+-- License     : BSD3
+--
+-- Maintainer  : Tom Lokhorst <tom@lokhorst.eu>
+-- Stability   : Experimental
+--
+-- This module exposes several functions and data types to send feeds to
+-- Twitter.
+--
+-------------------------------------------------------------------------------
+module Web.Feed2Twitter
+  (
+  -- * Types
+    Config (..)
+  , Tweet
+  , GUID
+
+  -- * Convenient user functions
+  , atom2twitter
+  , rss2twitter
+  , item2twitter
+
+  -- * Main function
+  , feed2twitter
+
+  -- * Util functions
+  , trunc4tweet
+  , trunc4url
+  ) where
+
+import Web.Feed2Twitter.Twitter
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as BSC
+import Network.Curl.Download
+import Text.Atom.Feed
+import Text.Feed.Import
+import Text.RSS.Syntax
+import Text.Feed.Types (Feed (AtomFeed, RSSFeed))
+import qualified Text.Feed.Types as Feed
+
+import Control.Exception
+import Control.Monad
+import Data.Maybe
+import Data.List
+import Data.Ord (comparing)
+import System.IO
+
+-- | Configuration data for the `feed2twitter` function.
+data Config = Config
+  { feedUrl   :: String
+  , username  :: String
+  , password  :: String
+  , cacheFile :: FilePath
+  , cacheSize :: Int
+  , debugMode :: Bool
+  }
+  deriving Show
+
+-- | Represents a single tweet.
+-- In principle this should be <= 140 characters, however, because Twitter uses
+-- a url-shortener, a tweet containing a url may exeed this limit.
+type Tweet = String
+
+-- | A unique identifier for a Tweet.
+-- Values of this type are stored in the cache file to make sure no duplicate
+-- messages are sent to Twitter.
+type GUID  = String
+
+-- | Sends an Atom feed to Twitter by using user-provided mapping function
+-- for individual entries.
+--
+-- This is a specialized version of `item2twitter`.
+atom2twitter :: Config -> (Entry -> Tweet) -> IO ()
+atom2twitter cfg f = item2twitter cfg g
+  where
+    err = "Web.Feed2Twitter.atom2twitter: "
+    g (Left e)  = f e
+    g (Right _) = error $ err ++ "Item not an Atom entry"
+
+-- | Sends a RSS feed to Twitter by using user-provided mapping function
+-- for individual items.
+--
+-- This is a specialized version of `item2twitter`.
+rss2twitter :: Config -> (RSSItem -> Tweet) -> IO ()
+rss2twitter cfg f = item2twitter cfg g
+  where
+    err = "Web.Feed2Twitter.rss2twitter: "
+    g (Left _)  = error $ err ++ "Item not a RSS item"
+    g (Right i) = f i
+
+-- | Sends feed items to Twitter by using user-provided mapping function
+-- for individual items.
+-- 
+-- Defined in terms of `feed2twitter`.
+item2twitter :: Config -> (Either Entry RSSItem -> Tweet) -> IO ()
+item2twitter cfg f = feed2twitter cfg g
+  where
+    err = "Web.Feed2Twitter.items2twitter: "
+
+    -- Feeds are asumed to be in reverse chronological order.
+    -- Tweets are posted in chronological order.
+    g (AtomFeed af) = map atom . reverse . feedEntries $ af
+    g (RSSFeed rf)  = map rss  . reverse . rssItems . rssChannel $ rf
+    g _             = error $ err ++ "Feed not an Atom or RSS feed"
+
+    atom entry      = (entryId entry, f (Left entry))
+    rss ri          = (guid, f (Right ri))
+     where
+       guid = maybe (filter (/='\n') $ show ri) rssGuidValue (rssItemGuid ri)
+
+-- | Function that does all the heavy lifting:
+--
+-- - Downloads the feed provided in the config value.
+--
+-- - Calls the provided user-function to map feed to a list of guids and tweets.
+--
+-- - Filters out already posted tweets using guid cache.
+--
+-- - Sends each tweet to Twitter and writes its guid to cache file.
+--
+-- - Truncate cache file to size provided in config value.
+-- 
+-- User-function is responsible for generating guids for tweets and the order
+-- of tweets.
+feed2twitter :: Config -> (Feed.Feed -> [(GUID, Tweet)]) -> IO ()
+feed2twitter cfg f = do
+    feed' <- openAsFeed (feedUrl cfg)
+    case feed' of
+      Left s     -> error $ err ++ s
+      Right feed -> do
+        let tweets' = f feed
+        touchFile (cacheFile cfg)
+        cfc <- BS.readFile (cacheFile cfg)
+        let guids = lines (BSC.unpack cfc)
+        let tweets = filter (pred guids) tweets'
+        mapM_ tweetAndCache tweets
+        when (not $ null tweets) cleanupCache
+  where
+    err = "Web.Feed2Twitter.feed2twitter: "
+
+    -- Quick hack to create the file if it didn't exist
+    touchFile f = openFile f ReadWriteMode >>= hClose
+
+    pred guids (g, t) = not (g `elem` guids || t == "")
+    cleanupCache = do
+      guids' <- BS.readFile (cacheFile cfg)
+      let guids = BSC.unlines . reverse . take (cacheSize cfg) . reverse
+                   . BSC.lines $ guids'
+      BS.writeFile (cacheFile cfg) guids
+
+    tweetAndCache (guid, t) =
+      if debugMode cfg
+       then putStrLn ("tweet: " ++ t)
+       else do
+            tweet (username cfg) (password cfg) t
+
+            -- Immediately write to cache after tweet, in case process crashes
+            BS.appendFile (cacheFile cfg) (BSC.pack $ guid ++ "\n")
+
+-- | Truncates a string to 140 characters for a tweet.
+--
+-- When input is longer than 140 characters, puts an ellipsis at the end.
+trunc4tweet :: String -> String
+trunc4tweet s = if length s <= 140
+                then s
+                else take 139 s ++ "\8230"
+
+-- | Truncates a string to 120 characters, leaving room for a space and a url.
+-- Due to Twitter using a url-shortener, urls are assumed to max 20 characters.
+--
+-- When input is shorter than 120 characters, returns it with a space,
+-- otherwise truncates and puts an ellipsis and space at the end.
+trunc4url :: String -> String
+trunc4url s = if length s <= 119
+              then s ++ " "
+              else take 118 s ++ "\8230 "
+
diff --git a/src/Web/Feed2Twitter/Twitter.hs b/src/Web/Feed2Twitter/Twitter.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Feed2Twitter/Twitter.hs
@@ -0,0 +1,27 @@
+-------------------------------------------------------------------------------
+-- |
+-- Module      : Web.Feed2Twitter.Twitter
+-- Copyright   : (c) 2009, Tom Lokhorst
+-- License     : BSD3
+--
+-- Maintainer  : Tom Lokhorst <tom@lokhorst.eu>
+-- Stability   : Experimental
+--
+-- Twitter functions, build on hs-twitter package.
+--
+-------------------------------------------------------------------------------
+module Web.Feed2Twitter.Twitter where
+
+import Web.Twitter.Fetch
+import Web.Twitter.Monad
+
+import System.Environment
+
+tweet :: String -> String -> String -> IO ()
+tweet username password message = do
+  runTM (AuthUser username password)
+    $ postMethod
+    $ restCall "update.json" (arg "status" message [])
+  return ()
+
+
