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

Updated README and implemented token requirements #10

Merged
merged 4 commits into from
Sep 29, 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
67 changes: 60 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,51 @@ You can check the current implementation status here: [Implementation Status](Ro
### Example Usage

```csharp
using TwitcheryNet.Models.Helix.Channels;
using TwitcheryNet.Services.Implementations;

var myClientId = "your-client-id";
var myRedirectUri = "http://localhost:8080";
var myScopes = new string[] { "chat:read", "chat:edit", "channel:moderate" };
const string myClientId = "your-client-id"; // Get this from your Twitch application
const string myRedirectUri = "http://localhost:8181"; // Must match the one in your Twitch application
var myScopes = new[]
{
"chat:read",
"chat:edit",
"user:read:chat",
"channel:moderate",
"channel:read:subscriptions",
"moderator:read:followers"
};

var twitchery = new Twitchery { ClientId = myClientId };
// Create a new Twitchery instance
var twitchery = new Twitchery();

// Authenticate with the user's default browser (Windows, Linux and OSX supported)
await twitchery.UserBrowserAuthAsync(redirectUri, scopes);
// If none is available, a URL will be printed to the console
await twitchery.UserBrowserAuthAsync(myClientId, myRedirectUri, myScopes);

// Print the user's display name
Console.WriteLine("Logged in as: " + twitchery.Me!.DisplayName);

// Get the authenticated user's channel
var myChannel = twitchery.Me.Channel;

// How many followers do I have?
Console.WriteLine("I have {0} followers", twitchery.ChannelFollowers[twitchery.Me!.Id].Count);
// This will fetch all followers, so it may take a while if you have a lot
// In a future version, you'll be able to access the total amount and the recent followers more easy
var followerCount = 0;
Follower lastFollower = null;
await foreach (var follower in myChannel.Followers)
{
// The first follower in the list is the most recent one
if (lastFollower is null)
{
lastFollower = follower;
}

followerCount++;
}

Console.WriteLine("I have {0} followers", followerCount);

// Am I streaming right now?
var myStream = twitchery.Streams[twitchery.Me!.Login];
Expand All @@ -48,9 +77,33 @@ else
}

// Who was the last person to follow me?
var lastFollower = twitchery.ChannelFollowers[twitchery.Me!.Id].First(); // First, because the list is sorted by follow date
Console.WriteLine("My last follower was {0}, who followed on {1}!", lastFollower.UserName, lastFollower.FollowedAt);

// Listen for events
myChannel.ChatMessage += (sender, e) =>
{
Console.WriteLine("{0} said: {1}", e.ChatterUserName, e.Message.Text);
return Task.CompletedTask;
};

myChannel.Follow += (sender, e) =>
{
Console.WriteLine("{0} followed me!", e.UserName);
return Task.CompletedTask;
};

myChannel.Subscribe += (sender, e) =>
{
Console.WriteLine("{0} subscribed to me!", e.UserName);
return Task.CompletedTask;
};

Console.WriteLine("Press any key to exit...");

// This is required to keep the bot running until the user presses a key
// To keep the bot running indefinitely, you can use `await Task.Delay(-1);`
await Task.Run(Console.ReadKey);

// Yes, it's that simple!
```

Expand Down
9 changes: 9 additions & 0 deletions Twitchery.Net/Attributes/RequiresTokenAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TwitcheryNet.Attributes;

[AttributeUsage(AttributeTargets.Method)]
public class RequiresTokenAttribute : Attribute
{
public TokenType TokenType { get; }

public RequiresTokenAttribute(TokenType tokenType) => TokenType = tokenType;
}
8 changes: 8 additions & 0 deletions Twitchery.Net/Attributes/TokenType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TwitcheryNet.Attributes;

public enum TokenType
{
Both,
UserAccess,
AppAccess
}
44 changes: 44 additions & 0 deletions Twitchery.Net/Extensions/LoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Net;
using Microsoft.Extensions.Logging;
using TwitcheryNet.Http;

namespace TwitcheryNet.Extensions;

public static class LoggerExtensions
{
public static bool Validate(this AsyncHttpResponse response, string url, HttpStatusCode expected = HttpStatusCode.OK, string? error = null, ILogger? logger = null, bool logAsError = true)
{
var msg = response.Response;
if (msg.StatusCode == expected)
{
return true;
}

var errorMsg = error ?? $"Received HTTP status code {msg.StatusCode} != {expected} at route {url}{(error != null ? $": {error}" : "")}";

if (logAsError)
{
if (logger is not null)
{
logger.LogError("{msg}", errorMsg);
}
else
{
Console.WriteLine("[ERROR] {0}", errorMsg);
}
}
else
{
if (logger is not null)
{
logger.LogWarning("{msg}", errorMsg);
}
else
{
Console.WriteLine("[WARNING] {0}", errorMsg);
}
}

return false;
}
}
103 changes: 26 additions & 77 deletions Twitchery.Net/Http/HttpRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Web;
using Newtonsoft.Json;
Expand All @@ -11,10 +13,10 @@ public class HttpRequestBuilder
{
public HttpMethod Method { get; private set; }
public Uri Uri { get; private set; }
public string ContentType { get; private set; } = MediaTypeNames.Application.Json;
public Dictionary<string, string> Headers { get; } = new();
public Dictionary<string, string> QueryParameters { get; } = new();
public string? Body { get; set; }
public HttpStatusCode? RequiredStatusCode { get; set; }
public HttpRequestMessage? Request { get; private set; }

public HttpRequestBuilder(HttpMethod method, Uri uri)
Expand All @@ -29,18 +31,6 @@ public HttpRequestBuilder SetPath(string path)
return this;
}

public HttpRequestBuilder RequireStatusCode(HttpStatusCode statusCode)
{
RequiredStatusCode = statusCode;
return this;
}

public HttpRequestBuilder RequireStatusCode(int statusCode)
{
RequiredStatusCode = (HttpStatusCode) statusCode;
return this;
}

public HttpRequestBuilder AddHeader(string key, string value)
{
Headers[key] = value;
Expand Down Expand Up @@ -73,7 +63,7 @@ public HttpRequestBuilder AddQueryParameterOptional(string? key, string? value)
return this;
}

public HttpRequestBuilder SetQueryString<TQuery>(TQuery? query) where TQuery : class, IQueryParameters
public HttpRequestBuilder SetQuery<TQuery>(TQuery? query) where TQuery : class, IQueryParameters
{
if (query is null)
{
Expand Down Expand Up @@ -137,8 +127,7 @@ public HttpRequestBuilder Build()

if (Body is not null)
{
var mediaType = Headers.GetValueOrDefault("Content-Type", "application/json");
var strContent = new StringContent(Body ?? string.Empty, Encoding.UTF8, mediaType);
var strContent = new StringContent(Body ?? string.Empty, Encoding.UTF8, ContentType);
request.Content = strContent;
}

Expand All @@ -147,84 +136,44 @@ public HttpRequestBuilder Build()
return this;
}

public async Task<AsyncHttpResponse> SendAsync(CancellationToken cancellationToken = default)
public async Task<AsyncHttpResponse> SendAsync(CancellationToken token = default)
{
if (Request is null)
{
throw new InvalidOperationException("Request is not built.");
}

switch (Method.Method)
var result = Method.Method switch
{
case "GET":
var getResult = await AsyncHttpClient.GetAsync(this, cancellationToken);

if (RequiredStatusCode is not null && getResult.Response.StatusCode != RequiredStatusCode)
{
throw new HttpRequestException($"Expected status code {RequiredStatusCode} but received {getResult.Response.StatusCode} with the following body: {getResult.RawBody}");
}

return getResult;

case "POST":
var postResult = await AsyncHttpClient.PostAsync(this, cancellationToken);

if (RequiredStatusCode is not null && postResult.Response.StatusCode != RequiredStatusCode)
{
throw new HttpRequestException($"Expected status code {RequiredStatusCode} but received {postResult.Response.StatusCode} with the following body: {postResult.RawBody}");
}

return postResult;

//case HttpMethod.Put:
// return await SendPutAsync(cancellationToken);

//case HttpMethod.Delete:
// return await SendDeleteAsync(cancellationToken);

default:
throw new ArgumentOutOfRangeException();
}
"GET" => await AsyncHttpClient.GetAsync(this, token),
"POST" => await AsyncHttpClient.PostAsync(this, token),
_ => throw new NotImplementedException($"Method {Method.Method} is not implemented yet.")
};

return result;
}

public async Task<AsyncHttpResponse<T>> SendAsync<T>(CancellationToken cancellationToken = default)
where T : class
public async Task<AsyncHttpResponse<T>> SendAsync<T>(CancellationToken token = default) where T : class
{
if (Request is null)
{
throw new InvalidOperationException("Request is not built.");
}

switch (Method.Method)
var result = Method.Method switch
{
case "GET":
var result = await AsyncHttpClient.GetAsync<T>(this, cancellationToken);

if (RequiredStatusCode is not null && result.Response.StatusCode != RequiredStatusCode)
{
throw new HttpRequestException($"Expected status code {RequiredStatusCode} but received {result.Response.StatusCode} with the following body: {result.RawBody}");
}
"GET" => await AsyncHttpClient.GetAsync<T>(this, token),
"POST" => await AsyncHttpClient.PostAsync<T>(this, token),
_ => throw new NotImplementedException($"Method {Method.Method} is not implemented yet.")
};

return result;

case "POST":
var postResult = await AsyncHttpClient.PostAsync<T>(this, cancellationToken);

if (RequiredStatusCode is not null && postResult.Response.StatusCode != RequiredStatusCode)
{
throw new HttpRequestException($"Expected status code {RequiredStatusCode} but received {postResult.Response.StatusCode} with the following body: {postResult.RawBody}");
}
return result;
}

return postResult;

//case HttpMethod.Put:
// return await SendPutAsync(cancellationToken);

//case HttpMethod.Delete:
// return await SendDeleteAsync(cancellationToken);

default:
throw new ArgumentOutOfRangeException();
}
public HttpRequestBuilder SetContentType(string type)
{
ContentType = type;

return this;
}
}
3 changes: 2 additions & 1 deletion Twitchery.Net/Misc/WebTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public static void OpenUrl(string url)
}
else
{
throw;
Console.WriteLine("Failed to start your browser.");
Console.WriteLine("Please open the following URL in your browser: {0}", url);
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion Twitchery.Net/Models/Helix/Route.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ public class Route
{
public ApiRoute ApiRoute { get; }
public ApiRules? ApiRules { get; }
public TokenType RequiredTokenType { get; }
public MethodInfo CallerMethod { get; }
public string Endpoint { get; }
public string FullUrl => $"{Endpoint}{ApiRoute.Path}";

public Route(string endpoint, ApiRoute apiRoute, MethodInfo callerMethod, ApiRules? apiRules = null)
public Route(string endpoint, ApiRoute apiRoute, MethodInfo callerMethod, TokenType requiredTokenType, ApiRules? apiRules = null)
{
Endpoint = endpoint;
ApiRoute = apiRoute;
ApiRules = apiRules;
RequiredTokenType = requiredTokenType;
CallerMethod = callerMethod;
}
}
6 changes: 3 additions & 3 deletions Twitchery.Net/Models/Helix/RouteRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace TwitcheryNet.Models.Helix;
[Flags]
public enum RouteRules
{
None = 0,
RequiresOwner = 1,
RequiresModerator = 2
None = 0,
RequiresOwner = 1 << 0,
RequiresModerator = 1 << 1
}
4 changes: 4 additions & 0 deletions Twitchery.Net/Models/Indexer/ChannelsIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public ChannelsIndex(ITwitchery api)
public Channel? this[string broadcasterId] => GetChannelInformationAsync(broadcasterId).Result;

[ApiRoute("GET", "channels")]
[RequiresToken(TokenType.Both)]
public async Task<GetChannelResponse?> GetChannelInformationAsync(GetChannelRequest request, CancellationToken cancellationToken = default)
{
return await Twitch.GetTwitchApiAsync<GetChannelRequest, GetChannelResponse>(request, typeof(ChannelsIndex), cancellationToken);
Expand Down Expand Up @@ -52,14 +53,17 @@ public ChannelsIndex(ITwitchery api)
return channel;
}

[ApiRules(RouteRules.RequiresOwner | RouteRules.RequiresModerator)]
[ApiRoute("GET", "channels/followers", "moderator:read:followers")]
[RequiresToken(TokenType.UserAccess)]
public async Task<GetChannelFollowersResponse?> GetChannelFollowersAsync(GetChannelFollowersRequest request, CancellationToken cancellationToken = default)
{
return await Twitch.GetTwitchApiAsync<GetChannelFollowersRequest, GetChannelFollowersResponse>(request, typeof(ChannelsIndex), cancellationToken);
}

[ApiRules(RouteRules.RequiresOwner | RouteRules.RequiresModerator)]
[ApiRoute("GET", "channels/followers", "moderator:read:followers")]
[RequiresToken(TokenType.UserAccess)]
public async Task<GetAllChannelFollowersResponse> GetAllChannelFollowersAsync(GetChannelFollowersRequest request, CancellationToken cancellationToken = default)
{
return await Twitch.GetTwitchApiAllAsync<GetChannelFollowersRequest, GetChannelFollowersResponse, GetAllChannelFollowersResponse>(request, typeof(ChannelsIndex), cancellationToken);
Expand Down
Loading
Loading