Skip to content

Commit 8cca903

Browse files
authored
Feature team detection (#441)
* Started TeamCommand * Added teammate load script * Initial attempt at video widget * Added Here Command to _hopefully_ prevent the bot from crashing during giveaways * Updated ToDo feature to allow checkboxes to be cleared * Completed teammate notification * Added Active and Deactivate TODO verbs
1 parent 3042a1c commit 8cca903

File tree

11 files changed

+387
-17
lines changed

11 files changed

+387
-17
lines changed

Fritz.Chatbot/Commands/HereCommand.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Fritz.StreamLib.Core;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Fritz.Chatbot.Commands
8+
{
9+
public class HereCommand : IBasicCommand
10+
{
11+
public string Trigger { get; } = "here";
12+
public string Description { get; } = "A do-nothing command that will not crash the bot when we run a giveaway";
13+
public TimeSpan? Cooldown { get; } = null;
14+
15+
public Task Execute(IChatService chatService, string userName, ReadOnlyMemory<char> rhs)
16+
{
17+
// do nothing
18+
return Task.CompletedTask;
19+
}
20+
}
21+
}

Fritz.Chatbot/Commands/TeamCommand.cs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using Fritz.StreamLib.Core;
2+
using Fritz.StreamTools.Hubs;
3+
using Microsoft.AspNetCore.SignalR;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
using Newtonsoft.Json;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Net.Http;
12+
using System.Text;
13+
using System.Threading.Tasks;
14+
15+
namespace Fritz.Chatbot.Commands
16+
{
17+
public class TeamCommand : IExtendedCommand
18+
{
19+
20+
private static HashSet<string> _Teammates = new HashSet<string>();
21+
private static Dictionary<string, DateTime> _TeammateCooldown = new Dictionary<string, DateTime>();
22+
private string _TeamName;
23+
private HttpClient _HttpClient;
24+
private readonly IHubContext<AttentionHub> _Context;
25+
private ILogger _Logger;
26+
27+
public string Name { get; } = "Team Detection";
28+
public string Description { get; } = "Alert when a teammate joins the stream and starts chatting";
29+
public int Order { get; } = 1;
30+
public bool Final { get; } = false;
31+
public TimeSpan? Cooldown { get; } = TimeSpan.FromSeconds(5);
32+
public TimeSpan ShoutoutCooldown;
33+
public string ShoutoutFormat;
34+
public Queue<string> _TeammateNotifications = new Queue<string>();
35+
36+
public TeamCommand(IConfiguration configuration, ILoggerFactory loggerFactory, IHubContext<AttentionHub> context, IHttpClientFactory httpClientFactory)
37+
{
38+
_TeamName = configuration["StreamServices:Twitch:Team"];
39+
ShoutoutCooldown = configuration.GetValue("StreamServices:Twitch:TeamCooldown", TimeSpan.FromHours(1));
40+
ShoutoutFormat = configuration.GetValue("StreamServices:Twitch:TeamShoutoutFormat", "");
41+
_Context = context;
42+
_Logger = loggerFactory.CreateLogger(nameof(TeamCommand));
43+
44+
if (!string.IsNullOrEmpty(TwitchTokenConfig.Tokens?.access_token))
45+
{
46+
_HttpClient = httpClientFactory.CreateClient("TeamLookup");
47+
_HttpClient.BaseAddress = new Uri($"https://api.twitch.tv/kraken/teams/");
48+
_HttpClient.DefaultRequestHeaders.Add("Client-ID", configuration["StreamServices:Twitch:ClientId"]);
49+
_HttpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.twitchtv.v5+json");
50+
51+
}
52+
else
53+
{
54+
_Logger.LogError("Unable to create HttpClient for Twitch with Bearer token");
55+
}
56+
57+
GetTeammates().GetAwaiter().GetResult();
58+
59+
Task.Run(SendNotificationsToWidget);
60+
61+
}
62+
63+
private void SendNotificationsToWidget()
64+
{
65+
66+
while (true) {
67+
68+
if (_TeammateNotifications.TryPeek(out var _)) {
69+
70+
_Context.Clients.All.SendAsync("Teammate", _TeammateNotifications.Dequeue());
71+
Task.Delay(5000);
72+
73+
}
74+
75+
}
76+
77+
}
78+
79+
public bool CanExecute(string userName, string fullCommandText)
80+
{
81+
82+
var u = userName.ToLowerInvariant();
83+
var isTeammate = _Teammates.Contains(u);
84+
var recentShoutout = _TeammateCooldown.ContainsKey(u) && (DateTime.UtcNow.Subtract(_TeammateCooldown[u]) < ShoutoutCooldown);
85+
86+
return isTeammate && !recentShoutout;
87+
88+
}
89+
90+
public async Task Execute(IChatService chatService, string userName, string fullCommandText)
91+
{
92+
_TeammateCooldown[userName.ToLowerInvariant()] = DateTime.UtcNow;
93+
if (ShoutoutFormat != "")
94+
{
95+
await chatService.SendMessageAsync(ShoutoutFormat.Replace("{teammate}", userName));
96+
}
97+
98+
_TeammateNotifications.Enqueue(userName);
99+
100+
}
101+
102+
private async Task GetTeammates()
103+
{
104+
105+
var response = await _HttpClient.GetStringAsync(_TeamName);
106+
var team = JsonConvert.DeserializeObject<TeamResponse>(response);
107+
108+
_Teammates = team.users.Select(u => u.name).ToHashSet();
109+
110+
}
111+
112+
113+
internal class TeamResponse
114+
{
115+
public int _id { get; set; }
116+
public object background { get; set; }
117+
public string banner { get; set; }
118+
public DateTime created_at { get; set; }
119+
public string display_name { get; set; }
120+
public string info { get; set; }
121+
public string logo { get; set; }
122+
public string name { get; set; }
123+
public DateTime updated_at { get; set; }
124+
public User[] users { get; set; }
125+
}
126+
127+
128+
internal class User
129+
{
130+
public int _id { get; set; }
131+
public string broadcaster_language { get; set; }
132+
public DateTime created_at { get; set; }
133+
public string display_name { get; set; }
134+
public int followers { get; set; }
135+
public string game { get; set; }
136+
public string language { get; set; }
137+
public string logo { get; set; }
138+
public bool mature { get; set; }
139+
public string name { get; set; }
140+
public bool partner { get; set; }
141+
public string profile_banner { get; set; }
142+
public object profile_banner_background_color { get; set; }
143+
public string status { get; set; }
144+
public DateTime updated_at { get; set; }
145+
public string url { get; set; }
146+
public object video_banner { get; set; }
147+
public int views { get; set; }
148+
}
149+
150+
151+
}
152+
153+
154+
}

Fritz.Chatbot/Commands/ToDoCommand.cs

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ public class ToDoCommand : IBasicCommand2
1818
private static Dictionary<int, (bool completed, string text)> _ToDos = new Dictionary<int, (bool, string)>();
1919
private readonly IHubContext<ObsHub> _HubContext;
2020

21+
private readonly Dictionary<string, Func<string[], ToDoCommand, Task>> _Verbs = new Dictionary<string, Func<string[], ToDoCommand, Task>> {
22+
{"add", AddTodo },
23+
{"remove", RemoveTodo },
24+
{"done", DoneTodo },
25+
{"clear", ClearTodo },
26+
{"active", Activate },
27+
{"deactivate", Deactivate },
28+
{"speed", SetSpeed }
29+
};
30+
2131
public ToDoCommand(IHubContext<ObsHub> hubContext)
2232
{
2333
_HubContext = hubContext;
@@ -29,35 +39,88 @@ public async Task Execute(IChatService chatService, string userName, bool isMode
2939
if (!(isBroadcaster || isModerator)) return;
3040

3141
var arrArgs = rhs.ToString().Split(' ');
32-
if (arrArgs[0] == "add") {
33-
var newKey = !_ToDos.Any() ? 1 : _ToDos.Max(t => t.Key) + 1;
34-
_ToDos.Add(newKey, (false, rhs.ToString().Substring(4).Trim()));
35-
await _HubContext.Clients.All.SendAsync("todo_new", newKey, _ToDos[newKey].text);
42+
if (_Verbs.ContainsKey(arrArgs[0]))
43+
{
44+
await _Verbs[arrArgs[0]](arrArgs, this);
3645
}
37-
else if (arrArgs[0] == "remove" && int.TryParse(arrArgs[1], out var removeid) && _ToDos.Any(t => t.Key == removeid))
46+
47+
}
48+
49+
public Task Execute(IChatService chatService, string userName, ReadOnlyMemory<char> rhs)
50+
{
51+
throw new NotImplementedException();
52+
}
53+
54+
private static async Task SetSpeed(string[] args, ToDoCommand cmd)
55+
{
56+
if (float.TryParse(args[1], out var speed))
3857
{
39-
_ToDos.Remove(removeid);
40-
await _HubContext.Clients.All.SendAsync("todo_remove", removeid);
58+
await cmd._HubContext.Clients.All.SendAsync("todo_speed", speed);
59+
}
60+
}
61+
62+
private static async Task Deactivate(string[] args, ToDoCommand cmd)
63+
{
64+
65+
await cmd._HubContext.Clients.All.SendAsync("todo_deactivate");
66+
67+
}
68+
69+
private static async Task Activate(string[] args, ToDoCommand cmd)
70+
{
71+
72+
if (int.TryParse(args[1], out var id) && _ToDos.Any(t => t.Key == id)) {
73+
74+
await cmd._HubContext.Clients.All.SendAsync("todo_activate", id);
75+
4176
}
42-
else if (arrArgs[0] == "done" && int.TryParse(arrArgs[1], out var id) && _ToDos.Any(t => t.Key == id))
77+
78+
}
79+
80+
private static async Task ClearTodo(string[] args, ToDoCommand cmd)
81+
{
82+
if (int.TryParse(args[1], out var clearId) && _ToDos.Any(t => t.Key == clearId))
83+
{
84+
var todo = _ToDos[clearId];
85+
todo.completed = false;
86+
_ToDos[clearId] = todo;
87+
await cmd._HubContext.Clients.All.SendAsync("todo_clear", clearId);
88+
}
89+
}
90+
91+
private static async Task DoneTodo(string[] args, ToDoCommand cmd)
92+
{
93+
94+
if (int.TryParse(args[1], out var id) && _ToDos.Any(t => t.Key == id))
4395
{
4496
var todo = _ToDos[id];
4597
todo.completed = true;
4698
_ToDos[id] = todo;
47-
await _HubContext.Clients.All.SendAsync("todo_done", id);
99+
await cmd._HubContext.Clients.All.SendAsync("todo_done", id);
48100
}
49-
else if (arrArgs[0] == "speed" && float.TryParse(arrArgs[1], out var speed))
101+
102+
}
103+
104+
private static async Task RemoveTodo(string[] args, ToDoCommand cmd)
105+
{
106+
107+
if (int.TryParse(args[1], out var removeid) && _ToDos.Any(t => t.Key == removeid))
50108
{
51-
await _HubContext.Clients.All.SendAsync("todo_speed", speed);
109+
_ToDos.Remove(removeid);
110+
await cmd._HubContext.Clients.All.SendAsync("todo_remove", removeid);
52111
}
53112

54113
}
55114

56-
public Task Execute(IChatService chatService, string userName, ReadOnlyMemory<char> rhs)
115+
private static async Task AddTodo(string[] args, ToDoCommand cmd)
57116
{
58-
throw new NotImplementedException();
117+
var newKey = !_ToDos.Any() ? 1 : _ToDos.Max(t => t.Key) + 1;
118+
_ToDos.Add(newKey, (false, string.Join(' ', args).Substring(4).Trim()));
119+
await cmd._HubContext.Clients.All.SendAsync("todo_new", newKey, _ToDos[newKey].text);
120+
59121
}
60122

123+
61124
public static Dictionary<int, (bool completed, string text)> ToDos { get { return _ToDos; } }
62125

63126
}

Fritz.StreamTools/Pages/Team.cshtml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
@page
2+
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
3+
<!DOCTYPE html>
4+
5+
<html>
6+
<head>
7+
<meta name="viewport" content="width=device-width" />
8+
<title>ToDo Widget</title>
9+
<link rel="stylesheet" href="~/css/site.css" />
10+
<style>
11+
12+
@@keyframes fadeIn {
13+
0% {
14+
opacity: 0;
15+
}
16+
17+
100% {
18+
opacity: 1;
19+
}
20+
}
21+
22+
@@keyframes fadeOut {
23+
0% {
24+
opacity: 1;
25+
}
26+
27+
100% {
28+
opacity: 0;
29+
}
30+
}
31+
32+
.fadeIn {
33+
animation-name: fadeIn;
34+
animation-duration: 1s;
35+
animation-fill-mode: both;
36+
}
37+
38+
.fadeOut {
39+
animation-name: fadeOut;
40+
animation-duration: 1s;
41+
animation-fill-mode: both;
42+
} </style>
43+
<script src="https://kit.fontawesome.com/8ac2e0bf60.js" crossorigin="anonymous"></script>
44+
</head>
45+
<body>
46+
47+
<div id="d" class="todoWidget fadeOut" style="padding:10px">
48+
<h4>Welcome <span id="teammate"></span> from @Configuration["StreamServices:Twitch:TeamDisplayName"]</h4>
49+
<img height="240" style="margin: auto;" src="~/contents/img/teamlogo.png" />
50+
</div>
51+
52+
<script src="~/lib/signalr/signalr-client.js"></script>
53+
<script src="~/js/attentionhub.js"></script>
54+
<script>
55+
56+
var debug = false;
57+
var audio = new Audio('@Url.Content(@"~/contents/teammate.wav")');
58+
var widget = document.getElementById("d");
59+
60+
(function () {
61+
62+
this._hub = new AttentionHub();
63+
64+
this._hub.onTeammate = (name) => {
65+
document.getElementById("teammate").innerText = name;
66+
widget.classList.remove("fadeOut");
67+
widget.classList.add("fadeIn");
68+
audio.play();
69+
70+
window.setTimeout(function () {
71+
widget.classList.remove("fadeIn");
72+
widget.classList.add("fadeOut");
73+
}, 3000);
74+
75+
};
76+
77+
this._hub.start();
78+
79+
})();
80+
</script>
81+
</body>
82+
</html>

0 commit comments

Comments
 (0)