Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix websocket bug and add GetChannelInfo method #88

Merged
merged 2 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TwitchEverywhere.Core/TwitchEverywhere.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<Version>0.5.1</Version>
<Version>0.6.1</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackRelease>true</PackRelease>
<TargetFramework>net8.0</TargetFramework>
Expand Down
17 changes: 17 additions & 0 deletions TwitchEverywhere.Core/Types/RestApi/Channel/ChannelInfoEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;

namespace TwitchEverywhere.Core.Types.RestApi.Channel;

public record ChannelInfoEntry(
[property: JsonPropertyName("broadcaster_id")] string BroadcasterId,
[property: JsonPropertyName("broadcaster_login")] string BroadcasterLogin,
[property: JsonPropertyName("broadcaster_name")] string BroadcasterName,
[property: JsonPropertyName("broadcaster_language")] string BroadcasterLanguage,
[property: JsonPropertyName("game_id")] string GameId,
[property: JsonPropertyName("game_name")] string GameName,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("delay")] int Delay,
[property: JsonPropertyName("tags")] List<string> Tags,
[property: JsonPropertyName("content_classification_labels")] List<string> ContentClassificationLabels,
[property: JsonPropertyName("is_branded_content")] bool IsBrandedContent
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;

namespace TwitchEverywhere.Core.Types.RestApi.Channel;

public record GetChannelInfoApiResponse(
[property: JsonPropertyName("data")] ChannelInfoEntry[] Data
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Net;
using TwitchEverywhere.Core.Types.RestApi.Channel;

namespace TwitchEverywhere.Core.Types.RestApi.Wrappers;

public record GetChannelInfoResponse(
GetChannelInfoApiResponse ApiResponse,
HttpStatusCode StatusCode
);

30 changes: 0 additions & 30 deletions TwitchEverywhere.Irc/IWebSocketConnection.cs

This file was deleted.

122 changes: 51 additions & 71 deletions TwitchEverywhere.Irc/Implementation/TwitchConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ namespace TwitchEverywhere.Irc.Implementation;

internal sealed class TwitchConnector(
IAuthorizer authorizer,
IWebSocketConnection webSocketConnection,
IMessageProcessor messageProcessor
) : ITwitchConnector {

private TwitchConnectionOptions m_options;
private readonly ClientWebSocket m_webSocketConnection = new();

async Task<bool> ITwitchConnector.TryConnect(
TwitchConnectionOptions options,
Expand All @@ -24,7 +24,6 @@ Action<IMessage> messageCallback
m_options = options;

bool result = await ConnectToWebsocket(
ws: webSocketConnection,
token: token,
callback: messageCallback
);
Expand All @@ -35,51 +34,51 @@ async Task<bool> ITwitchConnector.SendMessage(
IMessage message,
MessageType messageType
) {
if( webSocketConnection.State != WebSocketState.Open ) {
if( m_webSocketConnection.State != WebSocketState.Open ) {
return false;
}

switch( messageType ) {
case MessageType.PrivMsg:
// Console.WriteLine( $"@reply-parent-msg-id={lazyLoadedPrivMsg.ReplyParentMsgId} PRIVMSG #{m_options.Channel} :{lazyLoadedPrivMsg.Text}" );
Console.WriteLine( message.RawMessage );
await SendMessage( webSocketConnection, message.RawMessage.Replace( $":{m_options.Channel}!{m_options.Channel}@{m_options.Channel}.tmi.twitch.tv", "" ) );
await SendMessage( message.RawMessage.Replace( $":{m_options.Channel}!{m_options.Channel}@{m_options.Channel}.tmi.twitch.tv", "" ) );
break;
case MessageType.ClearChat:
await SendMessage( webSocketConnection, $"CLEARCHAT #${m_options.Channel}" );
await SendMessage( $"CLEARCHAT #${m_options.Channel}" );
break;
case MessageType.ClearMsg:
await SendMessage( webSocketConnection, $"CLEARMSG ${m_options.Channel}" );
await SendMessage( $"CLEARMSG ${m_options.Channel}" );
break;
case MessageType.GlobalUserState:
await SendMessage( webSocketConnection, $"GLOBALUSERSTATE ${m_options.Channel}" );
await SendMessage( $"GLOBALUSERSTATE ${m_options.Channel}" );
break;
case MessageType.Notice:
await SendMessage( webSocketConnection, $"NOTICE ${m_options.Channel}" );
await SendMessage( $"NOTICE ${m_options.Channel}" );
break;
case MessageType.RoomState:
await SendMessage( webSocketConnection, $"ROOMSTATE ${m_options.Channel}" );
await SendMessage( $"ROOMSTATE ${m_options.Channel}" );
break;
case MessageType.UserNotice:
await SendMessage( webSocketConnection, $"USERNOTICE ${m_options.Channel}" );
await SendMessage( $"USERNOTICE ${m_options.Channel}" );
break;
case MessageType.UserState:
await SendMessage( webSocketConnection, $"USERSTATE ${m_options.Channel}" );
await SendMessage( $"USERSTATE ${m_options.Channel}" );
break;
case MessageType.Whisper:
await SendMessage( webSocketConnection, $"WHISPER ${m_options.Channel}" );
await SendMessage( $"WHISPER ${m_options.Channel}" );
break;
case MessageType.Join:
await SendMessage( webSocketConnection, $"JOIN ${m_options.Channel}" );
await SendMessage( $"JOIN ${m_options.Channel}" );
break;
case MessageType.Part:
await SendMessage( webSocketConnection, $"PART ${m_options.Channel}" );
await SendMessage( $"PART ${m_options.Channel}" );
break;
case MessageType.HostTarget:
await SendMessage( webSocketConnection, $"HOSTTARGET ${m_options.Channel}" );
await SendMessage( $"HOSTTARGET ${m_options.Channel}" );
break;
case MessageType.Reconnect:
await SendMessage( webSocketConnection, $"RECONNECT ${m_options.Channel}" );
await SendMessage( $"RECONNECT ${m_options.Channel}" );
break;
case MessageType.Unknown:
default:
Expand All @@ -90,8 +89,8 @@ MessageType messageType
}

async Task<bool> ITwitchConnector.Disconnect() {
await SendMessage( webSocketConnection, $"PART ${m_options.Channel}" );
await webSocketConnection.CloseAsync(
await SendMessage( $"PART ${m_options.Channel}" );
await m_webSocketConnection.CloseAsync(
closeStatus: WebSocketCloseStatus.NormalClosure,
statusDescription: "Disconnect requested",
cancellationToken: CancellationToken.None
Expand All @@ -106,100 +105,81 @@ IEnumerable<string> users
}

private async Task<bool> ConnectToWebsocket(
IWebSocketConnection ws,
string token,
Action<IMessage> callback
) {
await ws.ConnectAsync(
uri: new Uri(uriString: "ws://irc-ws.chat.twitch.tv:80"),
cancellationToken: CancellationToken.None
);
byte[] buffer = new byte[4096];
try {
await m_webSocketConnection.ConnectAsync(
uri: new Uri(uriString: "ws://irc-ws.chat.twitch.tv:80"),
cancellationToken: CancellationToken.None
);
byte[] buffer = new byte[4096];

await SendMessage(
socketConnection: ws,
message: "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands"
);
await SendMessage( socketConnection: ws, message: $"PASS oauth:{token}" );
await SendMessage( socketConnection: ws, message: $"NICK {m_options.ClientName}" );
await SendMessage( socketConnection: ws, message: $"JOIN #{m_options.Channel}" );

while (ws.State == WebSocketState.Open) {
await ReceiveWebSocketResponse(
ws: ws,
buffer: buffer,
callback: callback
await SendMessage(
message: "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands"
);
}
await SendMessage( message: $"PASS oauth:{token}" );
await SendMessage( message: $"NICK {m_options.ClientName}" );
await SendMessage( message: $"JOIN #{m_options.Channel}" );

while (m_webSocketConnection.State == WebSocketState.Open) {
await ReceiveWebSocketResponse(
buffer: buffer,
callback: callback
);
}

return true;
return true;
}
catch (Exception e) {
Console.Error.WriteLine( e );
return false;
}
finally {
if (m_webSocketConnection.State == WebSocketState.Open) {
await m_webSocketConnection.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
}
}

private async Task ReceiveWebSocketResponse(
IWebSocketConnection ws,
byte[] buffer,
Action<IMessage> callback
) {
WebSocketReceiveResult result = await ws.ReceiveAsync(
WebSocketReceiveResult result = await m_webSocketConnection.ReceiveAsync(
buffer: buffer,
cancellationToken: CancellationToken.None
);

if ( result.MessageType == WebSocketMessageType.Close ) {
await ws.CloseAsync(
await m_webSocketConnection.CloseAsync(
closeStatus: WebSocketCloseStatus.NormalClosure,
statusDescription: null,
cancellationToken: CancellationToken.None
);
} else {

Parse( data: buffer, callback: callback );
// string twitchResponse = Encoding.ASCII.GetString(
// bytes: buffer,
// index: 0,
// count: result.Count
// );
//
// string[] responses = twitchResponse.Trim().Split( "\r\n" );
// foreach (string response in responses) {
// // keep alive, let twitch know we are still listening
// if( response.Contains( "PING :tmi.twitch.tv" ) ) {
// await SendMessage( ws, "PONG :tmi.twitch.tv" );
// } else {
// messageProcessor.ProcessMessage(
// response: response,
// channel: m_options.Channel,
// callback: callback
// );
// }
// }
}
}

private async Task Parse(ReadOnlyMemory<byte> data, Action<IMessage> callback) {
RawMessage message = new(data);

if( message.Type == MessageType.Ping ) {
await SendMessage( webSocketConnection, "PONG :tmi.twitch.tv" );
await SendMessage( "PONG :tmi.twitch.tv" );
} else {
messageProcessor.ProcessMessage(
response: message,
channel: m_options.Channel,
callback: callback
);
}


// if (message.HasMoreMessages) {
// Parse(data[message.NextMessageStartIndex..], callback);
// }
}

private async static Task SendMessage(
IWebSocketConnection socketConnection,
private async Task SendMessage(
string message
) {
await socketConnection.SendAsync(
await m_webSocketConnection.SendAsync(
buffer: Encoding.ASCII.GetBytes(message),
messageType: WebSocketMessageType.Text,
endOfMessage: true,
Expand Down
42 changes: 0 additions & 42 deletions TwitchEverywhere.Irc/Implementation/WebSocketConnection.cs

This file was deleted.

1 change: 0 additions & 1 deletion TwitchEverywhere.Irc/IrcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ TwitchConnectionOptions options

m_twitchConnector = new TwitchConnector(
authorizer: authorizer,
webSocketConnection: new WebSocketConnection(),
messageProcessor: new MessageProcessor()
);
}
Expand Down
2 changes: 1 addition & 1 deletion TwitchEverywhere.Irc/TwitchEverywhere.Irc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<Version>0.5.0</Version>
<Version>0.6.1</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackRelease>true</PackRelease>
<TargetFramework>net8.0</TargetFramework>
Expand Down
7 changes: 7 additions & 0 deletions TwitchEverywhere.Rest/IChannelApiService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using TwitchEverywhere.Core.Types.RestApi.Wrappers;

namespace TwitchEverywhere.Rest;

public interface IChannelApiService {
Task<GetChannelInfoResponse> GetChannelInfo( IHttpClientWrapper httpClient, string broadcasterId );
}
4 changes: 4 additions & 0 deletions TwitchEverywhere.Rest/IRestApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ Task<HttpStatusCode> BlockUser(
SourceContext? sourceContext,
Reason? reason
);

Task<GetChannelInfoResponse> GetChannelInfo(
string broadcasterId
);
}
Loading
Loading