All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hackernews4s.v0.HackerNews.scala Maven / Gradle / Ivy

There is a newer version: 0.6.1
Show newest version
package hackernews4s.v0

import skinny.http._
import skinny.logging.Logging
import skinny.util.JSONStringOps._

/**
 * HackerNews API client
 */
object HackerNews extends HackerNews

/**
 * HackerNews API client
 *
 * {{{
 * scala> import hackernews4s.v0._
 * scala> val hn = new HackerNews {}
 * scala> val item: Option[Item] = hn.getItem(ItemId(123))
 * }}}
 */
trait HackerNews extends Logging {
  import hackernews4s.v0.internal._

  val BASE_URL = "https://hacker-news.firebaseio.com/v0"

  private[this] val CONCURRENCY: Int = 10

  /**
   * Retrieves a HackerNews item from Items API.
   *
   * Stories, comments, jobs, Ask HNs and even polls are just items.
   * They're identified by their ids, which are unique integers, and live under https://hacker-news.firebaseio.com/v0/item/.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val item: Option[Item] = HackerNews.getItem(ItemId(123))
   * }}}
   */
  def getItem(itemId: ItemId): Option[Item] = {
    val response = HTTP.get(req(s"${BASE_URL}/item/${itemId.id}.json"))
    debugLogging("Items API", response)
    response.status match {
      case 200 => fromJSONString[RawItem](response.textBody).map(_.toItem)
      case 401 =>
        // HackerNews API sometimes returns (temporal?) 401
        logger.info(s"Failed to access an item (id: ${itemId}) - ${response.textBody}")
        None
      case s => throw new HackerNewsAPIException(s, response.textBody)
    }
  }

  /**
   * Retrieves a HackerNews user information from Users API.
   *
   * Users are identified by case-sensitive ids, and live under https://hacker-news.firebaseio.com/v0/user/.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val user: Option[User] = HackerNews.getUser(UserId("seratch"))
   * }}}
   */
  def getUser(userId: UserId): Option[User] = {
    val response = HTTP.get(req(s"${BASE_URL}/user/${userId.id}.json"))
    debugLogging("Users API", response)
    response.status match {
      case 200 => fromJSONString[RawUser](response.textBody).map(_.toUser)
      case 401 =>
        // HackerNews API sometimes returns (temporal?) 401
        logger.info(s"Failed to access an item (id: ${userId}) - ${response.textBody}")
        None
      case s => throw new HackerNewsAPIException(s, response.textBody)
    }
  }

  /**
   * Retrieves a HackerNews top stories' item ids from Top Stories API.
   *
   * The current top 100 stories are at https://hacker-news.firebaseio.com/v0/topstories.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val ids: Seq[ItemId] = HackerNews.getItemIdsForTopStories()
   * scala> ids.size
   * res0: Int = 100
   * }}}
   */
  def getItemIdsForTopStories(): Seq[ItemId] = {
    val response = HTTP.get(req(s"${BASE_URL}/topstories.json"))
    debugLogging("Top Stories API", response)
    response.status match {
      case 200 => fromJSONString[Seq[Long]](response.textBody).map(_.map(ItemId)).getOrElse(Nil)
      case s => throw new HackerNewsAPIException(s, response.textBody)
    }
  }

  /**
   * Retrieves actual top stories.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val items: Seq[Item] = HackerNews.getTopStories()
   * scala> items.size
   * res0: Int = 10
   * }}}
   */
  def getTopStories(limit: Int = 10): Seq[Item] = {
    getItemIdsForTopStories().take(limit).grouped(CONCURRENCY).flatMap(ids =>
      ids.par.flatMap(id => getItem(id)).toIndexedSeq
    ).toSeq
  }

  /**
   * Max Item ID
   *
   * The current largest item id is at https://hacker-news.firebaseio.com/v0/maxitem.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val itemId: ItemId = HackerNews.getMaxItemId()
   * scala> itemId.id > 8447116L
   * res0: Boolean = true
   * }}}
   */
  def getMaxItemId(): ItemId = {
    val response = HTTP.get(req(s"${BASE_URL}/maxitem.json"))
    debugLogging("Max Item ID API", response)
    response.status match {
      case 200 =>
        fromJSONString[Long](response.textBody).map(ItemId)
          .getOrElse(throw new HackerNewsAPIException(200, response.textBody))
      case s => throw new HackerNewsAPIException(s, response.textBody)
    }
  }

  /**
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val itemId: ItemId = HackerNews.getCurrentLargestItemId()
   * }}}
   */
  def getCurrentLargestItemId(): ItemId = getMaxItemId()

  /**
   * Retrieves changed item ids and user ids from Changed Items and Profiles API.
   *
   * The item and profile changes are at https://hacker-news.firebaseio.com/v0/updates.
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> val ChangedItemsAndProfiles(itemIds, userIds) = HackerNews.getIdsForChangedItemsAndProfiles()
   * }}}
   */
  def getIdsForChangedItemsAndProfiles(): ChangedItemsAndProfiles = {
    val response = HTTP.get(req(s"${BASE_URL}/updates.json"))
    debugLogging("Changed Items and Profiles API", response)
    response.status match {
      case 200 =>
        fromJSONString[RawChangedItemsAndProfiles](response.textBody).map(_.toChangedItemsAndProfiles)
          .getOrElse(throw new HackerNewsAPIException(200, response.textBody))
      case s => throw new HackerNewsAPIException(s, response.textBody)
    }
  }

  /**
   * Retrieves actual changed item data for ids from "Changed Items and Profiles API".
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> HackerNews.getChangedItems().size > 0
   * res0: Boolean = true
   * }}}
   */
  def getChangedItems(): Seq[Item] = {
    getIdsForChangedItemsAndProfiles().itemIds.grouped(CONCURRENCY).flatMap(ids =>
      ids.par.flatMap(itemId => getItem(itemId)).toIndexedSeq
    ).toSeq
  }

  /**
   * Retrieves actual changed user data for ids from "Changed Items and Profiles API".
   *
   * {{{
   * scala> import hackernews4s.v0._
   * scala> HackerNews.getChangedProfiles().size > 0
   * res0: Boolean = true
   * }}}
   */
  def getChangedProfiles(): Seq[User] = {
    getIdsForChangedItemsAndProfiles().userIds.grouped(CONCURRENCY).flatMap(ids =>
      ids.par.flatMap(userId => getUser(userId)).toIndexedSeq
    ).toSeq
  }

  // creates skinny.http.Request for each API call
  private[this] def req(url: String): Request = {
    new Request(url)
      .connectTimeoutMillis(2000) // HackerNews API sometimes cannot accept new connection
  }

  private[this] def debugLogging(api: String, response: Response): Unit = {
    logger.debug(s"${api} - (status: ${response.status}, body: ${response.textBody})")
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy