diff --git a/manifest.toml b/manifest.toml index 832bea0..f5c4d68 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,7 +3,7 @@ packages = [ { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, - { name = "dot_env", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "18F51CAFE99F6E3F2B10CF5DBACE63505DB6DC1FA69AFA0105756ACC8201C3A2" }, + { name = "dot_env", version = "0.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "AF5C972D6129F67AF3BB00134AB2808D37111A8D61686CFA86F3ADF652548982" }, { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, diff --git a/src/glitch/api/eventsub.gleam b/src/glitch/api/eventsub.gleam index 0da23b4..1a3376d 100644 --- a/src/glitch/api/eventsub.gleam +++ b/src/glitch/api/eventsub.gleam @@ -1,5 +1,4 @@ import gleam/dynamic.{type Decoder} -import gleam/io import gleam/json import gleam/result import glitch/api/api_request @@ -67,8 +66,6 @@ pub fn create_eventsub_subscription( |> api_request.set_body(send_message_request_to_json(request)) |> api_request.set_path("eventsub/subscriptions") - io.debug(api_req) - use response <- result.try(client.post(client, api_req)) api_response.get_eventsub_data( diff --git a/src/glitch/eventsub/eventsub.gleam b/src/glitch/eventsub/eventsub.gleam index 1c9ca68..d85780d 100644 --- a/src/glitch/eventsub/eventsub.gleam +++ b/src/glitch/eventsub/eventsub.gleam @@ -9,7 +9,7 @@ import glitch/api/eventsub.{CreateEventSubSubscriptionRequest} import glitch/eventsub/websocket_message.{type WebSocketMessage} import glitch/eventsub/websocket_server import glitch/types/condition.{Condition} -import glitch/types/subscription.{ChannelChatMessage} +import glitch/types/subscription import glitch/types/transport.{Transport, WebSocket} pub opaque type EventSub { @@ -57,52 +57,47 @@ fn loop(selector, mailbox, handle) { fn handle(state: EventSub, message: WebSocketMessage) { case message { websocket_message.Close -> { - io.println("Closed") io.debug(message) } websocket_message.NotificationMessage(..) -> { - io.println("NotificationMessage") io.debug(message) } websocket_message.SessionKeepaliveMessage(..) -> { - io.println("SessionKeepaliveMessage") io.debug(message) } websocket_message.UnhandledMessage(_) -> { - io.println("UnhandledMessage") io.debug(message) } websocket_message.WelcomeMessage(_, payload) -> { - // let resp = - // eventsub.create_eventsub_subscription( - // state.client, - // CreateEventSubSubscriptionRequest( - // ChannelChatMessage, - // "1", - // Condition( - // Some("209286766"), - // None, - // None, - // None, - // None, - // Some(client.client_id(state.client)), - // None, - // Some("209286766"), - // ), - // Transport( - // WebSocket, - // None, - // None, - // Some(payload.session.id), - // None, - // None, - // None, - // ), - // ), - // ) - // let _ = io.debug(resp) + let assert Ok(_) = + eventsub.create_eventsub_subscription( + state.client, + CreateEventSubSubscriptionRequest( + subscription.ChannelChatMessage, + "1", + Condition( + Some("209286766"), + None, + None, + None, + None, + Some(client.client_id(state.client)), + None, + Some("209286766"), + ), + Transport( + WebSocket, + None, + None, + Some(payload.session.id), + None, + None, + None, + ), + ), + ) - let resp = + let assert Ok(_) = eventsub.create_eventsub_subscription( state.client, CreateEventSubSubscriptionRequest( @@ -129,7 +124,7 @@ fn handle(state: EventSub, message: WebSocketMessage) { ), ), ) - let _ = io.debug(resp) + message } } diff --git a/src/glitch/eventsub/websocket_message.gleam b/src/glitch/eventsub/websocket_message.gleam index b2fc130..039626e 100644 --- a/src/glitch/eventsub/websocket_message.gleam +++ b/src/glitch/eventsub/websocket_message.gleam @@ -37,6 +37,7 @@ fn welcome_message_decoder() -> Decoder(WebSocketMessage) { ) } +// TODO: Figure out how to handle "strict", i.e. not allowing additional filds fn session_keepalive_message_decoder() -> Decoder(WebSocketMessage) { dynamic.decode1( SessionKeepaliveMessage, @@ -214,45 +215,170 @@ fn notification_message_payload_decoder() -> Decoder(NotificationMessagePayload) pub type Event { ChannelChatMessageEvent( + badges: List(Badge), broadcaster_user_id: String, broadcaster_user_login: String, broadcaster_user_name: String, chatter_user_id: String, chatter_user_login: String, chatter_user_name: String, - message_id: String, - message: ChannelChatMessage, color: String, - badges: List(Badge), + message: ChannelChatMessage, + message_id: String, message_type: ChannelChatMessageType, cheer: Option(Cheer), - reply: Option(ChannelChatMessageReply), channel_points_custom_reward_id: Option(String), + reply: Option(ChannelChatMessageReply), + ) + ChannelPointsCustomRewardRedemptionAddEvent( + id: String, + broadcaster_user_id: String, + broadcaster_user_login: String, + broadcaster_user_name: String, + user_id: String, + user_login: String, + user_name: String, + user_input: String, + status: RewardStatus, + reward: Reward, + redeemed_at: String, + ) +} + +pub type Reward { + Reward(id: String, title: String, cost: Int, prompt: String) +} + +pub fn reward_decoder() -> Decoder(Reward) { + dynamic.decode4( + Reward, + dynamic.field("id", dynamic.string), + dynamic.field("title", dynamic.string), + dynamic.field("cost", dynamic.int), + dynamic.field("prompt", dynamic.string), + ) +} + +pub type RewardStatus { + Canceled + Fulfilled + Unfulfilled + Unknown +} + +pub fn rewards_status_to_string(reward_status: RewardStatus) -> String { + case reward_status { + Canceled -> "canceled" + Fulfilled -> "fulfilled" + Unfulfilled -> "unfulfilled" + Unknown -> "unknown" + } +} + +pub fn reward_status_from_string(string: String) -> Result(RewardStatus, Nil) { + case string { + "canceled" -> Ok(Canceled) + "fulfilled" -> Ok(Fulfilled) + "unfulfilled" -> Ok(Unfulfilled) + "unknown" -> Ok(Unknown) + _ -> Error(Nil) + } +} + +pub fn reward_status_decoder() -> Decoder(RewardStatus) { + fn(data: Dynamic) { + use string <- result.try(dynamic.string(data)) + + string + |> reward_status_from_string + |> result.replace_error([ + dynamic.DecodeError( + expected: "RewardsStatus", + found: "String(" <> string <> ")", + path: [], + ), + ]) + } +} + +pub type Image { + Image(url_1x: Uri, url_2x: Uri, url_3x: Uri) +} + +pub fn image_decoder() -> Decoder(Image) { + dynamic.decode3( + Image, + dynamic.field("url_1x", dynamic_ext.uri), + dynamic.field("url_2x", dynamic_ext.uri), + dynamic.field("url_3x", dynamic_ext.uri), + ) +} + +pub type MaxPerStream { + MaxPerStream(is_enabled: Bool, value: Int) +} + +pub fn max_per_stream_decoder() -> Decoder(MaxPerStream) { + dynamic.decode2( + MaxPerStream, + dynamic.field("is_enabled", dynamic.bool), + dynamic.field("value", dynamic.int), + ) +} + +pub type Cooldown { + Cooldown(is_enabled: Bool, seconds: Int) +} + +pub fn cooldown_decoder() -> Decoder(Cooldown) { + dynamic.decode2( + Cooldown, + dynamic.field("is_enabled", dynamic.bool), + dynamic.field("second", dynamic.int), ) - RewardEvent(id: String, title: String, cost: Int, prompt: String) } fn event_decoder() -> Decoder(Event) { - dynamic.any([channel_chat_messsage_event_decoder(), reward_event_decoder()]) + dynamic.any([ + channel_chat_messsage_event_decoder(), + channel_points_custom_reward_redemption_add_event_decoder(), + ]) } fn channel_chat_messsage_event_decoder() -> Decoder(Event) { dynamic_ext.decode14( ChannelChatMessageEvent, + dynamic.field("badges", dynamic.list(of: badge_decoder())), dynamic.field("broadcaster_user_id", dynamic.string), dynamic.field("broadcaster_user_login", dynamic.string), dynamic.field("broadcaster_user_name", dynamic.string), dynamic.field("chatter_user_id", dynamic.string), dynamic.field("chatter_user_login", dynamic.string), dynamic.field("chatter_user_name", dynamic.string), - dynamic.field("message_id", dynamic.string), - dynamic.field("message", channel_chat_message_decoder()), dynamic.field("color", dynamic.string), - dynamic.field("badges", dynamic.list(of: badge_decoder())), + dynamic.field("message", channel_chat_message_decoder()), + dynamic.field("message_id", dynamic.string), dynamic.field("message_type", channel_chat_messsage_type_decoder()), dynamic.optional_field("cheer", cheer_decoder()), - dynamic.optional_field("reply", channel_chat_message_reply_decoder()), dynamic.optional_field("channel_points_custom_reward_id", dynamic.string), + dynamic.optional_field("reply", channel_chat_message_reply_decoder()), + ) +} + +fn channel_points_custom_reward_redemption_add_event_decoder() -> Decoder(Event) { + dynamic_ext.decode11( + ChannelPointsCustomRewardRedemptionAddEvent, + dynamic.field("id", dynamic.string), + dynamic.field("broadcaster_user_id", dynamic.string), + dynamic.field("broadcaster_user_login", dynamic.string), + dynamic.field("broadcaster_user_name", dynamic.string), + dynamic.field("user_id", dynamic.string), + dynamic.field("user_login", dynamic.string), + dynamic.field("user_name", dynamic.string), + dynamic.field("user_input", dynamic.string), + dynamic.field("status", reward_status_decoder()), + dynamic.field("reward", reward_decoder()), + dynamic.field("redeemed_at", dynamic.string), ) } @@ -500,13 +626,3 @@ pub fn mention_decoder() -> Decoder(Mention) { dynamic.field("user_login", dynamic.string), ) } - -fn reward_event_decoder() -> Decoder(Event) { - dynamic.decode4( - RewardEvent, - dynamic.field("id", dynamic.string), - dynamic.field("title", dynamic.string), - dynamic.field("cost", dynamic.int), - dynamic.field("prompt", dynamic.string), - ) -} diff --git a/src/glitch/eventsub/websocket_server.gleam b/src/glitch/eventsub/websocket_server.gleam index 0623c44..48239ed 100644 --- a/src/glitch/eventsub/websocket_server.gleam +++ b/src/glitch/eventsub/websocket_server.gleam @@ -80,18 +80,15 @@ fn handle_start(state: WebSockerServer) { loop: fn(message, state, _conn) { case message { stratus.Text(message) -> { - io.debug(message) let decoded_message = websocket_message.from_json(message) - |> function.tap(io.debug) |> result.unwrap(UnhandledMessage(message)) process.send(state.mailbox, decoded_message) actor.continue(state) } - message -> { + _ -> { io.println("Received unexpected message:") - io.debug(message) actor.continue(state) } } diff --git a/src/glitch/extended/dynamic_ext.gleam b/src/glitch/extended/dynamic_ext.gleam index 6101b42..8e335bc 100644 --- a/src/glitch/extended/dynamic_ext.gleam +++ b/src/glitch/extended/dynamic_ext.gleam @@ -195,3 +195,143 @@ fn all_errors(result: Result(a, List(DecodeError))) -> List(DecodeError) { Error(errors) -> errors } } + +pub fn decode20( + constructor: fn( + t1, + t2, + t3, + t4, + t5, + t6, + t7, + t8, + t9, + t10, + t11, + t12, + t13, + t14, + t15, + t16, + t17, + t18, + t19, + t20, + ) -> + t, + t1: Decoder(t1), + t2: Decoder(t2), + t3: Decoder(t3), + t4: Decoder(t4), + t5: Decoder(t5), + t6: Decoder(t6), + t7: Decoder(t7), + t8: Decoder(t8), + t9: Decoder(t9), + t10: Decoder(t10), + t11: Decoder(t11), + t12: Decoder(t12), + t13: Decoder(t13), + t14: Decoder(t14), + t15: Decoder(t15), + t16: Decoder(t16), + t17: Decoder(t17), + t18: Decoder(t18), + t19: Decoder(t19), + t20: Decoder(t20), +) -> Decoder(t) { + fn(x: Dynamic) { + case + t1(x), + t2(x), + t3(x), + t4(x), + t5(x), + t6(x), + t7(x), + t8(x), + t9(x), + t10(x), + t11(x), + t12(x), + t13(x), + t14(x), + t15(x), + t16(x), + t17(x), + t18(x), + t19(x), + t20(x) + { + Ok(a), + Ok(b), + Ok(c), + Ok(d), + Ok(e), + Ok(f), + Ok(g), + Ok(h), + Ok(i), + Ok(j), + Ok(k), + Ok(l), + Ok(m), + Ok(n), + Ok(o), + Ok(p), + Ok(q), + Ok(r), + Ok(s), + Ok(t) + -> + Ok(constructor( + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + )) + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t -> + Error( + list.concat([ + all_errors(a), + all_errors(b), + all_errors(c), + all_errors(d), + all_errors(e), + all_errors(f), + all_errors(g), + all_errors(h), + all_errors(i), + all_errors(j), + all_errors(k), + all_errors(l), + all_errors(m), + all_errors(n), + all_errors(o), + all_errors(p), + all_errors(q), + all_errors(r), + all_errors(s), + all_errors(t), + ]), + ) + } + } +}