This article describes OAuth and callback integration, design request parameter and response data classes and method, how to create classes by code generation, how to handle pagination and how to handle refresh token automatically and do exception handling.
Introduction
This article describes the following:
- OAuth and webhook callback integration
- Design request parameter and response data classes and method
- How to create classes by code generation
- How to handle pagination
- How to handle refresh token automatically
- Exception handling
So, let's go!
Update Info for Version 6.1.0.0
IJsonConverter
moved from static
property to instance property. IJsonConverter
object must pass to SlackClient
constructor.
Background
Last month, I had an opportunity to create a feature that uses Slack
API. So I Googled for some library for Slack.
I found several libraries, but these are not good architecture. Some libraries use HttpClient directory, missing API, not intuitive method design.
So, I decided to create a Slack library.
Using the Code
I released the initial version of HigLabo.Net.Slack
.
You can get it from Nuget by searching HigLabo.
To use this library, you must add references for the following:
HigLabo.Converter
HigLabo.Core
HigLabo.Mapper
HigLabo.Net.OAuth
HigLabo.Net.OAuth.NewtonsoftJson
HigLabo.Net.Slack
If you are interested in HigLabo.Mapper
, here is an article. This is the fastest C# object mapper library in the world.
Add using HigLabo.Net.Slack
namespace on top of your C# file.
This is a basic usage with access token:
var cl = new SlackClient(new HigLabo.Net.OAuth.JsonConverter(), "access token");
var profile = await cl.UsersProfileGetAsync(new UsersProfileGetParameter()
{ User = "userId" });
Console.WriteLine(profile.Profile.Display_Name);
Console.WriteLine(profile.Profile.Real_Name);
Console.WriteLine(profile.Profile.Image_192);
I will describe about OAuth, token handling, API endpoint design, exception handling in the below chapter.
OAuthSetting Class for OAuth Authentication
In HigLabo.Net.OAuth
package, you can see HigLabo.Net.OAuth.OAuthSetting
class.
And you can also see HigLabo.Net.Slack.OAuthSetting
class that inherits from HigLabo.Net.OAuth.OAuthSetting
class.
HigLabo.Net.OAuth.OAuthSetting
class provides basic feature for OAuth that will be used for other OAuth service such as Microsoft365, Google, Facebook workplace, Instagram, Twitter, discord and so on. (I will create another library in the future, maybe. Please wait!)
HigLabo.Net.OAuth.OAuthSetting
class:
namespace HigLabo.Net.OAuth
{
public abstract class OAuthSetting
{
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
public string RedirectUrl { get; set; } = "";
public abstract string CreateUrl();
}
}
This class provides simple property and abstract
method. CreateUrl
method will return authorization url for OAuth.
HigLabo.Net.Slack.OAuthSetting
class provides actual implementation for CreateUrl
method.
namespace HigLabo.Net.Slack
{
public enum AuthorizationScopeType
{
App,
User,
}
public class OAuthSetting : HigLabo.Net.OAuth.OAuthSetting, IAuthorizationUrlBuilder
{
public AuthorizationScopeType ScopeType { get; set; }
public List<Scope> ScopeList { get; init; } = new();
public OAuthSetting(string clientId, string clientSecret,
AuthorizationScopeType scopeType, string redirectUrl,
IEnumerable<Scope> scopeList)
{
this.ClientId = clientId;
this.ClientSecret = clientSecret;
this.ScopeType = scopeType;
this.RedirectUrl = redirectUrl;
this.ScopeList.AddRange(scopeList);
}
public override string CreateUrl()
{
var scopeList = String.Join(",",
this.ScopeList.Select(el => el.GetScopeName()));
switch (this.ScopeType)
{
case AuthorizationScopeType.App:
return $"https://slack.com/oauth/v2/authorize?client_id=
{this.ClientId}&scope={WebUtility.UrlEncode(scopeList)}
&redirect_uri={this.RedirectUrl}";
case AuthorizationScopeType.User:
return $"https://slack.com/oauth/v2/authorize?
client_id={this.ClientId}&user_scope=
{WebUtility.UrlEncode(scopeList)}&redirect_uri={this.RedirectUrl}";
default:throw new SwitchStatementNotImplementException
<AuthorizationScopeType>(this.ScopeType);
}
}
}
}
With these classes, you can easily create authentication process.
For example, in ASP.NET6, you add a link on .cshtml file like this:
@{
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "ClientSecret", ....);
}
<a href="@setting.CreateUrl()">Add Slack account</a>
And add Controller to receive webhook from Slack.
[HttpGet("/Slack/OAuthCallback")]
public async Task<IActionResult> Slack_OAuthCallback(String code)
{
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "ClientSecret", ....);
var cl = new SlackClient(setting);
var token = await cl.RequestCodeAsync(code);
}
Now you can get access token and you can call Slack API endpoint via HigLabo.Net.Slack.SlackClient
class.
IJsonConverter for json Handling
Before using SlackClient
to call API, you must prepare for JsonConverter
. In HigLabo.Net.OAuth
package, you can see IJsonConverter
interface.
namespace HigLabo.Net.OAuth
{
public interface IJsonConverter
{
string SerializeObject(object obj);
T DeserializeObject<T>(String json);
}
}
You can also see OAuthClient
class that has IJsonConverter
property and two methods, SerializeObject
.DeserializeObject
method.
namespace HigLabo.Net.OAuth
{
public class OAuthClient : HttpClient
{
public IJsonConverter? JsonConverter { get; set; }
protected OAuthClient(IJsonConverter jsonConverter)
{
this.JsonConverter = jsonConverter;
}
protected string SerializeObject(object obj)
{
return JsonConverter.SerializeObject(obj);
}
protected T DeserializeObject<T>(String json)
{
return JsonConverter.DeserializeObject<T>(json);
}
}
}
By default, we provide a JsonConverter
that uses NewtonsoftJson
library. But if you want to use other JSON library, you implement IJsonConverter
like this:
namespace Your.Cool.Converters
{
public class CoolJsonConverter : IJsonConverter
{
public string SerializeObject(object obj)
{
}
public T DeserializeObject<T>(string json)
{
}
}
}
And set to SlackClient constructor.
var slackClient = new SlackClient(new Your.Cool.Converters.CoolJsonConverter(),
"access Token");
Then you don't have to add reference to HigLabo.Net.OAuth.NewtonsoftJson
package.
Design Request and Response Classes for Slack and Future Other Services
Now it is a problem how to design API endpoint request and response.
Here is a concern before I implement it.
- I have been so lazy that I don't want to implement all endpoint by handy.
- I must be able to extend other OAuth API services. (M365, Google, FB workplace, Instagram, etc.)
- Intuitive class and method design.
- Easy to use for basic usage, fully customization for full feature to endpoint.
I'm a lazy man as a developer, I decided to create all endpoints by code generation. Fortunately, document of Slack API looks pretty well designed and pattern. It could be possible.
I searched other API, all services have these features.
- Endpoint URL and HTTP method
- Parameters
- Response JSON
I designed for Slack endpoint like this for https://slack.com/api/conversations.list endpoint.
namespace HigLabo.Net.Slack
{
public partial class ConversationsListParameter : IRestApiParameter, ICursor
{
string IRestApiParameter.ApiPath { get; } = "conversations.list";
string IRestApiParameter.HttpMethod { get; } = "GET";
public string Cursor { get; set; }
public bool? Exclude_Archived { get; set; }
public double? Limit { get; set; }
public string Team_Id { get; set; }
public string Types { get; set; }
}
public partial class ConversationsListResponse : RestApiResponse
{
}
public partial class SlackClient
{
public async Task<ConversationsListResponse>
ConversationsListAsync(ConversationsListParameter parameter)
{
}
}
}
The XXXParameter
class has all value for API endpoint parameter as property. So, you can set all of value as you want to pass API endpoint. The name of parameter, response, method is XXXParameter
, XXXResponse
, XXXAsync(XXXParameter)
. It is intuitive and easy to find because it is pretty much same as API documentation. I think it looks good. I have not created library for other OAuth services, maybe the design will work as far as parameter and method.
This parameter design will provide you to handle all features of API by setting property. But in some cases, it is redundant. So we provide method overload that only passes the required field of API endpoint.
For example, https://slack.com/api/conversations.list endpoint require channel field.
namespace HigLabo.Net.Slack
{
public partial class ConversationsHistoryParameter : IRestApiParameter, ICursor
{
}
public partial class ConversationsHistoryResponse : RestApiResponse
{
}
public partial class SlackClient
{
public async Task<ConversationsHistoryResponse>
ConversationsHistoryAsync(string channel)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendAsync<ConversationsHistoryParameter,
ConversationsHistoryResponse>(p);
}
}
}
Now you can call like this:
var cl = new SlackClient("access token");
var res = await cl.ConversationsHistoryAsync("channelId");
foreach (var message in res.Messages)
{
}
Now my library achieves both requirements, it is easy to use and uses full feature to endpoint.
Code Generation by ConsoleApplication
After library architecture design, I create code generator of the above class.
You can see the project at:
The actual code is here:
Only 333 lines of code thankfully for HigLabo.CodeGenerator
package.
HigLabo.CodeGenerator
is a C# code generator that has is missing some of C# language features. But it works very well for me because I don't use all C# language features when generating code.
HigLabo.Net.Slack.CodeGenerator
is a console program that read all Slack documentation and creates C# source code. Now I run HigLabo.Net.Slack.CodeGenerator
and it creates some of code at HigLabo.Net.Slack folder. All source code in Method folder and Scope.cs file is generated by HigLabo.Net.Slack.CodeGenerator
console program.
This is the code of Scope.cs by code generation.
using HigLabo.Core;
namespace HigLabo.Net.Slack
{
public enum Scope
{
Admin,
AdminAnalyticsRead,
AdminAppsRead,
UsersRead,
UsersReadEmail,
UsersWrite,
WorkflowStepsExecute,
}
public static class ScopeExtensions
{
public static string GetScopeName(this Scope value)
{
switch(value)
{
case Scope.Admin: return "admin";
case Scope.AdminAnalyticsRead: return "admin.analytics:read";
case Scope.AdminAppsRead: return "admin.apps:read";
case Scope.AdminAppsWrite: return "admin.apps:write";
case Scope.UsersRead: return "users:read";
case Scope.UsersReadEmail: return "users:read.email";
case Scope.UsersWrite: return "users:write";
case Scope.WorkflowStepsExecute: return "workflow.steps:execute";
default: throw new SwitchStatementNotImplementException<Scope>(value);
}
}
}
}
This is the code of ConversationsHistory.cs by code generation.
namespace HigLabo.Net.Slack
{
public partial class ConversationsHistoryParameter : IRestApiParameter, ICursor
{
string IRestApiParameter.ApiPath { get; } = "conversations.history";
string IRestApiParameter.HttpMethod { get; } = "GET";
public string Channel { get; set; }
public string Cursor { get; set; }
public bool? Include_All_Metadata { get; set; }
public bool? Inclusive { get; set; }
public string Latest { get; set; }
public double? Limit { get; set; }
public string Oldest { get; set; }
}
public partial class ConversationsHistoryResponse : RestApiResponse
{
}
public partial class SlackClient
{
public async Task<ConversationsHistoryResponse>
ConversationsHistoryAsync(string channel)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendAsync<ConversationsHistoryParameter,
ConversationsHistoryResponse>(p, CancellationToken.None);
}
public async Task<ConversationsHistoryResponse>
ConversationsHistoryAsync(string channel, CancellationToken cancellationToken)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendAsync<ConversationsHistoryParameter,
ConversationsHistoryResponse>(p, cancellationToken);
}
public async Task<ConversationsHistoryResponse>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter)
{
return await this.SendAsync<ConversationsHistoryParameter,
ConversationsHistoryResponse>(parameter, CancellationToken.None);
}
public async Task<ConversationsHistoryResponse>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter,
CancellationToken cancellationToken)
{
return await this.SendAsync<ConversationsHistoryParameter,
ConversationsHistoryResponse>(parameter, cancellationToken);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(string channel,
PagingContext<ConversationsHistoryResponse> context)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendBatchAsync(p, context, CancellationToken.None);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(string channel,
PagingContext<ConversationsHistoryResponse> context,
CancellationToken cancellationToken)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendBatchAsync(p, context, cancellationToken);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter,
PagingContext<ConversationsHistoryResponse> context)
{
return await this.SendBatchAsync
(parameter, context, CancellationToken.None);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter,
PagingContext<ConversationsHistoryResponse> context,
CancellationToken cancellationToken)
{
return await this.SendBatchAsync(parameter, context, cancellationToken);
}
}
}
I generate some overload version with CancellationToken
.
You may wonder about this overload.
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(string channel, PagingContext
<ConversationsHistoryResponse> context)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendBatchAsync(p, context, CancellationToken.None);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(string channel,
PagingContext<ConversationsHistoryResponse> context,
CancellationToken cancellationToken)
{
var p = new ConversationsHistoryParameter();
p.Channel = channel;
return await this.SendBatchAsync(p, context, cancellationToken);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter,
PagingContext<ConversationsHistoryResponse> context)
{
return await this.SendBatchAsync
(parameter, context, CancellationToken.None);
}
public async Task<List<ConversationsHistoryResponse>>
ConversationsHistoryAsync(ConversationsHistoryParameter parameter,
PagingContext<ConversationsHistoryResponse> context,
CancellationToken cancellationToken)
{
return await this.SendBatchAsync(parameter, context, cancellationToken);
}
You notice these methods delegate to SendBatchAsync
. It is a feature for pagination. I will explain it in the next chapter.
You will also notice that the generated response object has no property. Because I could not create these properties of response automatically from documentation. But thank you for partial class, I merge features by myself with auto generated class.
I created some classes in Entity folder and Response folder.
This is a Profile
class that I created by myself.
namespace HigLabo.Net.Slack
{
public class Profile
{
public String Title { get; set; }
public String Phone { get; set; }
public String Skype { get; set; }
public String Avatar_Hash { get; set; }
public String Status_Text { get; set; }
public String Status_Emoji { get; set; }
public String[] Status_Emoji_Display_Info { get; set; }
public Int32 Status_Expiration { get; set; }
public String Real_Name { get; set; }
public String Display_Name { get; set; }
public String Real_Name_Normalized { get; set; }
public String Display_Name_Normalized { get; set; }
public String EMail { get; set; }
public String First_Name { get; set; }
public String Last_Name { get; set; }
public Boolean Is_Custom_Image { get; set; }
public String Image_Original { get; set; }
public String Image_24 { get; set; }
public String Image_32 { get; set; }
public String Image_48 { get; set; }
public String Image_72 { get; set; }
public String Image_192 { get; set; }
public String Image_512 { get; set; }
public String Image_1024 { get; set; }
public String Team { get; set; }
public String Status_Text_Canonical { get; set; }
}
}
This is a UsersProfileGetResponse
:
namespace HigLabo.Net.Slack
{
public partial class UsersProfileGetResponse
{
public Profile Profile { get; set; }
}
}
You can get profile data like this:
var cl = new SlackClient(new MyJsonConverter(), "access token");
var profile = await cl.UsersProfileGetAsync(new UsersProfileGetParameter()
{ User = "userId" });
Console.WriteLine(profile.Profile.Display_Name);
Console.WriteLine(profile.Profile.Real_Name);
Console.WriteLine(profile.Profile.Image_192);
Unfortunately, some of response class does not have implementation because I have enough time to implement it. But you can access actual JSON body text via RestApiResponse
object like this:
var cl = new SlackClient(new MyJsonConverter(), "access token");
var res = await cl.UsersProfileGetAsync();
var iRes = res as IRestApiResponse;
var bodyText = iRes.ResponseBodyText;
I decided to use explicit implementation of interfaces to prevent from occurring name collision to API response property.
The definition of IRestApiResponse
and HigLabo.Net.OAuth.RestApiResponse
is here.
namespace HigLabo.Net.OAuth
{
public interface IRestApiResponse
{
object Parameter { get; }
HttpRequestMessage Request { get; }
HttpStatusCode StatusCode { get; }
Dictionary<String, String> Headers { get; }
string ResponseBodyText { get; }
}
public class RestApiResponse : IRestApiResponse
{
private Object _Parameter = null;
private HttpRequestMessage _Request = null;
private HttpStatusCode _StatusCode = HttpStatusCode.OK;
private Dictionary<String, String> _Headers = new Dictionary<string, string>();
private string _ResponseBodyText = "";
object IRestApiResponse.Parameter
{
get { return _Parameter; }
}
HttpRequestMessage IRestApiResponse.Request
{
get { return _Request; }
}
HttpStatusCode IRestApiResponse.StatusCode
{
get { return _StatusCode; }
}
Dictionary<String, String> IRestApiResponse.Headers
{
get { return _Headers; }
}
string IRestApiResponse.ResponseBodyText
{
get { return _ResponseBodyText; }
}
public void SetProperty(object parameter,
HttpRequestMessage request, HttpResponseMessage response, string bodyText)
{
var res = response;
_Parameter = parameter;
_Request = request;
_StatusCode = res.StatusCode;
foreach (var header in res.Headers)
{
_Headers[header.Key] = header.Value.ToString() ?? "";
}
_ResponseBodyText = bodyText;
}
}
}
With HigLabo.Net.OAuth.RestApiResponse
class, you can get parameter that used to call API endpoint. You can also get actual HttpRequestMessage
, HttpHeader
of response, and response body text.
I don't create AdminConversationsSearchResponse
class but you use it by creating your own JSON parser or class.
var cl = new SlackClient(new MyJsonConverter(), "access token");
var res = await cl.AdminConversationsSearchAsync();
var iRes = res as IRestApiResponse;
var json = iRes.ResponseBodyText;
var jo = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
var o = Newtonsoft.Json.JsonConvert.DeserializeObject<MyResponseClass>(json);
HigLabo.Net.Slack
package provides parameter definition, handle pagination, automatically updates access token. Even though the missing response, you can only handle JSON parsing.
I think done is better than perfect.
After all code generation, SlackClient
has only these codes.
I'm a super lazy man, I really am happy doing less work with code generation, all Slack API that has over 200 endpoints will be available by creating less than 120 lines of code for SlackClient and some entity and response classes.
Is it cool, right?
Help Your Contribution!
I really appreciate for your contribution. Here is the guide of contribution.
- Create missing response class in Response folder.
- If you need new entity, you add entity class to Entity folder.
- Send me PR via GitHub. (https://github.com/higty/higlabo)
I believe that HigLabo.Net.Slack
makes the world better place.
It is the power of open source.
Pagination with Cursor
I found that not only Slack API but also other M365, Google, Instagram, etc. provide feature for pagination. To prevent from overuse of API or network bandwidth, to add feature of pagination is the right thing for service provider.
But for the developer who uses these API, it will be a little bit redundant work to repeatedly call API.
So, I provide an easy way to call pagination API.
With HigLabo.Net.Slack
library, you can get all conversation list only write some pieces of code like this:
var cl = new SlackClient(new MyJsonConverter(), "access token");
var context = new PagingContext<ConversationsHistoryResponse>();
var responseList = await cl.ConversationsHistoryAsync("channelId", context);
Slack says in documentation if cursor field is exists in parameter, you call API multiple times to get next page data. I generate overload method if API endpoint has cursor field.
I created these overload versions only when the API has cursor field. You can see UsersProfileGetAsync
method does not have overload that call SendBatchAsync
method in it.
You can interrupt during pagination. PagingContext<T>
class will raise event per API call.
Here is the actual implementation of SendBatchAsync
method in SlackClient
class.
public async Task<List<TResponse>> SendBatchAsync<TParameter, TResponse>
(TParameter parameter, PagingContext<TResponse> context,
CancellationToken cancellationToken)
where TParameter : IRestApiParameter, ICursor
where TResponse : RestApiResponse, new()
{
var p = parameter;
var l = new List<TResponse>();
while (true)
{
var res = await this.SendAsync<TParameter, TResponse>(p, cancellationToken);
l.Add(res);
context.InvokeResponseReceived
(new ResponseReceivedEventArgs<TResponse>(this, l));
if (context.Break) { break; }
if (res.Response_MetaData == null ||
res.Response_MetaData.Next_Cursor.IsNullOrEmpty()) { break; }
p.Cursor = res.Response_MetaData.Next_Cursor;
}
return l;
}
You can see InvokeResponseReceived
is called and pass current response list to event subscriber.
If you want to get all data from endpoint until the end of data, you only pass PagingContext<T>
object to method. You can use Empty
property of PagingContext<T>
class.
If you want to stop some condition during pagination, you set PagingContext.Break
property as true
.
For example, Slack has a limit of record count such as 100
(I don't know the exact value...), your app wants to show 250 records, and 1000 records exists in Slack.
var cl = new SlackClient(new MyJsonConverter(), "access token");
var context = new PagingContext<ConversationsHistoryResponse>();
context.ResponseReceived += (sender, e) => {
if (e.ResponseList.Sum(el => el.Messages.Count) > 250)
{
e.Break = true;
}
};
var responseList = await cl.ConversationsHistoryAsync("channelId", context);
var messageList = responseList.SelectMany(el => el.Messages).Top(250);
e.ResponseList
contains all response that receive on this API call. You can stop when the sum of message is larger than 250 and get 250 records by LINQ.
Other example, you want to search something first one record, you can use PagingContext<T>
for it.
Update Access Token by Refresh Token and Save to Your Database
Slack required token rotation to improve your security. To update access token is little bit complex flow to implement on your own. HigLabo.Net.Slack
provides an easy way to update your access token.
You only pass refresh token and OAuthSetting
to constructor of SlackClient
.
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "clientSecret", ...);
var cl = new SlackClient(new MyJsonConverter(), "access token", "refresh token", setting);
SlackClient
automatically handles updating process of access token. Here is a code to update access token.
private async Task<TResponse> GetResponseAsync<TResponse>(Func<Task<TResponse>> func)
{
var isFirst = true;
while (true)
{
try
{
return await func();
}
catch
{
if (isFirst == false) { throw; }
isFirst = false;
}
var result = await this.UpdateAccessTokenAsync();
this.AccessToken = result.Authed_User.Access_Token;
this.RefreshToken = result.Authed_User.Refresh_Token;
this.OnAccessTokenUpdated(new AccessTokenUpdatedEventArgs(result));
}
}
You must only pass refresh token and OAuthSetting
. It is easy to use. But you may want to know the new access token value when update is occurred. SlackClient
has an event that will be triggered when access token updated.
This is a sample code to receive token updated.
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "clientSecret", ...);
var cl = new SlackClient(new MyJsonConverter(),
"access token", "refresh token", setting);
cl.AccessTokenUpdated += async (sender, e) =>
{
var scl = sender as SlackClient
if (r.Ok == true)
{
var AccessToken = scl.AccessToken;
var RefreshToken = scl.RefreshToken;
}
};
It is pretty easy to understand and to use.
Exception Handling
SlackClient
has a property whether throw exception or not.
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "clientSecret", ...);
var cl = new SlackClient(new MyJsonConverter(),
"access token", "refresh token", setting);
cl.IsThrowException = false;
The default value is true
. When set to false
, you must check the Ok
property of RestApiResponse
object to handle API error.
var setting = new HigLabo.Net.Slack.OAuthSetting("clientId", "clientSecret", ...);
var cl = new SlackClient(new MyJsonConverter(),
"access token", "refresh token", setting);
cl.IsThrowException = false;
var res = await cl.ConversationsHistoryAsync("channelId");
if (res.Ok == false)
{
}
Conclusion
I enjoy code generation and design the architecture of this library. I will try to other services if I could have time to work with. I hope you also enjoy this article. And I wish that HigLabo.Net.Slack
package makes your work easy.
Thank you for reading my article!
History
- 1st July, 2022: Initial post
- 10th July, 2022: Release 6.1.0.0 version that includes
HigLabo.Net.Microsoft