├── project
├── build.properties
└── plugins.sbt
├── core
└── src
│ ├── main
│ ├── resources
│ │ └── db
│ │ │ └── migration
│ │ │ ├── V4__unique_words.sql
│ │ │ ├── V8__add_repost_chat_username_to_chats.sql
│ │ │ ├── V7__subscriptions.sql
│ │ │ ├── V6__add_updated_at_to_pairs.sql
│ │ │ ├── V5__more_fks.sql
│ │ │ ├── V3__foreign_keys.sql
│ │ │ └── V2__init.sql
│ └── scala
│ │ └── com
│ │ └── pepeground
│ │ └── core
│ │ ├── enums
│ │ └── ChatType.scala
│ │ ├── entities
│ │ ├── WordEntity.scala
│ │ ├── SubscriptionEntity.scala
│ │ ├── ReplyEntity.scala
│ │ ├── PairEntity.scala
│ │ └── ChatEntity.scala
│ │ ├── CoreConfig.scala
│ │ ├── repositories
│ │ ├── SubscriptionRepository.scala
│ │ ├── LearnQueueRepository.scala
│ │ ├── ContextRepository.scala
│ │ ├── WordDeletionQueueRepository.scala
│ │ ├── ReplyRepository.scala
│ │ ├── WordRepository.scala
│ │ ├── ChatRepository.scala
│ │ └── PairRepository.scala
│ │ ├── support
│ │ └── PostgreSQLSyntaxSupport.scala
│ │ └── services
│ │ ├── LearnService.scala
│ │ └── StoryService.scala
│ └── test
│ ├── resources
│ ├── logback.xml
│ └── application.conf.example
│ └── scala
│ └── com
│ └── pepeground
│ └── core
│ ├── repositories
│ ├── SubscriptionRepositoryTest.scala
│ ├── WordRepositoryTest.scala
│ ├── ChatRepositoryTest.scala
│ ├── PairRepositoryTest.scala
│ └── ReplyRepositoryTest.scala
│ └── services
│ ├── StoryServiceTest.scala
│ └── LearnServiceTest.scala
├── Dockerfile
├── .gitignore
├── Makefile
├── .travis.yml
├── bot
└── src
│ └── main
│ ├── scala
│ └── com
│ │ └── pepeground
│ │ └── bot
│ │ ├── handlers
│ │ ├── PingHandler.scala
│ │ ├── GetGabHandler.scala
│ │ ├── GetRepostChatHandler.scala
│ │ ├── CoolStoryHandler.scala
│ │ ├── GetStatsHandler.scala
│ │ ├── SetGabHandler.scala
│ │ ├── RepostHandler.scala
│ │ ├── SetRepostChatHandler.scala
│ │ ├── MessageHandler.scala
│ │ └── GenericHandler.scala
│ │ ├── Config.scala
│ │ ├── CleanUp.scala
│ │ ├── Learn.scala
│ │ ├── Main.scala
│ │ ├── Prometheus.scala
│ │ └── Router.scala
│ └── resources
│ ├── logback.xml
│ ├── application.conf.example
│ └── reference.conf
├── README.md
├── fabfile.py
└── kubernetes
└── deployment.yaml
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.5.5
2 |
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V4__unique_words.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX IF NOT EXISTS unique_word_word ON words USING btree(word);
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:8-jdk
2 |
3 | ADD bot/target/scala-2.12/bot-assembly-0.1.jar /bot.jar
4 |
5 | CMD ["/usr/bin/java", "-jar", "-server", "/bot.jar"]
6 |
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V8__add_repost_chat_username_to_chats.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE chats ADD COLUMN IF NOT EXISTS repost_chat_username character varying;
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
3 | addCompilerPlugin(
4 | "org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full
5 | )
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target/
3 | bot/src/main/resources/application.conf
4 | core/src/test/resources/application.conf
5 | core/src/test/resources/application.conf
6 | bot/target
7 | core/target
8 | project/project
9 | project/target
10 |
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V7__subscriptions.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS subscriptions(
2 | id SERIAL PRIMARY KEY NOT NULL,
3 | chat_id bigint NOT NULL,
4 | name character varying NOT NULL,
5 | since_id bigint
6 | );
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V6__add_updated_at_to_pairs.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE pairs ADD COLUMN IF NOT EXISTS updated_at timestamp without time zone;
2 | UPDATE pairs SET updated_at = now() WHERE updated_at IS NULL;
3 | ALTER TABLE pairs ALTER COLUMN updated_at SET NOT NULL;
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | sbt bot/assembly
3 | docker build -t pepeground/pepeground-bot .
4 | docker push pepeground/pepeground-bot
5 | kubectl rollout restart -n pepeground deployment/pepeground-bot
6 | kubectl rollout restart -n pepeground deployment/pepeground-learn
7 | kubectl rollout restart -n pepeground deployment/pepeground-cleanup
8 |
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V5__more_fks.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE replies DROP CONSTRAINT IF EXISTS pair_id_fk;
2 | ALTER TABLE replies ADD CONSTRAINT pair_id_fk FOREIGN KEY (pair_id) REFERENCES pairs ON DELETE CASCADE;
3 |
4 | ALTER TABLE pairs DROP CONSTRAINT IF EXISTS chat_id_fk;
5 | ALTER TABLE pairs ADD CONSTRAINT chat_id_fk FOREIGN KEY (chat_id) REFERENCES chats ON DELETE CASCADE;
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | sudo: false
3 |
4 | language: scala
5 | scala:
6 | - 2.12.2
7 |
8 | jdk:
9 | - oraclejdk8
10 |
11 | cache:
12 | directories:
13 | - $HOME/.ivy2
14 | - $HOME/.m2
15 |
16 | addons:
17 | postgresql: "9.6"
18 |
19 | before_script:
20 | - cp core/src/test/resources/application.conf.example core/src/test/resources/application.conf
21 | - psql -c 'create database pepeground_test;' -U postgres
22 |
23 | script:
24 | - sbt coverage core/test
25 | - sbt bot/assembly
26 |
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V3__foreign_keys.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE pairs DROP CONSTRAINT IF EXISTS first_id_fk;
2 | ALTER TABLE pairs ADD CONSTRAINT first_id_fk FOREIGN KEY (first_id) REFERENCES words ON DELETE CASCADE;
3 | ALTER TABLE pairs DROP CONSTRAINT IF EXISTS second_id_fk;
4 | ALTER TABLE pairs ADD CONSTRAINT second_id_fk FOREIGN KEY (second_id) REFERENCES words ON DELETE CASCADE;
5 | ALTER TABLE replies DROP CONSTRAINT IF EXISTS word_id_fk;
6 | ALTER TABLE replies ADD CONSTRAINT word_id_fk FOREIGN KEY (word_id) REFERENCES words ON DELETE CASCADE;
--------------------------------------------------------------------------------
/core/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %date %level [%thread] %logger{64} %msg%n
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/PingHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.bot4s.telegram.models.Message
4 | import scalikejdbc.DBSession
5 |
6 | object PingHandler {
7 | def apply(message: Message)(implicit session: DBSession): PingHandler = {
8 | new PingHandler(message)
9 | }
10 | }
11 |
12 | class PingHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
13 | def call(): Option[String] = {
14 | super.before()
15 |
16 | Some("Pong.")
17 | }
18 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/enums/ChatType.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.enums
2 |
3 | object ChatType {
4 | def apply(v: Int): String = {
5 | v match {
6 | case 0 => "chat"
7 | case 1 => "faction"
8 | case 2 => "supergroup"
9 | case 3 => "channel"
10 | case _ => "chat"
11 | }
12 | }
13 |
14 | def apply(v: String): Int = {
15 | v match {
16 | case "chat" => 0
17 | case "faction" => 1
18 | case "supergroup" => 2
19 | case "channel" => 3
20 | case _ => 0
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/core/src/test/resources/application.conf.example:
--------------------------------------------------------------------------------
1 | # Connection Pool settings
2 | db.default.poolInitialSize=5
3 | db.default.poolMaxSize=7
4 | db.default.poolConnectionTimeoutMillis=1000
5 | db.default.poolValidationQuery="select 1 as one"
6 | db.default.poolFactoryName="commons-dbcp2"
7 |
8 | # PostgreSQL example
9 | db.default.driver="org.postgresql.Driver"
10 | db.default.username="postgres"
11 | db.default.url="jdbc:postgresql://localhost:5432/pepeground_test"
12 |
13 | redis.host = "127.0.0.1"
14 | redis.port = 6379
15 |
16 | punctuation.endSentence = [".", "!", "?"]
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/entities/WordEntity.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.entities
2 |
3 | import scalikejdbc._
4 |
5 | case class WordEntity(id: Long, word: String)
6 |
7 | object WordEntity extends SQLSyntaxSupport[WordEntity] {
8 | override val tableName = "words"
9 | override val useSnakeCaseColumnName = true
10 |
11 | def apply(g: SyntaxProvider[WordEntity])(rs: WrappedResultSet): WordEntity = apply(g.resultName)(rs)
12 | def apply(c: ResultName[WordEntity])(rs: WrappedResultSet) = new WordEntity(
13 | rs.long(c.id),
14 | rs.string(c.word)
15 | )
16 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/GetGabHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.bot4s.telegram.models.Message
4 | import scalikejdbc.DBSession
5 |
6 | object GetGabHandler {
7 | def apply(message: Message)(implicit session: DBSession): GetGabHandler = {
8 | new GetGabHandler(message)
9 | }
10 | }
11 |
12 | class GetGabHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
13 | def call(): Option[String] = {
14 | super.before()
15 |
16 | Some("Pizdlivost level is on %s".format(chat.randomChance))
17 | }
18 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pepeground bot [](https://travis-ci.org/pepeground/pepeground-bot)
2 |
3 | Scala implementation of [shizoid](https://github.com/top4ek/shizoid)
4 |
5 | ## Requirements
6 |
7 | * SBT
8 | * JDK 1.8
9 | * PostgreSQL 9.2 +
10 | * Redis Server 3.2.0 +
11 |
12 | ## Configuration
13 |
14 | ```
15 | cp src/main/resources/application.conf.example /path/to/your/application.conf
16 | vim /path/to/your/application.conf
17 | ```
18 |
19 | ## Running
20 |
21 | ```sh
22 | sbt assembly
23 | java -jar -Dconfig.file=/path/to/your/application.conf target/scala-2.12/bot-assembly-0.1.jar
24 | ```
25 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/CoreConfig.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import scala.collection.JavaConverters._
5 |
6 | object CoreConfig extends CoreConfig
7 |
8 | class CoreConfig {
9 | lazy val config = ConfigFactory.load()
10 |
11 | object redis {
12 | private lazy val redisConfig = config.getConfig("redis")
13 | lazy val host = redisConfig.getString("host")
14 | lazy val port = redisConfig.getInt("port")
15 | }
16 |
17 | object punctuation {
18 | private lazy val punctuationConfig = config.getConfig("punctuation")
19 |
20 | lazy val endSentence: List[String] = punctuationConfig.getStringList("endSentence").asScala.toList
21 | }
22 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/GetRepostChatHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.bot4s.telegram.models.Message
4 | import scalikejdbc.DBSession
5 |
6 | object GetRepostChatHandler {
7 | def apply(message: Message)(implicit session: DBSession): GetRepostChatHandler = {
8 | new GetRepostChatHandler(message)
9 | }
10 | }
11 |
12 | class GetRepostChatHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
13 | def call(): Option[String] = {
14 | super.before()
15 |
16 | chat.repostChatUsername match {
17 | case Some(username: String) => Some("Pidorskie quote is on %s".format(username))
18 | case None => None
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/CoolStoryHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.core.services.StoryService
4 | import com.bot4s.telegram.models.Message
5 | import scalikejdbc.DBSession
6 |
7 | object CoolStoryHandler {
8 | def apply(message: Message)(implicit session: DBSession): CoolStoryHandler = {
9 | new CoolStoryHandler(message)
10 | }
11 | }
12 |
13 | class CoolStoryHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
14 | private lazy val storyService: StoryService = new StoryService(List(), fullContext, chat.id, Some(50))
15 |
16 | def call(): Option[String] = {
17 | super.before()
18 |
19 | storyService.generate()
20 | }
21 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/GetStatsHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.core.repositories.PairRepository
4 | import com.bot4s.telegram.models.Message
5 | import scalikejdbc._
6 |
7 | object GetStatsHandler {
8 | def apply(message: Message)(implicit session: DBSession): GetStatsHandler = {
9 | new GetStatsHandler(message)
10 | }
11 | }
12 |
13 | class GetStatsHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
14 | def call(): Option[String] = {
15 | super.before()
16 |
17 | val count: Int = DB readOnly { implicit session => PairRepository.getPairsCount(chat.id) }
18 |
19 | Some("Known pairs in this chat: %s.".format(count))
20 | }
21 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/entities/SubscriptionEntity.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.entities
2 |
3 | import scalikejdbc._
4 |
5 | case class SubscriptionEntity(id: Long, name: String, chatId: Long, sinceId: Option[Long])
6 |
7 | object SubscriptionEntity extends SQLSyntaxSupport[SubscriptionEntity] {
8 | override val tableName = "subscriptions"
9 | override val useSnakeCaseColumnName = true
10 |
11 | def apply(g: SyntaxProvider[SubscriptionEntity])(rs: WrappedResultSet): SubscriptionEntity = apply(g.resultName)(rs)
12 | def apply(c: ResultName[SubscriptionEntity])(rs: WrappedResultSet) = new SubscriptionEntity(
13 | rs.long(c.id),
14 | rs.string(c.name),
15 | rs.long(c.chatId),
16 | rs.longOpt(c.sinceId)
17 | )
18 | }
--------------------------------------------------------------------------------
/bot/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %date %level [%thread] %logger{64} %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 | WARN
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/SetGabHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.core.repositories.ChatRepository
4 | import com.bot4s.telegram.models.Message
5 | import scalikejdbc.DBSession
6 |
7 | object SetGabHandler {
8 | def apply(message: Message)(implicit session: DBSession): SetGabHandler = {
9 | new SetGabHandler(message)
10 | }
11 | }
12 |
13 | class SetGabHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
14 | def call(level: Int): Option[String] = {
15 | super.before()
16 |
17 | if(level > 50 || level < 0) return Some("0-50 allowed, Dude!")
18 | ChatRepository.updateRandomChance(chat.id, level)
19 |
20 | Some(s"Ya wohl, Lord Helmet! Setting gab to ${level}")
21 | }
22 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/SubscriptionRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.entities.SubscriptionEntity
4 | import scalikejdbc._
5 |
6 | object SubscriptionRepository {
7 | private val c = SubscriptionEntity.syntax("c")
8 |
9 | def getList()(implicit session: DBSession): List[SubscriptionEntity] = {
10 | withSQL {
11 | select.from(SubscriptionEntity as c)
12 | }.map(rs => SubscriptionEntity(c)(rs)).list().apply()
13 | }
14 |
15 | def updateSubscription(id: Long, sinceId: Long)(implicit session: DBSession): Unit = {
16 | withSQL {
17 | update(SubscriptionEntity).set(
18 | SubscriptionEntity.column.sinceId -> sinceId
19 | ).where.eq(SubscriptionEntity.column.id, id)
20 | }.update.apply()
21 | }
22 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/entities/ReplyEntity.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.entities
2 |
3 | import scalikejdbc._
4 |
5 | case class ReplyEntity(id: Long, pairId: Long, wordId: Option[Long], count: Long)
6 |
7 | object ReplyEntity extends SQLSyntaxSupport[ReplyEntity] {
8 | override val tableName = "replies"
9 | override val useSnakeCaseColumnName = true
10 |
11 | def apply(g: SyntaxProvider[ReplyEntity])(rs: WrappedResultSet): ReplyEntity = apply(g.resultName)(rs)
12 | def apply(c: ResultName[ReplyEntity])(rs: WrappedResultSet) = new ReplyEntity(
13 | rs.long(c.id),
14 | rs.long(c.pairId),
15 | rs.longOpt(c.wordId),
16 | rs.long(c.count)
17 | )
18 |
19 | def opt(m: SyntaxProvider[ReplyEntity])(rs: WrappedResultSet): Option[ReplyEntity] =
20 | rs.longOpt(m.resultName.id).map(_ => ReplyEntity(m)(rs))
21 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/support/PostgreSQLSyntaxSupport.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.support
2 |
3 | import scalikejdbc._
4 |
5 | object PostgreSQLSyntaxSupport {
6 |
7 | implicit class RichInsertSQLBuilder(val self: InsertSQLBuilder) extends AnyVal {
8 | def onConflictUpdate(constraint: String)(columnsAndValues: (SQLSyntax, Any)*): InsertSQLBuilder = {
9 | val cvs = columnsAndValues map {
10 | case (c, v) => sqls"$c = $v"
11 | }
12 | self.append(sqls"ON CONFLICT ON CONSTRAINT ${SQLSyntax.createUnsafely(constraint)} DO UPDATE SET ${sqls.csv(cvs: _*)}")
13 | }
14 |
15 | def onConflictDoNothing(): InsertSQLBuilder = {
16 | self.append(sqls"ON CONFLICT DO NOTHING")
17 | }
18 | }
19 |
20 | implicit class RichSQLSyntax(val self: sqls.type) extends AnyVal {
21 | def values(column: SQLSyntax): SQLSyntax = sqls"values($column)"
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/Config.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import com.pepeground.core.CoreConfig
4 |
5 | import scala.collection.JavaConverters._
6 |
7 | object Config extends CoreConfig{
8 | object bot {
9 | private lazy val botConfig = config.getConfig("bot")
10 |
11 | lazy val twitter = botConfig.getBoolean("twitter")
12 |
13 | lazy val asyncLear: Boolean = botConfig.getBoolean("asyncLearn")
14 |
15 | lazy val cleanupLimit: Int = botConfig.getLong("cleanupLimit").toInt
16 | lazy val repostChatIds: List[Long] = botConfig.getLongList("repostChatIds").asScala.toList.map(_.toLong)
17 | lazy val repostChatId: Long = botConfig.getLong("repostChatId")
18 | lazy val telegramToken: String = botConfig.getString("telegramToken")
19 | lazy val anchors: List[String] = botConfig.getStringList("anchors").asScala.toList
20 | lazy val name: String = botConfig.getString("name")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/entities/PairEntity.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.entities
2 |
3 | import scalikejdbc._
4 | import org.joda.time._
5 | import scalikejdbc.jodatime.JodaTypeBinder._
6 |
7 | case class PairEntity(id: Long, chatId: Long, firstId: Option[Long], secondId: Option[Long], createdAt: DateTime,
8 | replies: Seq[ReplyEntity] = Nil, updatedAt: DateTime)
9 |
10 | object PairEntity extends SQLSyntaxSupport[PairEntity] {
11 | override val tableName = "pairs"
12 | override val useSnakeCaseColumnName = true
13 |
14 | def apply(g: SyntaxProvider[PairEntity])(rs: WrappedResultSet): PairEntity = apply(g.resultName)(rs)
15 | def apply(c: ResultName[PairEntity])(rs: WrappedResultSet) = new PairEntity(
16 | rs.long(c.id),
17 | rs.long(c.chatId),
18 | rs.longOpt(c.firstId),
19 | rs.longOpt(c.secondId),
20 | rs.get(c.createdAt),
21 | updatedAt = rs.get(c.updatedAt)
22 | )
23 | }
--------------------------------------------------------------------------------
/bot/src/main/resources/application.conf.example:
--------------------------------------------------------------------------------
1 | # Connection Pool settings
2 | db.default.poolInitialSize=5
3 | db.default.poolMaxSize=7
4 | db.default.poolConnectionTimeoutMillis=1000
5 | db.default.poolValidationQuery="select 1 as one"
6 | db.default.poolFactoryName="commons-dbcp2"
7 |
8 | # PostgreSQL example
9 | db.default.driver="org.postgresql.Driver"
10 | db.default.url="jdbc:postgresql://localhost:5432/shizoid"
11 |
12 | redis.host = "127.0.0.1"
13 | redis.port = 6379
14 |
15 | punctuation.endSentence = [".", "!", "?"]
16 |
17 | bot.telegramToken = ""
18 | bot.name = "mrazota_bot"
19 | bot.anchors = [
20 | "пепе",
21 | "pepe",
22 | "переграунд",
23 | "pepeground",
24 | "пепеграундес",
25 | "pepegroundes"
26 | ]
27 |
28 | akka {
29 | quartz {
30 | schedules {
31 | Cleanup {
32 | description = "A cron job that fires off every 30 seconds"
33 | expression = "0 */1 * ? * *"
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/bot/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | # Connection Pool settings
2 | db.default.poolInitialSize=5
3 | db.default.poolMaxSize=7
4 | db.default.poolConnectionTimeoutMillis=1000
5 | db.default.poolValidationQuery="select 1 as one"
6 | db.default.poolFactoryName="commons-dbcp2"
7 |
8 | redis.host = "127.0.0.1"
9 | redis.port = 6379
10 |
11 | punctuation.endSentence = [".", "!", "?"]
12 |
13 | bot.asyncLearn = false
14 | bot.cleanupLimit = 1000
15 | bot.twitter = false
16 | bot.repostChatIds = []
17 | bot.repostChatId = 0
18 | bot.telegramToken = ""
19 | bot.name = "someBot"
20 | bot.anchors = []
21 |
22 | akka {
23 | quartz {
24 | schedules {
25 | Cleanup {
26 | description = "A cron job that fires off every 30 seconds"
27 | expression = "0 */1 * ? * *"
28 | }
29 | Tweets {
30 | description = "A cron job that fires off every 30 seconds"
31 | expression = "0 */1 * ? * *"
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/CleanUp.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import com.pepeground.core.repositories.PairRepository
4 | import com.typesafe.scalalogging.Logger
5 | import org.slf4j.LoggerFactory
6 | import scalikejdbc.DB
7 |
8 | object CleanUp {
9 | private val logger = Logger(LoggerFactory.getLogger(this.getClass))
10 |
11 | def run = {
12 | while (true) {
13 | try {
14 | cleanUp
15 | } catch {
16 | case e: Throwable =>
17 | logger.error(e.getMessage)
18 | Thread.sleep(5000)
19 | }
20 | }
21 | }
22 | private def cleanUp = {
23 | DB localTx { implicit session =>
24 | val removedIds: List[Long] = PairRepository.removeOld(Config.bot.cleanupLimit)
25 |
26 | if (removedIds.isEmpty) {
27 | logger.info("NOTHING TO REMOVE")
28 | Thread.sleep(5000)
29 | } else {
30 | logger.info("REMOVED PAIRS %s (%s)".format(removedIds.size, removedIds.mkString(", ")))
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/entities/ChatEntity.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.entities
2 |
3 | import scalikejdbc._
4 | import org.joda.time._
5 | import scalikejdbc.jodatime.JodaTypeBinder._
6 |
7 | case class ChatEntity(id: Long, name: Option[String], telegramId: Long, repostChatUsername: Option[String],
8 | chatType: Int, randomChance: Int, createdAt: DateTime, updatedAt: DateTime)
9 |
10 | object ChatEntity extends SQLSyntaxSupport[ChatEntity] {
11 | override val tableName = "chats"
12 | override val useSnakeCaseColumnName = true
13 |
14 | def apply(g: SyntaxProvider[ChatEntity])(rs: WrappedResultSet): ChatEntity = apply(g.resultName)(rs)
15 | def apply(c: ResultName[ChatEntity])(rs: WrappedResultSet) = new ChatEntity(
16 | rs.long(c.id),
17 | rs.stringOpt(c.name),
18 | rs.long(c.telegramId),
19 | rs.stringOpt(c.repostChatUsername),
20 | rs.int(c.chatType),
21 | rs.int(c.randomChance),
22 | rs.get(c.createdAt),
23 | rs.get(c.updatedAt)
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/Learn.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import com.pepeground.core.repositories.LearnItem
4 | import com.pepeground.core.repositories.LearnQueueRepository
5 | import com.pepeground.core.services.LearnService
6 | import scalikejdbc.DB
7 | import com.typesafe.scalalogging.Logger
8 | import org.slf4j.LoggerFactory
9 |
10 | object Learn {
11 | private val logger = Logger(LoggerFactory.getLogger(this.getClass))
12 | private lazy val learnQueueRepository = LearnQueueRepository
13 |
14 | def run = {
15 | while (true) {
16 | try {
17 | learn
18 | } catch {
19 | case e: Throwable =>
20 | logger.error(e.getMessage)
21 | Thread.sleep(5000)
22 | }
23 | }
24 | }
25 |
26 | private def learn = {
27 | learnQueueRepository.pop() match {
28 | case Some(li: LearnItem) => DB localTx { implicit session =>
29 | new LearnService(li.message, li.chatId).learnPair()
30 | }
31 | case None => Thread.sleep(100)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/LearnQueueRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.CoreConfig
4 | import com.redis._
5 |
6 | import io.circe._
7 | import io.circe.generic.auto._
8 | import io.circe.parser._
9 | import io.circe.syntax._
10 |
11 | case class LearnItem(message: List[String], chatId: Long)
12 |
13 | object LearnQueueRepository {
14 | val clients = new RedisClientPool(CoreConfig.redis.host, CoreConfig.redis.port, database = 11)
15 | val learnQueue = "learn"
16 |
17 | def push(message: List[String], chatId: Long): Unit = {
18 | clients.withClient(cli => cli.lpush(key = learnQueue, value = LearnItem(message, chatId).asJson.noSpaces))
19 | }
20 |
21 | def pop(): Option[LearnItem] = {
22 | clients.withClient { cli =>
23 | val el = cli.rpop(learnQueue)
24 |
25 | el match {
26 | case Some(str: String) => decode[LearnItem](str) match {
27 | case Right(s: LearnItem) => Option(s)
28 | case Left(_: Error) => None
29 | }
30 | case None => None
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/fabfile.py:
--------------------------------------------------------------------------------
1 | from fabric.api import env, run, sudo, local, put, settings
2 | from contextlib import contextmanager
3 | import os
4 | import fabtools
5 |
6 | env.hosts = ['pepeground']
7 | env.shell = 'bash -c'
8 | env.use_ssh_config = True
9 |
10 | @contextmanager
11 | def locally():
12 | global run
13 | global sudo
14 | global local
15 | _run, _sudo = run, sudo
16 | run = sudo = local
17 | yield
18 | run, sudo = _run, _sudo
19 |
20 | def assembly():
21 | with locally():
22 | run("sbt assembly")
23 |
24 |
25 | def production():
26 | env.project_user = run("whoami")
27 | env.sudo_user = env.project_user
28 | env.base_dir = "/home/%s/pepeground" % (env.project_user)
29 |
30 | def setup():
31 | run("mkdir -p %s" % env.base_dir)
32 |
33 | def upload():
34 | put("bot/target/scala-2.12/bot-assembly-0.1.jar", "%s/bot.jar" % (env.base_dir))
35 |
36 | def restart():
37 | run("sudo systemctl restart pepeground")
38 |
39 | def deploy():
40 | assembly()
41 | production()
42 | setup()
43 | upload()
44 | restart()
45 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/ContextRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.CoreConfig
4 | import com.redis._
5 |
6 | object ContextRepository {
7 | val clients = new RedisClientPool(CoreConfig.redis.host, CoreConfig.redis.port, database = 11)
8 |
9 | def updateContext(path: String, words: List[String]): Unit = {
10 | val ctx: List[String] = getContext(path, 50)
11 |
12 | clients.withClient { client =>
13 | val filteredWords: List[String] = words.distinct.map(_.toLowerCase).filter(!ctx.contains(_))
14 | val aggregatedWords: List[String] = (filteredWords ++ ctx).take(50)
15 |
16 | client.pipeline { p =>
17 | p.del(path)
18 | p.lpush(path, aggregatedWords.headOption.getOrElse(""), aggregatedWords.tail: _*)
19 | }
20 | }
21 | }
22 |
23 | def getContext(path: String, limit: Int = 50): List[String] = {
24 | clients.withClient { client =>
25 | client
26 | .lrange[String](path, 0, limit)
27 | .getOrElse(List())
28 | .filter(_.nonEmpty)
29 | .map(_.get)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/Main.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import org.flywaydb.core.Flyway
4 | import scalikejdbc.ConnectionPool
5 | import scalikejdbc.config._
6 |
7 | import scala.concurrent.Await
8 | import scala.concurrent.duration.Duration
9 |
10 | object Main extends App {
11 | override def main(args: Array[String]): Unit = {
12 | java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"))
13 |
14 | DBs.setupAll()
15 |
16 | val flyway: Flyway = new Flyway()
17 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
18 |
19 | flyway.setDataSource(dataSource)
20 | flyway.baseline()
21 | flyway.migrate()
22 |
23 | args.headOption match {
24 | case Some("learn") => Learn.run
25 | case Some("cleanup") => CleanUp.run
26 | case Some("bot") =>
27 | print("Running bot")
28 | Await.ready(Router.run(), Duration.Inf)
29 | case Some(x: String) =>
30 | print(s"Unknown application argument: ${x}")
31 | System.exit(1)
32 | case None =>
33 | print("Missing application argument")
34 | System.exit(1)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/WordDeletionQueueRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.CoreConfig
4 | import com.redis._
5 |
6 | import io.circe._
7 | import io.circe.generic.auto._
8 | import io.circe.parser._
9 | import io.circe.syntax._
10 |
11 | case class WordForDeletion(wordId: Long, name: String)
12 |
13 | object WordDeletionQueueRepository {
14 | val clients = new RedisClientPool(CoreConfig.redis.host, CoreConfig.redis.port, database = 11)
15 | val wordDeletionQueue = "words_for_deletion"
16 |
17 | def push(wordId: Long, name: String): Unit = {
18 | clients.withClient(cli => cli.lpush(key = wordDeletionQueue, value = WordForDeletion(wordId, name).asJson.noSpaces))
19 | }
20 |
21 | def pop(): Option[WordForDeletion] = {
22 | clients.withClient { cli =>
23 | val el = cli.rpop(wordDeletionQueue)
24 |
25 | el match {
26 | case Some(str: String) => decode[WordForDeletion](str) match {
27 | case Right(s: WordForDeletion) => Option(s)
28 | case Left(_: Error) => None
29 | }
30 | case None => None
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/RepostHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.bot.Config
4 | import com.bot4s.telegram.methods.ForwardMessage
5 | import com.bot4s.telegram.models.{Message, User}
6 | import scalikejdbc.DBSession
7 |
8 | object RepostHandler {
9 | def apply(message: Message)(implicit session: DBSession): RepostHandler = {
10 | new RepostHandler(message)
11 | }
12 | }
13 |
14 | class RepostHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
15 | def call(): Option[ForwardMessage] = {
16 | super.before()
17 |
18 | if(canRepost) {
19 | Some(
20 | ForwardMessage(
21 | chat.repostChatUsername.get,
22 | message.chat.id,
23 | None,
24 | message.replyToMessage.get.messageId
25 | )
26 | )
27 | } else {
28 | None
29 | }
30 | }
31 |
32 | def canRepost: Boolean = {
33 | message.replyToMessage match {
34 | case None => false
35 | case Some(mo: Message) => mo.from match {
36 | case None => false
37 | case Some(u: User) => u.username match {
38 | case None => false
39 | case Some(username: String) => username.toLowerCase == Config.bot.name.toLowerCase
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/Prometheus.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import com.sun.net.httpserver.HttpExchange
4 | import io.micrometer.prometheus.{PrometheusConfig, PrometheusMeterRegistry}
5 | import com.sun.net.httpserver.HttpServer
6 | import java.io.IOException
7 | import java.net.InetSocketAddress
8 |
9 | object Prometheus {
10 | val prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
11 |
12 | def start(): Unit = server.start()
13 |
14 | private lazy val server = buildServer()
15 |
16 | private def buildServer() = {
17 | try {
18 | val server = HttpServer.create(new InetSocketAddress(8080), 0)
19 | server.createContext("/metrics", (httpExchange: HttpExchange) => {
20 | def serve(httpExchange: HttpExchange) = {
21 | val response = prometheusRegistry.scrape()
22 | httpExchange.sendResponseHeaders(200, response.getBytes.length)
23 | try {
24 | val os = httpExchange.getResponseBody
25 | try
26 | os.write(response.getBytes)
27 | finally if (os != null) os.close()
28 | }
29 | }
30 |
31 | serve(httpExchange)
32 | })
33 | new Thread { server.start() }
34 | } catch {
35 | case e: IOException =>
36 | throw new RuntimeException(e)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/SetRepostChatHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.core.repositories.ChatRepository
4 | import com.bot4s.telegram.models.{ChatMember, Message}
5 | import scalikejdbc.DBSession
6 |
7 | import scala.concurrent.{Await, Future}
8 | import scala.concurrent.duration._
9 |
10 | object SetRepostChatHandler {
11 | def apply(message: Message, chatMemberRequest: Option[Future[ChatMember]])(implicit session: DBSession): SetRepostChatHandler = {
12 | new SetRepostChatHandler(message, chatMemberRequest)
13 | }
14 | }
15 |
16 | class SetRepostChatHandler(message: Message, chatMemberRequest: Option[Future[ChatMember]])(implicit session: DBSession) extends GenericHandler(message) {
17 | final val AdminStatuses = Array("creator", "administrator")
18 |
19 | def call(chatUsername: String): Option[String] = {
20 | super.before()
21 |
22 | if(canSetRepostChat) {
23 | ChatRepository.updateRepostChat(chat.id, chatUsername)
24 | Some(s"Ya wohl, Lord Helmet! Setting repost channel to ${chatUsername}")
25 | } else {
26 | None
27 | }
28 | }
29 |
30 | def canSetRepostChat: Boolean = {
31 | chatMemberRequest match {
32 | case None => false
33 | case Some(c) => {
34 | val status = Await.result(c, 1 minute).status
35 | AdminStatuses.contains(status)
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/repositories/SubscriptionRepositoryTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import org.flywaydb.core.Flyway
4 | import scalikejdbc.scalatest.AutoRollback
5 | import org.scalatest._
6 | import org.scalatest.fixture.FlatSpec
7 | import scalikejdbc._
8 | import scalikejdbc.config.DBs
9 |
10 | class SubscriptionRepositoryTest extends FlatSpec with BeforeAndAfter with AutoRollback {
11 | before {
12 | DBs.setupAll()
13 |
14 | val flyway: Flyway = new Flyway()
15 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
16 |
17 | flyway.setDataSource(dataSource)
18 | flyway.baseline()
19 | flyway.migrate()
20 | }
21 |
22 | override def fixture(implicit session: DBSession) {
23 | val chat = ChatRepository.create(1, "Some chat", "private")
24 |
25 | sql"insert into subscriptions values (1, ${chat.id}, ${"Alice"}, 0)".update.apply()
26 | sql"insert into subscriptions values (2, ${chat.id}, ${"Bob"}, 0)".update.apply()
27 | }
28 |
29 | behavior of "getList"
30 |
31 | it should "return list of subscriptions" in { implicit session =>
32 | val subscriptions = SubscriptionRepository.getList()
33 |
34 | assert(subscriptions.size == 2)
35 | }
36 |
37 | behavior of "updateSubscription"
38 |
39 | it should "update subscription since_id" in { implicit session =>
40 | val subscriptions = SubscriptionRepository.getList()
41 |
42 | subscriptions.foreach(s => SubscriptionRepository.updateSubscription(s.id, 10))
43 |
44 | SubscriptionRepository.getList().foreach { s =>
45 | assert(s.sinceId.nonEmpty)
46 | assert(s.sinceId.get == 10)
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/MessageHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import com.pepeground.core.repositories.{ContextRepository, LearnQueueRepository}
4 | import com.pepeground.core.services.{LearnService, StoryService}
5 | import com.bot4s.telegram.models.Message
6 | import com.pepeground.bot.Config
7 | import com.typesafe.scalalogging._
8 | import org.slf4j.LoggerFactory
9 | import scalikejdbc.DBSession
10 |
11 | object MessageHandler {
12 | def apply(message: Message)(implicit session: DBSession): MessageHandler = {
13 | new MessageHandler(message)
14 | }
15 | }
16 |
17 | class MessageHandler(message: Message)(implicit session: DBSession) extends GenericHandler(message) {
18 | private val logger = Logger(LoggerFactory.getLogger(this.getClass))
19 | private lazy val learnService: LearnService = new LearnService(words, chat.id)
20 | private lazy val storyService: StoryService = new StoryService(words, context, chat.id)
21 | private lazy val learnQueueRepository = LearnQueueRepository
22 |
23 | def call(): Option[Either[Option[String], Option[String]]] = {
24 | super.before()
25 |
26 | if (!hasText || isEdition) return None
27 |
28 | logger.info("Message received: %s from %s (%s)".format(message.text.getOrElse(""), chatName, migrationId))
29 |
30 | learn()
31 |
32 | ContextRepository.updateContext(chatContext, words)
33 |
34 | if (isReplyToBot) return Option(Left(storyService.generate()))
35 | if (isMentioned) return Option(Left(storyService.generate()))
36 | if (isPrivate) return Option(Right(storyService.generate()))
37 | if (hasAnchors) return Option(Right(storyService.generate()))
38 | if (isRandomAnswer) return Option(Right(storyService.generate()))
39 |
40 | None
41 | }
42 |
43 | private def learn(): Unit = {
44 | if (Config.bot.asyncLear) {
45 | learnQueueRepository.push(words, chat.id)
46 | } else {
47 | learnService.learnPair()
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/core/src/main/resources/db/migration/V2__init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS chats(
2 | id SERIAL PRIMARY KEY NOT NULL,
3 | telegram_id bigint NOT NULL,
4 | chat_type smallint NOT NULL,
5 | random_chance smallint DEFAULT 5 NOT NULL,
6 | created_at timestamp without time zone NOT NULL,
7 | updated_at timestamp without time zone NOT NULL,
8 | name character varying
9 | );
10 |
11 | CREATE INDEX IF NOT EXISTS index_chats_on_telegram_id ON chats USING btree (telegram_id);
12 |
13 | CREATE TABLE IF NOT EXISTS pairs (
14 | id SERIAL PRIMARY KEY NOT NULL,
15 | chat_id integer NOT NULL,
16 | first_id integer,
17 | second_id integer,
18 | created_at timestamp without time zone NOT NULL
19 | );
20 |
21 | CREATE INDEX IF NOT EXISTS index_pairs_on_chat_id ON pairs USING btree (chat_id);
22 | CREATE INDEX IF NOT EXISTS index_pairs_on_first_id ON pairs USING btree (first_id);
23 | CREATE INDEX IF NOT EXISTS index_pairs_on_second_id ON pairs USING btree (second_id);
24 | CREATE UNIQUE INDEX IF NOT EXISTS unique_pair_chat_id_first_id ON pairs USING btree (chat_id, first_id) WHERE (second_id IS NULL);
25 | CREATE UNIQUE INDEX IF NOT EXISTS unique_pair_chat_id_first_id_second_id ON pairs USING btree (chat_id, first_id, second_id);
26 | CREATE UNIQUE INDEX IF NOT EXISTS unique_pair_chat_id_second_id ON pairs USING btree (chat_id, second_id) WHERE (first_id IS NULL);
27 |
28 | CREATE TABLE IF NOT EXISTS replies (
29 | id SERIAL PRIMARY KEY NOT NULL,
30 | pair_id integer NOT NULL,
31 | word_id integer,
32 | count bigint DEFAULT 1 NOT NULL
33 | );
34 |
35 | CREATE UNIQUE INDEX IF NOT EXISTS unique_reply_pair_id ON replies USING btree (pair_id) WHERE (word_id IS NULL);
36 | CREATE UNIQUE INDEX IF NOT EXISTS unique_reply_pair_id_word_id ON replies USING btree (pair_id, word_id);
37 |
38 | CREATE TABLE IF NOT EXISTS words (
39 | id SERIAL PRIMARY KEY NOT NULL,
40 | word character varying NOT NULL
41 | );
42 |
43 | CREATE INDEX IF NOT EXISTS index_words_on_word ON words USING btree (word);
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/services/LearnService.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.services
2 |
3 | import scalikejdbc._
4 | import com.pepeground.core.CoreConfig
5 | import com.pepeground.core.entities.{ReplyEntity, WordEntity}
6 | import com.pepeground.core.repositories.{PairRepository, ReplyRepository, WordRepository}
7 |
8 | import scala.collection.mutable.{Map => MMap}
9 | import scala.collection.mutable.ListBuffer
10 |
11 | class LearnService(words: List[String], chatId: Long)(implicit session: DBSession) {
12 | def learnPair(): Unit = {
13 | WordRepository.learnWords(words)
14 | var newWords: ListBuffer[Option[String]] = ListBuffer(None)
15 | val preloadedWords: Map[String, WordEntity] = WordRepository.getByWords(words).map(we => we.word -> we).toMap
16 |
17 | words.foreach { w =>
18 | newWords += Option(w)
19 |
20 | if (CoreConfig.punctuation.endSentence.contains(w.takeRight(1))) newWords += None
21 | }
22 |
23 | newWords.last match {
24 | case Some(s: String) => newWords += None
25 | case _ =>
26 | }
27 |
28 | var pairIds: ListBuffer[Long] = ListBuffer()
29 |
30 | while(newWords.nonEmpty) {
31 | val trigramMap: MMap[Int, Long] = MMap()
32 | val trigram = newWords.take(3)
33 | newWords.remove(0, 1)
34 |
35 | trigram.zipWithIndex.foreach { case (w,i) =>
36 | w match {
37 | case Some(s: String) => preloadedWords.get(s) match {
38 | case Some(we: WordEntity) => trigramMap.put(i, we.id)
39 | case None =>
40 | }
41 | case None =>
42 | }
43 | }
44 |
45 | val pair = PairRepository.getPairOrCreateBy(chatId, trigramMap.get(0), trigramMap.get(1))
46 | pairIds += pair.id
47 |
48 | ReplyRepository.getReplyBy(pair.id, trigramMap.get(2)) match {
49 | case Some(r: ReplyEntity) => ReplyRepository.incrementReply(r.id, r.count)
50 | case None => ReplyRepository.createReplyBy(pair.id, trigramMap.get(2))
51 | }
52 | }
53 |
54 | PairRepository.touch(pairIds.toList)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/ReplyRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import java.util.NoSuchElementException
4 |
5 | import com.pepeground.core.entities.ReplyEntity
6 | import scalikejdbc._
7 | import com.pepeground.core.support.PostgreSQLSyntaxSupport._
8 |
9 | object ReplyRepository {
10 | private val r = ReplyEntity.syntax("r")
11 |
12 | def hasWithWordId(wordId: Long)(implicit session: DBSession): Boolean = {
13 | withSQL {
14 | select(r.id).from(ReplyEntity as r).where.eq(r.wordId, wordId).limit(1)
15 | }.map(rs => rs.long("id")).single.apply().isDefined
16 | }
17 |
18 | def repliesForPair(pairId: Long)(implicit session: DBSession): List[ReplyEntity] = {
19 | withSQL {
20 | select
21 | .from(ReplyEntity as r)
22 | .where.eq(r.pairId, pairId)
23 | .orderBy(r.count.desc)
24 | .limit(3)
25 | }.map(rs => ReplyEntity(r)(rs)).list().apply()
26 | }
27 |
28 | def getReplyOrCreateBy(pairId: Long, wordId: Option[Long])(implicit session: DBSession): ReplyEntity = {
29 | getReplyBy(pairId, wordId) match {
30 | case Some(reply: ReplyEntity) => reply
31 | case None => createReplyBy(pairId, wordId)
32 | }
33 | }
34 |
35 | def incrementReply(id: Long, counter: Long)(implicit session: DBSession): Unit = {
36 | withSQL {
37 | update(ReplyEntity).set(
38 | ReplyEntity.column.count -> (counter + 1)
39 | ).where.eq(ReplyEntity.column.id, id)
40 | }.update.apply()
41 | }
42 |
43 | def getReplyBy(pairId: Long, wordId: Option[Long])(implicit session: DBSession): Option[ReplyEntity] = {
44 | withSQL {
45 | select.from(ReplyEntity as r).where.eq(r.wordId, wordId).and.eq(r.pairId, pairId).limit(1)
46 | }.map(rs => ReplyEntity(r)(rs)).single.apply()
47 | }
48 |
49 | def createReplyBy(pairId: Long, wordId: Option[Long])(implicit session: DBSession): ReplyEntity = {
50 | withSQL {
51 | insert.into(ReplyEntity).namedValues(
52 | ReplyEntity.column.pairId -> pairId,
53 | ReplyEntity.column.wordId -> wordId
54 | ).onConflictDoNothing()
55 | }.update().apply()
56 |
57 | getReplyBy(pairId, wordId) match {
58 | case Some(reply: ReplyEntity) => reply
59 | case None => throw new NoSuchElementException("No such reply")
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/services/StoryServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.services
2 |
3 | import com.pepeground.core.repositories.{ChatRepository, PairRepository, ReplyRepository, WordRepository}
4 | import org.flywaydb.core.Flyway
5 | import org.joda.time.DateTime
6 | import scalikejdbc.scalatest.AutoRollback
7 | import org.scalatest._
8 | import org.scalatest.fixture.FlatSpec
9 | import scalikejdbc._
10 | import scalikejdbc.config.DBs
11 |
12 | import scala.io.Source
13 |
14 | class StoryServiceTest extends FlatSpec with BeforeAndAfter with AutoRollback {
15 | before {
16 | DBs.setupAll()
17 |
18 | val flyway: Flyway = new Flyway()
19 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
20 |
21 | flyway.setDataSource(dataSource)
22 | flyway.baseline()
23 | flyway.migrate()
24 | }
25 |
26 | behavior of "generate"
27 |
28 | it should "generate empty on new dictionary" in { implicit session =>
29 | val newChat = ChatRepository.create(4, "Some chat", "private")
30 | val learnService = new LearnService(List("hello", "world", "scala"), newChat.id)
31 |
32 | learnService.learnPair()
33 |
34 | val storySetvice = new StoryService(List("hello", "world", "scala"), List(), newChat.id)
35 |
36 | val message = storySetvice.generate()
37 |
38 | assert(message.isEmpty)
39 | }
40 |
41 | it should "generate empty on empty dictionary" in { implicit session =>
42 | val newChat = ChatRepository.create(4, "Some chat", "private")
43 |
44 | val storySetvice = new StoryService(List("hello", "world", "scala"), List(), newChat.id)
45 |
46 | val message = storySetvice.generate()
47 |
48 | assert(message.isEmpty)
49 | }
50 |
51 | it should "generate non-empty message" in { implicit session =>
52 | val newChat = ChatRepository.create(4, "Some chat", "private")
53 |
54 | new LearnService(List("hello", "world", "scala"), newChat.id).learnPair()
55 |
56 | var timeOffset = new DateTime().minusMinutes(10)
57 |
58 | sql"UPDATE pairs SET created_at = ${timeOffset}".update.apply()
59 |
60 | val storySetvice = new StoryService(List("hello", "world", "scala"), List(), newChat.id)
61 |
62 | val message = storySetvice.generate()
63 |
64 | assert(message.nonEmpty)
65 | assert(message.get.contains("hello world scala"))
66 | }
67 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/WordRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.entities.WordEntity
4 | import scalikejdbc._
5 | import com.pepeground.core.support.PostgreSQLSyntaxSupport._
6 | import scala.util.control.Breaks._
7 | import com.typesafe.scalalogging._
8 | import org.slf4j.LoggerFactory
9 |
10 | object WordRepository {
11 | private val logger = Logger(LoggerFactory.getLogger(this.getClass))
12 | private val w = WordEntity.syntax("w")
13 |
14 | def getFirstId()(implicit session: DBSession): Option[Long] = {
15 | withSQL {
16 | select(sqls"MIN(id)").from(WordEntity as w)
17 | }.map(rs => rs.long(1)).single.apply()
18 | }
19 |
20 | def getNextId(id: Long)(implicit session: DBSession): Option[Long] = {
21 | withSQL {
22 | select(w.id).from(WordEntity as w).where.gt(w.id, id).orderBy(w.id.asc).limit(1)
23 | }.map(rs => rs.long("id")).single.apply()
24 | }
25 |
26 | def deleteById(id: Long)(implicit session: DBSession): Unit = {
27 | withSQL {
28 | delete.from(WordEntity as w).where.eq(w.id, id)
29 | }.update().apply()
30 | }
31 |
32 | def getByWords(words: List[String])(implicit session: DBSession): List[WordEntity] = {
33 | withSQL {
34 | select.from(WordEntity as w).where.in(w.word, words)
35 | }.map(rs => WordEntity(w)(rs)).list.apply()
36 | }
37 |
38 | def getWordById(id: Long)(implicit session: DBSession): Option[WordEntity] = {
39 | withSQL {
40 | select.from(WordEntity as w).where.eq(w.id, id).limit(1)
41 | }.map(rs => WordEntity(w)(rs)).single.apply()
42 | }
43 |
44 | def getByWord(word: String)(implicit session: DBSession): Option[WordEntity] = {
45 | withSQL {
46 | select.from(WordEntity as w).where.eq(w.word, word).limit(1)
47 | }.map(rs => WordEntity(w)(rs)).single.apply()
48 | }
49 |
50 | def create(word: String)(implicit session: DBSession): Option[Long] = {
51 | withSQL {
52 | insert.into(WordEntity).namedValues(
53 | WordEntity.column.word -> word
54 | ).onConflictDoNothing().returningId
55 | }.map(rs => rs.long("id")).single().apply()
56 | }
57 |
58 | def learnWords(words: List[String])(implicit session: DBSession): Unit = {
59 | val existedWords: List[String] = getByWords(words).map(_.word)
60 |
61 | words.foreach { word =>
62 | breakable {
63 | if (existedWords.contains(word)) break
64 | logger.info("Learn new word: %s".format(word))
65 | create(word)
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/repositories/WordRepositoryTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import org.flywaydb.core.Flyway
4 | import scalikejdbc.scalatest.AutoRollback
5 | import org.scalatest._
6 | import org.scalatest.fixture.FlatSpec
7 | import scalikejdbc.ConnectionPool
8 | import scalikejdbc.config.DBs
9 |
10 | class WordRepositoryTest extends FlatSpec with BeforeAndAfter with AutoRollback {
11 | before {
12 | DBs.setupAll()
13 |
14 | val flyway: Flyway = new Flyway()
15 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
16 |
17 | flyway.setDataSource(dataSource)
18 | flyway.baseline()
19 | flyway.migrate()
20 | }
21 |
22 | behavior of "create"
23 |
24 | it should "creates new word" in { implicit session =>
25 | val word = WordRepository.create("scala")
26 |
27 | assert(word.nonEmpty)
28 | }
29 |
30 | behavior of "getWordById"
31 |
32 | it should "return word by id" in { implicit session =>
33 | val word = WordRepository.create("scala")
34 | val sameWord = WordRepository.getWordById(word.get)
35 |
36 | assert(sameWord.nonEmpty)
37 | assert(sameWord.get.word == "scala")
38 | }
39 |
40 | behavior of "getByWord"
41 |
42 | it should "return word by string" in { implicit session =>
43 | val word = WordRepository.create("scala")
44 |
45 | val sameWord = WordRepository.getByWord("scala")
46 |
47 | assert(sameWord.nonEmpty)
48 | assert(word.get == sameWord.get.id)
49 | }
50 |
51 | behavior of "getByWords"
52 |
53 | it should "return list of words" in { implicit session =>
54 | val word1 = WordRepository.create("hello")
55 | val word2 = WordRepository.create("world")
56 | val word3 = WordRepository.create("scala")
57 |
58 | val wordIds = WordRepository.getByWords(List("hello", "world", "scala")).map(_.id)
59 |
60 | assert(wordIds == List(word1, word2, word3).map(_.get))
61 | }
62 |
63 | behavior of "learnWords"
64 |
65 | it should "learn new words" in { implicit session =>
66 | WordRepository.learnWords(List("hello", "world", "scala"))
67 |
68 | val words = WordRepository.getByWords(List("hello", "world", "scala"))
69 |
70 | assert(words.size == 3)
71 | }
72 |
73 | it should "skip already learned words" in { implicit session =>
74 | val word2 = WordRepository.create("world")
75 | WordRepository.learnWords(List("hello", "world", "scala"))
76 |
77 | val words = WordRepository.getByWords(List("hello", "world", "scala"))
78 |
79 | assert(words.size == 3)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/ChatRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import java.util.NoSuchElementException
4 |
5 | import com.pepeground.core.enums.{ChatType}
6 | import com.pepeground.core.entities.ChatEntity
7 | import scalikejdbc._
8 | import org.joda.time._
9 | import com.pepeground.core.support.PostgreSQLSyntaxSupport._
10 | import scalikejdbc.jodatime.JodaParameterBinderFactory._
11 |
12 | object ChatRepository {
13 | private val c = ChatEntity.syntax("c")
14 |
15 | def getList(limit: Int = 20, offset: Int = 0)(implicit session: DBSession): List[ChatEntity] = {
16 | withSQL {
17 | select.from(ChatEntity as c).limit(limit).offset(offset)
18 | }.map(rs => ChatEntity(c)(rs)).list().apply()
19 | }
20 |
21 | def getChatById(id: Long)(implicit session: DBSession): Option[ChatEntity] = {
22 | withSQL {
23 | select.from(ChatEntity as c).where.eq(c.id, id).limit(1)
24 | }.map(rs => ChatEntity(c)(rs)).single.apply()
25 | }
26 |
27 | def getOrCreateBy(telegramId: Long, name: String, chatType: String)(implicit session: DBSession): ChatEntity = {
28 | getByTelegramId(telegramId) match {
29 | case Some(chat: ChatEntity) => chat
30 | case None => create(telegramId, name, chatType)
31 | }
32 | }
33 |
34 | def updateRandomChance(id: Long, randomChance: Int)(implicit session: DBSession): Unit = {
35 | withSQL {
36 | update(ChatEntity).set(
37 | ChatEntity.column.randomChance -> randomChance,
38 | ChatEntity.column.updatedAt -> new DateTime()
39 | ).where.eq(ChatEntity.column.id, id)
40 | }.update().apply()
41 | }
42 |
43 | def updateRepostChat(id: Long, repostChatUsername: String)(implicit session: DBSession): Unit = {
44 | withSQL {
45 | update(ChatEntity).set(
46 | ChatEntity.column.repostChatUsername -> repostChatUsername,
47 | ChatEntity.column.updatedAt -> new DateTime()
48 | ).where.eq(ChatEntity.column.id, id)
49 | }.update().apply()
50 | }
51 |
52 | def updateChat(id: Long, name: Option[String], telegramId: Long)(implicit session: DBSession): Unit = {
53 | withSQL {
54 | update(ChatEntity).set(
55 | ChatEntity.column.name -> name,
56 | ChatEntity.column.telegramId -> telegramId,
57 | ChatEntity.column.updatedAt -> new DateTime()
58 | ).where.eq(ChatEntity.column.id, id)
59 | }.update.apply()
60 | }
61 |
62 | def create(telegramId: Long, name: String, chatType: String)(implicit session: DBSession): ChatEntity = {
63 | withSQL {
64 | insert.into(ChatEntity).namedValues(
65 | ChatEntity.column.telegramId -> telegramId,
66 | ChatEntity.column.name -> Option(name),
67 | ChatEntity.column.chatType -> ChatType(chatType.toLowerCase),
68 | ChatEntity.column.updatedAt -> new DateTime(),
69 | ChatEntity.column.createdAt -> new DateTime()
70 | ).onConflictDoNothing()
71 | }.update().apply()
72 |
73 | getByTelegramId(telegramId) match {
74 | case Some(chat: ChatEntity) => chat
75 | case None => throw new NoSuchElementException("No such chat")
76 | }
77 | }
78 |
79 | def getByTelegramId(telegramId: Long)(implicit session: DBSession): Option[ChatEntity] = {
80 | withSQL {
81 | select.from(ChatEntity as c).where.eq(c.telegramId, telegramId).limit(1)
82 | }.map(rs => ChatEntity(c)(rs)).single.apply()
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/services/StoryService.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.services
2 |
3 | import com.pepeground.core.CoreConfig
4 | import com.pepeground.core.entities.{PairEntity, ReplyEntity, WordEntity}
5 | import com.pepeground.core.repositories.{PairRepository, ReplyRepository, WordRepository}
6 | import scalikejdbc._
7 |
8 | import scala.collection.mutable.ListBuffer
9 | import scala.util.Random
10 | import scala.util.control.Breaks._
11 |
12 | class StoryService(words: List[String], context: List[String], chatId: Long, sentences: Option[Int] = None)(implicit session: DBSession) {
13 | var currentSentences: ListBuffer[String] = ListBuffer()
14 | var currentWordIds: ListBuffer[Long] = ListBuffer()
15 |
16 | def generate(): Option[String] = {
17 | val currentWords: Map[String, Long] = WordRepository.getByWords(words ++ context).map(w => w.word -> w.id).toMap
18 | currentWordIds = words.map(w => currentWords.get(w)).filter(_.isDefined).map(_.get).to[ListBuffer]
19 |
20 | for ( a <- 0 to sentences.getOrElse(Random.nextInt(2) + 1) ) {
21 | generateSentence()
22 | }
23 |
24 | if (currentSentences.nonEmpty) {
25 | Some(currentSentences.mkString(" "))
26 | } else {
27 | None
28 | }
29 | }
30 |
31 |
32 | private def generateSentence(): Unit = {
33 | var sentence: ListBuffer[String] = ListBuffer()
34 | var safetyCounter = 50
35 |
36 | var firstWordId: Option[Long] = None
37 | var secondWordId: List[Option[Long]] = currentWordIds.map(Option(_)).toList
38 |
39 | var pair: Option[PairEntity] = None
40 |
41 | pair = Random.shuffle(PairRepository.getPairWithReplies(chatId, firstWordId, secondWordId)).headOption
42 |
43 | breakable {
44 | while(true) {
45 | if ( safetyCounter < 0 ) break
46 | if ( pair.isEmpty ) break
47 |
48 | safetyCounter -= 1
49 |
50 | pair match {
51 | case Some(pe: PairEntity) =>
52 | val reply = Random.shuffle(ReplyRepository.repliesForPair(pe.id)).headOption
53 |
54 | firstWordId = pe.secondId
55 |
56 | WordRepository.getWordById(pe.secondId.getOrElse(0.toLong)) match {
57 | case Some(we: WordEntity) =>
58 | if(sentence.isEmpty) {
59 | sentence += we.word.toLowerCase
60 | currentWordIds -= pe.secondId.getOrElse(0)
61 | }
62 | case None =>
63 | }
64 |
65 | reply match {
66 | case Some(re: ReplyEntity) => re.wordId match {
67 | case Some(wordId: Long) =>
68 | secondWordId = List(re.wordId)
69 |
70 | WordRepository.getWordById(wordId) match {
71 | case Some(we: WordEntity) =>
72 | sentence += we.word
73 | case None =>
74 | break
75 | }
76 | case None =>
77 | }
78 | case None =>
79 | }
80 | case None =>
81 | break
82 | }
83 |
84 | pair = Random.shuffle(PairRepository.getPairWithReplies(chatId, firstWordId, secondWordId)).headOption
85 | }
86 | }
87 |
88 | if (sentence.nonEmpty) {
89 | currentSentences += setSentenceEnd(sentence.mkString(" ").stripLineEnd)
90 | }
91 | }
92 |
93 | private def setSentenceEnd(s: String): String = {
94 | if(CoreConfig.punctuation.endSentence.contains(s.takeRight(1))) {
95 | s
96 | } else {
97 | "%s%s".format(
98 | s,
99 | CoreConfig.punctuation.endSentence(Random.nextInt(endSentenceLength))
100 | )
101 | }
102 | }
103 |
104 | lazy val endSentenceLength: Int = CoreConfig.punctuation.endSentence.length
105 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/handlers/GenericHandler.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot.handlers
2 |
3 | import java.util.concurrent.atomic.AtomicInteger
4 |
5 | import com.pepeground.bot.{Config, Prometheus}
6 | import com.pepeground.core.entities.ChatEntity
7 | import com.pepeground.core.repositories.{ChatRepository, ContextRepository}
8 | import com.bot4s.telegram.models.{Message, MessageEntity, User}
9 | import com.bot4s.telegram.models.ChatType._
10 | import io.micrometer.core.instrument.Tag
11 | import scalikejdbc._
12 |
13 | import scala.util.Random
14 |
15 | class GenericHandler(message: Message)(implicit session: DBSession) {
16 | private val messagesCounter = Prometheus.prometheusRegistry.counter("bot_messages")
17 |
18 | def before(): Unit = {
19 | if (isChatChanged) ChatRepository.updateChat(chat.id, Option(chatName), migrationId)
20 |
21 | messagesCounter.increment()
22 | }
23 |
24 | def isChatChanged: Boolean = chatName != chat.name.getOrElse("") || migrationId != telegramId
25 | def isPrivate: Boolean = chatType == "chat"
26 | def isRandomAnswer: Boolean = scala.util.Random.nextInt(100) < chat.randomChance
27 |
28 | def isReplyToBot: Boolean = message.replyToMessage match {
29 | case Some(r: Message) => r.from match {
30 | case Some(f: User) => f.username.getOrElse("") == Config.bot.name
31 | case None => false
32 | }
33 | case None => false
34 | }
35 |
36 | def hasAnchors: Boolean = {
37 | hasText && (words.exists(Config.bot.anchors.contains(_)) || text.getOrElse("").contains(Config.bot.name))
38 | }
39 |
40 | def hasEntities: Boolean = message.entities match {
41 | case Some(s: Seq[MessageEntity]) => s.nonEmpty
42 | case None => false
43 | }
44 |
45 | def isEdition: Boolean = message.editDate match {
46 | case Some(_) => true
47 | case None => false
48 | }
49 |
50 | def hasText: Boolean = text match {
51 | case Some(t: String) => t.nonEmpty
52 | case None => false
53 | }
54 |
55 | def isMentioned: Boolean = text match {
56 | case Some(t: String) => t.contains(s"@${Config.bot.name}")
57 | case None => false
58 | }
59 |
60 | def isCommand: Boolean = text match {
61 | case Some(t: String) => t.startsWith("/")
62 | case None => false
63 | }
64 |
65 | def getWords(): List[String] = {
66 | var textCopy: String = text match {
67 | case Some(s: String) => s
68 | case None => return List()
69 | }
70 |
71 | message.entities match {
72 | case Some(s: Seq[MessageEntity]) => s.foreach { entity =>
73 | textCopy = textCopy
74 | .replace(textCopy.substring(entity.offset, entity.offset + entity.length), " " * entity.length)
75 | }
76 | case _ =>
77 | }
78 | textCopy
79 | .split("\\s+")
80 | .filterNot(s => s == " " || s.isEmpty || s.length > 2000)
81 | .map(_.toLowerCase)
82 | .toList
83 | }
84 |
85 | lazy val chat: ChatEntity = DB localTx { implicit session => ChatRepository.getOrCreateBy(telegramId, chatName, chatType) }
86 | lazy val telegramId: Long = message.chat.id
87 | lazy val migrationId: Long = message.migrateToChatId.getOrElse(telegramId)
88 | lazy val chatType: String = message.chat.`type` match {
89 | case Private => "chat"
90 | case Supergroup => "supergroup"
91 | case Group => "faction"
92 | case Channel => "channel"
93 | case _ => "chat"
94 | }
95 | lazy val chatName: String = message.chat.title.getOrElse(fromUsername)
96 | lazy val fromUsername: String = message.from match {
97 | case Some(u: User) => u.username.getOrElse("Unknown")
98 | case None => "Unknown"
99 | }
100 |
101 | lazy val context: List[String] = Random.shuffle(ContextRepository.getContext(chatContext, 10)).take(3)
102 | lazy val fullContext: List[String] = Random.shuffle(ContextRepository.getContext(chatContext, 50))
103 | lazy val words: List[String] = getWords()
104 | lazy val text: Option[String] = message.text
105 | lazy val chatContext: String = s"chat_context/${chat.id}"
106 | }
107 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/repositories/ChatRepositoryTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import com.pepeground.core.enums.ChatType
4 | import org.flywaydb.core.Flyway
5 | import scalikejdbc._
6 | import scalikejdbc.scalatest.AutoRollback
7 | import org.scalatest._
8 | import org.scalatest.fixture.FlatSpec
9 |
10 | import scalikejdbc.ConnectionPool
11 | import scalikejdbc.config.DBs
12 |
13 | class ChatRepositoryTest extends FlatSpec with BeforeAndAfter with AutoRollback {
14 | before {
15 | DBs.setupAll()
16 |
17 | val flyway: Flyway = new Flyway()
18 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
19 |
20 | flyway.setDataSource(dataSource)
21 | flyway.baseline()
22 | flyway.migrate()
23 | }
24 |
25 | behavior of "getList"
26 |
27 | it should "return list of chats" in { implicit session =>
28 | val chat1 = ChatRepository.create(1, "Some chat 1", "private")
29 | val chat2 = ChatRepository.create(2, "Some chat 2", "private")
30 |
31 | val chatIds = ChatRepository.getList().map(_.id)
32 | assert(chatIds == List(chat1.id, chat2.id))
33 | }
34 |
35 | behavior of "create"
36 |
37 | it should "create new chat" in { implicit session =>
38 | val newChat = ChatRepository.create(1, "Some chat", "private")
39 | assert(newChat.telegramId == 1)
40 | assert(newChat.name.get == "Some chat")
41 | assert(newChat.chatType == ChatType("private"))
42 | }
43 |
44 | behavior of "getChatById"
45 |
46 | it should "return chat by id" in { implicit session =>
47 | val newChat = ChatRepository.create(1, "Some chat", "private")
48 | val chat = ChatRepository.getChatById(newChat.id)
49 |
50 | assert(newChat.id == chat.get.id)
51 | }
52 |
53 | behavior of "getChatById"
54 |
55 | it should "return exited chat if chat already existed" in { implicit session =>
56 | val newChat = ChatRepository.create(1, "Some chat", "private")
57 | val chat = ChatRepository.getOrCreateBy(newChat.telegramId, newChat.name.get, ChatType(newChat.chatType))
58 |
59 | assert(chat.id == newChat.id)
60 | }
61 |
62 | it should "return new chat if chat not existed" in { implicit session =>
63 | val newChat = ChatRepository.create(1, "Some chat", "private")
64 | val chat = ChatRepository.getOrCreateBy(2, newChat.name.get, ChatType(newChat.chatType))
65 |
66 | assert(chat.id != newChat.id)
67 | }
68 |
69 | behavior of "updateRandomChance"
70 |
71 | it should "update random chance" in { implicit session =>
72 | val newChat = ChatRepository.create(1, "Some chat", "private")
73 | ChatRepository.updateRandomChance(newChat.id, 50)
74 |
75 | val updatedChat = ChatRepository.getChatById(newChat.id)
76 |
77 | assert(newChat.randomChance != updatedChat.get.randomChance)
78 | }
79 |
80 | behavior of "updateRepostChat"
81 |
82 | it should "update repost chat" in { implicit session =>
83 | val newChat = ChatRepository.create(1, "Some chat", "private")
84 | ChatRepository.updateRepostChat(newChat.id, "@ti_pidor")
85 |
86 | val updatedChat = ChatRepository.getChatById(newChat.id)
87 |
88 | assert(newChat.repostChatUsername != updatedChat.get.repostChatUsername)
89 | }
90 |
91 | behavior of "updateChat"
92 |
93 | it should "update chat" in { implicit session =>
94 | val newChat = ChatRepository.create(1, "Some chat", "private")
95 | ChatRepository.updateChat(newChat.id, Some("KEK"), 3)
96 | val updatedChat = ChatRepository.getChatById(newChat.id)
97 |
98 | assert(newChat.id == updatedChat.get.id)
99 | assert(newChat.name != updatedChat.get.name)
100 | assert(newChat.telegramId != updatedChat.get.telegramId)
101 | }
102 |
103 | behavior of "getByTelegramId"
104 |
105 | it should "return chat by telegram id" in { implicit session =>
106 | val newChat = ChatRepository.create(4, "Some chat", "private")
107 | val byTelegramId = ChatRepository.getByTelegramId(newChat.telegramId)
108 |
109 | assert(newChat.id == byTelegramId.get.id)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/pepeground/core/repositories/PairRepository.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import java.util.NoSuchElementException
4 |
5 | import com.pepeground.core.entities.{PairEntity, ReplyEntity}
6 | import org.joda.time.DateTime
7 | import scalikejdbc._
8 | import sqls.{count, distinct}
9 | import com.pepeground.core.support.PostgreSQLSyntaxSupport._
10 | import com.typesafe.scalalogging._
11 | import org.slf4j.LoggerFactory
12 | import scalikejdbc.jodatime.JodaParameterBinderFactory._
13 |
14 | object PairRepository {
15 | private val logger = Logger(LoggerFactory.getLogger(this.getClass))
16 | private val p = PairEntity.syntax("p")
17 | private val r = ReplyEntity.syntax("r")
18 |
19 | def hasWithWordId(wordId: Long)(implicit session: DBSession): Boolean = {
20 | withSQL {
21 | select(p.id).from(PairEntity as p).where.eq(p.firstId, wordId).or.eq(p.secondId, wordId).limit(1)
22 | }.map(rs => rs.long("id")).single.apply().isDefined
23 | }
24 |
25 | def getPairWithReplies(chatId: Long, firstIds: Option[Long], secondIds: List[Option[Long]])(implicit session: DBSession): List[PairEntity] = {
26 | var timeOffset = new DateTime
27 |
28 | timeOffset = timeOffset.minusMinutes(10)
29 |
30 | withSQL {
31 | select
32 | .from(PairEntity as p)
33 | .where.exists(select.from(ReplyEntity as r).where.eq(r.pairId, p.id))
34 | .and.lt(p.createdAt, timeOffset)
35 | .and.eq(p.chatId, chatId)
36 | .and.eq(p.firstId, firstIds)
37 | .and.in(p.secondId, secondIds)
38 | .limit(3)
39 | }.map(rs => PairEntity(p)(rs)).list().apply()
40 | }
41 |
42 | def getPairBy(chatId: Long, firstId: Option[Long], secondId: Option[Long])(implicit session: DBSession): Option[PairEntity] = {
43 | withSQL {
44 | select.from(PairEntity as p).where.eq(p.chatId, chatId).and.eq(p.firstId, firstId).and.eq(p.secondId, secondId).limit(1)
45 | }.map(rs => PairEntity(p)(rs)).single.apply()
46 | }
47 |
48 | def createPairBy(chatId: Long, firstId: Option[Long], secondId: Option[Long], updatedAt: DateTime = new DateTime())(implicit session: DBSession): PairEntity = {
49 | logger.info("Learn new pair for chat id %s".format(chatId))
50 |
51 | withSQL {
52 | insert.into(PairEntity).namedValues(
53 | PairEntity.column.chatId -> chatId,
54 | PairEntity.column.firstId -> firstId,
55 | PairEntity.column.secondId -> secondId,
56 | PairEntity.column.createdAt -> new DateTime(),
57 | PairEntity.column.updatedAt -> updatedAt
58 | ).onConflictDoNothing()
59 | }.update().apply()
60 |
61 | getPairBy(chatId, firstId, secondId) match {
62 | case Some(pair: PairEntity) => pair
63 | case None => throw new NoSuchElementException("No such pair")
64 | }
65 | }
66 |
67 | def touch(pairIds: List[Long])(implicit session: DBSession): Unit = {
68 | withSQL {
69 | update(PairEntity).set(
70 | PairEntity.column.updatedAt -> new DateTime()
71 | ).where.in(PairEntity.column.id, pairIds)
72 | }.update().apply()
73 | }
74 |
75 | def getPairsCount(chatId: Long)(implicit session: DBSession): Int = {
76 | withSQL {
77 | select(count(distinct(p.id))).from(PairEntity as p).where.eq(p.chatId, chatId)
78 | }.map(_.int(1)).single.apply().get
79 | }
80 |
81 | def removeOld(cleanupLimit: Int)(implicit session: DBSession): List[Long] = {
82 | val removeLt = new DateTime()
83 |
84 | val toRemovalIds: List[Long] = withSQL {
85 | select(p.id)
86 | .from(PairEntity as p)
87 | .where.lt(p.updatedAt, removeLt.minusMonths(3))
88 | .limit(cleanupLimit)
89 | }.map(_.long("id")).list().apply()
90 |
91 | withSQL {
92 | delete
93 | .from(PairEntity)
94 | .where
95 | .in(
96 | PairEntity.column.id,
97 | toRemovalIds
98 | )
99 | }.update().apply()
100 |
101 | toRemovalIds
102 | }
103 |
104 | def getPairOrCreateBy(chatId: Long, firstId: Option[Long], secondId: Option[Long])(implicit session: DBSession) = {
105 | getPairBy(chatId, firstId, secondId) match {
106 | case Some(p: PairEntity) => p
107 | case None => createPairBy(chatId, firstId, secondId)
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/repositories/PairRepositoryTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import org.flywaydb.core.Flyway
4 | import org.joda.time.DateTime
5 | import scalikejdbc.scalatest.AutoRollback
6 | import org.scalatest._
7 | import org.scalatest.fixture.FlatSpec
8 | import scalikejdbc.ConnectionPool
9 | import scalikejdbc.config.DBs
10 |
11 | class PairRepositoryTest extends FlatSpec with BeforeAndAfter with AutoRollback {
12 | before {
13 | DBs.setupAll()
14 |
15 | val flyway: Flyway = new Flyway()
16 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
17 |
18 | flyway.setDataSource(dataSource)
19 | flyway.baseline()
20 | flyway.migrate()
21 | }
22 |
23 | behavior of "createPairBy"
24 |
25 | it should "creates new pair" in { implicit session =>
26 | val chat = ChatRepository.create(1, "Some chat", "private")
27 | val word1 = WordRepository.create("hello")
28 | val word2 = WordRepository.create("world")
29 |
30 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
31 |
32 | assert(pair.firstId == word1)
33 | assert(pair.secondId == word2)
34 | }
35 |
36 | behavior of "getPairOrCreateBy"
37 |
38 | it should "return existed pair if pair already exists" in { implicit session =>
39 | val chat = ChatRepository.create(1, "Some chat", "private")
40 | val word1 = WordRepository.create("hello")
41 | val word2 = WordRepository.create("world")
42 |
43 | val existedPair = PairRepository.createPairBy(chat.id, word1, word2)
44 | val pair = PairRepository.getPairOrCreateBy(chat.id, word1, word2)
45 |
46 | assert(existedPair.id == pair.id)
47 | }
48 |
49 | it should "return new pair if pair does not exists" in { implicit session =>
50 | val chat = ChatRepository.create(1, "Some chat", "private")
51 | val word1 = WordRepository.create("hello")
52 | val word2 = WordRepository.create("world")
53 | val word3 = WordRepository.create("scala")
54 |
55 | val existedPair = PairRepository.createPairBy(chat.id, word1, word2)
56 | val pair = PairRepository.getPairOrCreateBy(chat.id, word1, word3)
57 |
58 | assert(existedPair.id != pair.id)
59 | }
60 |
61 | behavior of "getPairBy"
62 |
63 | it should "return pair" in { implicit session =>
64 | val chat = ChatRepository.create(1, "Some chat", "private")
65 |
66 | val word1 = WordRepository.create("hello")
67 | val word2 = WordRepository.create("world")
68 |
69 | val existedPair = PairRepository.createPairBy(chat.id, word1, word2)
70 | val pair = PairRepository.getPairBy(chat.id, word1, word2)
71 |
72 | assert(existedPair.id == pair.get.id)
73 | }
74 |
75 | behavior of "touch"
76 |
77 | it should "update updated_at column" in { implicit session =>
78 | val chat = ChatRepository.create(1, "Some chat", "private")
79 |
80 | val word1 = WordRepository.create("hello")
81 | val word2 = WordRepository.create("world")
82 |
83 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
84 |
85 | PairRepository.touch(List(pair.id))
86 |
87 | val updatedPair = PairRepository.getPairBy(pair.chatId, pair.firstId, pair.secondId)
88 |
89 | assert(updatedPair.nonEmpty)
90 | assert(updatedPair.get.updatedAt != pair.updatedAt)
91 | }
92 |
93 | behavior of "getPairsCount"
94 |
95 | it should "return pairs count" in { implicit session =>
96 | val chat = ChatRepository.create(1, "Some chat", "private")
97 |
98 | val word1 = WordRepository.create("hello")
99 | val word2 = WordRepository.create("world")
100 |
101 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
102 |
103 | assert(PairRepository.getPairsCount(chat.id) == 1)
104 | }
105 |
106 | behavior of "removeOld"
107 |
108 | it should "remove old pairs" in { implicit session =>
109 | val chat = ChatRepository.create(1, "Some chat", "private")
110 |
111 | val word1 = WordRepository.create("hello")
112 | val word2 = WordRepository.create("world")
113 |
114 | PairRepository.createPairBy(chat.id, word1, word2, new DateTime().minusMonths(3))
115 |
116 | assert(PairRepository.getPairsCount(chat.id) == 1)
117 |
118 | PairRepository.removeOld(1)
119 |
120 | assert(PairRepository.getPairsCount(chat.id) == 0)
121 |
122 | PairRepository.createPairBy(chat.id, word1, word2, new DateTime().minusMonths(1))
123 |
124 | PairRepository.removeOld(1)
125 |
126 | assert(PairRepository.getPairsCount(chat.id) == 1)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/repositories/ReplyRepositoryTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.repositories
2 |
3 | import org.flywaydb.core.Flyway
4 | import scalikejdbc.scalatest.AutoRollback
5 | import org.scalatest._
6 | import org.scalatest.fixture.FlatSpec
7 | import scalikejdbc.ConnectionPool
8 | import scalikejdbc.config.DBs
9 |
10 | class ReplyRepositoryTest extends FlatSpec with BeforeAndAfter with AutoRollback {
11 | before {
12 | DBs.setupAll()
13 |
14 | val flyway: Flyway = new Flyway()
15 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
16 |
17 | flyway.setDataSource(dataSource)
18 | flyway.baseline()
19 | flyway.migrate()
20 | }
21 |
22 | behavior of "createReplyBy"
23 |
24 | it should "creates new reply" in { implicit session =>
25 | val chat = ChatRepository.create(1, "Some chat", "private")
26 | val word1 = WordRepository.create("hello")
27 | val word2 = WordRepository.create("world")
28 | val word3 = WordRepository.create("scala")
29 |
30 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
31 |
32 | val reply = ReplyRepository.createReplyBy(pair.id, word3)
33 |
34 | assert(reply.pairId == pair.id)
35 | assert(reply.wordId == word3)
36 | }
37 |
38 | behavior of "getReplyBy"
39 |
40 | it should "return reply" in { implicit session =>
41 | val chat = ChatRepository.create(1, "Some chat", "private")
42 | val word1 = WordRepository.create("hello")
43 | val word2 = WordRepository.create("world")
44 | val word3 = WordRepository.create("scala")
45 |
46 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
47 |
48 | val reply = ReplyRepository.createReplyBy(pair.id, word3)
49 |
50 | val sameReply = ReplyRepository.getReplyBy(pair.id, word3)
51 |
52 | assert(sameReply.nonEmpty)
53 | assert(sameReply.get.id == reply.id)
54 | }
55 |
56 | behavior of "incrementReply"
57 |
58 | it should "increment counter by 1" in { implicit session =>
59 | val chat = ChatRepository.create(1, "Some chat", "private")
60 | val word1 = WordRepository.create("hello")
61 | val word2 = WordRepository.create("world")
62 | val word3 = WordRepository.create("scala")
63 |
64 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
65 |
66 | val reply = ReplyRepository.createReplyBy(pair.id, word3)
67 |
68 | ReplyRepository.incrementReply(reply.id, reply.count)
69 |
70 | val sameReply = ReplyRepository.getReplyBy(pair.id, word3)
71 |
72 | assert(sameReply.nonEmpty)
73 | assert(sameReply.get.id == reply.id)
74 | assert(sameReply.get.count > reply.count)
75 | }
76 |
77 | behavior of "repliesForPair"
78 |
79 | it should "return pair replies" in { implicit session =>
80 | val chat = ChatRepository.create(1, "Some chat", "private")
81 | val word1 = WordRepository.create("hello")
82 | val word2 = WordRepository.create("world")
83 | val word3 = WordRepository.create("scala")
84 |
85 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
86 |
87 | val reply = ReplyRepository.createReplyBy(pair.id, word3)
88 |
89 | val replies = ReplyRepository.repliesForPair(pair.id).map(_.id)
90 |
91 | assert(replies == List(reply.id))
92 | }
93 |
94 | behavior of "getReplyOrCreateBy"
95 |
96 | it should "return existed reply if reply exists" in { implicit session =>
97 | val chat = ChatRepository.create(1, "Some chat", "private")
98 | val word1 = WordRepository.create("hello")
99 | val word2 = WordRepository.create("world")
100 | val word3 = WordRepository.create("scala")
101 |
102 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
103 |
104 | val reply = ReplyRepository.createReplyBy(pair.id, word3)
105 |
106 | val sameReply = ReplyRepository.getReplyOrCreateBy(pair.id, word3)
107 |
108 | assert(reply.id == sameReply.id)
109 | }
110 |
111 | it should "create a new reply if reply does not exists" in { implicit session =>
112 | val chat = ChatRepository.create(1, "Some chat", "private")
113 | val word1 = WordRepository.create("hello")
114 | val word2 = WordRepository.create("world")
115 | val word3 = WordRepository.create("scala")
116 |
117 | val pair = PairRepository.createPairBy(chat.id, word1, word2)
118 |
119 | val reply = ReplyRepository.createReplyBy(pair.id, word2)
120 |
121 | val newReply = ReplyRepository.getReplyOrCreateBy(pair.id, word3)
122 |
123 | assert(reply.id != newReply.id)
124 | }
125 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/pepeground/core/services/LearnServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.core.services
2 |
3 | import com.pepeground.core.repositories.{ChatRepository, PairRepository, ReplyRepository, WordRepository}
4 | import org.flywaydb.core.Flyway
5 | import scalikejdbc.scalatest.AutoRollback
6 | import org.scalatest._
7 | import org.scalatest.fixture.FlatSpec
8 | import scalikejdbc.ConnectionPool
9 | import scalikejdbc.config.DBs
10 |
11 | class LearnServiceTest extends FlatSpec with BeforeAndAfter with AutoRollback {
12 | before {
13 | DBs.setupAll()
14 |
15 | val flyway: Flyway = new Flyway()
16 | val dataSource = ConnectionPool.dataSource(ConnectionPool.DEFAULT_NAME)
17 |
18 | flyway.setDataSource(dataSource)
19 | flyway.baseline()
20 | flyway.migrate()
21 | }
22 |
23 | behavior of "learn"
24 |
25 | it should "learn pairs" in { implicit session =>
26 | val newChat = ChatRepository.create(4, "Some chat", "private")
27 | val service = new LearnService(List("hello", "world", "scala"), newChat.id)
28 |
29 | service.learnPair()
30 |
31 | val word1 = WordRepository.getByWord("hello")
32 | val word2 = WordRepository.getByWord("world")
33 | val word3 = WordRepository.getByWord("scala")
34 |
35 | assert(word1.nonEmpty)
36 | assert(word2.nonEmpty)
37 | assert(word3.nonEmpty)
38 |
39 | assert(PairRepository.getPairsCount(newChat.id) == 5)
40 |
41 | val pair1 = PairRepository.getPairBy(newChat.id, None, Some(word1.get.id))
42 | val pair2 = PairRepository.getPairBy(newChat.id, Some(word1.get.id), Some(word2.get.id))
43 | val pair3 = PairRepository.getPairBy(newChat.id, Some(word2.get.id), Some(word3.get.id))
44 | val pair4 = PairRepository.getPairBy(newChat.id, Some(word3.get.id), None)
45 | val pair5 = PairRepository.getPairBy(newChat.id, None, None)
46 |
47 | assert(pair1.nonEmpty)
48 | assert(pair2.nonEmpty)
49 | assert(pair3.nonEmpty)
50 | assert(pair4.nonEmpty)
51 | assert(pair5.nonEmpty)
52 |
53 | val reply1 = ReplyRepository.getReplyBy(pair1.get.id, Some(word2.get.id))
54 | val reply2 = ReplyRepository.getReplyBy(pair2.get.id, Some(word3.get.id))
55 | val reply3 = ReplyRepository.getReplyBy(pair3.get.id, None)
56 | val reply4 = ReplyRepository.getReplyBy(pair4.get.id, None)
57 | val reply5 = ReplyRepository.getReplyBy(pair5.get.id, None)
58 |
59 | assert(reply1.nonEmpty)
60 | assert(reply2.nonEmpty)
61 | assert(reply3.nonEmpty)
62 | assert(reply4.nonEmpty)
63 | assert(reply5.nonEmpty)
64 | }
65 |
66 | it should "learn pairs once" in { implicit session =>
67 | 0 to 1 foreach { i =>
68 | val newChat = ChatRepository.create(4, "Some chat", "private")
69 | val service = new LearnService(List("hello", "world", "scala"), newChat.id)
70 |
71 | service.learnPair()
72 |
73 | val word1 = WordRepository.getByWord("hello")
74 | val word2 = WordRepository.getByWord("world")
75 | val word3 = WordRepository.getByWord("scala")
76 |
77 | assert(word1.nonEmpty)
78 | assert(word2.nonEmpty)
79 | assert(word3.nonEmpty)
80 |
81 | assert(PairRepository.getPairsCount(newChat.id) == 5)
82 |
83 | val pair1 = PairRepository.getPairBy(newChat.id, None, Some(word1.get.id))
84 | val pair2 = PairRepository.getPairBy(newChat.id, Some(word1.get.id), Some(word2.get.id))
85 | val pair3 = PairRepository.getPairBy(newChat.id, Some(word2.get.id), Some(word3.get.id))
86 | val pair4 = PairRepository.getPairBy(newChat.id, Some(word3.get.id), None)
87 | val pair5 = PairRepository.getPairBy(newChat.id, None, None)
88 |
89 | assert(pair1.nonEmpty)
90 | assert(pair2.nonEmpty)
91 | assert(pair3.nonEmpty)
92 | assert(pair4.nonEmpty)
93 | assert(pair5.nonEmpty)
94 |
95 | val reply1 = ReplyRepository.getReplyBy(pair1.get.id, Some(word2.get.id))
96 | val reply2 = ReplyRepository.getReplyBy(pair2.get.id, Some(word3.get.id))
97 | val reply3 = ReplyRepository.getReplyBy(pair3.get.id, None)
98 | val reply4 = ReplyRepository.getReplyBy(pair4.get.id, None)
99 | val reply5 = ReplyRepository.getReplyBy(pair5.get.id, None)
100 |
101 | assert(reply1.nonEmpty)
102 | assert(reply2.nonEmpty)
103 | assert(reply3.nonEmpty)
104 | assert(reply4.nonEmpty)
105 | assert(reply5.nonEmpty)
106 | }
107 | }
108 |
109 | it should "learn pairs with words which contains punctuation chars" in { implicit session =>
110 | val newChat = ChatRepository.create(4, "Some chat", "private")
111 | val service = new LearnService(List("hello", "world.", "scala"), newChat.id)
112 |
113 | service.learnPair()
114 |
115 | val word1 = WordRepository.getByWord("hello")
116 | val word2 = WordRepository.getByWord("world.")
117 | val word3 = WordRepository.getByWord("scala")
118 |
119 | assert(word1.nonEmpty)
120 | assert(word2.nonEmpty)
121 | assert(word3.nonEmpty)
122 |
123 | assert(PairRepository.getPairsCount(newChat.id) == 6)
124 |
125 | val pair1 = PairRepository.getPairBy(newChat.id, None, Some(word1.get.id))
126 | val pair2 = PairRepository.getPairBy(newChat.id, Some(word1.get.id), Some(word2.get.id))
127 | val pair3 = PairRepository.getPairBy(newChat.id, Some(word2.get.id), None)
128 | val pair4 = PairRepository.getPairBy(newChat.id, None, Some(word3.get.id))
129 | val pair5 = PairRepository.getPairBy(newChat.id, Some(word3.get.id), None)
130 | val pair6 = PairRepository.getPairBy(newChat.id, None, None)
131 |
132 | assert(pair1.nonEmpty)
133 | assert(pair2.nonEmpty)
134 | assert(pair3.nonEmpty)
135 | assert(pair4.nonEmpty)
136 | assert(pair5.nonEmpty)
137 | assert(pair6.nonEmpty)
138 |
139 | val reply1 = ReplyRepository.getReplyBy(pair1.get.id, Some(word2.get.id))
140 | val reply2 = ReplyRepository.getReplyBy(pair2.get.id, None)
141 | val reply3 = ReplyRepository.getReplyBy(pair3.get.id, Some(word3.get.id))
142 | val reply4 = ReplyRepository.getReplyBy(pair4.get.id, None)
143 | val reply5 = ReplyRepository.getReplyBy(pair5.get.id, None)
144 | val reply6 = ReplyRepository.getReplyBy(pair6.get.id, None)
145 |
146 | assert(reply1.nonEmpty)
147 | assert(reply2.nonEmpty)
148 | assert(reply3.nonEmpty)
149 | assert(reply4.nonEmpty)
150 | assert(reply5.nonEmpty)
151 | assert(reply6.nonEmpty)
152 | }
153 | }
--------------------------------------------------------------------------------
/bot/src/main/scala/com/pepeground/bot/Router.scala:
--------------------------------------------------------------------------------
1 | package com.pepeground.bot
2 |
3 | import com.bot4s.telegram.api.RequestHandler
4 | import com.bot4s.telegram.api.declarative.Commands
5 | import com.bot4s.telegram.clients.FutureSttpClient
6 | import com.pepeground.bot.handlers._
7 | import com.bot4s.telegram.future.{Polling, TelegramBot}
8 | import com.bot4s.telegram.methods._
9 | import com.bot4s.telegram.models._
10 |
11 | import sttp.client3.okhttp.OkHttpFutureBackend
12 |
13 | import scala.util.{Failure, Success, Try}
14 | import scala.concurrent.Future
15 | import com.typesafe.scalalogging._
16 | import scalikejdbc._
17 | import slogging.{LogLevel, LoggerConfig, PrintLoggerFactory}
18 |
19 | object Router extends TelegramBot with Polling with Commands[Future] {
20 | def token = Config.bot.telegramToken
21 |
22 | LoggerConfig.factory = PrintLoggerFactory()
23 | LoggerConfig.level = LogLevel.ERROR
24 |
25 |
26 | implicit val backend = OkHttpFutureBackend()
27 | override val client: RequestHandler[Future] = new FutureSttpClient(token)
28 |
29 | private val botName = Config.bot.name.toLowerCase
30 |
31 | override def receiveMessage(msg: Message): Future[Unit] = {
32 | DB localTx { implicit session =>
33 | Try(processMessage(msg)) match {
34 | case Success(_: Unit) => super.receiveMessage(msg)
35 | case Failure(e: Throwable) => throw e
36 | }
37 | }
38 | }
39 |
40 | private def processMessage(msg: Message)(implicit session: DBSession): Unit = {
41 | for (text <- msg.text) cleanCmd(text) match {
42 | case c if expectedCmd(c, "/repost") => RepostHandler(msg).call() match {
43 | case Some(s: ForwardMessage) =>
44 | request(s) onComplete {
45 | case Success(_) => makeResponse(text, SendMessage(msg.source, "reposted", replyToMessageId = Some(msg.messageId)))
46 | case Failure(_) =>
47 | }
48 |
49 | case None =>
50 | }
51 | case c if expectedCmd(c, "/get_stats") => GetStatsHandler(msg).call() match {
52 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
53 | case None =>
54 | }
55 | case c if expectedCmd(c, "/cool_story") => CoolStoryHandler(msg).call() match {
56 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s))
57 | case None =>
58 | }
59 | case c if expectedCmd(c, "/set_gab") =>
60 | val level: Option[Int] = text.split(" ").take(2) match {
61 | case Array(_, randomLevel) => Try(randomLevel.toInt).toOption match {
62 | case Some(l: Int) => Some(l)
63 | case None => None
64 | }
65 | case _ => None
66 | }
67 |
68 | level match {
69 | case Some(l: Int) =>
70 | SetGabHandler(msg).call(l) match {
71 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
72 | case None =>
73 | }
74 | case None => makeResponse(text, SendMessage(msg.source, "Wrong percent", replyToMessageId = Some(msg.messageId)))
75 | }
76 | case c if expectedCmd(c, "/get_gab") => GetGabHandler(msg).call() match {
77 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
78 | case None =>
79 | }
80 | case c if expectedCmd(c, "/ping") => PingHandler(msg).call() match {
81 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
82 | case None =>
83 | }
84 | case c if expectedCmd(c, "/set_repost_channel") => {
85 | val chatUsername: Option[String] = msg.entities match {
86 | case Some(msgEntities: Seq[MessageEntity]) => {
87 | msgEntities.find {
88 | msgEntity: MessageEntity => msgEntity.`type` == "mention"
89 | } match {
90 | case Some(msgEntity: MessageEntity) => {
91 | val offset: Int = msgEntity.offset
92 | Some(text.substring(offset, offset + msgEntity.length))
93 | }
94 | case None => None
95 | }
96 | }
97 | case _ => None
98 | }
99 |
100 | val chatMemberRequest: Option[Future[ChatMember]] = msg.from match {
101 | case Some(u: User) => Some(request(GetChatMember(msg.chat.id, u.id)))
102 | case None => None
103 | }
104 |
105 | chatUsername match {
106 | case Some(l: String) =>
107 | SetRepostChatHandler(msg, chatMemberRequest).call(l) match {
108 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
109 | case None =>
110 | }
111 | case None => makeResponse(text, SendMessage(msg.source, "No chat username", replyToMessageId = Some(msg.messageId)))
112 | }
113 | }
114 | case c if expectedCmd(c, "/get_repost_channel") => GetRepostChatHandler(msg).call() match {
115 | case Some(s: String) => makeResponse(text, SendMessage(msg.source, s, replyToMessageId = Some(msg.messageId)))
116 | case None =>
117 | }
118 | case s =>
119 | if(!s.startsWith("/")) handleMessage(msg)
120 | }
121 | }
122 |
123 | private def cleanCmd(cmd: String): String = cmd.takeWhile(s => s != ' ' ).toLowerCase
124 |
125 | private def expectedCmd(cmd: String, expected: String): Boolean = {
126 | cmd.split("@") match {
127 | case Array(c: String, name: String) => c == expected && name.toLowerCase == botName
128 | case Array(c: String) => c == expected
129 | case _ => false
130 | }
131 | }
132 |
133 | private def handleMessage(msg: Message)(implicit session: DBSession): Unit = {
134 | MessageHandler(msg).call() match {
135 | case Some(res: Either[Option[String], Option[String]]) => res match {
136 | case Left(s: Option[String]) => if(s.nonEmpty) makeResponse("message", SendMessage(msg.source, s.get, replyToMessageId = Some(msg.messageId)))
137 | case Right(s: Option[String]) => if(s.nonEmpty) makeResponse("message", SendMessage(msg.source, s.get))
138 | }
139 | case _ =>
140 | }
141 | }
142 |
143 | private def makeResponse(context: String, msg: SendMessage): Unit = {
144 | request(msg)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/kubernetes/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: Deployment
3 | apiVersion: apps/v1
4 | metadata:
5 | name: redis-master
6 | namespace: pepeground
7 | labels:
8 | app: redis
9 | spec:
10 | replicas: 1
11 | selector:
12 | matchLabels:
13 | app: redis
14 | template:
15 | metadata:
16 | creationTimestamp: null
17 | labels:
18 | app: redis
19 | spec:
20 | containers:
21 | - name: master
22 | image: 'redis:5.0'
23 | ports:
24 | - containerPort: 6379
25 | protocol: TCP
26 | resources:
27 | requests:
28 | cpu: 100m
29 | memory: 100Mi
30 | terminationMessagePath: /dev/termination-log
31 | terminationMessagePolicy: File
32 | imagePullPolicy: IfNotPresent
33 | restartPolicy: Always
34 | terminationGracePeriodSeconds: 30
35 | dnsPolicy: ClusterFirst
36 | securityContext: {}
37 | schedulerName: default-scheduler
38 | strategy:
39 | type: RollingUpdate
40 | rollingUpdate:
41 | maxUnavailable: 25%
42 | maxSurge: 25%
43 | revisionHistoryLimit: 10
44 | progressDeadlineSeconds: 600
45 | ---
46 | kind: Service
47 | apiVersion: v1
48 | metadata:
49 | name: redis
50 | namespace: pepeground
51 | spec:
52 | ports:
53 | - protocol: TCP
54 | port: 6379
55 | targetPort: 6379
56 | selector:
57 | app: redis
58 | clusterIP: 172.18.210.59
59 | type: ClusterIP
60 | sessionAffinity: None
61 | ---
62 | apiVersion: apps/v1
63 | kind: Deployment
64 | metadata:
65 | name: pepeground-bot
66 | namespace: pepeground
67 | spec:
68 | progressDeadlineSeconds: 600
69 | replicas: 1
70 | revisionHistoryLimit: 10
71 | selector:
72 | matchLabels:
73 | app: pepeground-bot
74 | strategy:
75 | rollingUpdate:
76 | maxSurge: 25%
77 | maxUnavailable: 25%
78 | type: RollingUpdate
79 | template:
80 | metadata:
81 | creationTimestamp: null
82 | labels:
83 | app: pepeground-bot
84 | spec:
85 | containers:
86 | - command:
87 | - /usr/local/openjdk-8/bin/java
88 | - -jar
89 | - -server
90 | - -Dconfig.file=/usr/local/etc/application.conf
91 | - /bot.jar
92 | - bot
93 | image: pepeground/pepeground-bot
94 | ports:
95 | - name: web
96 | containerPort: 8080
97 | protocol: TCP
98 | imagePullPolicy: Always
99 | name: pepeground-bot
100 | resources: {}
101 | terminationMessagePath: /dev/termination-log
102 | terminationMessagePolicy: File
103 | volumeMounts:
104 | - mountPath: /usr/local/etc
105 | name: pepeground-conf
106 | dnsPolicy: ClusterFirst
107 | restartPolicy: Always
108 | schedulerName: default-scheduler
109 | securityContext: {}
110 | terminationGracePeriodSeconds: 30
111 | volumes:
112 | - configMap:
113 | defaultMode: 420
114 | name: pepeground-conf
115 | name: pepeground-conf
116 | ---
117 | apiVersion: apps/v1
118 | kind: Deployment
119 | metadata:
120 | name: pepeground-learn
121 | namespace: pepeground
122 | spec:
123 | progressDeadlineSeconds: 600
124 | replicas: 1
125 | revisionHistoryLimit: 10
126 | selector:
127 | matchLabels:
128 | app: pepeground-learn
129 | template:
130 | metadata:
131 | creationTimestamp: null
132 | labels:
133 | app: pepeground-learn
134 | spec:
135 | containers:
136 | - command:
137 | - /usr/local/openjdk-8/bin/java
138 | - -jar
139 | - -server
140 | - -Dconfig.file=/usr/local/etc/application.conf
141 | - /bot.jar
142 | - learn
143 | image: pepeground/pepeground-bot
144 | ports:
145 | - name: web
146 | containerPort: 8080
147 | protocol: TCP
148 | imagePullPolicy: Always
149 | name: pepeground-learn
150 | resources: {}
151 | terminationMessagePath: /dev/termination-log
152 | terminationMessagePolicy: File
153 | volumeMounts:
154 | - mountPath: /usr/local/etc
155 | name: pepeground-conf
156 | resources:
157 | requests:
158 | cpu: "250m"
159 | dnsPolicy: ClusterFirst
160 | restartPolicy: Always
161 | schedulerName: default-scheduler
162 | securityContext: {}
163 | terminationGracePeriodSeconds: 30
164 | volumes:
165 | - configMap:
166 | defaultMode: 420
167 | name: pepeground-conf
168 | name: pepeground-conf
169 | ---
170 | apiVersion: apps/v1
171 | kind: Deployment
172 | metadata:
173 | name: pepeground-cleanup
174 | namespace: pepeground
175 | spec:
176 | progressDeadlineSeconds: 600
177 | replicas: 1
178 | revisionHistoryLimit: 10
179 | selector:
180 | matchLabels:
181 | app: pepeground-cleanup
182 | template:
183 | metadata:
184 | creationTimestamp: null
185 | labels:
186 | app: pepeground-cleanup
187 | spec:
188 | containers:
189 | - command:
190 | - /usr/local/openjdk-8/bin/java
191 | - -jar
192 | - -server
193 | - -Dconfig.file=/usr/local/etc/application.conf
194 | - /bot.jar
195 | - cleanup
196 | image: pepeground/pepeground-bot
197 | ports:
198 | - name: web
199 | containerPort: 8080
200 | protocol: TCP
201 | imagePullPolicy: Always
202 | name: pepeground-cleanup
203 | resources: {}
204 | terminationMessagePath: /dev/termination-log
205 | terminationMessagePolicy: File
206 | volumeMounts:
207 | - mountPath: /usr/local/etc
208 | name: pepeground-conf
209 | dnsPolicy: ClusterFirst
210 | restartPolicy: Always
211 | schedulerName: default-scheduler
212 | securityContext: {}
213 | terminationGracePeriodSeconds: 30
214 | volumes:
215 | - configMap:
216 | defaultMode: 420
217 | name: pepeground-conf
218 | name: pepeground-conf
219 | ---
220 | kind: Service
221 | apiVersion: v1
222 | metadata:
223 | name: prometheus-operator
224 | namespace: pepeground
225 | labels:
226 | app: pepeground-bot
227 | spec:
228 | ports:
229 | - name: web
230 | protocol: TCP
231 | port: 8080
232 | targetPort: web
233 | selector:
234 | app: pepeground-bot
235 | type: ClusterIP
236 | ---
237 | apiVersion: monitoring.coreos.com/v1
238 | kind: ServiceMonitor
239 | metadata:
240 | name: pepeground-bot
241 | namespace: pepeground
242 | spec:
243 | selector:
244 | matchLabels:
245 | app: pepeground-bot
246 | jobLabel: jobLabel
247 | namespaceSelector:
248 | matchNames:
249 | - pepeground
250 | endpoints:
251 | - port: web
252 |
--------------------------------------------------------------------------------