Skip to content

Commit

Permalink
- implemented #8(1);
Browse files Browse the repository at this point in the history
- started #8(2);
- updated some versions;
- refactoring;
  • Loading branch information
y9san9 committed Apr 20, 2021
1 parent 7f1fe7e commit c7f5c48
Show file tree
Hide file tree
Showing 45 changed files with 798 additions and 85 deletions.
24 changes: 19 additions & 5 deletions bot/src/main/kotlin/me/y9san9/prizebot/Prizebot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.send.sendMessage
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage
import dev.inmo.tgbotapi.types.message.abstracts.PrivateContentMessage
import dev.inmo.tgbotapi.types.message.abstracts.PublicContentMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
Expand All @@ -16,7 +18,7 @@ import me.y9san9.prizebot.actors.giveaway.AutoRaffleActor
import me.y9san9.prizebot.database.giveaways_active_messages_storage.GiveawaysActiveMessagesStorage
import me.y9san9.prizebot.database.giveaways_storage.GiveawaysStorage
import me.y9san9.prizebot.database.language_codes_storage.LanguageCodesStorage
import me.y9san9.prizebot.database.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.database.linked_channels_storage.LinkedChannelsStorage
import me.y9san9.prizebot.database.states_storage.PrizebotFSMStorage
import me.y9san9.prizebot.handlers.callback_queries.CallbackQueryHandler
import me.y9san9.prizebot.handlers.choosen_inline_result.ChosenInlineResultHandler
Expand All @@ -26,9 +28,13 @@ import me.y9san9.prizebot.handlers.private_messages.fsm.states.MainState
import me.y9san9.prizebot.handlers.private_messages.fsm.states.statesSerializers
import me.y9san9.prizebot.di.PrizebotDI
import me.y9san9.prizebot.extensions.telegram.PrizebotPrivateMessageUpdate
import me.y9san9.prizebot.handlers.channel_posts.ChannelPostsHandler
import me.y9san9.prizebot.handlers.private_messages.fsm.states.giveaway.*
import me.y9san9.prizebot.handlers.private_messages.fsm.states.giveaway.conditions.InvitationsCountInputState
import me.y9san9.prizebot.handlers.private_messages.fsm.states.giveaway.conditions.SubscriptionChannelInputState
import me.y9san9.telegram.updates.CallbackQueryUpdate
import me.y9san9.telegram.updates.ChosenInlineResultUpdate
import me.y9san9.telegram.updates.ChannelPostUpdate
import me.y9san9.telegram.updates.InlineQueryUpdate
import org.jetbrains.exposed.sql.Database

Expand All @@ -53,15 +59,21 @@ class Prizebot (
val di = PrizebotDI (
giveawaysStorage = GiveawaysStorage(database),
giveawaysActiveMessagesStorage = GiveawaysActiveMessagesStorage(database),
languageCodesStorage = LanguageCodesStorage(database)
languageCodesStorage = LanguageCodesStorage(database),
linkedChannelsStorage = LinkedChannelsStorage(database)
)
scheduleRaffles(bot, di)

val messages = messageFlow
val privateMessages = messageFlow
.mapNotNull { it.data as? PrivateContentMessage<*> }
.map { PrizebotPrivateMessageUpdate(bot, di, message = it) }

createFSM(events = messages)
createFSM(events = privateMessages)

channelPostFlow
.mapNotNull { it.data as? ChannelContentMessage<*> }
.map { ChannelPostUpdate(bot, di, message = it) }
.subscribeSafely(scope, ::logException, ChannelPostsHandler::handle)

inlineQueryFlow
.map { InlineQueryUpdate(bot, di, query = it) }
Expand All @@ -86,7 +98,9 @@ class Prizebot (
initial = MainState,
TitleInputState, ParticipateTextInputState,
RaffleDateInputState, TimezoneInputState,
CustomTimezoneInputState, WinnersCountInputState
CustomTimezoneInputState, WinnersCountInputState,
ConditionInputState, InvitationsCountInputState,
SubscriptionChannelInputState
),
storage = PrizebotFSMStorage(database, statesSerializers),
scope = scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import me.y9san9.prizebot.database.giveaways_active_messages_storage.GiveawaysActiveMessagesStorage
import me.y9san9.prizebot.database.giveaways_storage.ActiveGiveaway
import me.y9san9.prizebot.database.giveaways_storage.Giveaway
import me.y9san9.prizebot.database.giveaways_storage.GiveawaysStorage
import me.y9san9.prizebot.database.language_codes_storage.LanguageCodesStorage
import me.y9san9.prizebot.database.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.actors.telegram.updater.GiveawayActiveMessagesUpdater
import me.y9san9.prizebot.resources.locales.Locale
import me.y9san9.telegram.updates.hierarchies.DIBotUpdate
Expand Down Expand Up @@ -52,7 +50,7 @@ object AutoRaffleActor : CoroutineScope {
scheduledMutex.withLock {
if (giveaway.id in scheduled && di.getGiveawayById(giveaway.id) != null) {
scheduled.remove(giveaway.id)
handleRaffleResult(bot, di, giveaway, RaffleActor.raffle(giveaway))
handleRaffleResult(bot, di, giveaway, RaffleActor.raffle(bot, giveaway))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package me.y9san9.prizebot.actors.giveaway

import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.exceptions.RequestException
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.ChatMember.MemberChatMember
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.chat.abstracts.UsernameChat
import me.y9san9.prizebot.database.giveaways_storage.ActiveGiveaway
import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.Condition
import me.y9san9.prizebot.extensions.list.on


sealed class CheckConditionsResult {
object GiveawayInvalid : CheckConditionsResult()
object NotSubscribedToConditions : CheckConditionsResult()
class FriendsAreNotInvited(val invitedCount: Int, val requiredCount: Int) : CheckConditionsResult()
object Success : CheckConditionsResult()
}

object ConditionsChecker {
suspend fun check(bot: TelegramBot, participantId: Long, giveaway: ActiveGiveaway): CheckConditionsResult {
giveaway.conditions.list
.on { condition: Condition.Subscription ->
val channel = try {
bot.getChat(ChatId(condition.channelId)) as? UsernameChat
} catch (_: RequestException) { null }
?: return CheckConditionsResult.GiveawayInvalid

if(channel.username?.username != condition.channelUsername)
return CheckConditionsResult.GiveawayInvalid

try {
bot.getChatMember(channel.id, UserId(participantId))
.takeIf { it is MemberChatMember }
} catch (_: RequestException) { null }
?: return CheckConditionsResult.NotSubscribedToConditions

}.on { _: Condition.Invitations -> TODO() }

return CheckConditionsResult.Success
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import me.y9san9.fsm.FSMStateResult
import me.y9san9.fsm.stateResult
import me.y9san9.prizebot.database.giveaways_storage.WinnersCount
import me.y9san9.prizebot.actors.telegram.sender.GiveawayCreatedSender
import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.GiveawayConditions
import me.y9san9.prizebot.extensions.telegram.PrizebotPrivateMessageUpdate
import me.y9san9.prizebot.handlers.private_messages.fsm.states.MainState
import java.time.OffsetDateTime
Expand All @@ -15,13 +16,14 @@ object CreateGiveawayActor {
title: String,
participateText: String,
raffleDate: OffsetDateTime?,
winnersCount: WinnersCount
winnersCount: WinnersCount,
conditions: GiveawayConditions
): FSMStateResult<*> {

val giveaway = update.di.saveGiveaway (
update.chatId, title, participateText,
languageCode = update.di.getLanguageCode(update.chatId) ?: update.languageCode,
raffleDate, winnersCount
raffleDate, winnersCount, conditions
)

GiveawayCreatedSender.send(update, giveaway)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
package me.y9san9.prizebot.actors.giveaway

import dev.inmo.tgbotapi.bot.TelegramBot
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import me.y9san9.prizebot.database.giveaways_storage.ActiveGiveaway
import me.y9san9.prizebot.database.giveaways_storage.GiveawaysStorage
import me.y9san9.random.extensions.shuffledRandomOrg


object RaffleActor {
suspend fun raffle (
bot: TelegramBot,
giveaway: ActiveGiveaway
): Boolean {
val winnerIds = chooseWinners(giveaway) ?: return false
val winnerIds = chooseWinners(bot, giveaway) ?: return false
giveaway.finish(winnerIds)
return true
}

private suspend fun chooseWinners (
bot: TelegramBot,
giveaway: ActiveGiveaway,
) = giveaway.participants
.shuffledRandomOrg()
.asFlow()
.filter { userId -> ConditionsChecker.check(bot, userId, giveaway) is CheckConditionsResult.Success }
.take(giveaway.winnersCount.value)
.toList()
.takeIf { it.size == giveaway.winnersCount.value }
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package me.y9san9.prizebot.database.giveaways_storage

import kotlinx.serialization.Serializable
import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.ConditionsStorage
import me.y9san9.prizebot.database.giveaways_storage.giveaways_patch_storage.GiveawaysPatchStorage
import me.y9san9.prizebot.database.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.database.winners_storage.WinnersStorage
import me.y9san9.prizebot.database.giveaways_storage.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.database.giveaways_storage.winners_storage.WinnersStorage
import me.y9san9.prizebot.resources.locales.Locale
import java.time.OffsetDateTime


@Serializable
inline class WinnersCount(val value: Int) {
init {
require(value in 1..50_000) { "Winners count is out of range" }
Expand Down Expand Up @@ -36,6 +39,12 @@ sealed class Giveaway {
internal abstract val giveawaysPatchStorage: GiveawaysPatchStorage

fun delete() = giveawaysPatchStorage.deleteGiveaway(id)

/* Conditions composition */

internal abstract val conditionsStorage: ConditionsStorage

val conditions by lazy { conditionsStorage.loadConditions(id) }
}

data class ActiveGiveaway internal constructor (
Expand All @@ -47,6 +56,7 @@ data class ActiveGiveaway internal constructor (
override val raffleDate: OffsetDateTime?,
override val participantsStorage: ParticipantsStorage,
override val giveawaysPatchStorage: GiveawaysPatchStorage,
override val conditionsStorage: ConditionsStorage,
val winnersCount: WinnersCount
) : Giveaway() {
fun removeRaffleDate() = giveawaysPatchStorage.removeRaffleDate(id)
Expand All @@ -62,6 +72,7 @@ data class FinishedGiveaway internal constructor (
override val raffleDate: OffsetDateTime?,
override val participantsStorage: ParticipantsStorage,
override val giveawaysPatchStorage: GiveawaysPatchStorage,
override val conditionsStorage: ConditionsStorage,
private val winnersStorage: WinnersStorage
) : Giveaway() {
val winnerIds by lazy { winnersStorage.getWinners(id) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.y9san9.prizebot.database.giveaways_storage

import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.GiveawayConditions
import org.jetbrains.exposed.sql.Database
import java.time.OffsetDateTime

Expand All @@ -14,7 +15,8 @@ interface GiveawaysStorage {
participateButton: String,
languageCode: String?,
raffleDate: OffsetDateTime?,
winnersCount: WinnersCount
winnersCount: WinnersCount,
conditions: GiveawayConditions
): ActiveGiveaway
fun getUserGiveaways(ownerId: Long, count: Int = 20, offset: Long = 0): List<Giveaway>
fun getAllGiveaways(): List<Giveaway>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import me.y9san9.prizebot.database.giveaways_storage.Giveaways.GIVEAWAY_PARTICIP
import me.y9san9.prizebot.database.giveaways_storage.Giveaways.GIVEAWAY_RAFFLE_DATE
import me.y9san9.prizebot.database.giveaways_storage.Giveaways.GIVEAWAY_TITLE
import me.y9san9.prizebot.database.giveaways_storage.Giveaways.GIVEAWAY_WINNERS_COUNT
import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.ConditionsStorage
import me.y9san9.prizebot.database.giveaways_storage.conditions_storage.GiveawayConditions
import me.y9san9.prizebot.database.giveaways_storage.giveaways_patch_storage.GiveawaysPatchStorage
import me.y9san9.prizebot.database.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.database.winners_storage.WinnersStorage
import me.y9san9.prizebot.extensions.any.unit
import me.y9san9.prizebot.database.giveaways_storage.participants_storage.ParticipantsStorage
import me.y9san9.prizebot.database.giveaways_storage.winners_storage.WinnersStorage
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.OffsetDateTime
Expand All @@ -22,6 +23,7 @@ internal class TableGiveawaysStorage (

private val participantsStorage = ParticipantsStorage(database)
private val winnersStorage = WinnersStorage(database)
private val conditionsStorage = ConditionsStorage(database)
private val giveawaysPatchStorage = GiveawaysPatchStorage(database, winnersStorage)

init {
Expand All @@ -40,7 +42,8 @@ internal class TableGiveawaysStorage (
participateButton: String,
languageCode: String?,
raffleDate: OffsetDateTime?,
winnersCount: WinnersCount
winnersCount: WinnersCount,
conditions: GiveawayConditions
) = transaction(database) {
Giveaways.insert {
it[GIVEAWAY_OWNER_ID] = ownerId
Expand All @@ -51,11 +54,12 @@ internal class TableGiveawaysStorage (
it[GIVEAWAY_WINNERS_COUNT] = winnersCount.value
}
}.let { data ->
conditionsStorage.addConditions(data[GIVEAWAY_ID], conditions)
ActiveGiveaway (
id = data[GIVEAWAY_ID],
ownerId, title, participateButton,
languageCode, raffleDate, participantsStorage,
giveawaysPatchStorage, winnersCount
giveawaysPatchStorage, conditionsStorage, winnersCount
)
}

Expand All @@ -79,7 +83,7 @@ internal class TableGiveawaysStorage (
this[GIVEAWAY_PARTICIPATE_BUTTON],
this[GIVEAWAY_LANGUAGE_CODE],
this[GIVEAWAY_RAFFLE_DATE]?.let(OffsetDateTime::parse),
participantsStorage, giveawaysPatchStorage,
participantsStorage, giveawaysPatchStorage, conditionsStorage,
WinnersCount(this[GIVEAWAY_WINNERS_COUNT])
)
else
Expand All @@ -90,7 +94,8 @@ internal class TableGiveawaysStorage (
this[GIVEAWAY_PARTICIPATE_BUTTON],
this[GIVEAWAY_LANGUAGE_CODE],
this[GIVEAWAY_RAFFLE_DATE]?.let(OffsetDateTime::parse),
participantsStorage, giveawaysPatchStorage, winnersStorage
participantsStorage, giveawaysPatchStorage,
conditionsStorage, winnersStorage
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@file:Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")

package me.y9san9.prizebot.database.giveaways_storage.conditions_storage

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


@Serializable
inline class PositiveInt internal constructor(val int: Int) {
init {
require(int > 0) { "The value must be positive" }
}
}

fun Int.wrapPositiveInt() = PositiveInt(this)


@Serializable
sealed class Condition {
@Serializable
@SerialName("subscription")
data class Subscription(
val channelId: Long,
val channelUsername: String
) : Condition()

@Serializable
@SerialName("invitations")
data class Invitations (
val count: PositiveInt
) : Condition()
}


@Serializable
// It is not inline because of serialization bug :(
/*inline*/ class GiveawayConditions internal constructor (
val list: List<Condition>
) {
init {
require(list.count { it is Condition.Invitations } <= 1) {
"Only one invitations condition allowed"
}
require(list.filterIsInstance<Condition.Invitations>().isEmpty()
|| list.filterIsInstance<Condition.Subscription>().isNotEmpty()) {
"You should add at least one channel in case you want to invite friends"
}
}
}

fun List<Condition>.wrapGiveawayConditions() = GiveawayConditions(list = this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.y9san9.prizebot.database.giveaways_storage.conditions_storage

import org.jetbrains.exposed.sql.Database


internal fun ConditionsStorage(database: Database): ConditionsStorage = TableConditionsStorage(database)

internal interface ConditionsStorage {
fun addConditions(giveawayId: Long, conditions: GiveawayConditions)
fun loadConditions(giveawayId: Long): GiveawayConditions
}
Loading

0 comments on commit c7f5c48

Please sign in to comment.