This commit is contained in:
samvaio 2016-12-04 01:35:37 +05:30 committed by GitHub
parent e62bdb916d
commit 35ca4478da

View File

@ -0,0 +1,410 @@
using Discord.Commands;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Services;
using System.Threading;
using System.Collections.Generic;
using NadekoBot.Services.Database.Models;
using System.Net.Http;
using Discord.WebSocket;
using NadekoBot.Attributes;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using NLog;
using NadekoBot.Services.Database;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
public class StreamStatus
{
public bool IsLive { get; set; }
public string ApiLink { get; set; }
public string Views { get; set; }
}
public class HitboxResponse {
public bool Success { get; set; } = true;
[JsonProperty("media_is_live")]
public string MediaIsLive { get; set; }
public bool IsLive => MediaIsLive == "1";
[JsonProperty("media_views")]
public string Views { get; set; }
}
public class TwitchResponse
{
public string Error { get; set; } = null;
public bool IsLive => Stream != null;
public StreamInfo Stream { get; set; }
public class StreamInfo
{
public int Viewers { get; set; }
}
}
public class BeamResponse
{
public string Error { get; set; } = null;
[JsonProperty("online")]
public bool IsLive { get; set; }
public int ViewersCurrent { get; set; }
}
public class StreamNotFoundException : Exception
{
public StreamNotFoundException(string message) : base("Stream '" + message + "' not found.")
{
}
}
[Group]
public class StreamNotificationCommands
{
private Timer checkTimer { get; }
private ConcurrentDictionary<string, StreamStatus> oldCachedStatuses = new ConcurrentDictionary<string, StreamStatus>();
private ConcurrentDictionary<string, StreamStatus> cachedStatuses = new ConcurrentDictionary<string, StreamStatus>();
private Logger _log { get; }
private bool FirstPass { get; set; } = true;
public StreamNotificationCommands()
{
_log = NLog.LogManager.GetCurrentClassLogger();
checkTimer = new Timer(async (state) =>
{
oldCachedStatuses = new ConcurrentDictionary<string, StreamStatus>(cachedStatuses);
cachedStatuses.Clear();
IEnumerable<FollowedStream> streams;
using (var uow = DbHandler.UnitOfWork())
{
streams = uow.GuildConfigs.GetAllFollowedStreams();
}
await Task.WhenAll(streams.Select(async fs =>
{
try
{
var newStatus = await GetStreamStatus(fs).ConfigureAwait(false);
if (FirstPass)
{
return;
}
StreamStatus oldStatus;
if (oldCachedStatuses.TryGetValue(newStatus.ApiLink, out oldStatus) &&
oldStatus.IsLive != newStatus.IsLive)
{
var server = NadekoBot.Client.GetGuild(fs.GuildId);
var channel = server?.GetTextChannel(fs.ChannelId);
if (channel == null)
return;
try { await channel.EmbedAsync(fs.GetEmbed(newStatus).Build()).ConfigureAwait(false); } catch { }
}
}
catch (Exception ex)
{
}
}));
FirstPass = false;
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(60));
}
private async Task<StreamStatus> GetStreamStatus(FollowedStream stream, bool checkCache = true)
{
string response;
StreamStatus result;
switch (stream.Type)
{
case FollowedStream.FollowedStreamType.Hitbox:
var hitboxUrl = $"https://api.hitbox.tv/media/status/{stream.Username.ToLowerInvariant()}";
if (checkCache && cachedStatuses.TryGetValue(hitboxUrl, out result))
return result;
using (var http = new HttpClient())
{
response = await http.GetStringAsync(hitboxUrl).ConfigureAwait(false);
}
var hbData = JsonConvert.DeserializeObject<HitboxResponse>(response);
if (!hbData.Success)
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
result = new StreamStatus()
{
IsLive = hbData.IsLive,
ApiLink = hitboxUrl,
Views = hbData.Views
};
cachedStatuses.AddOrUpdate(hitboxUrl, result, (key, old) => result);
return result;
case FollowedStream.FollowedStreamType.Twitch:
var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6";
if (checkCache && cachedStatuses.TryGetValue(twitchUrl, out result))
return result;
using (var http = new HttpClient())
{
response = await http.GetStringAsync(twitchUrl).ConfigureAwait(false);
}
var twData = JsonConvert.DeserializeObject<TwitchResponse>(response);
if (twData.Error != null)
{
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
}
result = new StreamStatus()
{
IsLive = twData.IsLive,
ApiLink = twitchUrl,
Views = twData.Stream?.Viewers.ToString() ?? "0"
};
cachedStatuses.AddOrUpdate(twitchUrl, result, (key, old) => result);
return result;
case FollowedStream.FollowedStreamType.Beam:
var beamUrl = $"https://beam.pro/api/v1/channels/{stream.Username.ToLowerInvariant()}";
if (checkCache && cachedStatuses.TryGetValue(beamUrl, out result))
return result;
using (var http = new HttpClient())
{
response = await http.GetStringAsync(beamUrl).ConfigureAwait(false);
}
var bmData = JsonConvert.DeserializeObject<BeamResponse>(response);
if (bmData.Error != null)
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
result = new StreamStatus()
{
IsLive = bmData.IsLive,
ApiLink = beamUrl,
Views = bmData.ViewersCurrent.ToString()
};
cachedStatuses.AddOrUpdate(beamUrl, result, (key, old) => result);
return result;
default:
break;
}
return null;
}
//[NadekoCommand, Usage, Description, Aliases]
//[RequireContext(ContextType.Guild)]
//public async Task Test(IUserMessage imsg)
//{
// var channel = (ITextChannel)imsg.Channel;
// await channel.EmbedAsync(new Discord.API.Embed()
// {
// Title = "Imqtpie",
// Url = "https://twitch.tv/masterkwoth",
// Fields = new[] {
// new Discord.API.EmbedField()
// {
// Name = "Status",
// Value = "Online",
// Inline = true,
// },
// new Discord.API.EmbedField()
// {
// Name = "Viewers",
// Value = "123123",
// Inline = true
// },
// new Discord.API.EmbedField()
// {
// Name = "Platform",
// Value = "Twitch",
// Inline = true
// },
// },
// Color = NadekoBot.OkColor
// });
//}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequirePermission(GuildPermission.ManageMessages)]
public async Task Hitbox(IUserMessage msg, [Remainder] string username) =>
await TrackStream((ITextChannel)msg.Channel, username, FollowedStream.FollowedStreamType.Hitbox)
.ConfigureAwait(false);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequirePermission(GuildPermission.ManageMessages)]
public async Task Twitch(IUserMessage msg, [Remainder] string username) =>
await TrackStream((ITextChannel)msg.Channel, username, FollowedStream.FollowedStreamType.Twitch)
.ConfigureAwait(false);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequirePermission(GuildPermission.ManageMessages)]
public async Task Beam(IUserMessage msg, [Remainder] string username) =>
await TrackStream((ITextChannel)msg.Channel, username, FollowedStream.FollowedStreamType.Beam)
.ConfigureAwait(false);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ListStreams(IUserMessage imsg)
{
var channel = (ITextChannel)imsg.Channel;
IEnumerable<FollowedStream> streams;
using (var uow = DbHandler.UnitOfWork())
{
streams = uow.GuildConfigs
.For(channel.Guild.Id,
set => set.Include(gc => gc.FollowedStreams))
.FollowedStreams;
}
if (!streams.Any())
{
await channel.SendMessageAsync("You are not following any streams on this server.").ConfigureAwait(false);
return;
}
var text = string.Join("\n", streams.Select(snc =>
{
return $"`{snc.Username}`'s stream on **{channel.Guild.GetTextChannel(snc.ChannelId)?.Name}** channel. 【`{snc.Type.ToString()}`】";
}));
await channel.SendMessageAsync($"You are following **{streams.Count()}** streams on this server.\n\n" + text).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequirePermission(GuildPermission.ManageMessages)]
public async Task RemoveStream(IUserMessage msg, FollowedStream.FollowedStreamType type, [Remainder] string username)
{
var channel = (ITextChannel)msg.Channel;
username = username.ToLowerInvariant().Trim();
var fs = new FollowedStream()
{
ChannelId = channel.Id,
Username = username,
Type = type
};
bool removed;
using (var uow = DbHandler.UnitOfWork())
{
var config = uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.FollowedStreams));
removed = config.FollowedStreams.Remove(fs);
if (removed)
await uow.CompleteAsync().ConfigureAwait(false);
}
if (!removed)
{
await channel.SendMessageAsync(":anger: No such stream.").ConfigureAwait(false);
return;
}
await channel.SendMessageAsync($":ok: Removed `{username}`'s stream ({type}) from notifications.").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task CheckStream(IUserMessage imsg, FollowedStream.FollowedStreamType platform, [Remainder] string username)
{
var channel = (ITextChannel)imsg.Channel;
var stream = username?.Trim();
if (string.IsNullOrWhiteSpace(stream))
return;
try
{
var streamStatus = (await GetStreamStatus(new FollowedStream
{
Username = stream,
Type = platform,
}));
if (streamStatus.IsLive)
{
await channel.SendMessageAsync($"`Streamer {username} is online with {streamStatus.Views} viewers.`");
}
else
{
await channel.SendMessageAsync($"`Streamer {username} is offline.`");
}
}
catch
{
await channel.SendMessageAsync("No channel found.");
}
}
private async Task TrackStream(ITextChannel channel, string username, FollowedStream.FollowedStreamType type)
{
username = username.Trim();
var fs = new FollowedStream
{
GuildId = channel.Guild.Id,
ChannelId = channel.Id,
Username = username,
Type = type,
};
StreamStatus status;
try
{
status = await GetStreamStatus(fs).ConfigureAwait(false);
}
catch
{
await channel.SendMessageAsync(":anger: Stream probably doesn't exist.").ConfigureAwait(false);
return;
}
using (var uow = DbHandler.UnitOfWork())
{
uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.FollowedStreams))
.FollowedStreams
.Add(fs);
await uow.CompleteAsync().ConfigureAwait(false);
}
var msg = $":ok: I will notify this channel when status changes.";
await channel.EmbedAsync(fs.GetEmbed(status).Build(), msg).ConfigureAwait(false);
}
}
}
public static class FollowedStreamExtensions
{
public static EmbedBuilder GetEmbed(this FollowedStream fs, Searches.StreamStatus status)
{
var embed = new EmbedBuilder().WithTitle(fs.Username)
.WithUrl(fs.GetLink())
.AddField(efb => efb.WithName("Status")
.WithValue(status.IsLive ? "Online" : "Offline")
.WithIsInline(true))
.AddField(efb => efb.WithName("Viewers")
.WithValue(status.IsLive ? status.Views : "-")
.WithIsInline(true))
.AddField(efb => efb.WithName("Platform")
.WithValue(fs.Type.ToString())
.WithIsInline(true))
.WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor);
return embed;
}
public static string GetLink(this FollowedStream fs) {
if (fs.Type == FollowedStream.FollowedStreamType.Hitbox)
return $"http://www.hitbox.tv/{fs.Username}/";
else if (fs.Type == FollowedStream.FollowedStreamType.Twitch)
return $"http://www.twitch.tv/{fs.Username}/";
else if (fs.Type == FollowedStream.FollowedStreamType.Beam)
return $"https://beam.pro/{fs.Username}/";
else
return "??";
}
}
}