Skip to content

Instantly share code, notes, and snippets.

@pschichtel
Last active May 2, 2018 07:42
Show Gist options
  • Save pschichtel/57aecd15e28c0e02518bf4894151b3a4 to your computer and use it in GitHub Desktop.
Save pschichtel/57aecd15e28c0e02518bf4894151b3a4 to your computer and use it in GitHub Desktop.
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