diff --git a/README.MD b/README.MD index 69ef277..96e8d62 100644 --- a/README.MD +++ b/README.MD @@ -1,4 +1,4 @@ -# nanoboard [![Build Status](https://travis-ci.org/Karasiq/nanoboard.svg?branch=master)](https://travis-ci.org/Karasiq/nanoboard) [![Build status](https://ci.appveyor.com/api/projects/status/05l8dnixhn375kjm?svg=true)](https://ci.appveyor.com/project/Karasiq/nanoboard) [![Version](http://img.shields.io/badge/version-1.0.5--M1-blue.svg?style=flat)](https://github.com/Karasiq/nanoboard/releases) +# nanoboard [![Build Status](https://travis-ci.org/Karasiq/nanoboard.svg?branch=master)](https://travis-ci.org/Karasiq/nanoboard) [![Build status](https://ci.appveyor.com/api/projects/status/05l8dnixhn375kjm?svg=true)](https://ci.appveyor.com/project/Karasiq/nanoboard) [![Version](http://img.shields.io/badge/version-1.0.5-blue.svg?style=flat)](https://github.com/Karasiq/nanoboard/releases) Scala [nanoboard](https://github.com/nanoboard/nanoboard) implementation # Links diff --git a/build.sbt b/build.sbt index 9e75360..01b3283 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbtassembly.Plugin.AssemblyKeys._ lazy val commonSettings = Seq( organization := "com.github.karasiq", - version := "1.0.5-M1", + version := "1.0.5", isSnapshot := version.value.endsWith("SNAPSHOT"), scalaVersion := "2.11.7" ) diff --git a/frontend/src/main/scala/com/karasiq/nanoboard/frontend/components/ThreadContainer.scala b/frontend/src/main/scala/com/karasiq/nanoboard/frontend/components/ThreadContainer.scala index 1a3fad6..eb1aa84 100644 --- a/frontend/src/main/scala/com/karasiq/nanoboard/frontend/components/ThreadContainer.scala +++ b/frontend/src/main/scala/com/karasiq/nanoboard/frontend/components/ThreadContainer.scala @@ -185,7 +185,7 @@ final class ThreadContainer(val context: Var[NanoboardContext], postsPerPage: In val prevOffset = math.max(0, offset - postsPerPage) val previousButton = ButtonBuilder(ButtonStyle.info)( "angle-double-left".fontAwesome(FontAwesome.fixedWidth), - s"From $prevOffset to ${prevOffset + postsPerPage}", + locale.fromTo(prevOffset, prevOffset + postsPerPage), onclick := Bootstrap.jsClick { _ ⇒ context() = NanoboardContext.Recent(prevOffset) }) diff --git a/library/src/main/resources/reference.conf b/library/src/main/resources/reference.conf index de173d9..3b74bbe 100644 --- a/library/src/main/resources/reference.conf +++ b/library/src/main/resources/reference.conf @@ -1,5 +1,5 @@ nanoboard { - version = 1.0.5-M1 + version = 1.0.5 client-version = karasiq-nanoboard v${nanoboard.version} encryption-key = "nano" bitmessage { diff --git a/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessage.scala b/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessage.scala index 3d62269..95eb5d2 100644 --- a/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessage.scala +++ b/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessage.scala @@ -1,12 +1,6 @@ package com.karasiq.nanoboard -import java.time._ -import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, TextStyle} -import java.time.temporal.ChronoField -import java.util.Locale - import com.karasiq.nanoboard.encoding.{DataCipher, DefaultNanoboardMessageFormat} -import com.typesafe.config.ConfigFactory import org.apache.commons.codec.binary.Hex case class NanoboardMessage(parent: String, text: String) { @@ -15,27 +9,5 @@ case class NanoboardMessage(parent: String, text: String) { } object NanoboardMessage extends DefaultNanoboardMessageFormat { - private val clientVersion = ConfigFactory.load().getString("nanoboard.client-version") - - private val timestampFormat = new DateTimeFormatterBuilder() - .appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) - .appendLiteral(", ") - .appendValue(ChronoField.DAY_OF_MONTH) - .appendLiteral('/') - .appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT) - .appendLiteral('/') - .appendValue(ChronoField.YEAR) - .appendLiteral(", ") - .append(DateTimeFormatter.ISO_LOCAL_TIME) - .appendLiteral(" (") - .appendZoneOrOffsetId() - .appendLiteral(")") - .toFormatter(Locale.ENGLISH) - - val hashRegex = "(?i)[a-f0-9]{32}".r - - def newMessage(parent: String, text: String): NanoboardMessage = { - val header = s"[g]${timestampFormat.format(ZonedDateTime.now())}, client: $clientVersion[/g]" - NanoboardMessage(parent, s"$header\n$text") - } + val hashFormat = "(?i)[a-f0-9]{32}".r } diff --git a/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessageGenerator.scala b/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessageGenerator.scala new file mode 100644 index 0000000..04285cf --- /dev/null +++ b/library/src/main/scala/com/karasiq/nanoboard/NanoboardMessageGenerator.scala @@ -0,0 +1,38 @@ +package com.karasiq.nanoboard + +import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, TextStyle} +import java.time.temporal.ChronoField +import java.time.{ZoneId, ZonedDateTime} +import java.util.Locale + +import com.typesafe.config.{Config, ConfigFactory} + +import scala.util.Try + +object NanoboardMessageGenerator { + def fromConfig(config: Config = ConfigFactory.load()) = { + new NanoboardMessageGenerator(config.getString("nanoboard.client-version"), Try(ZoneId.of(config.getString("nanoboard.default-time-zone"))).getOrElse(ZoneId.systemDefault())) + } +} + +class NanoboardMessageGenerator(clientVersion: String, defaultTimeZone: ZoneId) { + protected val timestampFormat = new DateTimeFormatterBuilder() + .appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) + .appendLiteral(", ") + .appendValue(ChronoField.DAY_OF_MONTH) + .appendLiteral('/') + .appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT) + .appendLiteral('/') + .appendValue(ChronoField.YEAR) + .appendLiteral(", ") + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .appendLiteral(" (") + .appendZoneOrOffsetId() + .appendLiteral(")") + .toFormatter(Locale.ENGLISH) + + def newMessage(parent: String, text: String): NanoboardMessage = { + val header = s"[g]${timestampFormat.format(ZonedDateTime.now(defaultTimeZone))}, client: $clientVersion[/g]" + NanoboardMessage(parent, s"$header\n$text") + } +} diff --git a/library/src/main/scala/com/karasiq/nanoboard/sources/bitmessage/BitMessageTransport.scala b/library/src/main/scala/com/karasiq/nanoboard/sources/bitmessage/BitMessageTransport.scala index 3383c77..dab8dd3 100644 --- a/library/src/main/scala/com/karasiq/nanoboard/sources/bitmessage/BitMessageTransport.scala +++ b/library/src/main/scala/com/karasiq/nanoboard/sources/bitmessage/BitMessageTransport.scala @@ -81,7 +81,7 @@ final class BitMessageTransport(config: Config)(implicit ac: ActorSystem, am: Ac .run() post { - (path("api" / "add" / NanoboardMessage.hashRegex) & entity(as[String])) { (parent, message) ⇒ + (path("api" / "add" / NanoboardMessage.hashFormat) & entity(as[String])) { (parent, message) ⇒ queue.offer(NanoboardMessage(parent, BitMessageTransport.fromBase64(message))) complete(StatusCodes.OK) } diff --git a/library/src/main/scala/com/karasiq/nanoboard/sources/png/BoardPngSource.scala b/library/src/main/scala/com/karasiq/nanoboard/sources/png/BoardPngSource.scala index f9de7e8..71da25e 100644 --- a/library/src/main/scala/com/karasiq/nanoboard/sources/png/BoardPngSource.scala +++ b/library/src/main/scala/com/karasiq/nanoboard/sources/png/BoardPngSource.scala @@ -26,14 +26,12 @@ class BoardPngSource(encoding: DataEncodingStage)(implicit as: ActorSystem, am: val decoded: String = encoding.decode(data).utf8String NanoboardMessage.parseMessages(decoded) } - .recoverWith { case _ ⇒ Source.empty } } def imagesFromPage(url: String): Source[String, akka.NotUsed] = { Source.fromFuture(http.singleRequest(HttpRequest(uri = url))) .flatMapConcat(_.entity.dataBytes.fold(ByteString.empty)(_ ++ _)) .flatMapConcat(data ⇒ imagesFromPage(Jsoup.parse(data.utf8String, url))) - .recoverWith { case _ ⇒ Source.empty } } protected def imagesFromPage(page: Document): Source[String, akka.NotUsed] = { diff --git a/setup/setup.iss b/setup/setup.iss index a6f4d91..10e25c6 100644 --- a/setup/setup.iss +++ b/setup/setup.iss @@ -1,6 +1,6 @@ #define OutputName "nanoboard-server" #define MyAppName "Nanoboard" -#define MyAppVersion "1.0.5-M1" +#define MyAppVersion "1.0.5" #define MyAppPublisher "Karasiq, Inc." #define MyAppURL "http://www.github.com/Karasiq/nanoboard" #define MyAppExeName "nanoboard.exe" diff --git a/src/main/scala/com/karasiq/nanoboard/dispatcher/NanoboardSlickDispatcher.scala b/src/main/scala/com/karasiq/nanoboard/dispatcher/NanoboardSlickDispatcher.scala index 38efa3c..09b5ba4 100644 --- a/src/main/scala/com/karasiq/nanoboard/dispatcher/NanoboardSlickDispatcher.scala +++ b/src/main/scala/com/karasiq/nanoboard/dispatcher/NanoboardSlickDispatcher.scala @@ -10,7 +10,7 @@ import akka.util.ByteString import com.karasiq.nanoboard.encoding.DataEncodingStage._ import com.karasiq.nanoboard.encoding.stages.{GzipCompression, PngEncoding, SalsaCipher} import com.karasiq.nanoboard.model._ -import com.karasiq.nanoboard.{NanoboardCategory, NanoboardMessage} +import com.karasiq.nanoboard.{NanoboardCategory, NanoboardMessage, NanoboardMessageGenerator} import com.typesafe.config.{Config, ConfigFactory} import slick.driver.H2Driver.api._ @@ -24,6 +24,7 @@ object NanoboardSlickDispatcher { } private[dispatcher] final class NanoboardSlickDispatcher(db: Database, config: Config, postSink: Sink[NanoboardMessage, _])(implicit ec: ExecutionContext, as: ActorSystem, am: ActorMaterializer) extends NanoboardDispatcher { + private val messageGenerator = NanoboardMessageGenerator.fromConfig(config) private val postQueue = Source.queue(20, OverflowStrategy.dropHead) .to(postSink) .run() @@ -96,7 +97,7 @@ private[dispatcher] final class NanoboardSlickDispatcher(db: Database, config: C } override def reply(parent: String, text: String): Future[NanoboardMessageData] = { - val newMessage: NanoboardMessage = NanoboardMessage.newMessage(parent, text) + val newMessage: NanoboardMessage = messageGenerator.newMessage(parent, text) postQueue.offer(newMessage) db.run(Post.addReply(newMessage)).map(_ ⇒ NanoboardMessageData(Some(parent), newMessage.hash, newMessage.text, 0)) } diff --git a/src/main/scala/com/karasiq/nanoboard/server/Main.scala b/src/main/scala/com/karasiq/nanoboard/server/Main.scala index 22c0673..87d0c78 100644 --- a/src/main/scala/com/karasiq/nanoboard/server/Main.scala +++ b/src/main/scala/com/karasiq/nanoboard/server/Main.scala @@ -6,8 +6,8 @@ import java.util.concurrent.TimeUnit import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.Http.ServerBinding +import akka.stream._ import akka.stream.scaladsl._ -import akka.stream.{ActorMaterializer, ActorMaterializerSettings} import com.karasiq.nanoboard.dispatcher.NanoboardSlickDispatcher import com.karasiq.nanoboard.model.{Place, Post, _} import com.karasiq.nanoboard.server.cache.MapDbNanoboardCache @@ -107,14 +107,17 @@ object Main extends App { val messageSource = UrlPngSource.fromConfig(config) val updateInterval = FiniteDuration(config.getDuration("nanoboard.scheduler.update-interval", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) - actorSystem.scheduler.schedule(10 seconds, updateInterval) { - Source.fromPublisher(db.stream(Place.list())) - .flatMapMerge(4, messageSource.imagesFromPage) - .filterNot(cache.contains) - .alsoTo(Sink.foreach(image ⇒ cache += image)) - .flatMapMerge(8, messageSource.messagesFromImage) - .runWith(dbMessageSink) - } + val placeFlow = Flow[String] + .flatMapMerge(4, messageSource.imagesFromPage) + .filterNot(cache.contains) + .alsoTo(Sink.foreach(image ⇒ cache += image)) + .flatMapMerge(8, messageSource.messagesFromImage) + .withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider)) + + Source.tick(10 seconds, updateInterval, ()) + .flatMapConcat(_ ⇒ Source.fromPublisher(db.stream(Place.list()))) + .via(placeFlow) + .runWith(dbMessageSink) // REST server val server = NanoboardServer(dispatcher) diff --git a/src/main/scala/com/karasiq/nanoboard/server/NanoboardMessageStream.scala b/src/main/scala/com/karasiq/nanoboard/server/NanoboardMessageStream.scala index 4fa317b..297b19f 100644 --- a/src/main/scala/com/karasiq/nanoboard/server/NanoboardMessageStream.scala +++ b/src/main/scala/com/karasiq/nanoboard/server/NanoboardMessageStream.scala @@ -28,7 +28,6 @@ private[server] final class NanoboardMessageStream extends GraphStage[FanInShape override def onPush(): Unit = { subscription = grab(input) request() - // emit(output, NanoboardMessage.newMessage("cd94a3d60f2f521806abebcd3dc3f549", "Test")) } }) diff --git a/src/main/scala/com/karasiq/nanoboard/server/NanoboardServer.scala b/src/main/scala/com/karasiq/nanoboard/server/NanoboardServer.scala index d0b7095..5bae69d 100644 --- a/src/main/scala/com/karasiq/nanoboard/server/NanoboardServer.scala +++ b/src/main/scala/com/karasiq/nanoboard/server/NanoboardServer.scala @@ -63,11 +63,11 @@ private[server] final class NanoboardServer(dispatcher: NanoboardDispatcher)(imp val route = { get { encodeResponse { - path("post" / NanoboardMessage.hashRegex) { hash ⇒ + path("post" / NanoboardMessage.hashFormat) { hash ⇒ complete(StatusCodes.OK, dispatcher.post(hash)) } ~ (pathPrefix("posts") & parameters('offset.as[Int].?(0), 'count.as[Int].?(100))) { (offset, count) ⇒ - path(NanoboardMessage.hashRegex) { hash ⇒ + path(NanoboardMessage.hashFormat) { hash ⇒ complete(StatusCodes.OK, dispatcher.thread(hash, offset, count)) } ~ pathEndOrSingleSlash { @@ -112,13 +112,13 @@ private[server] final class NanoboardServer(dispatcher: NanoboardDispatcher)(imp } } ~ delete { - path("post" / NanoboardMessage.hashRegex) { hash ⇒ + path("post" / NanoboardMessage.hashFormat) { hash ⇒ extractLog { log ⇒ log.info("Post permanently deleted: {}", hash) complete(StatusCodes.OK, dispatcher.delete(hash)) } } ~ - path("pending" / NanoboardMessage.hashRegex) { hash ⇒ + path("pending" / NanoboardMessage.hashFormat) { hash ⇒ complete(StatusCodes.OK, dispatcher.markAsNotPending(hash)) } } ~ @@ -131,7 +131,7 @@ private[server] final class NanoboardServer(dispatcher: NanoboardDispatcher)(imp log.info("Categories updated: {}", categories) complete(StatusCodes.OK, dispatcher.updateCategories(categories)) } ~ - path("pending" / NanoboardMessage.hashRegex) { hash ⇒ + path("pending" / NanoboardMessage.hashFormat) { hash ⇒ complete(StatusCodes.OK, dispatcher.markAsPending(hash)) } } ~ diff --git a/src/main/scala/com/karasiq/nanoboard/server/util/MessageValidator.scala b/src/main/scala/com/karasiq/nanoboard/server/util/MessageValidator.scala index adb720f..1a9d9ba 100644 --- a/src/main/scala/com/karasiq/nanoboard/server/util/MessageValidator.scala +++ b/src/main/scala/com/karasiq/nanoboard/server/util/MessageValidator.scala @@ -16,7 +16,7 @@ private[server] final class MessageValidator(config: Config) { private val spamFilter = config.getStringList("nanoboard.scheduler.spam-filter").toVector def isMessageValid(message: NanoboardMessage): Boolean = { - message.parent.matches(NanoboardMessage.hashRegex.regex) && + message.parent.matches(NanoboardMessage.hashFormat.regex) && message.text.nonEmpty && message.text.length <= maxPostSize && spamFilter.forall(!message.text.matches(_)) diff --git a/src/test/scala/DatabaseTest.scala b/src/test/scala/DatabaseTest.scala index 4860f27..b948f77 100644 --- a/src/test/scala/DatabaseTest.scala +++ b/src/test/scala/DatabaseTest.scala @@ -1,4 +1,4 @@ -import com.karasiq.nanoboard.NanoboardMessage +import com.karasiq.nanoboard.NanoboardMessageGenerator import com.karasiq.nanoboard.model._ import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import slick.driver.H2Driver.api._ @@ -8,7 +8,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ class DatabaseTest extends FlatSpec with Matchers with BeforeAndAfterAll { - val testMessage = NanoboardMessage.newMessage("8b8cfb7574741838450e286909e8fd1f", "Hello world!") + val testMessage = NanoboardMessageGenerator.fromConfig().newMessage("8b8cfb7574741838450e286909e8fd1f", "Hello world!") val db = Database.forConfig("nanoboard.test-database") "Database" should "add entry" in {