Skip to content

Commit

Permalink
Add shoutout command
Browse files Browse the repository at this point in the history
  • Loading branch information
flex3r committed Jul 10, 2023
1 parent ce6639c commit 3666f70
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class AuthApiClient @Inject constructor(private val authApi: AuthApi, private va
"moderator:manage:chat_messages",
"moderator:manage:chat_settings",
"moderator:manage:shield_mode",
"moderator:manage:shoutouts",
"moderator:read:chatters",
"moderator:read:followers",
"user:manage:blocked_users",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,13 @@ class HelixApi(private val ktorClient: HttpClient, private val dankChatPreferenc
parameter("broadcaster_id", broadcasterUserId)
contentType(ContentType.Application.Json)
}

suspend fun postShoutout(broadcasterUserId: UserId, targetUserId: UserId, moderatorUserId: UserId): HttpResponse? = ktorClient.post("chat/shoutouts") {
val oAuth = dankChatPreferenceStore.oAuthKey?.withoutOAuthPrefix ?: return null
bearerAuth(oAuth)
parameter("from_broadcaster_id", broadcasterUserId)
parameter("to_broadcaster_id", targetUserId)
parameter("moderator_id", moderatorUserId)
contentType(ContentType.Application.Json)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ class HelixApiClient @Inject constructor(private val helixApi: HelixApi, private
.data
}

suspend fun postShoutout(broadcastUserId: UserId, targetUserId: UserId, moderatorUserId: UserId): Result<Unit> = runCatching {
helixApi.postShoutout(broadcastUserId, targetUserId, moderatorUserId)
.throwHelixApiErrorOnFailure()
}

private suspend inline fun <reified T> pageUntil(amountToFetch: Int, request: (cursor: String?) -> HttpResponse?): List<T> {
val initialPage = request(null)
.throwHelixApiErrorOnFailure()
Expand Down Expand Up @@ -252,9 +257,11 @@ class HelixApiClient @Inject constructor(private val helixApi: HelixApi, private
message.startsWith(USER_MAY_NOT_BE_BANNED_ERROR, ignoreCase = true) -> HelixError.TargetCannotBeBanned
message.startsWith(USER_NOT_BANNED_ERROR, ignoreCase = true) -> HelixError.TargetNotBanned
message.startsWith(INVALID_COLOR_ERROR, ignoreCase = true) -> HelixError.InvalidColor
message.startsWith(BROADCASTER_NOT_LIVE_ERROR, ignoreCase = true) -> HelixError.BroadcasterNotStreaming
message.startsWith(BROADCASTER_NOT_LIVE_ERROR, ignoreCase = true) -> HelixError.CommercialNotStreaming
message.startsWith(MISSING_REQUIRED_PARAM_ERROR, ignoreCase = true) -> HelixError.MissingLengthParameter
message.startsWith(RAID_SELF_ERROR, ignoreCase = true) -> HelixError.RaidSelf
message.startsWith(SHOUTOUT_SELF_ERROR, ignoreCase = true) -> HelixError.ShoutoutSelf
message.startsWith(SHOUTOUT_NOT_LIVE_ERROR, ignoreCase = true) -> HelixError.ShoutoutTargetNotStreaming
message.contains(NOT_IN_RANGE_ERROR, ignoreCase = true) -> {
val match = INVALID_RANGE_REGEX.find(message)?.groupValues
val start = match?.getOrNull(1)?.toIntOrNull()
Expand Down Expand Up @@ -328,6 +335,8 @@ class HelixApiClient @Inject constructor(private val helixApi: HelixApi, private
private const val MISSING_REQUIRED_PARAM_ERROR = "Missing required parameter"
private const val RAID_SELF_ERROR = "The IDs in from_broadcaster_id and to_broadcaster_id cannot be the same."
private const val NOT_IN_RANGE_ERROR = "must be in the range"
private const val SHOUTOUT_SELF_ERROR = "The broadcaster may not give themselves a Shoutout."
private const val SHOUTOUT_NOT_LIVE_ERROR = "The broadcaster is not streaming live or does not have one or more viewers."
private val INVALID_RANGE_REGEX = """(\d+) through (\d+)""".toRegex()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ sealed interface HelixError {
data object InvalidColor : HelixError
data class MarkerError(val message: String?) : HelixError
data object CommercialRateLimited : HelixError
data object BroadcasterNotStreaming : HelixError
data object CommercialNotStreaming : HelixError
data object MissingLengthParameter : HelixError
data object RaidSelf : HelixError
data object NoRaidPending : HelixError
data class NotInRange(val validRange: IntRange?) : HelixError
data object Forwarded : HelixError
data object ShoutoutSelf : HelixError
data object ShoutoutTargetNotStreaming : HelixError
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ enum class TwitchCommand(val trigger: String) {
AnnounceOrange(trigger = "announceorange"),
AnnouncePurple(trigger = "announcepurple"),
Ban(trigger = "ban"),
Unban(trigger = "unban"),
Clear(trigger = "clear"),
Color(trigger = "color"),
Commercial(trigger = "commercial"),
Expand All @@ -18,24 +17,26 @@ enum class TwitchCommand(val trigger: String) {
Followers(trigger = "followers"),
FollowersOff(trigger = "followersoff"),
Marker(trigger = "marker"),
Mods(trigger = "mods"),
Mod(trigger = "mod"),
Unmod(trigger = "unmod"),
Mods(trigger = "mods"),
R9kBeta(trigger = "r9kbeta"),
R9kBetaOff(trigger = "r9kbetaoff"),
Raid(trigger = "raid"),
Unraid(trigger = "unraid"),
Shoutout(trigger = "shoutout"),
Slow(trigger = "slow"),
SlowOff(trigger = "slowoff"),
Subscribers(trigger = "subscribers"),
SubscribersOff(trigger = "subscribersoff"),
Timeout(trigger = "timeout"),
Untimeout(trigger = "untimeout"),
Unban(trigger = "unban"),
UniqueChat(trigger = "uniquechat"),
UniqueChatOff(trigger = "uniquechatoff"),
Vips(trigger = "vips"),
Vip(trigger = "vip"),
Unmod(trigger = "unmod"),
Unraid(trigger = "unraid"),
Untimeout(trigger = "untimeout"),
Unvip(trigger = "unvip"),
Vip(trigger = "vip"),
Vips(trigger = "vips"),
Whisper(trigger = "w");

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ class TwitchCommandRepository @Inject constructor(
TwitchCommand.Vip -> addVip(command, context)
TwitchCommand.Vips -> getVips(command, context)
TwitchCommand.Whisper -> sendWhisper(command, currentUserId, context.trigger, context.args)
TwitchCommand.Shoutout -> sendShoutout(command, currentUserId, context)
}
}

suspend fun sendWhisper(command: TwitchCommand, currentUserId: UserId, trigger: String, args: List<String>): CommandResult {
if (args.size < 2 || args[0].isBlank() || args[1].isBlank()) {
return CommandResult.AcceptedTwitchCommand(command, response = "Usage: $trigger <username> <message>")
return CommandResult.AcceptedTwitchCommand(command, response = "Usage: $trigger <username> <message>.")
}

val targetName = args[0]
Expand Down Expand Up @@ -608,38 +609,60 @@ class TwitchCommandRepository @Inject constructor(
)
}

private suspend fun sendShoutout(command: TwitchCommand, currentUserId: UserId, context: CommandContext): CommandResult {
val args = context.args
if (args.isEmpty() || args.first().isBlank()) {
return CommandResult.AcceptedTwitchCommand(command, response = "Usage: ${context.trigger} <username> - Sends a shoutout to the specified Twitch user.")
}

val target = helixApiClient.getUserByName(args.first().toUserName()).getOrElse {
return CommandResult.AcceptedTwitchCommand(command, response = "No user matching that username.")
}

val result = helixApiClient.postShoutout(context.channelId, target.id, currentUserId)
return result.fold(
onSuccess = { CommandResult.AcceptedTwitchCommand(command, response = "Sent shoutout to ${target.displayName}") },
onFailure = {
val response = "Failed to send shoutout - ${it.toErrorMessage(command)}"
CommandResult.AcceptedTwitchCommand(command, response)
}
)
}

private fun Throwable.toErrorMessage(command: TwitchCommand, targetUser: UserName? = null, formatRange: ((IntRange) -> String)? = null): String {
Log.v(TAG, "Command failed: $this")
if (this !is HelixApiException) {
return GENERIC_ERROR_MESSAGE
}

return when (error) {
HelixError.UserNotAuthorized -> "You don't have permission to perform that action."
HelixError.Forwarded -> message ?: GENERIC_ERROR_MESSAGE
HelixError.MissingScopes -> "Missing required scope. Re-login with your account and try again."
HelixError.NotLoggedIn -> "Missing login credentials. Re-login with your account and try again."
HelixError.WhisperSelf -> "You cannot whisper yourself."
HelixError.NoVerifiedPhone -> "Due to Twitch restrictions, you are now required to have a verified phone number to send whispers. You can add a phone number in Twitch settings. https://www.twitch.tv/settings/security"
HelixError.RecipientBlockedUser -> "The recipient doesn't allow whispers from strangers or you directly."
HelixError.RateLimited -> "You are being rate-limited by Twitch. Try again in a few seconds."
HelixError.WhisperRateLimited -> "You may only whisper a maximum of 40 unique recipients per day. Within the per day limit, you may whisper a maximum of 3 whispers per second and a maximum of 100 whispers per minute."
HelixError.BroadcasterTokenRequired -> "Due to Twitch restrictions, this command can only be used by the broadcaster. Please use the Twitch website instead."
HelixError.TargetAlreadyModded -> "${targetUser ?: "The target user"} is already a moderator of this channel."
HelixError.TargetIsVip -> "${targetUser ?: "The target user"} is currently a VIP, /unvip them and retry this command."
HelixError.TargetNotModded -> "${targetUser ?: "The target user"} is not a moderator of this channel."
HelixError.TargetNotBanned -> "${targetUser ?: "The target user"} is not banned from this channel."
HelixError.TargetAlreadyBanned -> "${targetUser ?: "The target user"} is already banned in this channel."
HelixError.TargetCannotBeBanned -> "You cannot ${command.trigger} ${targetUser ?: "this user"}."
HelixError.ConflictingBanOperation -> "There was a conflicting ban operation on this user. Please try again."
HelixError.InvalidColor -> "Color must be one of Twitch's supported colors (${VALID_HELIX_COLORS.joinToString()}) or a hex code (#000000) if you have Turbo or Prime."
is HelixError.MarkerError -> error.message ?: GENERIC_ERROR_MESSAGE
HelixError.BroadcasterNotStreaming -> "You must be streaming live to run commercials."
HelixError.CommercialRateLimited -> "You must wait until your cool-down period expires before you can run another commercial."
HelixError.MissingLengthParameter -> "Command must include a desired commercial break length that is greater than zero."
HelixError.NoRaidPending -> "You don't have an active raid."
HelixError.RaidSelf -> "A channel cannot raid itself."
is HelixError.NotInRange -> {
HelixError.UserNotAuthorized -> "You don't have permission to perform that action."
HelixError.Forwarded -> message ?: GENERIC_ERROR_MESSAGE
HelixError.MissingScopes -> "Missing required scope. Re-login with your account and try again."
HelixError.NotLoggedIn -> "Missing login credentials. Re-login with your account and try again."
HelixError.WhisperSelf -> "You cannot whisper yourself."
HelixError.NoVerifiedPhone -> "Due to Twitch restrictions, you are now required to have a verified phone number to send whispers. You can add a phone number in Twitch settings. https://www.twitch.tv/settings/security"
HelixError.RecipientBlockedUser -> "The recipient doesn't allow whispers from strangers or you directly."
HelixError.RateLimited -> "You are being rate-limited by Twitch. Try again in a few seconds."
HelixError.WhisperRateLimited -> "You may only whisper a maximum of 40 unique recipients per day. Within the per day limit, you may whisper a maximum of 3 whispers per second and a maximum of 100 whispers per minute."
HelixError.BroadcasterTokenRequired -> "Due to Twitch restrictions, this command can only be used by the broadcaster. Please use the Twitch website instead."
HelixError.TargetAlreadyModded -> "${targetUser ?: "The target user"} is already a moderator of this channel."
HelixError.TargetIsVip -> "${targetUser ?: "The target user"} is currently a VIP, /unvip them and retry this command."
HelixError.TargetNotModded -> "${targetUser ?: "The target user"} is not a moderator of this channel."
HelixError.TargetNotBanned -> "${targetUser ?: "The target user"} is not banned from this channel."
HelixError.TargetAlreadyBanned -> "${targetUser ?: "The target user"} is already banned in this channel."
HelixError.TargetCannotBeBanned -> "You cannot ${command.trigger} ${targetUser ?: "this user"}."
HelixError.ConflictingBanOperation -> "There was a conflicting ban operation on this user. Please try again."
HelixError.InvalidColor -> "Color must be one of Twitch's supported colors (${VALID_HELIX_COLORS.joinToString()}) or a hex code (#000000) if you have Turbo or Prime."
is HelixError.MarkerError -> error.message ?: GENERIC_ERROR_MESSAGE
HelixError.CommercialNotStreaming -> "You must be streaming live to run commercials."
HelixError.CommercialRateLimited -> "You must wait until your cool-down period expires before you can run another commercial."
HelixError.MissingLengthParameter -> "Command must include a desired commercial break length that is greater than zero."
HelixError.NoRaidPending -> "You don't have an active raid."
HelixError.RaidSelf -> "A channel cannot raid itself."
HelixError.ShoutoutSelf -> "The broadcaster may not give themselves a Shoutout."
HelixError.ShoutoutTargetNotStreaming -> "The broadcaster is not streaming live or does not have one or more viewers."
is HelixError.NotInRange -> {
val range = error.validRange
when (val formatted = range?.let { formatRange?.invoke(it) }) {
null -> message ?: GENERIC_ERROR_MESSAGE
Expand All @@ -648,7 +671,7 @@ class TwitchCommandRepository @Inject constructor(

}

HelixError.Unknown -> GENERIC_ERROR_MESSAGE
HelixError.Unknown -> GENERIC_ERROR_MESSAGE
}
}

Expand Down

0 comments on commit 3666f70

Please sign in to comment.