Skip to content

Commit b8022bb

Browse files
committed
Added the ability to monitor twitch streams with the .beef monitor command, which will notify the configured channel when it goes live.
1 parent 02d5556 commit b8022bb

12 files changed

+947
-221
lines changed

Beef/Application.cs

+99-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Beef.MmrReader;
2+
using Beef.TwitchManager;
23
using Discord;
34
using Discord.WebSocket;
45
using System;
@@ -9,8 +10,8 @@
910
using System.Windows.Threading;
1011

1112
namespace Beef {
12-
class Application : ProfileInfoProvider, MmrListener {
13-
private readonly String _version = "1.8.0";
13+
class Application : ProfileInfoProvider, MmrListener, TwitchLiveListener {
14+
private readonly String _version = "1.9.0";
1415
private BeefConfig _config;
1516
private String _botPrefix;
1617
private String _beefCommand;
@@ -21,6 +22,7 @@ class Application : ProfileInfoProvider, MmrListener {
2122
private bool _dynamicVoiceChannelsEnabled = false;
2223
private String _exePath;
2324
private MmrReader.MmrReader _mmrReader;
25+
private TwitchPollingService _twitchPollingService;
2426
private DispatcherSynchronizationContext _mainContext;
2527

2628
private DiscordSocketClient _discordClient;
@@ -54,8 +56,13 @@ public Application(BeefConfig config, String exePath) {
5456

5557
// The MMR reader is optional. If the settings are null, just skip it
5658
if (_config.MmrReaderConfig != null) {
57-
_mmrReader = new MmrReader.MmrReader(_config.MmrReaderConfig);
58-
_mmrReader.StartThread(this, this);
59+
_mmrReader = new MmrReader.MmrReader(this, this, _config.MmrReaderConfig);
60+
_mmrReader.StartThread();
61+
}
62+
63+
if (_config.TwitchConfig != null) {
64+
_twitchPollingService = new TwitchPollingService(_config.TwitchConfig, _exePath + "/TwitchPollingService", this);
65+
_twitchPollingService.StartThread();
5966
}
6067
}
6168

@@ -108,6 +115,11 @@ private async Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState sta
108115
await CheckDynamicChannels();
109116
}
110117

118+
/// <summary>
119+
/// Maintains exactly 1 empty channel of each dynamic channel by
120+
/// creating/deleting empty ones as needed.
121+
/// </summary>
122+
/// <returns>A task that can be waited on.</returns>
111123
private async Task CheckDynamicChannels() {
112124
if (_dynamicChannels == null)
113125
return; // Dynamic channels are not configured.
@@ -210,6 +222,74 @@ private void HandleCommand(SocketMessage userInput) {
210222
MessageChannel(channel, "Disabling dynamic channels.").GetAwaiter().GetResult();
211223
DisableDynamicChannels();
212224
code = ErrorCode.Success;
225+
} else if (arguments[1] == "monitor") {
226+
if (!IsLeader(author)) {
227+
MessageChannel(channel, "Who do you think you are? You don't have permission for that! Maybe get a job...make something of yourself. ¯\\_(ツ)_/¯").GetAwaiter().GetResult();
228+
return;
229+
}
230+
231+
if (_twitchPollingService == null) {
232+
MessageChannel(channel, "The twitch configuration has not been setup. Whoever manages this bot is pretty lazy tbh.").GetAwaiter().GetResult();
233+
return;
234+
}
235+
236+
if (arguments.Length != 3) {
237+
MessageChannel(channel, "That's not how you use this command, broski. Did you know that there's a **" + _botPrefix + _beefCommand + "** help command? It's like nobody even reads anything anymore.").GetAwaiter().GetResult();
238+
return;
239+
}
240+
241+
code = _twitchPollingService.MonitorStream(arguments[2]);
242+
if (code.Ok()) {
243+
MessageChannel(channel, "Monitoring " + arguments[2]).GetAwaiter().GetResult();
244+
}
245+
} else if (arguments[1] == "unmonitor") {
246+
if (!IsLeader(author)) {
247+
MessageChannel(channel, "Who do you think you are? You don't have permission for that! Maybe get a job...make something of yourself. ¯\\_(ツ)_/¯").GetAwaiter().GetResult();
248+
return;
249+
}
250+
251+
if (_twitchPollingService == null) {
252+
MessageChannel(channel, "The twitch configuration has not been setup. Whoever manages this bot is pretty lazy tbh.").GetAwaiter().GetResult();
253+
return;
254+
}
255+
256+
if (arguments.Length != 3) {
257+
MessageChannel(channel, "That's not how you use this command yo. Did you know that there's a **" + _botPrefix + _beefCommand + "** help command? It's like nobody even reads anything anymore.").GetAwaiter().GetResult();
258+
return;
259+
}
260+
261+
code = _twitchPollingService.StopMonitoringStream(arguments[2]);
262+
if (code.Ok()) {
263+
MessageChannel(channel, "No longer monitoring " + arguments[2]).GetAwaiter().GetResult();
264+
}
265+
} else if (arguments[1] == "listMonitoredStreams") {
266+
if (!IsLeader(author)) {
267+
MessageChannel(channel, "Who do you think you are? You don't have permission for that! Maybe get a job...make something of yourself. ¯\\_(ツ)_/¯").GetAwaiter().GetResult();
268+
return;
269+
}
270+
271+
if (_twitchPollingService == null) {
272+
MessageChannel(channel, "The twitch configuration has not been setup. Whoever manages this bot is pretty lazy tbh.").GetAwaiter().GetResult();
273+
return;
274+
}
275+
276+
if (arguments.Length != 2) {
277+
MessageChannel(channel, "That's not how you use this command yo. Did you know that there's a **" + _botPrefix + _beefCommand + "** help command? It's like nobody even reads anything anymore.").GetAwaiter().GetResult();
278+
return;
279+
}
280+
281+
List<TwitchPollingService.StreamInfo> monitoredStreams = _twitchPollingService.GetMonitoredStreams();
282+
if (monitoredStreams.Count == 0) {
283+
MessageChannel(channel, "No streams are being monitored yet.").GetAwaiter().GetResult();
284+
return;
285+
} else {
286+
String message = "_ _\nThe following streams are being monitored:";
287+
foreach (TwitchPollingService.StreamInfo stream in monitoredStreams) {
288+
message += "\n" + stream.GetTwitchUsername() + ": " + stream.StreamUrl;
289+
}
290+
MessageChannel(channel, message).GetAwaiter().GetResult();
291+
return;
292+
}
213293
} else if (arguments[1] == "register") {
214294
if (!IsLeader(author)) {
215295
MessageChannel(channel, "You don't have permission to do that.").GetAwaiter().GetResult();
@@ -595,6 +675,9 @@ private void HandleCommand(SocketMessage userInput) {
595675
help += "\t **%beef% undo** - Undoes the last change to the ladder (renames, wins, etc..).\n";
596676
help += "\t **%beef% enableDynamicChannels** - Enables dynamic channels (default). If there are dynamic channels set, the bot will ensure there is always exactly 1 empty of each configured dynamic channel.\n";
597677
help += "\t **%beef% disableDynamicChannels** - Disables dynamic channels. If there are dynamic channels set, the bot will no longer ensure there is always exactly 1 empty of each configured dynamic channel.\n";
678+
help += "\t **%beef% monitor <TwitchStreamLink>** - Starts monitoring the given twitch stream and notifies the configured channel when they go live. <TwitchStreamLink> must be of the form https://www.twitch.tv/twitchname \n";
679+
help += "\t **%beef% unmonitor <TwitchStreamLink>** - Stops monitoring the given twitch stream and no longer notifies the configured channel when they go live. <TwitchStreamLink> must be of the form https://www.twitch.tv/twitchname \n";
680+
help += "\t **%beef% listMonitoredStreams** - Lists the channels being monitored currently.\n";
598681
help += "\t **%beef% version** - Prints the version of BeefBot\n";
599682
help = help.Replace("%beef%", _botPrefix + _beefCommand);
600683

@@ -837,5 +920,17 @@ public void OnMmrRead(List<Tuple<ProfileInfo, LadderInfo>> mmrList) {
837920
}
838921
}, null);
839922
}
923+
924+
public void OnTwitchStreamLive(string twitchName, string goLiveMessage) {
925+
_mainContext.Post((_) => {
926+
foreach (SocketGuild guild in _discordClient.Guilds) {
927+
foreach (SocketTextChannel channel in guild.TextChannels) {
928+
if (channel.Name.Equals(_twitchPollingService.GetConfiguration().GoLiveChannel)) {
929+
channel.SendMessageAsync(goLiveMessage).GetAwaiter().GetResult();
930+
}
931+
}
932+
}
933+
}, null);
934+
}
840935
}
841936
}

Beef/Beef.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@
166166
<Compile Include="Properties\AssemblyInfo.cs" />
167167
<Compile Include="BeefConfig.cs" />
168168
<Compile Include="MmrReader\ReaderConfig.cs" />
169+
<Compile Include="SharedServices\AccessToken.cs" />
170+
<Compile Include="SharedServices\PollingService.cs" />
171+
<Compile Include="TwitchManager\TwitchConfig.cs" />
172+
<Compile Include="TwitchManager\ResponseModels\TwitchHelixStreamsResponse.cs" />
173+
<Compile Include="TwitchManager\TwitchLiveListener.cs" />
174+
<Compile Include="TwitchManager\TwitchPollingService.cs" />
169175
</ItemGroup>
170176
<ItemGroup>
171177
<None Include="App.config" />

Beef/BeefConfig.cs

+43-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Beef {
77
/// </summary>
88
public class BeefConfig {
99
// This is the current version and should always be incremented when changing the config file format.
10-
public static int BeefConfigVersion = 3;
10+
public static int BeefConfigVersion = 4;
1111

1212
// Version
1313
public int Version { get; set; } = BeefConfigVersion; // This identifies the version of the config file.
@@ -25,8 +25,12 @@ public class BeefConfig {
2525
public String GoogleApiPresentationId { get; set; } = ""; // This is the Google doc IDs of the presentation.
2626
public String BeefLadderLink { get; set; } = ""; // This is the ladders that can be viewed by users.
2727

28+
// Config for reading MMRs using the battle.net API
2829
public ReaderConfig MmrReaderConfig { get; set; } = ReaderConfig.CreateDefault();
2930

31+
// Config for interacting with Twitch
32+
public TwitchConfig TwitchConfig {get; set; } = new TwitchConfig();
33+
3034
/// <summary>
3135
/// Creates a ReaderConfig with default settings.
3236
/// </summary>
@@ -43,6 +47,44 @@ public static BeefConfig CreateDefault() {
4347
// Keep track of old config versions in case we want to be
4448
// able to update old versions in a smarter way.
4549
#region OldConfigVersions
50+
/// <summary>
51+
/// Keeps track of settings for the config file. This class needs to exactly match the Config.json.example file so that it can be
52+
/// deserialized into it. So if you rename or change parameters here, you must upgrade the config file format as well.
53+
/// </summary>
54+
public class BeefConfigV3 {
55+
// This is the current version and should always be incremented when changing the config file format.
56+
public static int BeefConfigVersion = 3;
57+
58+
// Version
59+
public int Version { get; set; } = BeefConfigVersion; // This identifies the version of the config file.
60+
61+
// Discord stuff
62+
public String DiscordBotToken { get; set; } = "";
63+
public String BotPrefix { get; set; } = ".";
64+
public String BeefCommand { get; set; } = "beef";
65+
public String[] LeaderRoles { get; set; } = new String[] { "ExampleRole1", "ExampleRole2" };
66+
public String[] DynamicChannels { get; set; } = new String[] { "Teams" }; // Names of channels that there should always be exactly 1 empty of
67+
68+
// Presentation Stuff
69+
public String GoogleApiCredentialFile { get; set; } = "credentials.json";
70+
public String GoogleApiApplicationName { get; set; } = "";
71+
public String GoogleApiPresentationId { get; set; } = ""; // This is the Google doc IDs of the presentation.
72+
public String BeefLadderLink { get; set; } = ""; // This is the ladders that can be viewed by users.
73+
74+
public ReaderConfig MmrReaderConfig { get; set; } = ReaderConfig.CreateDefault();
75+
76+
/// <summary>
77+
/// Creates a ReaderConfig with default settings.
78+
/// </summary>
79+
/// <returns>Returns the created config.</returns>
80+
public static BeefConfig CreateDefault() {
81+
// Fill out the default settings and version
82+
BeefConfig config = new BeefConfig();
83+
84+
// The credentials are left blank and need to be filled out after
85+
return config;
86+
}
87+
}
4688

4789
/// <summary>
4890
/// Keeps track of settings for the config file. This class needs to exactly match the Config.json.example file so that it can be

Beef/ErrorCodes.cs

+14
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public enum ErrorCode {
5757
// Presentation API
5858
RequestException,
5959

60+
// Twitch
61+
TwitchFileCouldNotSerialize,
62+
TwitchFileCouldNotWriteFile,
63+
TwitchUserDoesNotExist,
64+
InvalidTwitchUrl,
65+
6066
}
6167

6268
public static class ErrorCodeMethods {
@@ -116,6 +122,14 @@ public static String GetUserMessage(this ErrorCode code, String botPrefix) {
116122
return "I couldn't write the beef file for some reason!";
117123
case ErrorCode.CouldNotDeleteFile:
118124
return "I couldn't delete the file for some reason!";
125+
case ErrorCode.TwitchFileCouldNotSerialize:
126+
return "I wasn't able to make the record serialize. I'm sure this is your fault somehow.";
127+
case ErrorCode.TwitchFileCouldNotWriteFile:
128+
return "The file couldn't be written. Did you corrupt the file system or something? How could you?";
129+
case ErrorCode.TwitchUserDoesNotExist:
130+
return "That twitch stream isn't being monitored, scrub.";
131+
case ErrorCode.InvalidTwitchUrl:
132+
return "That twitch URL isn't even valid. Maybe you should monitor your copy/paste skills, huh?";
119133
default:
120134
return "Well I don't know what happened but it can't be good.";
121135
}

0 commit comments

Comments
 (0)