Last active
May 2, 2018 07:42
-
-
Save pschichtel/57aecd15e28c0e02518bf4894151b3a4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Net; | |
using System.Threading.Tasks; | |
using System.Web; | |
using RestSharp; | |
using RestSharp.Authenticators; | |
using TwitchLib.Client; | |
using TwitchLib.Client.Events; | |
using TwitchLib.Client.Models; | |
using WebSocketSharp; | |
using System.Reactive.Linq; | |
using System.Reactive.Subjects; | |
using System.Text; | |
using System.Threading; | |
using TwitchLib.Client.Enums; | |
using TwitchLib.Client.Interfaces; | |
using TwitchLib.Client.Services; | |
namespace VoteBot | |
{ | |
internal static class Program | |
{ | |
private static string GenerateState() | |
{ | |
return Guid.NewGuid().ToString("N").Substring(0, 8); | |
} | |
private static string AuthUri(string state, string clientId, string redirectHost) | |
{ | |
const string scope = "chat_login user_read"; | |
var query = HttpUtility.ParseQueryString(String.Empty); | |
query.Add("client_id", clientId); | |
query.Add("redirect_uri", redirectHost); | |
query.Add("response_type","code"); | |
query.Add("scope", scope); | |
query.Add("state", state); | |
return $"https://api.twitch.tv/kraken/oauth2/authorize?{query}"; | |
} | |
private static async Task<IAuthenticationResult> AuthRequest(Uri returnedUrl, string expectedState, string clientId, string clientSecret, string redirectUri) | |
{ | |
var args = HttpUtility.ParseQueryString(returnedUrl.Query); | |
if (args["state"] == expectedState) | |
{ | |
try | |
{ | |
var code = args["code"]; | |
var client = new RestClient("https://api.twitch.tv/kraken"); | |
var request = new RestRequest("oauth2/token", Method.POST); | |
request.AddParameter("client_id", clientId); | |
request.AddParameter("client_secret", clientSecret); | |
request.AddParameter("code", code); | |
request.AddParameter("grant_type", "authorization_code"); | |
request.AddParameter("redirect_uri", redirectUri); | |
request.AddParameter("state", expectedState); | |
var response = client.Execute<TwitchResponse>(request); | |
if (response.StatusCode == HttpStatusCode.OK) | |
{ | |
var requestUser = new RestRequest("user", Method.GET); | |
client.Authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator(response.Data.access_token); | |
requestUser.AddHeader("Client-ID", clientId); | |
var responseUser = await client.ExecuteTaskAsync<TwitchUserResponse>(requestUser); | |
if (response.StatusCode == HttpStatusCode.OK) | |
{ | |
return new SuccessfulAuthentication(responseUser.Data.name, response.Data.access_token); | |
} | |
} | |
return new FailedAuthentication(AuthenticationFailure.HttpError, response.StatusCode.ToString()); | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine("Error: {0}", ex); | |
} | |
} | |
else | |
{ | |
return new FailedAuthentication(AuthenticationFailure.InvalidState); | |
} | |
return new FailedAuthentication(AuthenticationFailure.Unknown); | |
} | |
public static void Main(string[] args) | |
{ | |
if (args.Length == 4) | |
{ | |
StartBot(args[0], args[1], args[2], args[3]).Wait(); | |
} | |
else | |
{ | |
Console.WriteLine("Usage: <clientId> <clientSecret> <returnUrl> <channel>"); | |
} | |
} | |
private static async Task<IAuthenticationResult> Auth(string clientId, string clientSecret, string returnUrl) | |
{ | |
var state = GenerateState(); | |
var authUrl = AuthUri(state, clientId, returnUrl); | |
Console.WriteLine("url={0}", authUrl); | |
var httpListener = new HttpListener(); | |
httpListener.Prefixes.Add(new Uri(new Uri(returnUrl), "/").ToString()); | |
httpListener.Start(); | |
var ctx = await httpListener.GetContextAsync(); | |
var req = ctx.Request; | |
var returnedUrl = req.Url; | |
var responseText = "Close Me!<script>window.open('', '_self').close()</script>"; | |
var responseData = Encoding.UTF8.GetBytes(responseText); | |
var res = ctx.Response; | |
res.StatusCode = 201; | |
res.ContentLength64 = responseData.Length; | |
res.ContentType = "text/html"; | |
await res.OutputStream.WriteAsync(responseData, 0, responseData.Length); | |
res.OutputStream.Close(); | |
httpListener.Close(); | |
return await AuthRequest(returnedUrl, state, clientId, clientSecret, returnUrl); | |
} | |
private static async Task StartBot(string clientId, string clientSecret, string returnUrl, string channelName) | |
{ | |
var result = await Auth(clientId, clientSecret, returnUrl); | |
if (result is SuccessfulAuthentication success) | |
{ | |
await RunBot(success.Name, success.Token, channelName); | |
} | |
} | |
private static async Task RunBot(string name, string token, string channelName) | |
{ | |
var client = await InitBot(name, token); | |
var channel = await JoinIntoChannel(client, channelName); | |
Console.WriteLine("Joined channel: {0}", channel.Channel); | |
client.SendMessage(channel, "Hi everyone!"); | |
var messages = MessageStream(client); | |
var commands = messages.Where(m => m.Message.StartsWith("!")).Select(Command.Parse); | |
await commands.ForEachAsync(c => | |
{ | |
if (c.Is("exit")) | |
{ | |
if (c.IsSender(UserType.Moderator)) | |
{ | |
QuitBot(client, channel); | |
} | |
else | |
{ | |
Whisper(client, c.Message.Username, "You are not an admin!"); | |
} | |
} | |
else | |
{ | |
Console.WriteLine("Command: {0} -> {1}", c.Name, string.Join(", ", c.Args)); | |
} | |
}); | |
} | |
private static Task Whisper(ITwitchClient client, string receiver, string message) | |
{ | |
var source = new TaskCompletionSource<bool>(); | |
void EventHandler(object a, OnWhisperSentArgs args) | |
{ | |
if (args.Message == message && args.Receiver == receiver) | |
{ | |
client.OnWhisperSent -= EventHandler; | |
source.SetResult(true); | |
} | |
} | |
client.OnWhisperSent += EventHandler; | |
client.SendWhisper(receiver, message); | |
Thread.Sleep(1000); | |
return source.Task; | |
} | |
private static async void QuitBot(ITwitchClient client, JoinedChannel channel) | |
{ | |
await SendMessage(client, channel, "Bye bye!"); | |
await LeaveChannel(client, channel); | |
} | |
private static Task<SentMessage> SendMessage(ITwitchClient client, JoinedChannel channel, string message) | |
{ | |
var source = new TaskCompletionSource<SentMessage>(); | |
void EventHandler(object a, OnMessageSentArgs args) | |
{ | |
var sent = args.SentMessage; | |
if (sent.Message == message && sent.Channel == channel.Channel) | |
{ | |
client.OnMessageSent -= EventHandler; | |
source.SetResult(sent); | |
} | |
} | |
client.OnMessageSent += EventHandler; | |
client.SendMessage(channel, message); | |
Thread.Sleep(1000); | |
return source.Task; | |
} | |
private static IObservable<ChatMessage> MessageStream(ITwitchClient client) | |
{ | |
var subject = new Subject<ChatMessage>(); | |
void OnMessage(object sender, OnMessageReceivedArgs args) | |
{ | |
subject.OnNext(args.ChatMessage); | |
} | |
void OnDisconnect(object sender, OnDisconnectedArgs args) | |
{ | |
client.OnMessageReceived -= OnMessage; | |
client.OnDisconnected -= OnDisconnect; | |
subject.OnCompleted(); | |
} | |
client.OnMessageReceived += OnMessage; | |
client.OnDisconnected += OnDisconnect; | |
return subject.AsObservable(); | |
} | |
private static Task<TwitchClient> InitBot(string name, string token) | |
{ | |
var source = new TaskCompletionSource<TwitchClient>(); | |
var client = new TwitchClient(); | |
var throttle = new MessageThrottler(client, 20, TimeSpan.FromSeconds(30), applyThrottlingToRawMessages:true); | |
client.ChatThrottler = throttle; | |
client.WhisperThrottler = throttle; | |
throttle.StartQueue(); | |
var debug = Environment.GetEnvironmentVariable("DEBUG"); | |
if (debug != null && debug.Trim().ToLowerInvariant().Equals("true")) | |
{ | |
client.OnLog += (sender, args) => | |
{ | |
Console.WriteLine("[{0}] {1}: {2}", args.DateTime, args.BotUsername, args.Data); | |
}; | |
} | |
client.OnConnectionError += (sender, args) => | |
{ | |
Console.WriteLine("ERROR: user={0} error={1}", args.BotUsername, args.Error.Message); | |
Console.WriteLine(args.Error.Exception.ToString()); | |
}; | |
client.OnUnaccountedFor += (sender, args) => | |
{ | |
Console.WriteLine("LIB ERROR: channel={0} location={1} raw={2}", args.Channel, args.Location, args.RawIRC); | |
}; | |
void EventHandler(object a, OnConnectedArgs args) | |
{ | |
client.OnConnected -= EventHandler; | |
source.SetResult(client); | |
} | |
client.OnConnected += EventHandler; | |
client.Initialize(new ConnectionCredentials(name, token)); | |
client.Connect(); | |
return source.Task; | |
} | |
private static Task<JoinedChannel> JoinIntoChannel(TwitchClient client, string channel) | |
{ | |
var source = new TaskCompletionSource<JoinedChannel>(); | |
void Handler(object a, OnJoinedChannelArgs args) | |
{ | |
if (args.Channel == channel) | |
{ | |
client.OnJoinedChannel -= Handler; | |
source.SetResult(client.GetJoinedChannel(channel)); | |
} | |
} | |
client.OnJoinedChannel += Handler; | |
client.JoinChannel(channel); | |
return source.Task; | |
} | |
private static Task LeaveChannel(ITwitchClient client, JoinedChannel channel) | |
{ | |
var source = new TaskCompletionSource<bool>(); | |
void Handler(object a, OnLeftChannelArgs args) | |
{ | |
if (args.Channel == channel.Channel) | |
{ | |
client.OnLeftChannel -= Handler; | |
source.SetResult(true); | |
} | |
} | |
client.OnLeftChannel += Handler; | |
client.LeaveChannel(channel); | |
return source.Task; | |
} | |
} | |
public interface IAuthenticationResult { } | |
public class SuccessfulAuthentication : IAuthenticationResult | |
{ | |
public string Name; | |
public string Token; | |
public SuccessfulAuthentication(string name, string token) | |
{ | |
Name = name; | |
Token = token; | |
} | |
} | |
public class FailedAuthentication : IAuthenticationResult | |
{ | |
public AuthenticationFailure Failure; | |
public string Reason; | |
public FailedAuthentication(AuthenticationFailure failure, string reason) | |
{ | |
Failure = failure; | |
Reason = reason; | |
} | |
public FailedAuthentication(AuthenticationFailure failure) | |
{ | |
Failure = failure; | |
} | |
} | |
public enum AuthenticationFailure | |
{ | |
InvalidState, | |
HttpError, | |
Unknown | |
} | |
public class TwitchResponse | |
{ | |
public string access_token { get; set; } | |
public string refresh_token { get; set; } | |
public List<string> scope { get; set; } | |
} | |
public class TwitchUserResponse | |
{ | |
public string _id { get; set; } | |
public string bio { get; set; } | |
public string created_at { get; set; } | |
public string display_name { get; set; } | |
public string email { get; set; } | |
public string email_verified { get; set; } | |
public string logo { get; set; } | |
public string name { get; set; } | |
public string partnered { get; set; } | |
public string type { get; set; } | |
public string updated_at { get; set; } | |
} | |
struct Command | |
{ | |
public readonly ChatMessage Message; | |
public readonly string Name; | |
public readonly string[] Args; | |
public Command(ChatMessage message, string name, string[] args) | |
{ | |
Message = message; | |
Name = name; | |
Args = args; | |
} | |
public bool Is(string name) | |
{ | |
return Name.Equals(name.ToLowerInvariant()); | |
} | |
public bool IsSender(UserType type) | |
{ | |
return Message.UserType >= type; | |
} | |
public static Command Parse(ChatMessage message, string commandLine) | |
{ | |
var parts = commandLine.Substring(1).Split(' '); | |
return new Command(message, parts[0].ToLowerInvariant(), parts.SubArray(1, parts.Length - 1)); | |
} | |
public static Command Parse(ChatMessage message) | |
{ | |
return Parse(message, message.Message); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment