Killed history

This commit is contained in:
Kwoth
2021-09-06 21:29:22 +02:00
commit 7aca29ae8a
950 changed files with 366651 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
using AngleSharp;
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using AngleSharp.Html.Dom;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class AnimeSearchCommands : NadekoSubmodule<AnimeSearchService>
{
[NadekoCommand, Usage, Description, Aliases]
public async Task Novel([Leftover] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
var novelData = await _service.GetNovelData(query).ConfigureAwait(false);
if (novelData == null)
{
await ReplyErrorLocalizedAsync("failed_finding_novel").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(novelData.Description.Replace("<br>", Environment.NewLine, StringComparison.InvariantCulture))
.WithTitle(novelData.Title)
.WithUrl(novelData.Link)
.WithImageUrl(novelData.ImageUrl)
.AddField(efb => efb.WithName(GetText("authors")).WithValue(string.Join("\n", novelData.Authors)).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("status")).WithValue(novelData.Status).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(" ", novelData.Genres.Any() ? novelData.Genres : new[] { "none" })).WithIsInline(true))
.WithFooter(efb => efb.WithText(GetText("score") + " " + novelData.Score));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Mal([Leftover] string name)
{
if (string.IsNullOrWhiteSpace(name))
return;
var fullQueryLink = "https://myanimelist.net/profile/" + name;
var config = Configuration.Default.WithDefaultLoader();
using (var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false))
{
var imageElem = document.QuerySelector("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img");
var imageUrl = ((IHtmlImageElement)imageElem)?.Source ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png";
var stats = document.QuerySelectorAll("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span").Select(x => x.InnerHtml).ToList();
var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc");
var favAnime = GetText("anime_no_fav");
if (favorites.Length > 0 && favorites[0].QuerySelector("p") == null)
favAnime = string.Join("\n", favorites[0].QuerySelectorAll("ul > li > div.di-tc.va-t > a")
.Shuffle()
.Take(3)
.Select(x =>
{
var elem = (IHtmlAnchorElement)x;
return $"[{elem.InnerHtml}]({elem.Href})";
}));
var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix")
.Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml))
.ToList();
var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div")
.Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray())
.ToArray();
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("mal_profile", name))
.AddField(efb => efb.WithName("💚 " + GetText("watching")).WithValue(stats[0]).WithIsInline(true))
.AddField(efb => efb.WithName("💙 " + GetText("completed")).WithValue(stats[1]).WithIsInline(true));
if (info.Count < 3)
embed.AddField(efb => efb.WithName("💛 " + GetText("on_hold")).WithValue(stats[2]).WithIsInline(true));
embed
.AddField(efb => efb.WithName("💔 " + GetText("dropped")).WithValue(stats[3]).WithIsInline(true))
.AddField(efb => efb.WithName("⚪ " + GetText("plan_to_watch")).WithValue(stats[4]).WithIsInline(true))
.AddField(efb => efb.WithName("🕐 " + daysAndMean[0][0]).WithValue(daysAndMean[0][1]).WithIsInline(true))
.AddField(efb => efb.WithName("📊 " + daysAndMean[1][0]).WithValue(daysAndMean[1][1]).WithIsInline(true))
.AddField(efb => efb.WithName(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1).WithValue(info[0].Item2.TrimTo(20)).WithIsInline(true))
.AddField(efb => efb.WithName(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1).WithValue(info[1].Item2.TrimTo(20)).WithIsInline(true));
if (info.Count > 2)
embed.AddField(efb => efb.WithName(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1).WithValue(info[2].Item2.TrimTo(20)).WithIsInline(true));
embed
.WithDescription($@"
** https://myanimelist.net/animelist/{ name } **
**{GetText("top_3_fav_anime")}**
{favAnime}"
)
.WithUrl(fullQueryLink)
.WithImageUrl(imageUrl);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
private static string MalInfoToEmoji(string info)
{
info = info.Trim().ToLowerInvariant();
switch (info)
{
case "gender":
return "🚁";
case "location":
return "🗺";
case "last online":
return "👥";
case "birthday":
return "📆";
default:
return "❔";
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task Mal(IGuildUser usr) => Mal(usr.Username);
[NadekoCommand, Usage, Description, Aliases]
public async Task Anime([Leftover] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
var animeData = await _service.GetAnimeData(query).ConfigureAwait(false);
if (animeData == null)
{
await ReplyErrorLocalizedAsync("failed_finding_anime").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(animeData.Synopsis.Replace("<br>", Environment.NewLine, StringComparison.InvariantCulture))
.WithTitle(animeData.TitleEnglish)
.WithUrl(animeData.Link)
.WithImageUrl(animeData.ImageUrlLarge)
.AddField(efb => efb.WithName(GetText("episodes")).WithValue(animeData.TotalEpisodes.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("status")).WithValue(animeData.AiringStatus.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" })).WithIsInline(true))
.WithFooter(efb => efb.WithText(GetText("score") + " " + animeData.AverageScore + " / 100"));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Manga([Leftover] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
var mangaData = await _service.GetMangaData(query).ConfigureAwait(false);
if (mangaData == null)
{
await ReplyErrorLocalizedAsync("failed_finding_manga").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(mangaData.Synopsis.Replace("<br>", Environment.NewLine, StringComparison.InvariantCulture))
.WithTitle(mangaData.TitleEnglish)
.WithUrl(mangaData.Link)
.WithImageUrl(mangaData.ImageUrlLge)
.AddField(efb => efb.WithName(GetText("chapters")).WithValue(mangaData.TotalChapters.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("status")).WithValue(mangaData.PublishingStatus.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" })).WithIsInline(true))
.WithFooter(efb => efb.WithText(GetText("score") + " " + mangaData.AverageScore + " / 100"));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,26 @@
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common
{
public class AnimeResult
{
public int Id { get; set; }
public string AiringStatus => AiringStatusParsed.ToTitleCase();
[JsonProperty("airing_status")]
public string AiringStatusParsed { get; set; }
[JsonProperty("title_english")]
public string TitleEnglish { get; set; }
[JsonProperty("total_episodes")]
public int TotalEpisodes { get; set; }
public string Description { get; set; }
[JsonProperty("image_url_lge")]
public string ImageUrlLarge { get; set; }
public string[] Genres { get; set; }
[JsonProperty("average_score")]
public string AverageScore { get; set; }
public string Link => "http://anilist.co/anime/" + Id;
public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "...";
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class BibleVerses
{
public string Error { get; set; }
public BibleVerse[] Verses { get; set; }
}
public class BibleVerse
{
[JsonProperty("book_name")]
public string BookName { get; set; }
public int Chapter { get; set; }
public int Verse { get; set; }
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class CryptoResponse
{
public List<CryptoResponseData> Data { get; set; }
}
public class CryptoResponseData
{
public string Id { get; set; }
public string Name { get; set; }
public string Symbol { get; set; }
public string Slug { get; set; }
[JsonProperty("cmc_rank")]
public int Rank { get; set; }
public CurrencyQuotes Quote { get; set; }
}
public class CurrencyQuotes
{
public Quote Usd { get; set; }
}
public class Quote
{
public double Price { get; set; }
public double Market_Cap { get; set; }
public string Percent_Change_1h { get; set; }
public string Percent_Change_24h { get; set; }
public string Percent_Change_7d { get; set; }
public double? Volume_24h { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NadekoBot.Modules.Searches.Common
{
public class Audio
{
public string Url { get; set; }
}
public class Example
{
public List<Audio> Audio { get; set; }
public string Text { get; set; }
}
public class GramaticalInfo
{
public string Type { get; set; }
}
public class Sens
{
public object Definition { get; set; }
public List<Example> Examples { get; set; }
[JsonProperty("gramatical_info")]
public GramaticalInfo GramaticalInfo { get; set; }
}
public class Result
{
[JsonProperty("part_of_speech")]
public string PartOfSpeech { get; set; }
public List<Sens> Senses { get; set; }
public string Url { get; set; }
}
public class DefineModel
{
public List<Result> Results { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace NadekoBot.Modules.Searches.Common
{
public class E621Object
{
public class FileData
{
public string Url { get; set; }
}
public class TagData
{
public string[] General { get; set; }
}
public class ScoreData
{
public string Total { get; set; }
}
public FileData File { get; set; }
public TagData Tags { get; set; }
public ScoreData Score { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace NadekoBot.Modules.Searches.Common.Exceptions
{
public class StreamNotFoundException : Exception
{
public StreamNotFoundException()
{
}
public StreamNotFoundException(string message) : base(message)
{
}
public StreamNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
namespace NadekoBot.Core.Modules.Searches.Common
{
public sealed class Tag
{
public string Name { get; set; }
public string Url { get; set; }
}
public sealed class Gallery
{
public string Id { get; }
public string Url { get; }
public string FullTitle { get; }
public string Title { get; }
public string Thumbnail { get; }
public int PageCount { get; }
public int Likes { get; }
public DateTime UploadedAt { get; }
public Tag[] Tags { get; }
public Gallery(
string id,
string url,
string fullTitle,
string title,
string thumbnail,
int pageCount,
int likes,
DateTime uploadedAt,
Tag[] tags)
{
Id = id;
Url = url;
FullTitle = fullTitle;
Title = title;
Thumbnail = thumbnail;
PageCount = pageCount;
Likes = likes;
UploadedAt = uploadedAt;
Tags = tags;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class UserData
{
[JsonProperty("abbr")] public object Abbr { get; set; }
[JsonProperty("clanid")] public object Clanid { get; set; }
[JsonProperty("country")] public string Country { get; set; }
[JsonProperty("favourite_mode")] public int FavouriteMode { get; set; }
[JsonProperty("followers_count")] public int FollowersCount { get; set; }
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("latest_activity")] public int LatestActivity { get; set; }
[JsonProperty("play_style")] public int PlayStyle { get; set; }
[JsonProperty("privileges")] public int Privileges { get; set; }
[JsonProperty("registered_on")] public int RegisteredOn { get; set; }
[JsonProperty("username")] public string Username { get; set; }
[JsonProperty("username_aka")] public string UsernameAka { get; set; }
}
public class GatariUserResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("users")] public List<UserData> Users { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class UserStats
{
[JsonProperty("a_count")] public int ACount { get; set; }
[JsonProperty("avg_accuracy")] public double AvgAccuracy { get; set; }
[JsonProperty("avg_hits_play")] public double AvgHitsPlay { get; set; }
[JsonProperty("country_rank")] public int CountryRank { get; set; }
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("level")] public int Level { get; set; }
[JsonProperty("level_progress")] public int LevelProgress { get; set; }
[JsonProperty("max_combo")] public int MaxCombo { get; set; }
[JsonProperty("playcount")] public int Playcount { get; set; }
[JsonProperty("playtime")] public int Playtime { get; set; }
[JsonProperty("pp")] public int Pp { get; set; }
[JsonProperty("rank")] public int Rank { get; set; }
[JsonProperty("ranked_score")] public int RankedScore { get; set; }
[JsonProperty("replays_watched")] public int ReplaysWatched { get; set; }
[JsonProperty("s_count")] public int SCount { get; set; }
[JsonProperty("sh_count")] public int ShCount { get; set; }
[JsonProperty("total_hits")] public int TotalHits { get; set; }
[JsonProperty("total_score")] public long TotalScore { get; set; }
[JsonProperty("x_count")] public int XCount { get; set; }
[JsonProperty("xh_count")] public int XhCount { get; set; }
}
public class GatariUserStatsResponse
{
[JsonProperty("code")] public int Code { get; set; }
[JsonProperty("stats")] public UserStats Stats { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
namespace NadekoBot.Modules.Searches.Common
{
public sealed class GoogleSearchResult
{
public string Title { get; }
public string Link { get; }
public string Text { get; }
public GoogleSearchResult(string title, string link, string text)
{
this.Title = title;
this.Link = link;
this.Text = text;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace NadekoBot.Core.Modules.Searches.Common
{
public class HearthstoneCardData
{
public string Text { get; set; }
public string Flavor { get; set; }
public bool Collectible { get; set; }
public string Img { get; set; }
public string ImgGold { get; set; }
public string PlayerClass { get; set; }
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
namespace NadekoBot.Modules.Searches.Common
{
public class ImageCacherObject : IComparable<ImageCacherObject>
{
public DapiSearchType SearchType { get; }
public string FileUrl { get; }
public HashSet<string> Tags { get; }
public string Rating { get; }
public ImageCacherObject(DapiImageObject obj, DapiSearchType type)
{
if (type == DapiSearchType.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
{
this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
}
else
{
this.FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture) ? obj.FileUrl : "https:" + obj.FileUrl;
}
this.SearchType = type;
this.Rating = obj.Rating;
this.Tags = new HashSet<string>((obj.Tags ?? obj.TagString).Split(' '));
}
public ImageCacherObject(string url, DapiSearchType type, string tags, string rating)
{
this.SearchType = type;
this.FileUrl = url;
this.Tags = new HashSet<string>(tags.Split(' '));
this.Rating = rating;
}
public override string ToString()
{
return FileUrl;
}
public int CompareTo(ImageCacherObject other)
{
return string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Searches.Common
{
public class MagicItem
{
public string Name { get; set; }
public string Description { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common
{
public class MangaResult
{
public int Id { get; set; }
[JsonProperty("publishing_status")]
public string PublishingStatus { get; set; }
[JsonProperty("image_url_lge")]
public string ImageUrlLge { get; set; }
[JsonProperty("title_english")]
public string TitleEnglish { get; set; }
[JsonProperty("total_chapters")]
public int TotalChapters { get; set; }
[JsonProperty("total_volumes")]
public int TotalVolumes { get; set; }
public string Description { get; set; }
public string[] Genres { get; set; }
[JsonProperty("average_score")]
public string AverageScore { get; set; }
public string Link => "http://anilist.co/manga/" + Id;
public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "...";
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class MtgData
{
public string Name { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public string StoreUrl { get; set; }
public string Types { get; set; }
public string ManaCost { get; set; }
}
public class MtgResponse
{
public class Data
{
public string Name { get; set; }
public string ManaCost { get; set; }
public string Text { get; set; }
public List<string> Types { get; set; }
public string ImageUrl { get; set; }
}
public List<Data> Cards { get; set; }
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public static class NhentaiApiModel
{
public class Title
{
[JsonProperty("english")] public string English { get; set; }
[JsonProperty("japanese")] public string Japanese { get; set; }
[JsonProperty("pretty")] public string Pretty { get; set; }
}
public class Page
{
[JsonProperty("t")] public string T { get; set; }
[JsonProperty("w")] public int W { get; set; }
[JsonProperty("h")] public int H { get; set; }
}
public class Cover
{
[JsonProperty("t")] public string T { get; set; }
[JsonProperty("w")] public int W { get; set; }
[JsonProperty("h")] public int H { get; set; }
}
public class Thumbnail
{
[JsonProperty("t")] public string T { get; set; }
[JsonProperty("w")] public int W { get; set; }
[JsonProperty("h")] public int H { get; set; }
}
public class Images
{
[JsonProperty("pages")] public List<Page> Pages { get; set; }
[JsonProperty("cover")] public Cover Cover { get; set; }
[JsonProperty("thumbnail")] public Thumbnail Thumbnail { get; set; }
}
public class Tag
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("count")] public int Count { get; set; }
}
public class Gallery
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("media_id")] public string MediaId { get; set; }
[JsonProperty("title")] public Title Title { get; set; }
[JsonProperty("images")] public Images Images { get; set; }
[JsonProperty("scanlator")] public string Scanlator { get; set; }
[JsonProperty("upload_date")] public double UploadDate { get; set; }
[JsonProperty("tags")] public Tag[] Tags { get; set; }
[JsonProperty("num_pages")] public int NumPages { get; set; }
[JsonProperty("num_favorites")] public int NumFavorites { get; set; }
}
public class SearchResult
{
[JsonProperty("result")] public Gallery[] Result { get; set; }
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NadekoBot.Modules.Searches.Common
{
public class NovelResult
{
public string Description { get; set; }
public string Title { get; set; }
public string Link { get; set; }
public string ImageUrl { get; set; }
public string[] Authors { get; set; }
public string Status { get; set; }
public string[] Genres { get; set; }
public string Score { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
namespace NadekoBot.Core.Modules.Searches.Common
{
public class OmdbMovie
{
public string Title { get; set; }
public string Year { get; set; }
public string ImdbRating { get; set; }
public string ImdbId { get; set; }
public string Genre { get; set; }
public string Plot { get; set; }
public string Poster { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class OsuUserData
{
[JsonProperty("user_id")] public string UserId { get; set; }
[JsonProperty("username")] public string Username { get; set; }
[JsonProperty("join_date")] public string JoinDate { get; set; }
[JsonProperty("count300")] public string Count300 { get; set; }
[JsonProperty("count100")] public string Count100 { get; set; }
[JsonProperty("count50")] public string Count50 { get; set; }
[JsonProperty("playcount")] public string Playcount { get; set; }
[JsonProperty("ranked_score")] public string RankedScore { get; set; }
[JsonProperty("total_score")] public string TotalScore { get; set; }
[JsonProperty("pp_rank")] public string PpRank { get; set; }
[JsonProperty("level")] public double Level { get; set; }
[JsonProperty("pp_raw")] public double PpRaw { get; set; }
[JsonProperty("accuracy")] public double Accuracy { get; set; }
[JsonProperty("count_rank_ss")] public string CountRankSs { get; set; }
[JsonProperty("count_rank_ssh")] public string CountRankSsh { get; set; }
[JsonProperty("count_rank_s")] public string CountRankS { get; set; }
[JsonProperty("count_rank_sh")] public string CountRankSh { get; set; }
[JsonProperty("count_rank_a")] public string CountRankA { get; set; }
[JsonProperty("country")] public string Country { get; set; }
[JsonProperty("total_seconds_played")] public string TotalSecondsPlayed { get; set; }
[JsonProperty("pp_country_rank")] public string PpCountryRank { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
using Newtonsoft.Json;
using System;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class Account
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("league")]
public string League { get; set; }
[JsonProperty("classId")]
public int ClassId { get; set; }
[JsonProperty("ascendancyClass")]
public int AscendancyClass { get; set; }
[JsonProperty("class")]
public string Class { get; set; }
[JsonProperty("level")]
public int Level { get; set; }
}
public class Leagues
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("startAt")]
public DateTime StartAt { get; set; }
[JsonProperty("endAt")]
public object EndAt { get; set; }
}
}

View File

@@ -0,0 +1,312 @@
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Serilog;
namespace NadekoBot.Modules.Searches.Common
{
// note: this is not the code that public nadeko is using
public class SearchImageCacher
{
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly IHttpClientFactory _httpFactory;
private readonly Random _rng;
private readonly SortedSet<ImageCacherObject> _cache;
private static readonly List<string> defaultTagBlacklist = new List<string>() {
"loli",
"lolicon",
"shota"
};
public SearchImageCacher(IHttpClientFactory http)
{
_httpFactory = http;
_rng = new Random();
_cache = new SortedSet<ImageCacherObject>();
}
public async Task<ImageCacherObject> GetImage(string[] tags, bool forceExplicit, DapiSearchType type,
HashSet<string> blacklistedTags = null)
{
tags = tags.Select(tag => tag?.ToLowerInvariant()).ToArray();
blacklistedTags = blacklistedTags ?? new HashSet<string>();
foreach (var item in defaultTagBlacklist)
{
blacklistedTags.Add(item);
}
blacklistedTags = blacklistedTags.Select(t => t.ToLowerInvariant()).ToHashSet();
if (tags.Any(x => blacklistedTags.Contains(x)))
{
throw new Exception("One of the specified tags is blacklisted");
}
if (type == DapiSearchType.E621)
tags = tags.Select(tag => tag?.Replace("yuri", "female/female", StringComparison.InvariantCulture))
.ToArray();
await _lock.WaitAsync().ConfigureAwait(false);
try
{
ImageCacherObject[] imgs;
if (tags.Any())
{
imgs = _cache.Where(x => x.Tags.IsSupersetOf(tags) && x.SearchType == type && (!forceExplicit || x.Rating == "e")).ToArray();
}
else
{
imgs = _cache.Where(x => x.SearchType == type).ToArray();
}
imgs = imgs.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant()))).ToArray();
ImageCacherObject img;
if (imgs.Length == 0)
img = null;
else
img = imgs[_rng.Next(imgs.Length)];
if (img != null)
{
_cache.Remove(img);
return img;
}
else
{
var images = await DownloadImagesAsync(tags, forceExplicit, type).ConfigureAwait(false);
images = images
.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant())))
.ToArray();
if (images.Length == 0)
return null;
var toReturn = images[_rng.Next(images.Length)];
foreach (var dledImg in images)
{
if (dledImg != toReturn)
_cache.Add(dledImg);
}
return toReturn;
}
}
finally
{
_lock.Release();
}
}
public async Task<ImageCacherObject[]> DownloadImagesAsync(string[] tags, bool isExplicit, DapiSearchType type)
{
isExplicit = type == DapiSearchType.Safebooru
? false
: isExplicit;
var tag = "";
tag += string.Join('+', tags.Select(x => x.Replace(" ", "_", StringComparison.InvariantCulture).ToLowerInvariant()));
if (isExplicit)
tag = "rating%3Aexplicit+" + tag;
var website = "";
switch (type)
{
case DapiSearchType.Safebooru:
website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}&json=1";
break;
case DapiSearchType.E621:
website = $"https://e621.net/posts.json?limit=200&tags={tag}";
break;
case DapiSearchType.Danbooru:
website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}";
break;
case DapiSearchType.Gelbooru:
website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Rule34:
website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Konachan:
website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Yandere:
website = $"https://yande.re/post.json?limit=100&tags={tag}";
break;
case DapiSearchType.Derpibooru:
tag = string.IsNullOrWhiteSpace(tag) ? "safe" : tag;
website = $"https://www.derpibooru.org/api/v1/json/search/images?q={tag?.Replace('+', ',')}&per_page=49";
break;
case DapiSearchType.Sankaku:
website = $"https://capi-v2.sankakucomplex.com/posts?tags={tag}&limit=50";
break;
}
try
{
using (var _http = _httpFactory.CreateClient())
{
_http.AddFakeHeaders();
if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || type == DapiSearchType.Danbooru)
{
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
return JsonConvert.DeserializeObject<DapiImageObject[]>(data)
.Where(x => x.FileUrl != null)
.Select(x => new ImageCacherObject(x, type))
.ToArray();
}
if (type == DapiSearchType.Sankaku)
{
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
return JsonConvert.DeserializeObject<SankakuImageObject[]>(data)
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
.Select(x => new ImageCacherObject(
x.FileUrl,
DapiSearchType.Sankaku,
x.Tags.Select(x => x.Name).JoinWith(','),
x.Score))
.ToArray();
}
if (type == DapiSearchType.E621)
{
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
return JsonConvert.DeserializeAnonymousType(data, new { posts = new List<E621Object>() })
.posts
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
.Select(x => new ImageCacherObject(x.File.Url,
type, string.Join(' ', x.Tags.General), x.Score.Total))
.ToArray();
}
if (type == DapiSearchType.Derpibooru)
{
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
return JsonConvert.DeserializeObject<DerpiContainer>(data)
.Images
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
.Select(x => new ImageCacherObject(x.ViewUrl,
type, string.Join("\n", x.Tags), x.Score))
.ToArray();
}
if (type == DapiSearchType.Safebooru)
{
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
return JsonConvert.DeserializeObject<SafebooruElement[]>(data)
.Select(x => new ImageCacherObject(x.FileUrl, type, x.Tags, x.Rating))
.ToArray();
}
return (await LoadXmlAsync(website, type).ConfigureAwait(false)).ToArray();
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error downloading an image: {Message}", ex.Message);
return Array.Empty<ImageCacherObject>();
}
}
private async Task<ImageCacherObject[]> LoadXmlAsync(string website, DapiSearchType type)
{
var list = new List<ImageCacherObject>();
using (var http = _httpFactory.CreateClient())
using (var stream = await http.GetStreamAsync(website).ConfigureAwait(false))
using (var reader = XmlReader.Create(stream, new XmlReaderSettings()
{
Async = true,
}))
{
while (await reader.ReadAsync().ConfigureAwait(false))
{
if (reader.NodeType == XmlNodeType.Element &&
reader.Name == "post")
{
list.Add(new ImageCacherObject(new DapiImageObject()
{
FileUrl = reader["file_url"],
Tags = reader["tags"],
Rating = reader["rating"] ?? "e"
}, type));
}
}
}
return list.ToArray();
}
public void Clear()
{
_cache.Clear();
}
}
public class DapiImageObject
{
[JsonProperty("File_Url")]
public string FileUrl { get; set; }
public string Tags { get; set; }
[JsonProperty("Tag_String")]
public string TagString { get; set; }
public string Rating { get; set; }
}
public class DerpiContainer
{
public DerpiImageObject[] Images { get; set; }
}
public class DerpiImageObject
{
[JsonProperty("view_url")]
public string ViewUrl { get; set; }
public string[] Tags { get; set; }
public string Score { get; set; }
}
public class SankakuImageObject
{
public class Tag
{
public string Name { get; set; }
}
[JsonProperty("file_url")]
public string FileUrl { get; set; }
[JsonProperty("file_type")]
public string FileType { get; set; }
public Tag[] Tags { get; set; }
[JsonProperty("total_score")]
public string Score { get; set; }
}
public enum DapiSearchType
{
Safebooru,
E621,
Derpibooru,
Gelbooru,
Konachan,
Rule34,
Yandere,
Danbooru,
Sankaku,
}
public class SafebooruElement
{
public string Directory { get; set; }
public string Image { get; set; }
public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}";
public string Rating { get; set; }
public string Tags { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches
{
public class SteamGameId
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("appid")]
public int AppId { get; set; }
}
public class SteamGameData
{
public string ShortDescription { get; set; }
public class Container
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("data")]
public SteamGameData Data { get; set; }
}
}
public enum TimeErrors
{
InvalidInput,
ApiKeyMissing,
NotFound,
Unknown
}
}

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class PicartoChannelResponse
{
[JsonProperty("user_id")] public int UserId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("avatar")] public string Avatar { get; set; }
[JsonProperty("online")] public bool Online { get; set; }
[JsonProperty("viewers")] public int Viewers { get; set; }
[JsonProperty("viewers_total")] public int ViewersTotal { get; set; }
[JsonProperty("thumbnails")] public Thumbnails Thumbnails { get; set; }
[JsonProperty("followers")] public int Followers { get; set; }
[JsonProperty("subscribers")] public int Subscribers { get; set; }
[JsonProperty("adult")] public bool Adult { get; set; }
[JsonProperty("category")] public string Category { get; set; }
[JsonProperty("account_type")] public string AccountType { get; set; }
[JsonProperty("commissions")] public bool Commissions { get; set; }
[JsonProperty("recordings")] public bool Recordings { get; set; }
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("description_panels")] public List<DescriptionPanel> DescriptionPanels { get; set; }
[JsonProperty("private")] public bool Private { get; set; }
[JsonProperty("private_message")] public string PrivateMessage { get; set; }
[JsonProperty("gaming")] public bool Gaming { get; set; }
[JsonProperty("chat_settings")] public ChatSettings ChatSettings { get; set; }
[JsonProperty("last_live")] public DateTime LastLive { get; set; }
[JsonProperty("tags")] public List<string> Tags { get; set; }
[JsonProperty("multistream")] public List<Multistream> Multistream { get; set; }
[JsonProperty("languages")] public List<Language> Languages { get; set; }
[JsonProperty("following")] public bool Following { get; set; }
}
public class Thumbnails
{
[JsonProperty("web")] public string Web { get; set; }
[JsonProperty("web_large")] public string WebLarge { get; set; }
[JsonProperty("mobile")] public string Mobile { get; set; }
[JsonProperty("tablet")] public string Tablet { get; set; }
}
public class DescriptionPanel
{
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("body")] public string Body { get; set; }
[JsonProperty("image")] public string Image { get; set; }
[JsonProperty("image_link")] public string ImageLink { get; set; }
[JsonProperty("button_text")] public string ButtonText { get; set; }
[JsonProperty("button_link")] public string ButtonLink { get; set; }
[JsonProperty("position")] public int Position { get; set; }
}
public class ChatSettings
{
[JsonProperty("guest_chat")] public bool GuestChat { get; set; }
[JsonProperty("links")] public bool Links { get; set; }
[JsonProperty("level")] public int Level { get; set; }
}
public class Multistream
{
[JsonProperty("user_id")] public int UserId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("online")] public bool Online { get; set; }
[JsonProperty("adult")] public bool Adult { get; set; }
}
public class Language
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("name")] public string Name { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class StreamData
{
public FollowedStream.FType StreamType { get; set; }
public string Name { get; set; }
public string UniqueName { get; set; }
public int Viewers { get; set; }
public string Title { get; set; }
public string Game { get; set; }
public string Preview { get; set; }
public bool IsLive { get; set; }
public string StreamUrl { get; set; }
public string AvatarUrl { get; set; }
public StreamDataKey CreateKey() => new StreamDataKey(StreamType, UniqueName.ToLower());
}
}

View File

@@ -0,0 +1,16 @@
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Core.Modules.Searches.Common
{
public readonly struct StreamDataKey
{
public FollowedStream.FType Type { get; }
public string Name { get; }
public StreamDataKey(FollowedStream.FType type, string name)
{
Type = type;
Name = name;
}
}
}

View File

@@ -0,0 +1,22 @@
namespace NadekoBot.Core.Modules.Searches.Common
{
//
// public class TwitchResponse
// {
// public List<StreamApiData> Data { get; set; }
//
// public class StreamApiData
// {
// public string Id { get; set; }
// public string UserId { get; set; }
// public string UserName { get; set; }
// public string GameId { get; set; }
// public string Type { get; set; }
// public string Title { get; set; }
// public int ViewerCount { get; set; }
// public string Language { get; set; }
// public string ThumbnailUrl { get; set; }
// public DateTime StartedAt { get; set; }
// }
// }
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class TwitchResponseV5
{
public List<Stream> Streams { get; set; }
public class Channel
{
[JsonProperty("_id")] public int Id { get; set; }
[JsonProperty("broadcaster_language")] public string BroadcasterLanguage { get; set; }
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
[JsonProperty("display_name")] public string DisplayName { get; set; }
[JsonProperty("followers")] public int Followers { get; set; }
[JsonProperty("game")] public string Game { get; set; }
[JsonProperty("language")] public string Language { get; set; }
[JsonProperty("logo")] public string Logo { get; set; }
[JsonProperty("mature")] public bool Mature { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("partner")] public bool Partner { get; set; }
[JsonProperty("profile_banner")] public string ProfileBanner { get; set; }
[JsonProperty("profile_banner_background_color")]
public object ProfileBannerBackgroundColor { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("updated_at")] public DateTime UpdatedAt { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("video_banner")] public string VideoBanner { get; set; }
[JsonProperty("views")] public int Views { get; set; }
}
public class Preview
{
[JsonProperty("large")] public string Large { get; set; }
[JsonProperty("medium")] public string Medium { get; set; }
[JsonProperty("small")] public string Small { get; set; }
[JsonProperty("template")] public string Template { get; set; }
}
public class Stream
{
[JsonProperty("_id")] public long Id { get; set; }
[JsonProperty("average_fps")] public double AverageFps { get; set; }
[JsonProperty("channel")] public Channel Channel { get; set; }
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
[JsonProperty("delay")] public double Delay { get; set; }
[JsonProperty("game")] public string Game { get; set; }
[JsonProperty("is_playlist")] public bool IsPlaylist { get; set; }
[JsonProperty("preview")] public Preview Preview { get; set; }
[JsonProperty("video_height")] public int VideoHeight { get; set; }
[JsonProperty("viewers")] public int Viewers { get; set; }
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class TwitchUsersResponseV5
{
[JsonProperty("users")] public List<User> Users { get; set; }
public class User
{
[JsonProperty("_id")]
public string Id { get; set; }
// [JsonProperty("bio")]
// public string Bio { get; set; }
//
// [JsonProperty("created_at")]
// public DateTime CreatedAt { get; set; }
//
// [JsonProperty("display_name")]
// public string DisplayName { get; set; }
//
// [JsonProperty("logo")]
// public string Logo { get; set; }
//
// [JsonProperty("name")]
// public string Name { get; set; }
//
// [JsonProperty("type")]
// public string Type { get; set; }
//
// [JsonProperty("updated_at")]
// public DateTime UpdatedAt { get; set; }
}
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers;
using NadekoBot.Core.Services.Database.Models;
using Newtonsoft.Json;
using Serilog;
using StackExchange.Redis;
#nullable enable
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications
{
public class NotifChecker
{
private readonly ConnectionMultiplexer _multi;
private readonly string _key;
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
private readonly Dictionary<FollowedStream.FType, Provider> _streamProviders;
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
public NotifChecker(IHttpClientFactory httpClientFactory, ConnectionMultiplexer multi, string uniqueCacheKey,
bool isMaster)
{
_multi = multi;
_key = $"{uniqueCacheKey}_followed_streams_data";
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
{
{FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory)},
{FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory)}
};
_offlineBuffer = new HashSet<(FollowedStream.FType, string)>();
if (isMaster)
{
CacheClearAllData();
}
}
// gets all streams which have been failing for more than the provided timespan
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
{
var toReturn = _streamProviders.SelectMany(prov => prov.Value
.FailingStreams
.Where(fs => DateTime.UtcNow - fs.ErroringSince > duration)
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Item1)))
.ToList();
if (remove)
{
foreach (var toBeRemoved in toReturn)
{
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
}
}
return toReturn;
}
public Task RunAsync() => Task.Run(async () =>
{
while (true)
{
try
{
var allStreamData = CacheGetAllData();
var oldStreamDataDict = allStreamData
// group by type
.GroupBy(entry => entry.Key.Type)
.ToDictionary(
entry => entry.Key,
entry => entry.AsEnumerable().ToDictionary(x => x.Key.Name, x => x.Value)
);
var newStreamData = await Task.WhenAll(oldStreamDataDict
.Select(x =>
{
// get all stream data for the streams of this type
if (_streamProviders.TryGetValue(x.Key, out var provider))
{
return provider.GetStreamDataAsync(x.Value.Select(entry => entry.Key).ToList());
}
// this means there's no provider for this stream data, (and there was before?)
return Task.FromResult(new List<StreamData>());
}));
var newlyOnline = new List<StreamData>();
var newlyOffline = new List<StreamData>();
// go through all new stream data, compare them with the old ones
foreach (var newData in newStreamData.SelectMany(x => x))
{
// update cached data
var key = newData.CreateKey();
CacheAddData(key, newData, replace: true);
// compare old data with new data
var oldData = oldStreamDataDict[key.Type][key.Name];
// this is the first pass
if (oldData is null)
continue;
// if the stream is offline, we need to check if it was
// marked as offline once previously
// if it was, that means this is second time we're getting offline
// status for that stream -> notify subscribers
// Note: This is done because twitch api will sometimes return an offline status
// shortly after the stream is already online, which causes duplicate notifications.
// (stream is online -> stream is offline -> stream is online again (and stays online))
// This offlineBuffer will make it so that the stream has to be marked as offline TWICE
// before it sends an offline notification to the subscribers.
var streamId = (key.Type, key.Name);
if (!newData.IsLive && _offlineBuffer.Remove(streamId))
{
newlyOffline.Add(newData);
}
else if (newData.IsLive != oldData.IsLive)
{
if (newData.IsLive)
{
_offlineBuffer.Remove(streamId);
newlyOnline.Add(newData);
}
else
{
_offlineBuffer.Add(streamId);
// newlyOffline.Add(newData);
}
}
}
var tasks = new List<Task>
{
Task.Delay(30_000)
};
if (newlyOnline.Count > 0)
{
tasks.Add(OnStreamsOnline(newlyOnline));
}
if (newlyOffline.Count > 0)
{
tasks.Add(OnStreamsOffline(newlyOffline));
}
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
Log.Error(ex, $"Error getting stream notifications: {ex.Message}");
}
}
});
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
{
var db = _multi.GetDatabase();
return db.HashSet(
_key,
JsonConvert.SerializeObject(key),
JsonConvert.SerializeObject(data),
when: replace ? When.Always : When.NotExists);
}
public void CacheDeleteData(StreamDataKey key)
{
var db = _multi.GetDatabase();
db.HashDelete(_key, JsonConvert.SerializeObject(key));
}
public void CacheClearAllData()
{
var db = _multi.GetDatabase();
db.KeyDelete(_key);
}
public Dictionary<StreamDataKey, StreamData?> CacheGetAllData()
{
var db = _multi.GetDatabase();
if (!db.KeyExists(_key))
{
return new Dictionary<StreamDataKey, StreamData?>();
}
return db.HashGetAll(_key)
.ToDictionary(
entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
entry => entry.Value.IsNullOrEmpty
? default(StreamData)
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
}
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
// loop through all providers and see which regex matches
foreach (var (_, provider) in _streamProviders)
{
var isValid = await provider.IsValidUrl(url);
if (!isValid)
continue;
// if it's not a valid url, try another provider
var data = await provider.GetStreamDataByUrlAsync(url);
return data;
}
// if no provider found, return null
return null;
}
/// <summary>
/// Return currently available stream data, get new one if none available, and start tracking the stream.
/// </summary>
/// <param name="url">Url of the stream</param>
/// <returns>Stream data, if any</returns>
public async Task<StreamData?> TrackStreamByUrlAsync(string url)
{
var data = await GetStreamDataByUrlAsync(url);
EnsureTracked(data);
return data;
}
/// <summary>
/// Make sure a stream is tracked using its stream data.
/// </summary>
/// <param name="data">Data to try to track if not already tracked</param>
/// <returns>Whether it's newly added</returns>
private bool EnsureTracked(StreamData? data)
{
// something failed, don't add anything to cache
if (data is null)
return false;
// if stream is found, add it to the cache for tracking only if it doesn't already exist
// because stream will be checked and events will fire in a loop. We don't want to override old state
return CacheAddData(data.CreateKey(), data, replace: false);
}
public void UntrackStreamByKey(in StreamDataKey key)
{
CacheDeleteData(key);
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NadekoBot.Core.Services.Database.Models;
using Newtonsoft.Json;
using Serilog;
#nullable enable
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
{
public class PicartoProvider : Provider
{
private readonly IHttpClientFactory _httpClientFactory;
private static Regex Regex { get; } = new Regex(@"picarto.tv/(?<name>.+[^/])/?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override FollowedStream.FType Platform => FollowedStream.FType.Picarto;
public PicartoProvider(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
if (!match.Success)
return Task.FromResult(false);
// var username = match.Groups["name"].Value;
return Task.FromResult(true);
}
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
var match = Regex.Match(url);
if (match.Success)
{
var name = match.Groups["name"].Value;
return GetStreamDataAsync(name);
}
return Task.FromResult<StreamData?>(null);
}
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> {id});
return data.FirstOrDefault();
}
public async override Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new List<StreamData>();
using (var http = _httpClientFactory.CreateClient())
{
var toReturn = new List<StreamData>();
foreach (var login in logins)
{
try
{
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// get id based on the username
var res = await http.GetAsync($"https://api.picarto.tv/v1/channel/name/{login}");
if (!res.IsSuccessStatusCode)
continue;
var userData = JsonConvert.DeserializeObject<PicartoChannelResponse>(await res.Content.ReadAsStringAsync());
toReturn.Add(ToStreamData(userData));
_failingStreams.TryRemove(login, out _);
}
catch (Exception ex)
{
Log.Warning(ex, $"Something went wrong retreiving {Platform} stream data for {login}: {ex.Message}");
_failingStreams.TryAdd(login, DateTime.UtcNow);
}
}
return toReturn;
}
}
private StreamData ToStreamData(PicartoChannelResponse stream)
{
return new StreamData()
{
StreamType = FollowedStream.FType.Picarto,
Name = stream.Name,
UniqueName = stream.Name,
Viewers = stream.Viewers,
Title = stream.Title,
IsLive = stream.Online,
Preview = stream.Thumbnails.Web,
Game = stream.Category,
StreamUrl = $"https://picarto.tv/{stream.Name}",
AvatarUrl = stream.Avatar
};
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Services.Database.Models;
#nullable enable
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
{
/// <summary>
/// Abstract class implemented by providers of all supported platforms
/// </summary>
public abstract class Provider
{
/// <summary>
/// Type of the platform.
/// </summary>
public abstract FollowedStream.FType Platform { get; }
/// <summary>
/// Checks whether the specified url is a valid stream url for this platform.
/// </summary>
/// <param name="url">Url to check</param>
/// <returns>True if valid, otherwise false</returns>
public abstract Task<bool> IsValidUrl(string url);
/// <summary>
/// Gets stream data of the stream on the specified url on this <see cref="Platform"/>
/// </summary>
/// <param name="url">Url of the stream</param>
/// <returns><see cref="StreamData"/> of the specified stream. Null if none found</returns>
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
/// <summary>
/// Gets stream data of the specified id/username on this <see cref="Platform"/>
/// </summary>
/// <param name="id">Name (or id where applicable) of the user on the platform</param>
/// <returns><see cref="StreamData"/> of the user. Null if none found</returns>
public abstract Task<StreamData?> GetStreamDataAsync(string id);
/// <summary>
/// Gets stream data of all specified ids/usernames on this <see cref="Platform"/>
/// </summary>
/// <param name="usernames">List of ids/usernames</param>
/// <returns><see cref="StreamData"/> of all users, in the same order. Null for every id/user not found.</returns>
public abstract Task<List<StreamData>> GetStreamDataAsync(List<string> usernames);
/// <summary>
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
/// </summary>
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams =>
_failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
/// <summary>
/// When was the first time the stream continually had errors while being retrieved
/// </summary>
protected readonly ConcurrentDictionary<string, DateTime> _failingStreams =
new ConcurrentDictionary<string, DateTime>();
public void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
}
}

View File

@@ -0,0 +1,180 @@
// using System;
// using System.Collections.Generic;
// using System.Linq;
// using System.Net.Http;
// using System.Text.Json;
// using System.Text.Json.Serialization;
// using System.Text.RegularExpressions;
// using System.Threading.Tasks;
// using NadekoBot.Core.Services.Database.Models;
// using NadekoBot.Extensions;
// using Serilog;
// using JsonSerializer = System.Text.Json.JsonSerializer;
//
// namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
// {
// public sealed class TwitchHelixProvider : Provider
// {
// private readonly IHttpClientFactory _httpClientFactory;
//
// //
// private static Regex Regex { get; } = new Regex(@"twitch.tv/(?<name>.+[^/])/?",
// RegexOptions.Compiled | RegexOptions.IgnoreCase);
//
// public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
//
// private (string Token, DateTime Expiry) _token = default;
//
// public TwitchHelixProvider(IHttpClientFactory httpClientFactory)
// {
// _httpClientFactory = httpClientFactory;
// }
//
// private async Task EnsureTokenValidAsync()
// {
// if (_token != default && (DateTime.UtcNow - _token.Expiry) > TimeSpan.FromHours(1))
// return;
//
// const string clientId = "";
// const string clientSecret = "";
//
// var client = _httpClientFactory.CreateClient();
// var res = await client.PostAsync("https://id.twitch.tv/oauth2/token" +
// $"?client_id={clientId}" +
// $"&client_secret={clientSecret}" +
// "&grant_type=client_credentials", new StringContent(""));
//
// var data = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
//
// _token = (data.GetProperty("access_token").GetString(),
// DateTime.UtcNow + TimeSpan.FromSeconds(data.GetProperty("expires_in").GetInt32()));
//
// }
//
// public override Task<bool> IsValidUrl(string url)
// {
// var match = Regex.Match(url);
// if (!match.Success)
// return Task.FromResult(false);
//
// var username = match.Groups["name"].Value;
// return Task.FromResult(true);
// }
//
// public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
// {
// var match = Regex.Match(url);
// if (match.Success)
// {
// var name = match.Groups["name"].Value;
// return GetStreamDataAsync(name);
// }
//
// return Task.FromResult<StreamData?>(null);
// }
//
// public override async Task<StreamData?> GetStreamDataAsync(string id)
// {
// var data = await GetStreamDataAsync(new List<string> {id});
//
// return data.FirstOrDefault();
// }
//
// public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
// {
// if (logins.Count == 0)
// return new List<StreamData>();
//
// await EnsureTokenValidAsync();
//
// using var http = _httpClientFactory.CreateClient();
// http.DefaultRequestHeaders.Clear();
// http.DefaultRequestHeaders.Add("client-id","67w6z9i09xv2uoojdm9l0wsyph4hxo6");
// http.DefaultRequestHeaders.Add("Authorization",$"Bearer {_token.Token}");
//
// var res = new TwitchResponse()
// {
// Data = new List<TwitchResponse.StreamApiData>()
// };
// foreach (var chunk in logins.Chunk(500))
// {
// try
// {
// var str = await http.GetStringAsync($"https://api.twitch.tv/helix/streams" +
// $"?user_login={chunk.JoinWith(',')}" +
// $"&first=100");
//
// res = JsonSerializer.Deserialize<TwitchResponse>(str);
// }
// catch (Exception ex)
// {
// Log.Warning(ex, "Something went wrong retreiving {StreamPlatform} streams", Platform);
// return new List<StreamData>();
// }
//
// if (res.Data.Count == 0)
// {
// return new List<StreamData>();
// }
// }
//
// return res.Data.Select(ToStreamData).ToList();
// }
//
// private StreamData ToStreamData(TwitchResponse.StreamApiData apiData)
// {
// return new StreamData()
// {
// StreamType = FollowedStream.FType.Twitch,
// Name = apiData.UserName,
// UniqueName = apiData.UserId,
// Viewers = apiData.ViewerCount,
// Title = apiData.Title,
// IsLive = apiData.Type == "live",
// Preview = apiData.ThumbnailUrl
// ?.Replace("{width}", "640")
// ?.Replace("{height}", "480"),
// Game = apiData.GameId,
// };
// }
// }
//
// public class TwitchResponse
// {
// [JsonPropertyName("data")]
// public List<StreamApiData> Data { get; set; }
//
// public class StreamApiData
// {
// [JsonPropertyName("id")]
// public string Id { get; set; }
//
// [JsonPropertyName("user_id")]
// public string UserId { get; set; }
//
// [JsonPropertyName("user_name")]
// public string UserName { get; set; }
//
// [JsonPropertyName("game_id")]
// public string GameId { get; set; }
//
// [JsonPropertyName("type")]
// public string Type { get; set; }
//
// [JsonPropertyName("title")]
// public string Title { get; set; }
//
// [JsonPropertyName("viewer_count")]
// public int ViewerCount { get; set; }
//
// [JsonPropertyName("language")]
// public string Language { get; set; }
//
// [JsonPropertyName("thumbnail_url")]
// public string ThumbnailUrl { get; set; }
//
// [JsonPropertyName("started_at")]
// public DateTime StartedAt { get; set; }
// }
// }
// }

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NadekoBot.Core.Services.Database.Models;
using Newtonsoft.Json;
using Serilog;
#nullable enable
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
{
public class TwitchProvider : Provider
{
private readonly IHttpClientFactory _httpClientFactory;
private static Regex Regex { get; } = new Regex(@"twitch.tv/(?<name>.+[^/])/?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
public TwitchProvider(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
if (!match.Success)
return Task.FromResult(false);
// var username = match.Groups["name"].Value;
return Task.FromResult(true);
}
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
var match = Regex.Match(url);
if (match.Success)
{
var name = match.Groups["name"].Value;
return GetStreamDataAsync(name);
}
return Task.FromResult<StreamData?>(null);
}
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> {id});
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new List<StreamData>();
using (var http = _httpClientFactory.CreateClient())
{
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
http.DefaultRequestHeaders.Add("Accept", "application/vnd.twitchtv.v5+json");
var toReturn = new List<StreamData>();
foreach (var login in logins)
{
try
{
// get id based on the username
var idsStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/users?login={login}");
var userData = JsonConvert.DeserializeObject<TwitchUsersResponseV5>(idsStr);
var user = userData.Users.FirstOrDefault();
// if user can't be found, skip, it means there is no such user
if (user is null)
continue;
// get stream data
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}");
var resObj =
JsonConvert.DeserializeAnonymousType(str, new {Stream = new TwitchResponseV5.Stream()});
// if stream is null, user is not streaming
if (resObj.Stream is null)
{
// if user is not streaming, get his offline banner
var chStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/channels/{user.Id}");
var ch = JsonConvert.DeserializeObject<TwitchResponseV5.Channel>(chStr);
toReturn.Add(new StreamData
{
StreamType = FollowedStream.FType.Twitch,
Name = ch.DisplayName,
UniqueName = ch.Name,
Title = ch.Status,
IsLive = false,
AvatarUrl = ch.Logo,
StreamUrl = $"https://twitch.tv/{ch.Name}",
Preview = ch.VideoBanner // set video banner as the preview,
});
continue; // move on
}
toReturn.Add(ToStreamData(resObj.Stream));
_failingStreams.TryRemove(login, out _);
}
catch (Exception ex)
{
Log.Warning($"Something went wrong retreiving {Platform} stream data for {login}: {ex.Message}");
_failingStreams.TryAdd(login, DateTime.UtcNow);
}
}
return toReturn;
}
}
private StreamData ToStreamData(TwitchResponseV5.Stream stream)
{
return new StreamData()
{
StreamType = FollowedStream.FType.Twitch,
Name = stream.Channel.DisplayName,
UniqueName = stream.Channel.Name,
Viewers = stream.Viewers,
Title = stream.Channel.Status,
IsLive = true,
Preview = stream.Preview.Large,
Game = stream.Channel.Game,
StreamUrl = $"https://twitch.tv/{stream.Channel.Name}",
AvatarUrl = stream.Channel.Logo
};
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace NadekoBot.Core.Modules.Searches.Common
{
public class TimeData
{
public string Address { get; set; }
public DateTime Time { get; set; }
public string TimeZoneName { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches.Common
{
public class TimeZoneResult
{
[JsonProperty("abbreviation")]
public string TimezoneName { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
}
public class LocationIqResponse
{
public float Lat { get; set; }
public float Lon { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
namespace NadekoBot.Core.Modules.Searches.Common
{
public class UrbanResponse
{
public UrbanDef[] List { get; set; }
}
public class UrbanDef
{
public string Word { get; set; }
public string Definition { get; set; }
public string Permalink { get; set; }
}
}

View File

@@ -0,0 +1,66 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NadekoBot.Modules.Searches.Common
{
public class Coord
{
public double Lon { get; set; }
public double Lat { get; set; }
}
public class Weather
{
public int Id { get; set; }
public string Main { get; set; }
public string Description { get; set; }
public string Icon { get; set; }
}
public class Main
{
public double Temp { get; set; }
public float Pressure { get; set; }
public float Humidity { get; set; }
[JsonProperty("temp_min")]
public double TempMin { get; set; }
[JsonProperty("temp_max")]
public double TempMax { get; set; }
}
public class Wind
{
public double Speed { get; set; }
public double Deg { get; set; }
}
public class Clouds
{
public int All { get; set; }
}
public class Sys
{
public int Type { get; set; }
public int Id { get; set; }
public double Message { get; set; }
public string Country { get; set; }
public double Sunrise { get; set; }
public double Sunset { get; set; }
}
public class WeatherData
{
public Coord Coord { get; set; }
public List<Weather> Weather { get; set; }
public Main Main { get; set; }
public int Visibility { get; set; }
public Wind Wind { get; set; }
public Clouds Clouds { get; set; }
public int Dt { get; set; }
public Sys Sys { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public int Cod { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
namespace NadekoBot.Modules.Searches.Common
{
public class WikipediaApiModel
{
public WikipediaQuery Query { get; set; }
public class WikipediaQuery
{
public WikipediaPage[] Pages { get; set; }
public class WikipediaPage
{
public bool Missing { get; set; } = false;
public string FullUrl { get; set; }
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Searches.Common
{
public class WoWJoke
{
public string Question { get; set; }
public string Answer { get; set; }
public override string ToString() => $"`{Question}`\n\n**{Answer}**";
}
}

View File

@@ -0,0 +1,62 @@
using Discord;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Modules.Searches.Services;
using NadekoBot.Extensions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
public class CryptoCommands : NadekoSubmodule<CryptoService>
{
[NadekoCommand, Usage, Description, Aliases]
public async Task Crypto(string name)
{
name = name?.ToUpperInvariant();
if (string.IsNullOrWhiteSpace(name))
return;
var (crypto, nearest) = await _service.GetCryptoData(name).ConfigureAwait(false);
if (nearest != null)
{
var embed = new EmbedBuilder()
.WithTitle(GetText("crypto_not_found"))
.WithDescription(GetText("did_you_mean", Format.Bold($"{nearest.Name} ({nearest.Symbol})")));
if (await PromptUserConfirmAsync(embed).ConfigureAwait(false))
{
crypto = nearest;
}
}
if (crypto == null)
{
await ReplyErrorLocalizedAsync("crypto_not_found").ConfigureAwait(false);
return;
}
var sevenDay = decimal.TryParse(crypto.Quote.Usd.Percent_Change_7d, out var sd)
? sd.ToString("F2")
: crypto.Quote.Usd.Percent_Change_7d;
var lastDay = decimal.TryParse(crypto.Quote.Usd.Percent_Change_24h, out var ld)
? ld.ToString("F2")
: crypto.Quote.Usd.Percent_Change_24h;
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
.AddField(GetText("market_cap"), $"${crypto.Quote.Usd.Market_Cap:n0}", true)
.AddField(GetText("price"), $"${crypto.Quote.Usd.Price}", true)
.AddField(GetText("volume_24h"), $"${crypto.Quote.Usd.Volume_24h:n0}", true)
.AddField(GetText("change_7d_24h"), $"{sevenDay}% / {lastDay}%", true)
.WithImageUrl($"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{crypto.Id}.png")).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,96 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class FeedCommands : NadekoSubmodule<FeedsService>
{
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task Feed(string url, [Leftover] ITextChannel channel = null)
{
var success = Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
if (success)
{
channel = channel ?? (ITextChannel)ctx.Channel;
try
{
var feeds = await CodeHollow.FeedReader.FeedReader.ReadAsync(url).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Information(ex, "Unable to get feeds from that url");
success = false;
}
}
if (success)
{
success = _service.AddFeed(ctx.Guild.Id, channel.Id, url);
if (success)
{
await ReplyConfirmLocalizedAsync("feed_added").ConfigureAwait(false);
return;
}
}
await ReplyErrorLocalizedAsync("feed_not_valid").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task FeedRemove(int index)
{
if (_service.RemoveFeed(ctx.Guild.Id, --index))
{
await ReplyConfirmLocalizedAsync("feed_removed").ConfigureAwait(false);
}
else
await ReplyErrorLocalizedAsync("feed_out_of_range").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task FeedList()
{
var feeds = _service.GetFeeds(ctx.Guild.Id);
if (!feeds.Any())
{
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithDescription(GetText("feed_no_feed")))
.ConfigureAwait(false);
return;
}
await ctx.SendPaginatedConfirmAsync(0, (cur) =>
{
var embed = new EmbedBuilder()
.WithOkColor();
var i = 0;
var fs = string.Join("\n", feeds.Skip(cur * 10)
.Take(10)
.Select(x => $"`{(cur * 10) + (++i)}.` <#{x.ChannelId}> {x.Url}"));
return embed.WithDescription(fs);
}, feeds.Count, 10).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,62 @@
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class JokeCommands : NadekoSubmodule<SearchesService>
{
[NadekoCommand, Usage, Description, Aliases]
public async Task Yomama()
{
await ctx.Channel.SendConfirmAsync(await _service.GetYomamaJoke().ConfigureAwait(false)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Randjoke()
{
var (setup, punchline) = await _service.GetRandomJoke().ConfigureAwait(false);
await ctx.Channel.SendConfirmAsync(setup, punchline).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task ChuckNorris()
{
await ctx.Channel.SendConfirmAsync(await _service.GetChuckNorrisJoke().ConfigureAwait(false)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task WowJoke()
{
if (!_service.WowJokes.Any())
{
await ReplyErrorLocalizedAsync("jokes_not_loaded").ConfigureAwait(false);
return;
}
var joke = _service.WowJokes[new NadekoRandom().Next(0, _service.WowJokes.Count)];
await ctx.Channel.SendConfirmAsync(joke.Question, joke.Answer).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task MagicItem()
{
if (!_service.WowJokes.Any())
{
await ReplyErrorLocalizedAsync("magicitems_not_loaded").ConfigureAwait(false);
return;
}
var item = _service.MagicItems[new NadekoRandom().Next(0, _service.MagicItems.Count)];
await ctx.Channel.SendConfirmAsync("✨" + item.Name, item.Description).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class MemegenCommands : NadekoSubmodule
{
private class MemegenTemplate
{
public string Name { get; set; }
public string Id { get; set; }
}
private static readonly ImmutableDictionary<char, string> _map = new Dictionary<char, string>()
{
{'?', "~q"},
{'%', "~p"},
{'#', "~h"},
{'/', "~s"},
{' ', "-"},
{'-', "--"},
{'_', "__"},
{'"', "''"}
}.ToImmutableDictionary();
private readonly IHttpClientFactory _httpFactory;
public MemegenCommands(IHttpClientFactory factory)
{
_httpFactory = factory;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Memelist(int page = 1)
{
if (--page < 0)
return;
using (var http = _httpFactory.CreateClient("memelist"))
{
var res = await http.GetAsync("https://api.memegen.link/templates/")
.ConfigureAwait(false);
var rawJson = await res.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<List<MemegenTemplate>>(rawJson);
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var templates = "";
foreach (var template in data.Skip(curPage * 15).Take(15))
{
templates += $"**{template.Name}:**\n key: `{template.Id}`\n";
}
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(templates);
return embed;
}, data.Count, 15).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Memegen(string meme, [Leftover] string memeText = null)
{
var memeUrl = $"http://api.memegen.link/{meme}";
if (!string.IsNullOrWhiteSpace(memeText))
{
var memeTextArray = memeText.Split(';');
foreach(var text in memeTextArray)
{
var newText = Replace(text);
memeUrl += $"/{newText}";
}
}
memeUrl += ".png";
await ctx.Channel.SendMessageAsync(memeUrl)
.ConfigureAwait(false);
}
private static string Replace(string input)
{
var sb = new StringBuilder();
foreach (var c in input)
{
if (_map.TryGetValue(c, out var tmp))
sb.Append(tmp);
else
sb.Append(c);
}
return sb.ToString();
}
}
}
}

View File

@@ -0,0 +1,328 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Searches.Common;
using Newtonsoft.Json;
using Serilog;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class OsuCommands : NadekoSubmodule
{
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _httpFactory;
public OsuCommands(IBotCredentials creds, IHttpClientFactory factory)
{
_creds = creds;
_httpFactory = factory;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Osu(string user, [Leftover] string mode = null)
{
if (string.IsNullOrWhiteSpace(user))
return;
using (var http = _httpFactory.CreateClient())
{
var modeNumber = string.IsNullOrWhiteSpace(mode)
? 0
: ResolveGameMode(mode);
try
{
if (string.IsNullOrWhiteSpace(_creds.OsuApiKey))
{
await ReplyErrorLocalizedAsync("osu_api_key").ConfigureAwait(false);
return;
}
var smode = ResolveGameMode(modeNumber);
var userReq = $"https://osu.ppy.sh/api/get_user?k={_creds.OsuApiKey}&u={user}&m={modeNumber}";
var userResString = await http.GetStringAsync(userReq)
.ConfigureAwait(false);
var objs = JsonConvert.DeserializeObject<List<OsuUserData>>(userResString);
if (objs.Count == 0)
{
await ReplyErrorLocalizedAsync("osu_user_not_found").ConfigureAwait(false);
return;
}
var obj = objs[0];
var userId = obj.UserId;
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithTitle($"osu! {smode} profile for {user}")
.WithThumbnailUrl($"https://a.ppy.sh/{userId}")
.WithDescription($"https://osu.ppy.sh/u/{userId}")
.AddField("Official Rank", $"#{obj.PpRank}", true)
.AddField("Country Rank", $"#{obj.PpCountryRank} :flag_{obj.Country.ToLower()}:", true)
.AddField("Total PP", Math.Round(obj.PpRaw, 2), true)
.AddField("Accuracy", Math.Round(obj.Accuracy, 2) + "%", true)
.AddField("Playcount", obj.Playcount, true)
.AddField("Level", Math.Round(obj.Level), true)
);
}
catch (ArgumentOutOfRangeException)
{
await ReplyErrorLocalizedAsync("osu_user_not_found").ConfigureAwait(false);
}
catch (Exception ex)
{
await ReplyErrorLocalizedAsync("osu_failed").ConfigureAwait(false);
Log.Warning(ex, "Osu command failed");
}
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Gatari(string user, [Leftover] string mode = null)
{
using (var http = _httpFactory.CreateClient())
{
var modeNumber = string.IsNullOrWhiteSpace(mode)
? 0
: ResolveGameMode(mode);
var modeStr = ResolveGameMode(modeNumber);
var resString = await http
.GetStringAsync($"https://api.gatari.pw/user/stats?u={user}&mode={modeNumber}")
.ConfigureAwait(false);
var statsResponse = JsonConvert.DeserializeObject<GatariUserStatsResponse>(resString);
if (statsResponse.Code != 200 || statsResponse.Stats.Id == 0)
{
await ReplyErrorLocalizedAsync("osu_user_not_found").ConfigureAwait(false);
return;
}
var usrResString = await http.GetStringAsync($"https://api.gatari.pw/users/get?u={user}")
.ConfigureAwait(false);
var userData = JsonConvert.DeserializeObject<GatariUserResponse>(usrResString).Users[0];
var userStats = statsResponse.Stats;
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle($"osu!Gatari {modeStr} profile for {user}")
.WithThumbnailUrl($"https://a.gatari.pw/{userStats.Id}")
.WithDescription($"https://osu.gatari.pw/u/{userStats.Id}")
.AddField("Official Rank", $"#{userStats.Rank}", true)
.AddField("Country Rank", $"#{userStats.CountryRank} :flag_{userData.Country.ToLower()}:", true)
.AddField("Total PP", userStats.Pp, true)
.AddField("Accuracy", $"{Math.Round(userStats.AvgAccuracy, 2)}%", true)
.AddField("Playcount", userStats.Playcount, true)
.AddField("Level", userStats.Level, true);
await ctx.Channel.EmbedAsync(embed);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Osu5(string user, [Leftover] string mode = null)
{
var channel = (ITextChannel) ctx.Channel;
if (string.IsNullOrWhiteSpace(_creds.OsuApiKey))
{
await channel.SendErrorAsync("An osu! API key is required.").ConfigureAwait(false);
return;
}
if (string.IsNullOrWhiteSpace(user))
{
await channel.SendErrorAsync("Please provide a username.").ConfigureAwait(false);
return;
}
using (var http = _httpFactory.CreateClient())
{
var m = 0;
if (!string.IsNullOrWhiteSpace(mode))
{
m = ResolveGameMode(mode);
}
var reqString = $"https://osu.ppy.sh/api/get_user_best" +
$"?k={_creds.OsuApiKey}" +
$"&u={Uri.EscapeDataString(user)}" +
$"&type=string" +
$"&limit=5" +
$"&m={m}";
var resString = await http.GetStringAsync(reqString).ConfigureAwait(false);
var obj = JsonConvert.DeserializeObject<List<OsuUserBests>>(resString);
var mapTasks = obj.Select(async item =>
{
var mapReqString = $"https://osu.ppy.sh/api/get_beatmaps" +
$"?k={_creds.OsuApiKey}" +
$"&b={item.BeatmapId}";
var mapResString = await http.GetStringAsync(mapReqString).ConfigureAwait(false);
var map = JsonConvert.DeserializeObject<List<OsuMapData>>(mapResString).FirstOrDefault();
if (map is null)
return default;
var pp = Math.Round(item.Pp, 2);
var acc = CalculateAcc(item, m);
var mods = ResolveMods(item.EnabledMods);
var title = $"{map.Artist}-{map.Title} ({map.Version})";
var desc = $@"[/b/{item.BeatmapId}](https://osu.ppy.sh/b/{item.BeatmapId})
{pp + "pp",-7} | {acc + "%",-7}
";
if (mods != "+")
{
desc += Format.Bold(mods);
}
return (title, desc);
});
var eb = new EmbedBuilder()
.WithOkColor()
.WithTitle($"Top 5 plays for {user}");
var mapData = await Task.WhenAll(mapTasks);
foreach (var (title, desc) in mapData.Where(x => x != default))
{
eb.AddField(title, desc, inline: false);
}
await channel.EmbedAsync(eb).ConfigureAwait(false);
}
}
//https://osu.ppy.sh/wiki/Accuracy
private static double CalculateAcc(OsuUserBests play, int mode)
{
double hitPoints;
double totalHits;
if (mode == 0)
{
hitPoints = play.Count50 * 50 +
play.Count100 * 100 +
play.Count300 * 300;
totalHits = play.Count50 + play.Count100 +
play.Count300 + play.Countmiss;
totalHits *= 300;
}
else if (mode == 1)
{
hitPoints = play.Countmiss * 0 + play.Count100 * 0.5 + play.Count300;
totalHits = (play.Countmiss + play.Count100 + play.Count300) * 300;
hitPoints *= 300;
}
else if (mode == 2)
{
hitPoints = play.Count50 + play.Count100 + play.Count300;
totalHits = play.Countmiss + play.Count50 + play.Count100 + play.Count300 +
play.Countkatu;
}
else
{
hitPoints = play.Count50 * 50 +
play.Count100 * 100 +
play.Countkatu * 200 +
(play.Count300 + play.Countgeki) * 300;
totalHits = (play.Countmiss + play.Count50 + play.Count100 +
play.Countkatu + play.Count300 + play.Countgeki) * 300;
}
return Math.Round(hitPoints / totalHits * 100, 2);
}
private static int ResolveGameMode(string mode)
{
switch (mode.ToUpperInvariant())
{
case "STD":
case "STANDARD":
return 0;
case "TAIKO":
return 1;
case "CTB":
case "CATCHTHEBEAT":
return 2;
case "MANIA":
case "OSU!MANIA":
return 3;
default:
return 0;
}
}
private static string ResolveGameMode(int mode)
{
switch (mode)
{
case 0:
return "Standard";
case 1:
return "Taiko";
case 2:
return "Catch";
case 3:
return "Mania";
default:
return "Standard";
}
}
//https://github.com/ppy/osu-api/wiki#mods
private static string ResolveMods(int mods)
{
var modString = $"+";
if (IsBitSet(mods, 0))
modString += "NF";
if (IsBitSet(mods, 1))
modString += "EZ";
if (IsBitSet(mods, 8))
modString += "HT";
if (IsBitSet(mods, 3))
modString += "HD";
if (IsBitSet(mods, 4))
modString += "HR";
if (IsBitSet(mods, 6) && !IsBitSet(mods, 9))
modString += "DT";
if (IsBitSet(mods, 9))
modString += "NC";
if (IsBitSet(mods, 10))
modString += "FL";
if (IsBitSet(mods, 5))
modString += "SD";
if (IsBitSet(mods, 14))
modString += "PF";
if (IsBitSet(mods, 7))
modString += "RX";
if (IsBitSet(mods, 11))
modString += "AT";
if (IsBitSet(mods, 12))
modString += "SO";
return modString;
}
private static bool IsBitSet(int mods, int pos) =>
(mods & (1 << pos)) != 0;
}
}
}

View File

@@ -0,0 +1,329 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Modules.Searches.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class PathOfExileCommands : NadekoSubmodule<SearchesService>
{
private const string _poeURL = "https://www.pathofexile.com/character-window/get-characters?accountName=";
private const string _ponURL = "http://poe.ninja/api/Data/GetCurrencyOverview?league=";
private const string _pogsURL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search=";
private const string _pogURL = "https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject=";
private const string _pogiURL = "https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:";
private const string _profileURL = "https://www.pathofexile.com/account/view-profile/";
private readonly DiscordSocketClient _client;
private readonly IHttpClientFactory _httpFactory;
public PathOfExileCommands(DiscordSocketClient client, IHttpClientFactory httpFactory)
{
_client = client;
_httpFactory = httpFactory;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task PathOfExile(string usr, string league = "", int page = 1)
{
if (--page < 0)
return;
if (string.IsNullOrWhiteSpace(usr))
{
await ctx.Channel.SendErrorAsync("Please provide an account name.").ConfigureAwait(false);
return;
}
var characters = new List<Account>();
try
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync($"{_poeURL}{usr}").ConfigureAwait(false);
characters = JsonConvert.DeserializeObject<List<Account>>(res);
}
}
catch
{
var embed = new EmbedBuilder()
.WithDescription(GetText("account_not_found"))
.WithErrorColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
return;
}
if (!string.IsNullOrWhiteSpace(league))
{
characters.RemoveAll(c => c.League != league);
}
await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
{
var embed = new EmbedBuilder()
.WithAuthor(eau => eau.WithName($"Characters on {usr}'s account")
.WithUrl($"{_profileURL}{usr}")
.WithIconUrl("https://web.poecdn.com/image/favicon/ogimage.png"))
.WithOkColor();
var tempList = characters.Skip(curPage * 9).Take(9).ToList();
if (characters.Count == 0)
{
return embed.WithDescription("This account has no characters.");
}
else
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"```{"#",-5}{"Character Name",-23}{"League",-10}{"Class",-13}{"Level",-3}");
for (int i = 0; i < tempList.Count; i++)
{
var character = tempList[i];
sb.AppendLine($"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}");
}
sb.AppendLine("```");
embed.WithDescription(sb.ToString());
return embed;
}
}, characters.Count, 9, true).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task PathOfExileLeagues()
{
var leagues = new List<Leagues>();
try
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync("http://api.pathofexile.com/leagues?type=main&compact=1").ConfigureAwait(false);
leagues = JsonConvert.DeserializeObject<List<Leagues>>(res);
}
}
catch
{
var eembed = new EmbedBuilder()
.WithDescription(GetText("leagues_not_found"))
.WithErrorColor();
await ctx.Channel.EmbedAsync(eembed).ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithAuthor(eau => eau.WithName($"Path of Exile Leagues")
.WithUrl("https://www.pathofexile.com")
.WithIconUrl("https://web.poecdn.com/image/favicon/ogimage.png"))
.WithOkColor();
var sb = new System.Text.StringBuilder();
sb.AppendLine($"```{"#",-5}{"League Name",-23}");
for (int i = 0; i < leagues.Count; i++)
{
var league = leagues[i];
sb.AppendLine($"#{i + 1,-4}{league.Id,-23}");
}
sb.AppendLine("```");
embed.WithDescription(sb.ToString());
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task PathOfExileCurrency(string leagueName, string currencyName, string convertName = "Chaos Orb")
{
if (string.IsNullOrWhiteSpace(leagueName))
{
await ctx.Channel.SendErrorAsync("Please provide league name.").ConfigureAwait(false);
return;
}
if (string.IsNullOrWhiteSpace(currencyName))
{
await ctx.Channel.SendErrorAsync("Please provide currency name.").ConfigureAwait(false);
return;
}
var cleanCurrency = ShortCurrencyName(currencyName);
var cleanConvert = ShortCurrencyName(convertName);
try
{
var res = $"{_ponURL}{leagueName}";
using (var http = _httpFactory.CreateClient())
{
var obj = JObject.Parse(await http.GetStringAsync(res).ConfigureAwait(false));
float chaosEquivalent = 0.0F;
float conversionEquivalent = 0.0F;
// poe.ninja API does not include a "chaosEquivalent" property for Chaos Orbs.
if (cleanCurrency == "Chaos Orb")
{
chaosEquivalent = 1.0F;
}
else
{
var currencyInput = obj["lines"].Values<JObject>()
.Where(i => i["currencyTypeName"].Value<string>() == cleanCurrency)
.FirstOrDefault();
chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(), System.Globalization.CultureInfo.InvariantCulture);
}
if (cleanConvert == "Chaos Orb")
{
conversionEquivalent = 1.0F;
}
else
{
var currencyOutput = obj["lines"].Values<JObject>()
.Where(i => i["currencyTypeName"].Value<string>() == cleanConvert)
.FirstOrDefault();
conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(), System.Globalization.CultureInfo.InvariantCulture);
}
var embed = new EmbedBuilder().WithAuthor(eau => eau.WithName($"{leagueName} Currency Exchange")
.WithUrl("http://poe.ninja")
.WithIconUrl("https://web.poecdn.com/image/favicon/ogimage.png"))
.AddField(efb => efb.WithName("Currency Type").WithValue(cleanCurrency).WithIsInline(true))
.AddField(efb => efb.WithName($"{cleanConvert} Equivalent").WithValue(chaosEquivalent / conversionEquivalent).WithIsInline(true))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
catch
{
var embed = new EmbedBuilder()
.WithDescription(GetText("ninja_not_found"))
.WithErrorColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
private Dictionary<string, string> currencyDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{"Chaos Orb", "Chaos Orb" },
{"Orb of Alchemy", "Orb of Alchemy" },
{"Jeweller's Orb", "Jeweller's Orb" },
{"Exalted Orb", "Exalted Orb" },
{"Mirror of Kalandra", "Mirror of Kalandra" },
{"Vaal Orb", "Vaal Orb" },
{"Orb of Alteration", "Orb of Alteration" },
{"Orb of Scouring", "Orb of Scouring" },
{"Divine Orb", "Divine Orb" },
{"Orb of Annulment", "Orb of Annulment" },
{"Master Cartographer's Sextant", "Master Cartographer's Sextant" },
{"Journeyman Cartographer's Sextant", "Journeyman Cartographer's Sextant" },
{"Apprentice Cartographer's Sextant", "Apprentice Cartographer's Sextant" },
{"Blessed Orb", "Blessed Orb" },
{"Orb of Regret", "Orb of Regret" },
{"Gemcutter's Prism", "Gemcutter's Prism" },
{"Glassblower's Bauble", "Glassblower's Bauble" },
{"Orb of Fusing", "Orb of Fusing" },
{"Cartographer's Chisel", "Cartographer's Chisel" },
{"Chromatic Orb", "Chromatic Orb" },
{"Orb of Augmentation", "Orb of Augmentation" },
{"Blacksmith's Whetstone", "Blacksmith's Whetstone" },
{"Orb of Transmutation", "Orb of Transmutation" },
{"Armourer's Scrap", "Armourer's Scrap" },
{"Scroll of Wisdom", "Scroll of Wisdom" },
{"Regal Orb", "Regal Orb" },
{"Chaos", "Chaos Orb" },
{"Alch", "Orb of Alchemy" },
{"Alchs", "Orb of Alchemy" },
{"Jews", "Jeweller's Orb" },
{"Jeweller", "Jeweller's Orb" },
{"Jewellers", "Jeweller's Orb" },
{"Jeweller's", "Jeweller's Orb" },
{"X", "Exalted Orb" },
{"Ex", "Exalted Orb" },
{"Exalt", "Exalted Orb" },
{"Exalts", "Exalted Orb" },
{"Mirror", "Mirror of Kalandra" },
{"Mirrors", "Mirror of Kalandra" },
{"Vaal", "Vaal Orb" },
{"Alt", "Orb of Alteration" },
{"Alts", "Orb of Alteration" },
{"Scour", "Orb of Scouring" },
{"Scours", "Orb of Scouring" },
{"Divine", "Divine Orb" },
{"Annul", "Orb of Annulment" },
{"Annulment", "Orb of Annulment" },
{"Master Sextant", "Master Cartographer's Sextant" },
{"Journeyman Sextant", "Journeyman Cartographer's Sextant" },
{"Apprentice Sextant", "Apprentice Cartographer's Sextant" },
{"Blessed", "Blessed Orb" },
{"Regret", "Orb of Regret" },
{"Regrets", "Orb of Regret" },
{"Gcp", "Gemcutter's Prism" },
{"Glassblowers", "Glassblower's Bauble" },
{"Glassblower's", "Glassblower's Bauble" },
{"Fusing", "Orb of Fusing" },
{"Fuses", "Orb of Fusing" },
{"Fuse", "Orb of Fusing" },
{"Chisel", "Cartographer's Chisel" },
{"Chisels", "Cartographer's Chisel" },
{"Chance", "Orb of Chance" },
{"Chances", "Orb of Chance" },
{"Chrome", "Chromatic Orb" },
{"Chromes", "Chromatic Orb" },
{"Aug", "Orb of Augmentation" },
{"Augmentation", "Orb of Augmentation" },
{"Augment", "Orb of Augmentation" },
{"Augments", "Orb of Augmentation" },
{"Whetstone", "Blacksmith's Whetstone" },
{"Whetstones", "Blacksmith's Whetstone" },
{"Transmute", "Orb of Transmutation" },
{"Transmutes", "Orb of Transmutation" },
{"Armourers", "Armourer's Scrap" },
{"Armourer's", "Armourer's Scrap" },
{"Wisdom Scroll", "Scroll of Wisdom" },
{"Wisdom Scrolls", "Scroll of Wisdom" },
{"Regal", "Regal Orb" },
{"Regals", "Regal Orb" }
};
private string ShortCurrencyName(string str)
{
if (currencyDictionary.ContainsValue(str))
{
return str;
}
var currency = currencyDictionary[str];
return currency;
}
private static string ShortLeagueName(string str)
{
var league = Regex.Replace(str, "Hardcore", "HC", RegexOptions.IgnoreCase);
return league;
}
}
}
}

View File

@@ -0,0 +1,82 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class PlaceCommands : NadekoSubmodule
{
private static readonly string _typesStr =
string.Join(", ", Enum.GetNames(typeof(PlaceType)));
public enum PlaceType
{
Cage, //http://www.placecage.com
Steven, //http://www.stevensegallery.com
Beard, //http://placebeard.it
Fill, //http://www.fillmurray.com
Bear, //https://www.placebear.com
Kitten, //http://placekitten.com
Bacon, //http://baconmockup.com
Xoart, //http://xoart.link
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Placelist()
{
await ctx.Channel.SendConfirmAsync(GetText("list_of_place_tags", Prefix),
_typesStr)
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Place(PlaceType placeType, uint width = 0, uint height = 0)
{
var url = "";
switch (placeType)
{
case PlaceType.Cage:
url = "http://www.placecage.com";
break;
case PlaceType.Steven:
url = "http://www.stevensegallery.com";
break;
case PlaceType.Beard:
url = "http://placebeard.it";
break;
case PlaceType.Fill:
url = "http://www.fillmurray.com";
break;
case PlaceType.Bear:
url = "https://www.placebear.com";
break;
case PlaceType.Kitten:
url = "http://placekitten.com";
break;
case PlaceType.Bacon:
url = "http://baconmockup.com";
break;
case PlaceType.Xoart:
url = "http://xoart.link";
break;
}
var rng = new NadekoRandom();
if (width <= 0 || width > 1000)
width = (uint)rng.Next(250, 850);
if (height <= 0 || height > 1000)
height = (uint)rng.Next(250, 850);
url += $"/{width}/{height}";
await ctx.Channel.SendMessageAsync(url).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,80 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Common.Pokemon;
using NadekoBot.Core.Services;
using System;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class PokemonSearchCommands : NadekoSubmodule<SearchesService>
{
private readonly IDataCache _cache;
public IReadOnlyDictionary<string, SearchPokemon> Pokemons => _cache.LocalData.Pokemons;
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities => _cache.LocalData.PokemonAbilities;
public PokemonSearchCommands(IDataCache cache)
{
_cache = cache;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Pokemon([Leftover] string pokemon = null)
{
pokemon = pokemon?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(pokemon))
return;
foreach (var kvp in Pokemons)
{
if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant())
{
var p = kvp.Value;
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(kvp.Key.ToTitleCase())
.WithDescription(p.BaseStats.ToString())
.WithThumbnailUrl($"https://assets.pokemon.com/assets/cms2/img/pokedex/detail/{p.Id.ToString("000")}.png")
.AddField(efb => efb.WithName(GetText("types")).WithValue(string.Join("\n", p.Types)).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("height_weight")).WithValue(GetText("height_weight_val", p.HeightM, p.WeightKg)).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("abilities")).WithValue(string.Join("\n", p.Abilities.Select(a => a.Value))).WithIsInline(true))).ConfigureAwait(false);
return;
}
}
await ReplyErrorLocalizedAsync("pokemon_none").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task PokemonAbility([Leftover] string ability = null)
{
ability = ability?.Trim().ToUpperInvariant().Replace(" ", "", StringComparison.InvariantCulture);
if (string.IsNullOrWhiteSpace(ability))
return;
foreach (var kvp in PokemonAbilities)
{
if (kvp.Key.ToUpperInvariant() == ability)
{
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(kvp.Value.Name)
.WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc)
? kvp.Value.ShortDesc
: kvp.Value.Desc)
.AddField(efb => efb.WithName(GetText("rating"))
.WithValue(kvp.Value.Rating.ToString(_cultureInfo)).WithIsInline(true))
).ConfigureAwait(false);
return;
}
}
await ReplyErrorLocalizedAsync("pokemon_ability_none").ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,797 @@
using AngleSharp;
using AngleSharp.Html.Dom;
using Discord;
using Discord.Commands;
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Modules.Searches.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Common;
using NadekoBot.Modules.Searches.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Modules.Administration.Services;
using Serilog;
using Configuration = AngleSharp.Configuration;
namespace NadekoBot.Modules.Searches
{
public partial class Searches : NadekoModule<SearchesService>
{
private readonly IBotCredentials _creds;
private readonly IGoogleApiService _google;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _cache;
private static readonly NadekoRandom _rng = new NadekoRandom();
private readonly GuildTimezoneService _tzSvc;
public Searches(IBotCredentials creds, IGoogleApiService google, IHttpClientFactory factory, IMemoryCache cache,
GuildTimezoneService tzSvc)
{
_creds = creds;
_google = google;
_httpFactory = factory;
_cache = cache;
_tzSvc = tzSvc;
}
//for anonymasen :^)
[NadekoCommand, Usage, Description, Aliases]
public async Task Rip([Leftover] IGuildUser usr)
{
var av = usr.RealAvatarUrl(128);
if (av == null)
return;
using (var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av).ConfigureAwait(false))
{
await ctx.Channel.SendFileAsync(
picStream,
"rip.png",
$"Rip {Format.Bold(usr.ToString())} \n\t- " +
Format.Italics(ctx.User.ToString()))
.ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Weather([Leftover] string query)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
var embed = new EmbedBuilder();
var data = await _service.GetWeatherDataAsync(query).ConfigureAwait(false);
if (data == null)
{
embed.WithDescription(GetText("city_not_found"))
.WithErrorColor();
}
else
{
Func<double, double> f = StandardConversions.CelsiusToFahrenheit;
var tz = Context.Guild is null
? TimeZoneInfo.Utc
: _tzSvc.GetTimeZoneOrUtc(Context.Guild.Id);
var sunrise = data.Sys.Sunrise.ToUnixTimestamp();
var sunset = data.Sys.Sunset.ToUnixTimestamp();
sunrise = sunrise.ToOffset(tz.GetUtcOffset(sunrise));
sunset = sunset.ToOffset(tz.GetUtcOffset(sunset));
var timezone = $"UTC{sunrise:zzz}";
embed.AddField(fb => fb.WithName("🌍 " + Format.Bold(GetText("location"))).WithValue($"[{data.Name + ", " + data.Sys.Country}](https://openweathermap.org/city/{data.Id})").WithIsInline(true))
.AddField(fb => fb.WithName("📏 " + Format.Bold(GetText("latlong"))).WithValue($"{data.Coord.Lat}, {data.Coord.Lon}").WithIsInline(true))
.AddField(fb => fb.WithName("☁ " + Format.Bold(GetText("condition"))).WithValue(string.Join(", ", data.Weather.Select(w => w.Main))).WithIsInline(true))
.AddField(fb => fb.WithName("😓 " + Format.Bold(GetText("humidity"))).WithValue($"{data.Main.Humidity}%").WithIsInline(true))
.AddField(fb => fb.WithName("💨 " + Format.Bold(GetText("wind_speed"))).WithValue(data.Wind.Speed + " m/s").WithIsInline(true))
.AddField(fb => fb.WithName("🌡 " + Format.Bold(GetText("temperature"))).WithValue($"{data.Main.Temp:F1}°C / {f(data.Main.Temp):F1}°F").WithIsInline(true))
.AddField(fb => fb.WithName("🔆 " + Format.Bold(GetText("min_max"))).WithValue($"{data.Main.TempMin:F1}°C - {data.Main.TempMax:F1}°C\n{f(data.Main.TempMin):F1}°F - {f(data.Main.TempMax):F1}°F").WithIsInline(true))
.AddField(fb => fb.WithName("🌄 " + Format.Bold(GetText("sunrise"))).WithValue($"{sunrise:HH:mm} {timezone}").WithIsInline(true))
.AddField(fb => fb.WithName("🌇 " + Format.Bold(GetText("sunset"))).WithValue($"{sunset:HH:mm} {timezone}").WithIsInline(true))
.WithOkColor()
.WithFooter(efb => efb.WithText("Powered by openweathermap.org").WithIconUrl($"http://openweathermap.org/img/w/{data.Weather[0].Icon}.png"));
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Time([Leftover] string query)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var (data, err) = await _service.GetTimeDataAsync(query).ConfigureAwait(false);
if (!(err is null))
{
string errorKey;
switch (err)
{
case TimeErrors.ApiKeyMissing:
errorKey = "api_key_missing";
break;
case TimeErrors.InvalidInput:
errorKey = "invalid_input";
break;
case TimeErrors.NotFound:
errorKey = "not_found";
break;
default:
errorKey = "error_occured";
break;
}
await ReplyErrorLocalizedAsync(errorKey).ConfigureAwait(false);
return;
}
else if (string.IsNullOrWhiteSpace(data.TimeZoneName))
{
await ReplyErrorLocalizedAsync("timezone_db_api_key").ConfigureAwait(false);
return;
}
var eb = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("time_new"))
.WithDescription(Format.Code(data.Time.ToString()))
.AddField(GetText("location"), string.Join('\n', data.Address.Split(", ")), inline: true)
.AddField(GetText("timezone"), data.TimeZoneName, inline: true);
await ctx.Channel.SendMessageAsync(embed: eb.Build()).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Youtube([Leftover] string query = null)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
var result = (await _google.GetVideoLinksByKeywordAsync(query, 1).ConfigureAwait(false)).FirstOrDefault();
if (string.IsNullOrWhiteSpace(result))
{
await ReplyErrorLocalizedAsync("no_results").ConfigureAwait(false);
return;
}
await ctx.Channel.SendMessageAsync(result).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Movie([Leftover] string query = null)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var movie = await _service.GetMovieDataAsync(query).ConfigureAwait(false);
if (movie == null)
{
await ReplyErrorLocalizedAsync("imdb_fail").ConfigureAwait(false);
return;
}
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(movie.Title)
.WithUrl($"http://www.imdb.com/title/{movie.ImdbId}/")
.WithDescription(movie.Plot.TrimTo(1000))
.AddField(efb => efb.WithName("Rating").WithValue(movie.ImdbRating).WithIsInline(true))
.AddField(efb => efb.WithName("Genre").WithValue(movie.Genre).WithIsInline(true))
.AddField(efb => efb.WithName("Year").WithValue(movie.Year).WithIsInline(true))
.WithImageUrl(movie.Poster)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public Task RandomCat() => InternalRandomImage(SearchesService.ImageTag.Cats);
[NadekoCommand, Usage, Description, Aliases]
public Task RandomDog() => InternalRandomImage(SearchesService.ImageTag.Dogs);
[NadekoCommand, Usage, Description, Aliases]
public Task RandomFood() => InternalRandomImage(SearchesService.ImageTag.Food);
[NadekoCommand, Usage, Description, Aliases]
public Task RandomBird() => InternalRandomImage(SearchesService.ImageTag.Birds);
private Task InternalRandomImage(SearchesService.ImageTag tag)
{
var url = _service.GetRandomImageUrl(tag);
return ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithImageUrl(url));
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Image([Leftover] string query = null)
{
var oterms = query?.Trim();
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
query = WebUtility.UrlEncode(oterms).Replace(' ', '+');
try
{
var res = await _google.GetImageAsync(oterms).ConfigureAwait(false);
var embed = new EmbedBuilder()
.WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("image_search_for") + " " + oterms.TrimTo(50))
.WithUrl("https://www.google.rs/search?q=" + query + "&source=lnms&tbm=isch")
.WithIconUrl("http://i.imgur.com/G46fm8J.png"))
.WithDescription(res.Link)
.WithImageUrl(res.Link)
.WithTitle(ctx.User.ToString());
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
catch
{
Log.Warning("Falling back to Imgur");
var fullQueryLink = $"http://imgur.com/search?q={ query }";
var config = Configuration.Default.WithDefaultLoader();
using (var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false))
{
var elems = document.QuerySelectorAll("a.image-list-link").ToList();
if (!elems.Any())
return;
var img = (elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Count))?.Children?.FirstOrDefault() as IHtmlImageElement);
if (img?.Source == null)
return;
var source = img.Source.Replace("b.", ".", StringComparison.InvariantCulture);
var embed = new EmbedBuilder()
.WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("image_search_for") + " " + oterms.TrimTo(50))
.WithUrl(fullQueryLink)
.WithIconUrl("http://s.imgur.com/images/logo-1200-630.jpg?"))
.WithDescription(source)
.WithImageUrl(source)
.WithTitle(ctx.User.ToString());
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Lmgtfy([Leftover] string ffs = null)
{
if (!await ValidateQuery(ctx.Channel, ffs).ConfigureAwait(false))
return;
await ctx.Channel.SendConfirmAsync("<" + await _google.ShortenUrl($"http://lmgtfy.com/?q={ Uri.EscapeUriString(ffs) }").ConfigureAwait(false) + ">")
.ConfigureAwait(false);
}
public class ShortenData
{
[JsonProperty("result_url")]
public string ResultUrl { get; set; }
}
private static readonly ConcurrentDictionary<string, string> cachedShortenedLinks = new ConcurrentDictionary<string, string>();
[NadekoCommand, Usage, Description, Aliases]
public async Task Shorten([Leftover] string query)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
query = query.Trim();
if (!cachedShortenedLinks.TryGetValue(query, out var shortLink))
{
try
{
using (var _http = _httpFactory.CreateClient())
using (var req = new HttpRequestMessage(HttpMethod.Post, "https://goolnk.com/api/v1/shorten"))
{
var formData = new MultipartFormDataContent
{
{ new StringContent(query), "url" }
};
req.Content = formData;
using (var res = await _http.SendAsync(req).ConfigureAwait(false))
{
var content = await res.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<ShortenData>(content);
if (!string.IsNullOrWhiteSpace(data?.ResultUrl))
cachedShortenedLinks.TryAdd(query, data.ResultUrl);
else
return;
shortLink = data.ResultUrl;
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Error shortening a link: {Message}", ex.Message);
return;
}
}
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithColor(NadekoBot.OkColor)
.AddField(efb => efb.WithName(GetText("original_url"))
.WithValue($"<{query}>"))
.AddField(efb => efb.WithName(GetText("short_url"))
.WithValue($"<{shortLink}>")))
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Google([Leftover] string query = null)
{
query = query?.Trim();
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
_ = ctx.Channel.TriggerTypingAsync();
var data = await _service.GoogleSearchAsync(query);
if (data is null)
{
await ReplyErrorLocalizedAsync("no_results");
return;
}
var desc = data.Results.Take(5).Select(res =>
$@"[**{res.Title}**]({res.Link})
{res.Text.TrimTo(400 - res.Title.Length - res.Link.Length)}");
var descStr = string.Join("\n\n", desc);
var embed = new EmbedBuilder()
.WithAuthor(ctx.User.ToString(),
iconUrl: "http://i.imgur.com/G46fm8J.png")
.WithTitle(ctx.User.ToString())
.WithFooter(efb => efb.WithText(data.TotalResults))
.WithDescription($"{GetText("search_for")} **{query}**\n\n" +descStr)
.WithOkColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task DuckDuckGo([Leftover] string query = null)
{
query = query?.Trim();
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
_ = ctx.Channel.TriggerTypingAsync();
var data = await _service.DuckDuckGoSearchAsync(query);
if (data is null)
{
await ReplyErrorLocalizedAsync("no_results");
return;
}
var desc = data.Results.Take(5).Select(res =>
$@"[**{res.Title}**]({res.Link})
{res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
var descStr = string.Join("\n\n", desc);
var embed = new EmbedBuilder()
.WithAuthor(ctx.User.ToString(),
iconUrl: "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
.WithDescription($"{GetText("search_for")} **{query}**\n\n" + descStr)
.WithOkColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task MagicTheGathering([Leftover] string search)
{
if (!await ValidateQuery(ctx.Channel, search))
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var card = await _service.GetMtgCardAsync(search).ConfigureAwait(false);
if (card == null)
{
await ReplyErrorLocalizedAsync("card_not_found").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(card.Name)
.WithDescription(card.Description)
.WithImageUrl(card.ImageUrl)
.AddField(efb => efb.WithName(GetText("store_url")).WithValue(card.StoreUrl).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("cost")).WithValue(card.ManaCost).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("types")).WithValue(card.Types).WithIsInline(true));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Hearthstone([Leftover] string name)
{
var arg = name;
if (!await ValidateQuery(ctx.Channel, name).ConfigureAwait(false))
return;
if (string.IsNullOrWhiteSpace(_creds.MashapeKey))
{
await ReplyErrorLocalizedAsync("mashape_api_missing").ConfigureAwait(false);
return;
}
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var card = await _service.GetHearthstoneCardDataAsync(name).ConfigureAwait(false);
if (card == null)
{
await ReplyErrorLocalizedAsync("card_not_found").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithOkColor()
.WithImageUrl(card.Img);
if (!string.IsNullOrWhiteSpace(card.Flavor))
embed.WithDescription(card.Flavor);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task UrbanDict([Leftover] string query = null)
{
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync($"http://api.urbandictionary.com/v0/define?term={Uri.EscapeUriString(query)}").ConfigureAwait(false);
try
{
var items = JsonConvert.DeserializeObject<UrbanResponse>(res).List;
if (items.Any())
{
await ctx.SendPaginatedConfirmAsync(0, (p) =>
{
var item = items[p];
return new EmbedBuilder().WithOkColor()
.WithUrl(item.Permalink)
.WithAuthor(eab => eab.WithIconUrl("http://i.imgur.com/nwERwQE.jpg").WithName(item.Word))
.WithDescription(item.Definition);
}, items.Length, 1).ConfigureAwait(false);
return;
}
}
catch
{
}
}
await ReplyErrorLocalizedAsync("ud_error").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Define([Leftover] string word)
{
if (!await ValidateQuery(ctx.Channel, word).ConfigureAwait(false))
return;
using (var _http = _httpFactory.CreateClient())
{
string res;
try
{
res = await _cache.GetOrCreateAsync($"define_{word}", e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return _http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword=" + WebUtility.UrlEncode(word));
}).ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<DefineModel>(res);
var datas = data.Results
.Where(x => !(x.Senses is null) && x.Senses.Count > 0 && !(x.Senses[0].Definition is null))
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech));
if (!datas.Any())
{
Log.Warning("Definition not found: {Word}", word);
await ReplyErrorLocalizedAsync("define_unknown").ConfigureAwait(false);
}
var col = datas.Select(data => (
Definition: data.Sense.Definition is string
? data.Sense.Definition.ToString()
: ((JArray)JToken.Parse(data.Sense.Definition.ToString())).First.ToString(),
Example: data.Sense.Examples is null || data.Sense.Examples.Count == 0
? string.Empty
: data.Sense.Examples[0].Text,
Word: word,
WordType: string.IsNullOrWhiteSpace(data.PartOfSpeech) ? "-" : data.PartOfSpeech
)).ToList();
Log.Information($"Sending {col.Count} definition for: {word}");
await ctx.SendPaginatedConfirmAsync(0, page =>
{
var data = col.Skip(page).First();
var embed = new EmbedBuilder()
.WithDescription(ctx.User.Mention)
.AddField(GetText("word"), data.Word, inline: true)
.AddField(GetText("class"), data.WordType, inline: true)
.AddField(GetText("definition"), data.Definition)
.WithOkColor();
if (!string.IsNullOrWhiteSpace(data.Example))
embed.AddField(efb => efb.WithName(GetText("example")).WithValue(data.Example));
return embed;
}, col.Count, 1);
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving definition data for: {Word}", word);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Catfact()
{
using (var http = _httpFactory.CreateClient())
{
var response = await http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false);
if (response == null)
return;
var fact = JObject.Parse(response)["fact"].ToString();
await ctx.Channel.SendConfirmAsync("🐈" + GetText("catfact"), fact).ConfigureAwait(false);
}
}
//done in 3.0
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Revav([Leftover] IGuildUser usr = null)
{
if (usr == null)
usr = (IGuildUser)ctx.User;
var av = usr.RealAvatarUrl();
if (av == null)
return;
await ctx.Channel.SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={av}").ConfigureAwait(false);
}
//done in 3.0
[NadekoCommand, Usage, Description, Aliases]
public async Task Revimg([Leftover] string imageLink = null)
{
imageLink = imageLink?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(imageLink))
return;
await ctx.Channel.SendConfirmAsync($"https://images.google.com/searchbyimage?image_url={imageLink}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public Task Safebooru([Leftover] string tag = null)
=> InternalDapiCommand(ctx.Message, tag, DapiSearchType.Safebooru);
[NadekoCommand, Usage, Description, Aliases]
public async Task Wiki([Leftover] string query = null)
{
query = query?.Trim();
if (!await ValidateQuery(ctx.Channel, query).ConfigureAwait(false))
return;
using (var http = _httpFactory.CreateClient())
{
var result = await http.GetStringAsync("https://en.wikipedia.org//w/api.php?action=query&format=json&prop=info&redirects=1&formatversion=2&inprop=url&titles=" + Uri.EscapeDataString(query)).ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl))
await ReplyErrorLocalizedAsync("wiki_page_not_found").ConfigureAwait(false);
else
await ctx.Channel.SendMessageAsync(data.Query.Pages[0].FullUrl).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Color(params SixLabors.ImageSharp.Color[] colors)
{
if (!colors.Any())
return;
var colorObjects = colors.Take(10)
.ToArray();
using (var img = new Image<Rgba32>(colorObjects.Length * 50, 50))
{
for (int i = 0; i < colorObjects.Length; i++)
{
var x = i * 50;
img.Mutate(m => m.FillPolygon(colorObjects[i], new PointF[] {
new PointF(x, 0),
new PointF(x + 50, 0),
new PointF(x + 50, 50),
new PointF(x, 50)
}));
}
using (var ms = img.ToStream())
{
await ctx.Channel.SendFileAsync(ms, $"colors.png").ConfigureAwait(false);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Avatar([Leftover] IGuildUser usr = null)
{
if (usr == null)
usr = (IGuildUser)ctx.User;
var avatarUrl = usr.RealAvatarUrl(2048);
if (avatarUrl == null)
{
await ReplyErrorLocalizedAsync("avatar_none", usr.ToString()).ConfigureAwait(false);
return;
}
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.AddField(efb => efb.WithName("Username").WithValue(usr.ToString()).WithIsInline(false))
.AddField(efb => efb.WithName("Avatar Url").WithValue(avatarUrl).WithIsInline(false))
.WithThumbnailUrl(avatarUrl.ToString()), ctx.User.Mention).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Wikia(string target, [Leftover] string query)
{
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query))
{
await ReplyErrorLocalizedAsync("wikia_input_error").ConfigureAwait(false);
return;
}
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Clear();
try
{
var res = await http.GetStringAsync($"https://{Uri.EscapeUriString(target)}.fandom.com/api.php" +
$"?action=query" +
$"&format=json" +
$"&list=search" +
$"&srsearch={Uri.EscapeUriString(query)}" +
$"&srlimit=1").ConfigureAwait(false);
var items = JObject.Parse(res);
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
if (string.IsNullOrWhiteSpace(title))
{
await ReplyErrorLocalizedAsync("wikia_error").ConfigureAwait(false);
return;
}
var url = Uri.EscapeUriString($"https://{target}.fandom.com/wiki/{title}");
var response = $@"`{GetText("title")}` {title?.SanitizeMentions()}
`{GetText("url")}:` {url}";
await ctx.Channel.SendMessageAsync(response).ConfigureAwait(false);
}
catch
{
await ReplyErrorLocalizedAsync("wikia_error").ConfigureAwait(false);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Bible(string book, string chapterAndVerse)
{
var obj = new BibleVerses();
try
{
using (var http = _httpFactory.CreateClient())
{
var res = await http
.GetStringAsync("https://bible-api.com/" + book + " " + chapterAndVerse).ConfigureAwait(false);
obj = JsonConvert.DeserializeObject<BibleVerses>(res);
}
}
catch
{
}
if (obj.Error != null || obj.Verses == null || obj.Verses.Length == 0)
await ctx.Channel.SendErrorAsync(obj.Error ?? "No verse found.").ConfigureAwait(false);
else
{
var v = obj.Verses[0];
await ctx.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
.WithDescription(v.Text)).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Steam([Leftover] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
var appId = await _service.GetSteamAppIdByName(query).ConfigureAwait(false);
if (appId == -1)
{
await ReplyErrorLocalizedAsync("not_found").ConfigureAwait(false);
return;
}
//var embed = new EmbedBuilder()
// .WithOkColor()
// .WithDescription(gameData.ShortDescription)
// .WithTitle(gameData.Name)
// .WithUrl(gameData.Link)
// .WithImageUrl(gameData.HeaderImage)
// .AddField(efb => efb.WithName(GetText("genres")).WithValue(gameData.TotalEpisodes.ToString()).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("price")).WithValue(gameData.IsFree ? GetText("FREE") : game).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("links")).WithValue(gameData.GetGenresString()).WithIsInline(true))
// .WithFooter(efb => efb.WithText(GetText("recommendations", gameData.TotalRecommendations)));
await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}").ConfigureAwait(false);
}
public async Task InternalDapiCommand(IUserMessage umsg, string tag, DapiSearchType type)
{
var channel = umsg.Channel;
tag = tag?.Trim() ?? "";
var imgObj = await _service.DapiSearch(tag, type, ctx.Guild?.Id).ConfigureAwait(false);
if (imgObj == null)
await channel.SendErrorAsync(umsg.Author.Mention + " " + GetText("no_results")).ConfigureAwait(false);
else
await channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithDescription($"{umsg.Author.Mention} [{tag ?? "url"}]({imgObj.FileUrl})")
.WithImageUrl(imgObj.FileUrl)
.WithFooter(efb => efb.WithText(type.ToString()))).ConfigureAwait(false);
}
public async Task<bool> ValidateQuery(IMessageChannel ch, string query)
{
if (!string.IsNullOrWhiteSpace(query))
{
return true;
}
await ErrorLocalizedAsync("specify_search_params").ConfigureAwait(false);
return false;
}
}
}

View File

@@ -0,0 +1,158 @@
using AngleSharp;
using AngleSharp.Html.Dom;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Searches.Common;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Modules.Searches.Services
{
public class AnimeSearchService : INService
{
private readonly IDataCache _cache;
private readonly IHttpClientFactory _httpFactory;
public AnimeSearchService(IDataCache cache, IHttpClientFactory httpFactory)
{
_cache = cache;
_httpFactory = httpFactory;
}
public async Task<AnimeResult> GetAnimeData(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
try
{
var link = "https://aniapi.nadeko.bot/anime/" + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
link = link.ToLowerInvariant();
var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false);
if (!ok)
{
using (var http = _httpFactory.CreateClient())
{
data = await http.GetStringAsync(link).ConfigureAwait(false);
}
await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false);
}
return JsonConvert.DeserializeObject<AnimeResult>(data);
}
catch
{
return null;
}
}
public async Task<NovelResult> GetNovelData(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
query = query.Replace(" ", "-", StringComparison.InvariantCulture);
try
{
var link = "http://www.novelupdates.com/series/" + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
link = link.ToLowerInvariant();
var (ok, data) = await _cache.TryGetNovelDataAsync(link).ConfigureAwait(false);
if (!ok)
{
var config = Configuration.Default.WithDefaultLoader();
using (var document = await BrowsingContext.New(config).OpenAsync(link).ConfigureAwait(false))
{
var imageElem = document.QuerySelector("div.seriesimg > img");
if (imageElem == null)
return null;
var imageUrl = ((IHtmlImageElement)imageElem).Source;
var descElem = document.QuerySelector("div#editdescription > p");
var desc = descElem.InnerHtml;
var genres = document.QuerySelector("div#seriesgenre").Children
.Select(x => x as IHtmlAnchorElement)
.Where(x => x != null)
.Select(x => $"[{x.InnerHtml}]({x.Href})")
.ToArray();
var authors = document
.QuerySelector("div#showauthors")
.Children
.Select(x => x as IHtmlAnchorElement)
.Where(x => x != null)
.Select(x => $"[{x.InnerHtml}]({x.Href})")
.ToArray();
var score = ((IHtmlSpanElement)document
.QuerySelector("h5.seriesother > span.uvotes"))
.InnerHtml;
var status = document
.QuerySelector("div#editstatus")
.InnerHtml;
var title = document
.QuerySelector("div.w-blog-content > div.seriestitlenu")
.InnerHtml;
var obj = new NovelResult()
{
Description = desc,
Authors = authors,
Genres = genres,
ImageUrl = imageUrl,
Link = link,
Score = score,
Status = status,
Title = title,
};
await _cache.SetNovelDataAsync(link,
JsonConvert.SerializeObject(obj)).ConfigureAwait(false);
return obj;
}
}
return JsonConvert.DeserializeObject<NovelResult>(data);
}
catch (Exception ex)
{
Log.Error(ex, "Error getting novel data");
return null;
}
}
public async Task<MangaResult> GetMangaData(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
try
{
var link = "https://aniapi.nadeko.bot/manga/" + Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
link = link.ToLowerInvariant();
var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false);
if (!ok)
{
using (var http = _httpFactory.CreateClient())
{
data = await http.GetStringAsync(link).ConfigureAwait(false);
}
await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false);
}
return JsonConvert.DeserializeObject<MangaResult>(data);
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,105 @@
using NadekoBot.Core.Modules.Searches.Common;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Core.Modules.Searches.Services
{
public class CryptoService : INService
{
private readonly IDataCache _cache;
private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds;
public CryptoService(IDataCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
{
_cache = cache;
_httpFactory = httpFactory;
_creds = creds;
}
public async Task<(CryptoResponseData Data, CryptoResponseData Nearest)> GetCryptoData(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return (null, null);
}
name = name.ToUpperInvariant();
var cryptos = await CryptoData().ConfigureAwait(false);
var crypto = cryptos
?.FirstOrDefault(x => x.Id.ToUpperInvariant() == name || x.Name.ToUpperInvariant() == name
|| x.Symbol.ToUpperInvariant() == name);
(CryptoResponseData Elem, int Distance)? nearest = null;
if (crypto == null)
{
nearest = cryptos.Select(x => (x, Distance: x.Name.ToUpperInvariant().LevenshteinDistance(name)))
.OrderBy(x => x.Distance)
.Where(x => x.Distance <= 2)
.FirstOrDefault();
crypto = nearest?.Elem;
}
if (nearest != null)
{
return (null, crypto);
}
return (crypto, null);
}
private readonly SemaphoreSlim getCryptoLock = new SemaphoreSlim(1, 1);
public async Task<List<CryptoResponseData>> CryptoData()
{
await getCryptoLock.WaitAsync();
try
{
var fullStrData = await _cache.GetOrAddCachedDataAsync("nadeko:crypto_data", async _ =>
{
try
{
using (var _http = _httpFactory.CreateClient())
{
var strData = await _http.GetStringAsync(new Uri($"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?" +
$"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}" +
$"&start=1" +
$"&limit=500" +
$"&convert=USD"));
JsonConvert.DeserializeObject<CryptoResponse>(strData); // just to see if its' valid
return strData;
}
}
catch (Exception ex)
{
Log.Error(ex, "Error getting crypto data: {Message}", ex.Message);
return default;
}
}, "", TimeSpan.FromHours(1));
return JsonConvert.DeserializeObject<CryptoResponse>(fullStrData).Data;
}
catch (Exception ex)
{
Log.Error(ex, "Error retreiving crypto data: {Message}", ex.Message);
return default;
}
finally
{
getCryptoLock.Release();
}
}
}
}

View File

@@ -0,0 +1,246 @@
using CodeHollow.FeedReader.Feeds;
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Searches.Services
{
public class FeedsService : INService
{
private readonly DbService _db;
private readonly ConcurrentDictionary<string, HashSet<FeedSub>> _subs;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<string, DateTime> _lastPosts =
new ConcurrentDictionary<string, DateTime>();
public FeedsService(NadekoBot bot, DbService db, DiscordSocketClient client)
{
_db = db;
using (var uow = db.GetDbContext())
{
var guildConfigIds = bot.AllGuildConfigs.Select(x => x.Id).ToList();
_subs = uow._context.GuildConfigs
.AsQueryable()
.Where(x => guildConfigIds.Contains(x.Id))
.Include(x => x.FeedSubs)
.ThenInclude(x => x.GuildConfig)
.ToList()
.SelectMany(x => x.FeedSubs)
.GroupBy(x => x.Url.ToLower())
.ToDictionary(x => x.Key, x => x.ToHashSet())
.ToConcurrent();
}
_client = client;
var _ = Task.Run(TrackFeeds);
}
public async Task<EmbedBuilder> TrackFeeds()
{
while (true)
{
var allSendTasks = new List<Task>(_subs.Count);
foreach (var kvp in _subs)
{
if (kvp.Value.Count == 0)
continue;
var rssUrl = kvp.Key;
try
{
var feed = await CodeHollow.FeedReader.FeedReader.ReadAsync(rssUrl).ConfigureAwait(false);
var items = feed
.Items
.Select(item => (Item: item, LastUpdate: item.PublishingDate?.ToUniversalTime()
?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate
?.ToUniversalTime()))
.Where(data => !(data.LastUpdate is null))
.Select(data => (data.Item, LastUpdate: (DateTime) data.LastUpdate))
.OrderByDescending(data => data.LastUpdate)
.Reverse() // start from the oldest
.ToList();
if (!_lastPosts.TryGetValue(kvp.Key, out DateTime lastFeedUpdate))
{
lastFeedUpdate = _lastPosts[kvp.Key] =
items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow;
}
foreach (var (feedItem, itemUpdateDate) in items)
{
if (itemUpdateDate <= lastFeedUpdate)
{
continue;
}
var embed = new EmbedBuilder()
.WithFooter(rssUrl);
_lastPosts[kvp.Key] = itemUpdateDate;
var link = feedItem.SpecificItem.Link;
if (!string.IsNullOrWhiteSpace(link) && Uri.IsWellFormedUriString(link, UriKind.Absolute))
embed.WithUrl(link);
var title = string.IsNullOrWhiteSpace(feedItem.Title)
? "-"
: feedItem.Title;
var gotImage = false;
if (feedItem.SpecificItem is MediaRssFeedItem mrfi &&
(mrfi.Enclosure?.MediaType?.StartsWith("image/") ?? false))
{
var imgUrl = mrfi.Enclosure.Url;
if (!string.IsNullOrWhiteSpace(imgUrl) &&
Uri.IsWellFormedUriString(imgUrl, UriKind.Absolute))
{
embed.WithImageUrl(imgUrl);
gotImage = true;
}
}
if (!gotImage && feedItem.SpecificItem is AtomFeedItem afi)
{
var previewElement = afi.Element.Elements()
.FirstOrDefault(x => x.Name.LocalName == "preview");
if (previewElement == null)
{
previewElement = afi.Element.Elements()
.FirstOrDefault(x => x.Name.LocalName == "thumbnail");
}
if (previewElement != null)
{
var urlAttribute = previewElement.Attribute("url");
if (urlAttribute != null && !string.IsNullOrWhiteSpace(urlAttribute.Value)
&& Uri.IsWellFormedUriString(urlAttribute.Value,
UriKind.Absolute))
{
embed.WithImageUrl(urlAttribute.Value);
gotImage = true;
}
}
}
embed.WithTitle(title.TrimTo(256));
var desc = feedItem.Description?.StripHTML();
if (!string.IsNullOrWhiteSpace(feedItem.Description))
embed.WithDescription(desc.TrimTo(2048));
//send the created embed to all subscribed channels
var feedSendTasks = kvp.Value
.Where(x => x.GuildConfig != null)
.Select(x => _client.GetGuild(x.GuildConfig.GuildId)
?.GetTextChannel(x.ChannelId))
.Where(x => x != null)
.Select(x => x.EmbedAsync(embed));
allSendTasks.Add(Task.WhenAll(feedSendTasks));
}
}
catch
{
}
}
await Task.WhenAll(Task.WhenAll(allSendTasks), Task.Delay(10000)).ConfigureAwait(false);
}
}
public List<FeedSub> GetFeeds(ulong guildId)
{
using (var uow = _db.GetDbContext())
{
return uow.GuildConfigs.ForId(guildId,
set => set.Include(x => x.FeedSubs)
.ThenInclude(x => x.GuildConfig))
.FeedSubs
.OrderBy(x => x.Id)
.ToList();
}
}
public bool AddFeed(ulong guildId, ulong channelId, string rssFeed)
{
rssFeed.ThrowIfNull(nameof(rssFeed));
var fs = new FeedSub()
{
ChannelId = channelId,
Url = rssFeed.Trim(),
};
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId,
set => set.Include(x => x.FeedSubs)
.ThenInclude(x => x.GuildConfig));
if (gc.FeedSubs.Any(x => x.Url.ToLower() == fs.Url.ToLower()))
{
return false;
}
else if (gc.FeedSubs.Count >= 10)
{
return false;
}
gc.FeedSubs.Add(fs);
uow.SaveChanges();
//adding all, in case bot wasn't on this guild when it started
foreach (var feed in gc.FeedSubs)
{
_subs.AddOrUpdate(feed.Url.ToLower(), new HashSet<FeedSub>() {feed}, (k, old) =>
{
old.Add(feed);
return old;
});
}
}
return true;
}
public bool RemoveFeed(ulong guildId, int index)
{
if (index < 0)
return false;
using (var uow = _db.GetDbContext())
{
var items = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.FeedSubs))
.FeedSubs
.OrderBy(x => x.Id)
.ToList();
if (items.Count <= index)
return false;
var toRemove = items[index];
_subs.AddOrUpdate(toRemove.Url.ToLower(), new HashSet<FeedSub>(), (key, old) =>
{
old.Remove(toRemove);
return old;
});
uow._context.Remove(toRemove);
uow.SaveChanges();
}
return true;
}
}
}

View File

@@ -0,0 +1,945 @@
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Core.Modules.Searches.Common;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Microsoft.EntityFrameworkCore.Internal;
using Serilog;
using HorizontalAlignment = SixLabors.Fonts.HorizontalAlignment;
using Image = SixLabors.ImageSharp.Image;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NadekoBot.Modules.Searches.Services
{
public class SearchesService : INService, IUnloadableService
{
private readonly IHttpClientFactory _httpFactory;
private readonly DiscordSocketClient _client;
private readonly IGoogleApiService _google;
private readonly DbService _db;
private readonly IImageCache _imgs;
private readonly IDataCache _cache;
private readonly FontProvider _fonts;
private readonly IBotCredentials _creds;
private readonly NadekoRandom _rng;
public ConcurrentDictionary<ulong, bool> TranslatedChannels { get; } = new ConcurrentDictionary<ulong, bool>();
// (userId, channelId)
public ConcurrentDictionary<(ulong UserId, ulong ChannelId), string> UserLanguages { get; } = new ConcurrentDictionary<(ulong, ulong), string>();
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
public List<MagicItem> MagicItems { get; } = new List<MagicItem>();
private readonly ConcurrentDictionary<ulong, SearchImageCacher> _imageCacher = new ConcurrentDictionary<ulong, SearchImageCacher>();
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new ConcurrentDictionary<ulong, Timer>();
private readonly ConcurrentDictionary<ulong, HashSet<string>> _blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>();
private readonly List<string> _yomamaJokes;
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
DbService db, NadekoBot bot, IDataCache cache, IHttpClientFactory factory,
FontProvider fonts, IBotCredentials creds)
{
_httpFactory = factory;
_client = client;
_google = google;
_db = db;
_imgs = cache.LocalImages;
_cache = cache;
_fonts = fonts;
_creds = creds;
_rng = new NadekoRandom();
_blacklistedTags = new ConcurrentDictionary<ulong, HashSet<string>>(
bot.AllGuildConfigs.ToDictionary(
x => x.GuildId,
x => new HashSet<string>(x.NsfwBlacklistedTags.Select(y => y.Tag))));
//translate commands
_client.MessageReceived += (msg) =>
{
var _ = Task.Run(async () =>
{
try
{
if (!(msg is SocketUserMessage umsg))
return;
if (!TranslatedChannels.TryGetValue(umsg.Channel.Id, out var autoDelete))
return;
var key = (umsg.Author.Id, umsg.Channel.Id);
if (!UserLanguages.TryGetValue(key, out string langs))
return;
var text = await Translate(langs, umsg.Resolve(TagHandling.Ignore))
.ConfigureAwait(false);
if (autoDelete)
try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { }
await umsg.Channel.SendConfirmAsync($"{umsg.Author.Mention} `:` "
+ text.Replace("<@ ", "<@", StringComparison.InvariantCulture)
.Replace("<@! ", "<@!", StringComparison.InvariantCulture)).ConfigureAwait(false);
}
catch { }
});
return Task.CompletedTask;
};
//joke commands
if (File.Exists("data/wowjokes.json"))
{
WowJokes = JsonConvert.DeserializeObject<List<WoWJoke>>(File.ReadAllText("data/wowjokes.json"));
}
else
Log.Warning("data/wowjokes.json is missing. WOW Jokes are not loaded.");
if (File.Exists("data/magicitems.json"))
{
MagicItems = JsonConvert.DeserializeObject<List<MagicItem>>(File.ReadAllText("data/magicitems.json"));
}
else
Log.Warning("data/magicitems.json is missing. Magic items are not loaded.");
if (File.Exists("data/yomama.txt"))
{
_yomamaJokes = File.ReadAllLines("data/yomama.txt")
.Shuffle()
.ToList();
}
else
{
_yomamaJokes = new List<string>();
Log.Warning("data/yomama.txt is missing. .yomama command won't work");
}
}
public async Task<Stream> GetRipPictureAsync(string text, Uri imgUrl)
{
byte[] data = await _cache.GetOrAddCachedDataAsync($"nadeko_rip_{text}_{imgUrl}",
GetRipPictureFactory,
(text, imgUrl),
TimeSpan.FromDays(1)).ConfigureAwait(false);
return data.ToStream();
}
private void DrawAvatar(Image bg, Image avatarImage)
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new Point(83, 139), new GraphicsOptions()));
public async Task<byte[]> GetRipPictureFactory((string text, Uri avatarUrl) arg)
{
var (text, avatarUrl) = arg;
using (var bg = Image.Load<Rgba32>(_imgs.Rip.ToArray()))
{
var (succ, data) = (false, (byte[])null); //await _cache.TryGetImageDataAsync(avatarUrl);
if (!succ)
{
using (var http = _httpFactory.CreateClient())
{
data = await http.GetByteArrayAsync(avatarUrl);
using (var avatarImg = Image.Load<Rgba32>(data))
{
avatarImg.Mutate(x => x
.Resize(85, 85)
.ApplyRoundedCorners(42));
data = avatarImg.ToStream().ToArray();
DrawAvatar(bg, avatarImg);
}
await _cache.SetImageDataAsync(avatarUrl, data);
}
}
else
{
using (var avatarImg = Image.Load<Rgba32>(data))
{
DrawAvatar(bg, avatarImg);
}
}
bg.Mutate(x => x.DrawText(
new TextGraphicsOptions()
{
TextOptions = new TextOptions
{
HorizontalAlignment = HorizontalAlignment.Center,
WrapTextWidth = 190,
}.WithFallbackFonts(_fonts.FallBackFonts)
},
text,
_fonts.RipFont,
SixLabors.ImageSharp.Color.Black,
new PointF(25, 225)));
//flowa
using (var flowers = Image.Load(_imgs.RipOverlay.ToArray()))
{
bg.Mutate(x => x.DrawImage(flowers, new Point(0, 0), new GraphicsOptions()));
}
return bg.ToStream().ToArray();
}
}
public Task<WeatherData> GetWeatherDataAsync(string query)
{
query = query.Trim().ToLowerInvariant();
return _cache.GetOrAddCachedDataAsync($"nadeko_weather_{query}",
GetWeatherDataFactory,
query,
expiry: TimeSpan.FromHours(3));
}
private async Task<WeatherData> GetWeatherDataFactory(string query)
{
using (var http = _httpFactory.CreateClient())
{
try
{
var data = await http.GetStringAsync($"http://api.openweathermap.org/data/2.5/weather?" +
$"q={query}&" +
$"appid=42cd627dd60debf25a5739e50a217d74&" +
$"units=metric").ConfigureAwait(false);
if (data == null)
return null;
return JsonConvert.DeserializeObject<WeatherData>(data);
}
catch (Exception ex)
{
Log.Warning(ex.Message);
return null;
}
}
}
public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg)
{
return GetTimeDataFactory(arg);
//return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}",
// GetTimeDataFactory,
// arg,
// TimeSpan.FromMinutes(1));
}
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory(string query)
{
query = query.Trim();
if (string.IsNullOrEmpty(query))
{
return (default, TimeErrors.InvalidInput);
}
if (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey)
|| string.IsNullOrWhiteSpace(_creds.TimezoneDbApiKey))
{
return (default, TimeErrors.ApiKeyMissing);
}
try
{
using (var _http = _httpFactory.CreateClient())
{
var res = await _cache.GetOrAddCachedDataAsync($"geo_{query}", _ =>
{
var url = "https://eu1.locationiq.com/v1/search.php?" +
(string.IsNullOrWhiteSpace(_creds.LocationIqApiKey) ? "key=" : $"key={_creds.LocationIqApiKey}&") +
$"q={Uri.EscapeDataString(query)}&" +
$"format=json";
var res = _http.GetStringAsync(url);
return res;
}, "", TimeSpan.FromHours(1));
var responses = JsonConvert.DeserializeObject<LocationIqResponse[]>(res);
if (responses is null || responses.Length == 0)
{
Log.Warning("Geocode lookup failed for: {Query}", query);
return (default, TimeErrors.NotFound);
}
var geoData = responses[0];
using (var req = new HttpRequestMessage(HttpMethod.Get, "http://api.timezonedb.com/v2.1/get-time-zone?" +
$"key={_creds.TimezoneDbApiKey}&format=json&" +
"by=position&" +
$"lat={geoData.Lat}&lng={geoData.Lon}"))
{
using (var geoRes = await _http.SendAsync(req))
{
var resString = await geoRes.Content.ReadAsStringAsync();
var timeObj = JsonConvert.DeserializeObject<TimeZoneResult>(resString);
var time = new DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc).AddSeconds(timeObj.Timestamp);
return ((
Address: responses[0].DisplayName,
Time: time,
TimeZoneName: timeObj.TimezoneName
), default);
}
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Weather error: {Message}", ex.Message);
return (default, TimeErrors.NotFound);
}
}
public enum ImageTag
{
Food,
Dogs,
Cats,
Birds
}
public string GetRandomImageUrl(ImageTag tag)
{
var subpath = tag.ToString().ToLowerInvariant();
int max;
switch (tag)
{
case ImageTag.Food:
max = 773;
break;
case ImageTag.Dogs:
max = 750;
break;
case ImageTag.Cats:
max = 773;
break;
case ImageTag.Birds:
max = 578;
break;
default:
max = 100;
break;
}
return $"https://nadeko-pictures.nyc3.digitaloceanspaces.com/{subpath}/" +
_rng.Next(1, max).ToString("000") + ".png";
}
public async Task<string> Translate(string langs, string text = null)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text is empty or null", nameof(text));
var langarr = langs.ToLowerInvariant().Split('>');
if (langarr.Length != 2)
throw new ArgumentException("Langs does not have 2 parts separated by a >", nameof(langs));
var from = langarr[0];
var to = langarr[1];
text = text?.Trim();
return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(true);
}
public Task<ImageCacherObject> DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false)
{
tag = tag ?? "";
if (string.IsNullOrWhiteSpace(tag)
&& (tag.Contains("loli") || tag.Contains("shota")))
{
return null;
}
var tags = tag
.Split('+')
.Select(x => x.ToLowerInvariant().Replace(' ', '_'))
.ToArray();
if (guild.HasValue)
{
var blacklistedTags = GetBlacklistedTags(guild.Value);
var cacher = _imageCacher.GetOrAdd(guild.Value, (key) => new SearchImageCacher(_httpFactory));
return cacher.GetImage(tags, isExplicit, type, blacklistedTags);
}
else
{
var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher(_httpFactory));
return cacher.GetImage(tags, isExplicit, type);
}
}
public HashSet<string> GetBlacklistedTags(ulong guildId)
{
if (_blacklistedTags.TryGetValue(guildId, out var tags))
return tags;
return new HashSet<string>();
}
public bool ToggleBlacklistedTag(ulong guildId, string tag)
{
var tagObj = new NsfwBlacklitedTag
{
Tag = tag
};
bool added;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(y => y.NsfwBlacklistedTags));
if (gc.NsfwBlacklistedTags.Add(tagObj))
added = true;
else
{
gc.NsfwBlacklistedTags.Remove(tagObj);
var toRemove = gc.NsfwBlacklistedTags.FirstOrDefault(x => x.Equals(tagObj));
if (toRemove != null)
uow._context.Remove(toRemove);
added = false;
}
var newTags = new HashSet<string>(gc.NsfwBlacklistedTags.Select(x => x.Tag));
_blacklistedTags.AddOrUpdate(guildId, newTags, delegate { return newTags; });
uow.SaveChanges();
}
return added;
}
public void ClearCache()
{
foreach (var c in _imageCacher)
{
c.Value?.Clear();
}
}
private readonly object yomamaLock = new object();
private int yomamaJokeIndex = 0;
public Task<string> GetYomamaJoke()
{
string joke;
lock (yomamaLock)
{
if (yomamaJokeIndex >= _yomamaJokes.Count)
{
yomamaJokeIndex = 0;
var newList = _yomamaJokes.ToList();
_yomamaJokes.Clear();
_yomamaJokes.AddRange(newList.Shuffle());
}
joke = _yomamaJokes[yomamaJokeIndex++];
}
return Task.FromResult(joke);
// using (var http = _httpFactory.CreateClient())
// {
// var response = await http.GetStringAsync(new Uri("http://api.yomomma.info/")).ConfigureAwait(false);
// return JObject.Parse(response)["joke"].ToString() + " 😆";
// }
}
public async Task<(string Setup, string Punchline)> GetRandomJoke()
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync("https://official-joke-api.appspot.com/random_joke");
var resObj = JsonConvert.DeserializeAnonymousType(res, new {setup = "", punchline = ""});
return (resObj.setup, resObj.punchline);
}
}
public async Task<string> GetChuckNorrisJoke()
{
using (var http = _httpFactory.CreateClient())
{
var response = await http.GetStringAsync(new Uri("http://api.icndb.com/jokes/random/")).ConfigureAwait(false);
return JObject.Parse(response)["value"]["joke"].ToString() + " 😆";
}
}
public Task Unload()
{
AutoBoobTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
AutoBoobTimers.Clear();
AutoButtTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
AutoButtTimers.Clear();
AutoHentaiTimers.ForEach(x => x.Value.Change(Timeout.Infinite, Timeout.Infinite));
AutoHentaiTimers.Clear();
_imageCacher.Clear();
return Task.CompletedTask;
}
public async Task<MtgData> GetMtgCardAsync(string search)
{
search = search.Trim().ToLowerInvariant();
var data = await _cache.GetOrAddCachedDataAsync($"nadeko_mtg_{search}",
GetMtgCardFactory,
search,
TimeSpan.FromDays(1)).ConfigureAwait(false);
if (data == null || data.Length == 0)
return null;
return data[_rng.Next(0, data.Length)];
}
private async Task<MtgData[]> GetMtgCardFactory(string search)
{
async Task<MtgData> GetMtgDataAsync(MtgResponse.Data card)
{
string storeUrl;
try
{
storeUrl = await _google.ShortenUrl($"https://shop.tcgplayer.com/productcatalog/product/show?" +
$"newSearch=false&" +
$"ProductType=All&" +
$"IsProductNameExact=false&" +
$"ProductName={Uri.EscapeUriString(card.Name)}").ConfigureAwait(false);
}
catch { storeUrl = "<url can't be found>"; }
return new MtgData
{
Description = card.Text,
Name = card.Name,
ImageUrl = card.ImageUrl,
StoreUrl = storeUrl,
Types = string.Join(",\n", card.Types),
ManaCost = card.ManaCost,
};
}
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Clear();
var response = await http.GetStringAsync($"https://api.magicthegathering.io/v1/cards?name={Uri.EscapeUriString(search)}")
.ConfigureAwait(false);
var responseObject = JsonConvert.DeserializeObject<MtgResponse>(response);
if (responseObject == null)
return new MtgData[0];
var cards = responseObject.Cards.Take(5).ToArray();
if (cards.Length == 0)
return new MtgData[0];
var tasks = new List<Task<MtgData>>(cards.Length);
for (int i = 0; i < cards.Length; i++)
{
var card = cards[i];
tasks.Add(GetMtgDataAsync(card));
}
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
public Task<HearthstoneCardData> GetHearthstoneCardDataAsync(string name)
{
name = name.ToLowerInvariant();
return _cache.GetOrAddCachedDataAsync($"nadeko_hearthstone_{name}",
HearthstoneCardDataFactory,
name,
TimeSpan.FromDays(1));
}
private async Task<HearthstoneCardData> HearthstoneCardDataFactory(string name)
{
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.MashapeKey);
try
{
var response = await http.GetStringAsync($"https://omgvamp-hearthstone-v1.p.rapidapi.com/" +
$"cards/search/{Uri.EscapeUriString(name)}").ConfigureAwait(false);
var objs = JsonConvert.DeserializeObject<HearthstoneCardData[]>(response);
if (objs == null || objs.Length == 0)
return null;
var data = objs.FirstOrDefault(x => x.Collectible)
?? objs.FirstOrDefault(x => !string.IsNullOrEmpty(x.PlayerClass))
?? objs.FirstOrDefault();
if (data == null)
return null;
if (!string.IsNullOrWhiteSpace(data.Img))
{
data.Img = await _google.ShortenUrl(data.Img).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(data.Text))
{
var converter = new Html2Markdown.Converter();
data.Text = converter.Convert(data.Text);
}
return data;
}
catch (Exception ex)
{
Log.Error(ex.Message);
return null;
}
}
}
public Task<OmdbMovie> GetMovieDataAsync(string name)
{
name = name.Trim().ToLowerInvariant();
return _cache.GetOrAddCachedDataAsync($"nadeko_movie_{name}",
GetMovieDataFactory,
name,
TimeSpan.FromDays(1));
}
private async Task<OmdbMovie> GetMovieDataFactory(string name)
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/?t={0}&y=&plot=full&r=json",
name.Trim().Replace(' ', '+'))).ConfigureAwait(false);
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
if (movie?.Title == null)
return null;
movie.Poster = await _google.ShortenUrl(movie.Poster).ConfigureAwait(false);
return movie;
}
}
public async Task<int> GetSteamAppIdByName(string query)
{
var redis = _cache.Redis;
var db = redis.GetDatabase();
const string STEAM_GAME_IDS_KEY = "steam_names_to_appid";
var exists = await db.KeyExistsAsync(STEAM_GAME_IDS_KEY).ConfigureAwait(false);
// if we didn't get steam name to id map already, get it
//if (!exists)
//{
// using (var http = _httpFactory.CreateClient())
// {
// // https://api.steampowered.com/ISteamApps/GetAppList/v2/
// var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/").ConfigureAwait(false);
// var apps = JsonConvert.DeserializeAnonymousType(gamesStr, new { applist = new { apps = new List<SteamGameId>() } }).applist.apps;
// //await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray()).ConfigureAwait(false);
// await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
// //await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget).ConfigureAwait(false);
// }
//}
var gamesMap = await _cache.GetOrAddCachedDataAsync(STEAM_GAME_IDS_KEY, async _ =>
{
using (var http = _httpFactory.CreateClient())
{
// https://api.steampowered.com/ISteamApps/GetAppList/v2/
var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/").ConfigureAwait(false);
var apps = JsonConvert.DeserializeAnonymousType(gamesStr, new { applist = new { apps = new List<SteamGameId>() } }).applist.apps;
return apps
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.GroupBy(x => x.Name)
.ToDictionary(x => x.Key, x => x.First().AppId);
//await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray()).ConfigureAwait(false);
//await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
//await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget).ConfigureAwait(false);
}
}, default(string), TimeSpan.FromHours(24));
if (gamesMap == null)
return -1;
query = query.Trim();
var keyList = gamesMap.Keys.ToList();
var key = keyList.FirstOrDefault(x => x.Equals(query, StringComparison.OrdinalIgnoreCase));
if (key == default)
{
key = keyList.FirstOrDefault(x => x.StartsWith(query, StringComparison.OrdinalIgnoreCase));
if (key == default)
return -1;
}
return gamesMap[key];
//// try finding the game id
//var val = db.HashGet(STEAM_GAME_IDS_KEY, query);
//if (val == default)
// return -1; // not found
//var appid = (int)val;
//return appid;
// now that we have appid, get the game info with that appid
//var gameData = await _cache.GetOrAddCachedDataAsync($"steam_game:{appid}", SteamGameDataFactory, appid, TimeSpan.FromHours(12))
// .ConfigureAwait(false);
//return gameData;
}
//private async Task<SteamGameData> SteamGameDataFactory(int appid)
//{
// using (var http = _httpFactory.CreateClient())
// {
// // https://store.steampowered.com/api/appdetails?appids=
// var responseStr = await http.GetStringAsync($"https://store.steampowered.com/api/appdetails?appids={appid}").ConfigureAwait(false);
// var data = JsonConvert.DeserializeObject<Dictionary<int, SteamGameData.Container>>(responseStr);
// if (!data.ContainsKey(appid) || !data[appid].Success)
// return null; // for some reason we can't get the game with valid appid. SHould never happen
// return data[appid].Data;
// }
//}
public class GoogleSearchResultData
{
public IReadOnlyList<GoogleSearchResult> Results { get; }
public string FullQueryLink { get; }
public string TotalResults { get; }
public GoogleSearchResultData(IReadOnlyList<GoogleSearchResult> results, string fullQueryLink,
string totalResults)
{
Results = results;
FullQueryLink = fullQueryLink;
TotalResults = totalResults;
}
}
private static readonly HtmlParser _googleParser = new HtmlParser(new HtmlParserOptions()
{
IsScripting = false,
IsEmbedded = false,
IsSupportingProcessingInstructions = false,
IsKeepingSourceReferences = false,
IsNotSupportingFrames = true,
});
public async Task<GoogleSearchResultData> GoogleSearchAsync(string query)
{
query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
var fullQueryLink = $"https://www.google.ca/search?q={ query }&safe=on&lr=lang_eng&hl=en&ie=utf-8&oe=utf-8";
using var msg = new HttpRequestMessage(HttpMethod.Get, fullQueryLink);
msg.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
msg.Headers.Add("Cookie", "CONSENT=YES+shp.gws-20210601-0-RC2.en+FX+423;");
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
using var response = await http.SendAsync(msg);
var content = await response.Content.ReadAsStreamAsync();
using var document = await _googleParser.ParseDocumentAsync(content);
var elems = document.QuerySelectorAll("div.g > div > div");
var resultsElem = document.QuerySelectorAll("#resultStats").FirstOrDefault();
var totalResults = resultsElem?.TextContent;
//var time = resultsElem.Children.FirstOrDefault()?.TextContent
//^ this doesn't work for some reason, <nobr> is completely missing in parsed collection
if (!elems.Any())
return default;
var results = elems.Select(elem =>
{
var children = elem.Children.ToList();
if (children.Count < 2)
return null;
var href = (children[0].QuerySelector("a") as IHtmlAnchorElement)?.Href;
var name = children[0].QuerySelector("h3")?.TextContent;
if (href == null || name == null)
return null;
var txt = children[1].TextContent;
if (string.IsNullOrWhiteSpace(txt))
return null;
return new GoogleSearchResult(name, href, txt);
})
.Where(x => x != null)
.ToList();
return new GoogleSearchResultData(
results.AsReadOnly(),
fullQueryLink,
totalResults);
}
public async Task<GoogleSearchResultData> DuckDuckGoSearchAsync(string query)
{
query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
var fullQueryLink = $"https://html.duckduckgo.com/html";
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
using var formData = new MultipartFormDataContent();
formData.Add(new StringContent(query), "q");
using var response = await http.PostAsync(fullQueryLink, formData);
var content = await response.Content.ReadAsStringAsync();
using var document = await _googleParser.ParseDocumentAsync(content);
var searchResults = document.QuerySelector(".results");
var elems = searchResults.QuerySelectorAll(".result");
if (!elems.Any())
return default;
var results = elems.Select(elem =>
{
var anchor = elem.QuerySelector(".result__a") as IHtmlAnchorElement;
if (anchor is null)
return null;
var href = anchor.Href;
var name = anchor.TextContent;
if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(name))
return null;
var txt = elem.QuerySelector(".result__snippet")?.TextContent;
if (string.IsNullOrWhiteSpace(txt))
return null;
return new GoogleSearchResult(name, href, txt);
})
.Where(x => x != null)
.ToList();
return new GoogleSearchResultData(
results.AsReadOnly(),
fullQueryLink,
"0");
}
#region Nhentai
private string GetNhentaiExtensionInternal(string s)
=> s switch
{
"j" => "jpg",
"p" => "png",
"g" => "gif",
_ => "jpg"
};
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
{
var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb."
+ GetNhentaiExtensionInternal(model.Images.Thumbnail.T);
var url = $"https://nhentai.net/g/{model.Id}";
return new Gallery(
model.Id.ToString(),
url,
model.Title.English,
model.Title.Pretty,
thumbnail,
model.NumPages,
model.NumFavorites,
model.UploadDate.ToUnixTimestamp().UtcDateTime,
model.Tags.Map(x => new Tag()
{
Name = x.Name,
Url = "https://nhentai.com/" + x.Url
}));
}
public async Task<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
{
using var http = _httpFactory.CreateClient();
try
{
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with id {NhentaiId} not found", id);
return null;
}
}
private async Task<NhentaiApiModel.Gallery[]> SearchNhentaiInternalAsync(string search)
{
using var http = _httpFactory.CreateClient();
try
{
var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search);
return JsonConvert.DeserializeObject<NhentaiApiModel.SearchResult>(res).Result;
}
catch (HttpRequestException)
{
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
return null;
}
}
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
{
var model = await GetNhentaiByIdInternalAsync(id);
return ModelToGallery(model);
}
private static readonly string[] _bannedTags =
{
"loli",
"lolicon",
"shota",
"shotacon",
"cub"
};
public async Task<Gallery> GetNhentaiBySearchAsync(string search)
{
var models = await SearchNhentaiInternalAsync(search);
models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray();
if (models.Length == 0)
return null;
return ModelToGallery(models[_rng.Next(0, models.Length)]);
}
#endregion
}
}

View File

@@ -0,0 +1,578 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Core.Modules.Searches.Common;
using NadekoBot.Core.Modules.Searches.Common.StreamNotifications;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using StackExchange.Redis;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.Collections;
using NadekoBot.Common.Replacements;
using Serilog;
namespace NadekoBot.Modules.Searches.Services
{
public class StreamNotificationService : INService
{
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly Random _rng = new NadekoRandom();
private readonly DiscordSocketClient _client;
private readonly NotifChecker _streamTracker;
private readonly object _shardLock = new object();
private readonly Dictionary<StreamDataKey, HashSet<ulong>> _trackCounter =
new Dictionary<StreamDataKey, HashSet<ulong>>();
private readonly Dictionary<StreamDataKey, Dictionary<ulong, HashSet<FollowedStream>>> _shardTrackedStreams;
private readonly ConcurrentHashSet<ulong> _offlineNotificationServers;
private readonly ConnectionMultiplexer _multi;
private readonly IBotCredentials _creds;
private readonly Timer _notifCleanupTimer;
public StreamNotificationService(DbService db, DiscordSocketClient client,
IBotStrings strings, IDataCache cache, IBotCredentials creds, IHttpClientFactory httpFactory,
NadekoBot bot)
{
_db = db;
_client = client;
_strings = strings;
_multi = cache.Redis;
_creds = creds;
_streamTracker = new NotifChecker(httpFactory, cache.Redis, creds.RedisKey(), client.ShardId == 0);
using (var uow = db.GetDbContext())
{
var ids = client.GetGuildIds();
var guildConfigs = uow._context.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.FollowedStreams)
.Where(x => ids.Contains(x.GuildId))
.ToList();
_offlineNotificationServers = new ConcurrentHashSet<ulong>(guildConfigs
.Where(gc => gc.NotifyStreamOffline)
.Select(x => x.GuildId)
.ToList());
var followedStreams = guildConfigs
.SelectMany(x => x.FollowedStreams)
.ToList();
_shardTrackedStreams = followedStreams
.GroupBy(x => new {Type = x.Type, Name = x.Username.ToLower()})
.ToList()
.ToDictionary(
x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()),
x => x.GroupBy(y => y.GuildId)
.ToDictionary(y => y.Key, y => y.AsEnumerable().ToHashSet()));
// shard 0 will keep track of when there are no more guilds which track a stream
if (client.ShardId == 0)
{
var allFollowedStreams = uow._context.Set<FollowedStream>()
.AsQueryable()
.ToList();
foreach (var fs in allFollowedStreams)
{
_streamTracker.CacheAddData(fs.CreateKey(), null, replace: false);
}
_trackCounter = allFollowedStreams
.GroupBy(x => new {Type = x.Type, Name = x.Username.ToLower()})
.ToDictionary(
x => new StreamDataKey(x.Key.Type, x.Key.Name),
x => x.Select(fs => fs.GuildId).ToHashSet());
}
}
var sub = _multi.GetSubscriber();
sub.Subscribe($"{_creds.RedisKey()}_streams_offline", HandleStreamsOffline);
sub.Subscribe($"{_creds.RedisKey()}_streams_online", HandleStreamsOnline);
if (client.ShardId == 0)
{
// only shard 0 will run the tracker,
// and then publish updates with redis to other shards
_streamTracker.OnStreamsOffline += OnStreamsOffline;
_streamTracker.OnStreamsOnline += OnStreamsOnline;
_ = _streamTracker.RunAsync();
_notifCleanupTimer = new Timer(_ =>
{
try
{
var errorLimit = TimeSpan.FromHours(12);
var failingStreams = _streamTracker.GetFailingStreams(errorLimit, true)
.ToList();
if (!failingStreams.Any())
return;
var deleteGroups = failingStreams.GroupBy(x => x.Type)
.ToDictionary(x => x.Key, x => x.Select(x => x.Name).ToList());
using (var uow = _db.GetDbContext())
{
foreach (var kvp in deleteGroups)
{
Log.Information($"Deleting {kvp.Value.Count} {kvp.Key} streams because " +
$"they've been erroring for more than {errorLimit}: {string.Join(", ", kvp.Value)}");
var toDelete = uow._context.Set<FollowedStream>()
.AsQueryable()
.Where(x => x.Type == kvp.Key && kvp.Value.Contains(x.Username))
.ToList();
uow._context.RemoveRange(toDelete);
uow.SaveChanges();
foreach(var loginToDelete in kvp.Value)
_streamTracker.UntrackStreamByKey(new StreamDataKey(kvp.Key, loginToDelete));
}
}
}
catch (Exception ex)
{
Log.Error("Error cleaning up FollowedStreams");
Log.Error(ex.ToString());
}
}, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
sub.Subscribe($"{_creds.RedisKey()}_follow_stream", HandleFollowStream);
sub.Subscribe($"{_creds.RedisKey()}_unfollow_stream", HandleUnfollowStream);
}
bot.JoinedGuild += ClientOnJoinedGuild;
client.LeftGuild += ClientOnLeftGuild;
}
/// <summary>
/// Handles follow_stream pubs to keep the counter up to date.
/// When counter reaches 0, stream is removed from tracking because
/// that means no guilds are subscribed to that stream anymore
/// </summary>
private void HandleFollowStream(RedisChannel ch, RedisValue val)
=> Task.Run(() =>
{
var info = JsonConvert.DeserializeAnonymousType(
val.ToString(),
new {Key = default(StreamDataKey), GuildId = 0ul});
_streamTracker.CacheAddData(info.Key, null, replace: false);
lock (_shardLock)
{
var key = info.Key;
if (_trackCounter.ContainsKey(key))
{
_trackCounter[key].Add(info.GuildId);
}
else
{
_trackCounter[key] = new HashSet<ulong>()
{
info.GuildId
};
}
}
});
/// <summary>
/// Handles unfollow_stream pubs to keep the counter up to date.
/// When counter reaches 0, stream is removed from tracking because
/// that means no guilds are subscribed to that stream anymore
/// </summary>
private void HandleUnfollowStream(RedisChannel ch, RedisValue val)
=> Task.Run(() =>
{
var info = JsonConvert.DeserializeAnonymousType(val.ToString(),
new {Key = default(StreamDataKey), GuildId = 0ul});
lock (_shardLock)
{
var key = info.Key;
if (!_trackCounter.TryGetValue(key, out var set))
{
// it should've been removed already?
_streamTracker.UntrackStreamByKey(in key);
return;
}
set.Remove(info.GuildId);
if (set.Count != 0)
return;
_trackCounter.Remove(key);
// if no other guilds are following this stream
// untrack the stream
_streamTracker.UntrackStreamByKey(in key);
}
});
private void HandleStreamsOffline(RedisChannel arg1, RedisValue val) => Task.Run(async () =>
{
var offlineStreams = JsonConvert.DeserializeObject<List<StreamData>>(val.ToString());
foreach (var stream in offlineStreams)
{
var key = stream.CreateKey();
if (_shardTrackedStreams.TryGetValue(key, out var fss))
{
var sendTasks = fss
// send offline stream notifications only to guilds which enable it with .stoff
.SelectMany(x => x.Value)
.Where(x => _offlineNotificationServers.Contains(x.GuildId))
.Select(fs => _client.GetGuild(fs.GuildId)
?.GetTextChannel(fs.ChannelId)
?.EmbedAsync(GetEmbed(fs.GuildId, stream)));
await Task.WhenAll(sendTasks);
}
}
});
private void HandleStreamsOnline(RedisChannel arg1, RedisValue val) => Task.Run(async () =>
{
var onlineStreams = JsonConvert.DeserializeObject<List<StreamData>>(val.ToString());
foreach (var stream in onlineStreams)
{
var key = stream.CreateKey();
if (_shardTrackedStreams.TryGetValue(key, out var fss))
{
var sendTasks = fss
.SelectMany(x => x.Value)
.Select(fs =>
{
var textChannel = _client.GetGuild(fs.GuildId)?.GetTextChannel(fs.ChannelId);
if (textChannel is null)
return Task.CompletedTask;
var rep = new ReplacementBuilder()
.WithOverride("%user%", () => fs.Username)
.WithOverride("%platform%", () => fs.Type.ToString())
.Build();
var message = string.IsNullOrWhiteSpace(fs.Message)
? ""
: rep.Replace(fs.Message);
return textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
});
await Task.WhenAll(sendTasks);
}
}
});
private Task OnStreamsOffline(List<StreamData> data)
{
var sub = _multi.GetSubscriber();
return sub.PublishAsync($"{_creds.RedisKey()}_streams_offline", JsonConvert.SerializeObject(data));
}
private Task OnStreamsOnline(List<StreamData> data)
{
var sub = _multi.GetSubscriber();
return sub.PublishAsync($"{_creds.RedisKey()}_streams_online", JsonConvert.SerializeObject(data));
}
private Task ClientOnJoinedGuild(GuildConfig guildConfig)
{
using (var uow = _db.GetDbContext())
{
var gc = uow._context.GuildConfigs
.AsQueryable()
.Include(x => x.FollowedStreams)
.FirstOrDefault(x => x.GuildId == guildConfig.GuildId);
if (gc is null)
return Task.CompletedTask;
if (gc.NotifyStreamOffline)
_offlineNotificationServers.Add(gc.GuildId);
foreach (var followedStream in gc.FollowedStreams)
{
var key = followedStream.CreateKey();
var streams = GetLocalGuildStreams(key, gc.GuildId);
streams.Add(followedStream);
PublishFollowStream(followedStream);
}
}
return Task.CompletedTask;
}
private Task ClientOnLeftGuild(SocketGuild guild)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guild.Id, set => set.Include(x => x.FollowedStreams));
_offlineNotificationServers.TryRemove(gc.GuildId);
foreach (var followedStream in gc.FollowedStreams)
{
var streams = GetLocalGuildStreams(followedStream.CreateKey(), guild.Id);
streams.Remove(followedStream);
PublishUnfollowStream(followedStream);
}
}
return Task.CompletedTask;
}
public int ClearAllStreams(ulong guildId)
{
// todo future clear streams
int count;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.FollowedStreams));
count = gc.FollowedStreams.Count;
gc.FollowedStreams.Clear();
uow.SaveChanges();
}
return count;
}
public async Task<FollowedStream> UnfollowStreamAsync(ulong guildId, int index)
{
FollowedStream fs;
using (var uow = _db.GetDbContext())
{
var fss = uow._context.Set<FollowedStream>()
.AsQueryable()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Id)
.ToList();
// out of range
if (fss.Count <= index)
return null;
fs = fss[index];
uow._context.Remove(fs);
await uow.SaveChangesAsync();
// remove from local cache
lock (_shardLock)
{
var key = fs.CreateKey();
var streams = GetLocalGuildStreams(key, guildId);
streams.Remove(fs);
}
}
PublishUnfollowStream(fs);
return fs;
}
private void PublishUnfollowStream(FollowedStream fs)
{
var sub = _multi.GetSubscriber();
sub.Publish($"{_creds.RedisKey()}_unfollow_stream",
JsonConvert.SerializeObject(new {Key = fs.CreateKey(), GuildId = fs.GuildId}));
}
private void PublishFollowStream(FollowedStream fs)
{
var sub = _multi.GetSubscriber();
sub.Publish($"{_creds.RedisKey()}_follow_stream",
JsonConvert.SerializeObject(new {Key = fs.CreateKey(), GuildId = fs.GuildId}),
CommandFlags.FireAndForget);
}
public async Task<StreamData> FollowStream(ulong guildId, ulong channelId, string url)
{
// this will
var data = await _streamTracker.GetStreamDataByUrlAsync(url);
if (data is null)
return null;
FollowedStream fs;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.FollowedStreams));
// add it to the database
fs = new FollowedStream()
{
Type = data.StreamType,
Username = data.UniqueName,
ChannelId = channelId,
GuildId = guildId,
};
if (gc.FollowedStreams.Count >= 10)
return null;
gc.FollowedStreams.Add(fs);
await uow.SaveChangesAsync();
// add it to the local cache of tracked streams
// this way this shard will know it needs to post a message to discord
// when shard 0 publishes stream status changes for this stream
lock (_shardLock)
{
var key = data.CreateKey();
var streams = GetLocalGuildStreams(key, guildId);
streams.Add(fs);
}
}
PublishFollowStream(fs);
return data;
}
public EmbedBuilder GetEmbed(ulong guildId, StreamData status)
{
var embed = new EmbedBuilder()
.WithTitle(status.Name)
.WithUrl(status.StreamUrl)
.WithDescription(status.StreamUrl)
.AddField(efb => efb.WithName(GetText(guildId, "status"))
.WithValue(status.IsLive ? "🟢 Online" : "🔴 Offline")
.WithIsInline(true))
.AddField(efb => efb.WithName(GetText(guildId, "viewers"))
.WithValue(status.IsLive ? status.Viewers.ToString() : "-")
.WithIsInline(true))
.WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor);
if (!string.IsNullOrWhiteSpace(status.Title))
embed.WithAuthor(status.Title);
if (!string.IsNullOrWhiteSpace(status.Game))
embed.AddField(GetText(guildId, "streaming"), status.Game, true);
if (!string.IsNullOrWhiteSpace(status.AvatarUrl))
embed.WithThumbnailUrl(status.AvatarUrl);
if (!string.IsNullOrWhiteSpace(status.Preview))
embed.WithImageUrl(status.Preview + "?dv=" + _rng.Next());
return embed;
}
private string GetText(ulong guildId, string key, params object[] replacements)
=> _strings.GetText(key, guildId, replacements);
public bool ToggleStreamOffline(ulong guildId)
{
bool newValue;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigs.ForId(guildId, set => set);
newValue = gc.NotifyStreamOffline = !gc.NotifyStreamOffline;
uow.SaveChanges();
if (newValue)
{
_offlineNotificationServers.Add(guildId);
}
else
{
_offlineNotificationServers.TryRemove(guildId);
}
}
return newValue;
}
public Task<StreamData> GetStreamDataAsync(string url)
{
return _streamTracker.GetStreamDataByUrlAsync(url);
}
private HashSet<FollowedStream> GetLocalGuildStreams(in StreamDataKey key, ulong guildId)
{
if (_shardTrackedStreams.TryGetValue(key, out var map))
{
if (map.TryGetValue(guildId, out var set))
{
return set;
}
else
{
return map[guildId] = new HashSet<FollowedStream>();
}
}
else
{
_shardTrackedStreams[key] = new Dictionary<ulong, HashSet<FollowedStream>>()
{
{guildId, new HashSet<FollowedStream>()}
};
return _shardTrackedStreams[key][guildId];
}
}
public bool SetStreamMessage(ulong guildId, int index, string message, out FollowedStream fs)
{
using (var uow = _db.GetDbContext())
{
var fss = uow._context.Set<FollowedStream>()
.AsQueryable()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Id)
.ToList();
if (fss.Count <= index)
{
fs = null;
return false;
}
fs = fss[index];
fs.Message = message;
lock (_shardLock)
{
var streams = GetLocalGuildStreams(fs.CreateKey(), guildId);
// message doesn't participate in equality checking
// removing and adding = update
streams.Remove(fs);
streams.Add(fs);
}
uow.SaveChanges();
}
return true;
}
public int SetStreamMessageForAll(ulong guildId, string message)
{
using var uow = _db.GetDbContext();
var all = uow._context.Set<FollowedStream>()
.ToList();
if (all.Count == 0)
return 0;
all.ForEach(x => x.Message = message);
uow.SaveChanges();
return all.Count;
}
}
}

View File

@@ -0,0 +1,137 @@
using NadekoBot.Core.Services;
namespace NadekoBot.Core.Modules.Searches.Services
{
// public class YtTrackService : INService
// {
// private readonly IGoogleApiService _google;
// private readonly IHttpClientFactory httpClientFactory;
// private readonly DiscordSocketClient _client;
// private readonly DbService _db;
// private readonly ConcurrentDictionary<string, ConcurrentDictionary<ulong, List<YtFollowedChannel>>> followedChannels;
// private readonly ConcurrentDictionary<string, DateTime> _latestPublishes = new ConcurrentDictionary<string, DateTime>();
//
// public YtTrackService(IGoogleApiService google, IHttpClientFactory httpClientFactory, DiscordSocketClient client,
// DbService db)
// {
// this._google = google;
// this.httpClientFactory = httpClientFactory;
// this._client = client;
// this._db = db;
//
// if (_client.ShardId == 0)
// {
// _ = CheckLoop();
// }
// }
//
// public async Task CheckLoop()
// {
// while (true)
// {
// await Task.Delay(10000);
// using (var http = httpClientFactory.CreateClient())
// {
// await Task.WhenAll(followedChannels.Select(kvp => CheckChannel(kvp.Key, kvp.Value.SelectMany(x => x.Value).ToList())));
// }
// }
// }
//
// /// <summary>
// /// Checks the specified youtube channel, and sends a message to all provided
// /// </summary>
// /// <param name="youtubeChannelId">Id of the youtube channel</param>
// /// <param name="followedChannels">Where to post updates if there is a new update</param>
// private async Task CheckChannel(string youtubeChannelId, List<YtFollowedChannel> followedChannels)
// {
// var latestVid = (await _google.GetLatestChannelVideosAsync(youtubeChannelId, 1))
// .FirstOrDefault();
// if (latestVid is null)
// {
// return;
// }
//
// if (_latestPublishes.TryGetValue(youtubeChannelId, out var latestPub) && latestPub >= latestVid.PublishedAt)
// {
// return;
// }
// _latestPublishes[youtubeChannelId] = latestVid.PublishedAt;
//
// foreach (var chObj in followedChannels)
// {
// var gCh = _client.GetChannel(chObj.ChannelId);
// if (gCh is ITextChannel ch)
// {
// var msg = latestVid.GetVideoUrl();
// if (!string.IsNullOrWhiteSpace(chObj.UploadMessage))
// msg = chObj.UploadMessage + Environment.NewLine + msg;
//
// await ch.SendMessageAsync(msg);
// }
// }
// }
//
// /// <summary>
// /// Starts posting updates on the specified discord channel when a new video is posted on the specified YouTube channel.
// /// </summary>
// /// <param name="guildId">Id of the discord guild</param>
// /// <param name="channelId">Id of the discord channel</param>
// /// <param name="ytChannelId">Id of the youtube channel</param>
// /// <param name="uploadMessage">Message to post when a new video is uploaded, along with video URL</param>
// /// <returns>Whether adding was successful</returns>
// public async Task<bool> ToggleChannelFollowAsync(ulong guildId, ulong channelId, string ytChannelId, string uploadMessage)
// {
// // to to see if we can get a video from that channel
// var vids = await _google.GetLatestChannelVideosAsync(ytChannelId, 1);
// if (vids.Count == 0)
// return false;
//
// using(var uow = _db.GetDbContext())
// {
// var gc = uow.GuildConfigs.ForId(guildId, set => set.Include(x => x.YtFollowedChannels));
//
// // see if this yt channel was already followed on this discord channel
// var oldObj = gc.YtFollowedChannels
// .FirstOrDefault(x => x.ChannelId == channelId && x.YtChannelId == ytChannelId);
//
// if(!(oldObj is null))
// {
// return false;
// }
//
// // can only add up to 10 tracked channels per server
// if (gc.YtFollowedChannels.Count >= 10)
// {
// return false;
// }
//
// var obj = new YtFollowedChannel
// {
// ChannelId = channelId,
// YtChannelId = ytChannelId,
// UploadMessage = uploadMessage
// };
//
// // add to database
// gc.YtFollowedChannels.Add(obj);
//
// // add to the local cache:
//
// // get follows on all guilds
// var allGuildFollows = followedChannels.GetOrAdd(ytChannelId, new ConcurrentDictionary<ulong, List<YtFollowedChannel>>());
// // add to this guild's follows
// allGuildFollows.AddOrUpdate(guildId,
// new List<YtFollowedChannel>(),
// (key, old) =>
// {
// old.Add(obj);
// return old;
// });
//
// await uow.SaveChangesAsync();
// }
//
// return true;
// }
// }
}

View File

@@ -0,0 +1,229 @@
using Discord.Commands;
using Discord;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using System.Collections.Generic;
using NadekoBot.Core.Services.Database.Models;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using Discord.WebSocket;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class StreamNotificationCommands : NadekoSubmodule<StreamNotificationService>
{
private readonly DbService _db;
public StreamNotificationCommands(DbService db)
{
_db = db;
}
// private static readonly Regex picartoRegex = new Regex(@"picarto.tv/(?<name>.+[^/])/?",
// RegexOptions.Compiled | RegexOptions.IgnoreCase);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task StreamAdd(string link)
{
var data = await _service.FollowStream(ctx.Guild.Id, ctx.Channel.Id, link);
if (data is null)
{
await ReplyErrorLocalizedAsync("stream_not_added").ConfigureAwait(false);
return;
}
var embed = _service.GetEmbed(ctx.Guild.Id, data);
await ctx.Channel.EmbedAsync(embed, GetText("stream_tracked")).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task StreamRemove(int index)
{
if (--index < 0)
return;
var fs = await _service.UnfollowStreamAsync(ctx.Guild.Id, index);
if (fs is null)
{
await ReplyErrorLocalizedAsync("stream_no").ConfigureAwait(false);
return;
}
await ReplyConfirmLocalizedAsync(
"stream_removed",
Format.Bold(fs.Username),
fs.Type).ConfigureAwait(false);
}
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// [UserPerm(GuildPerm.Administrator)]
// public async Task StreamsClear()
// {
// var count = _service.ClearAllStreams(ctx.Guild.Id);
// await ReplyConfirmLocalizedAsync("streams_cleared", count).ConfigureAwait(false);
// }
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task StreamList(int page = 1)
{
if (page-- < 1)
{
return;
}
List<FollowedStream> streams = new List<FollowedStream>();
using (var uow = _db.GetDbContext())
{
var all = uow.GuildConfigs
.ForId(ctx.Guild.Id, set => set.Include(gc => gc.FollowedStreams))
.FollowedStreams
.OrderBy(x => x.Id)
.ToList();
for (var index = all.Count - 1; index >= 0; index--)
{
var fs = all[index];
if (((SocketGuild) ctx.Guild).GetTextChannel(fs.ChannelId) is null)
{
await _service.UnfollowStreamAsync(fs.GuildId, index);
}
else
{
streams.Insert(0, fs);
}
}
}
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var elements = streams.Skip(cur * 12).Take(12)
.ToList();
if (elements.Count == 0)
{
return new EmbedBuilder()
.WithDescription(GetText("streams_none"))
.WithErrorColor();
}
var eb = new EmbedBuilder()
.WithTitle(GetText("streams_follow_title"))
.WithOkColor();
for (var index = 0; index < elements.Count; index++)
{
var elem = elements[index];
eb.AddField(
$"**#{(index + 1) + (12 * cur)}** {elem.Username.ToLower()}",
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
true);
}
return eb;
}, streams.Count, 12).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task StreamOffline()
{
var newValue = _service.ToggleStreamOffline(ctx.Guild.Id);
if (newValue)
{
await ReplyConfirmLocalizedAsync("stream_off_enabled").ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync("stream_off_disabled").ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task StreamMessage(int index, [Leftover] string message)
{
if (--index < 0)
return;
if (!_service.SetStreamMessage(ctx.Guild.Id, index, message, out var fs))
{
await ReplyConfirmLocalizedAsync("stream_not_following").ConfigureAwait(false);
return;
}
if (string.IsNullOrWhiteSpace(message))
{
await ReplyConfirmLocalizedAsync("stream_message_reset", Format.Bold(fs.Username))
.ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync("stream_message_set", Format.Bold(fs.Username))
.ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task StreamMessageAll([Leftover] string message)
{
var count = _service.SetStreamMessageForAll(ctx.Guild.Id, message);
if (count == 0)
{
await ReplyConfirmLocalizedAsync("stream_not_following_any");
return;
}
await ReplyConfirmLocalizedAsync("stream_message_set_all", count);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task StreamCheck(string url)
{
try
{
var data = await _service.GetStreamDataAsync(url).ConfigureAwait(false);
if (data is null)
{
await ReplyErrorLocalizedAsync("no_channel_found").ConfigureAwait(false);
return;
}
if (data.IsLive)
{
await ReplyConfirmLocalizedAsync("streamer_online",
Format.Bold(data.Name),
Format.Bold(data.Viewers.ToString()))
.ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync("streamer_offline", data.Name)
.ConfigureAwait(false);
}
}
catch
{
await ReplyErrorLocalizedAsync("no_channel_found").ConfigureAwait(false);
}
}
}
}
}

View File

@@ -0,0 +1,133 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using System.Threading.Tasks;
using System.Linq;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Searches.Services;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class TranslateCommands : NadekoSubmodule
{
private readonly SearchesService _searches;
private readonly IGoogleApiService _google;
public TranslateCommands(SearchesService searches, IGoogleApiService google)
{
_searches = searches;
_google = google;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Translate(string langs, [Leftover] string text = null)
{
try
{
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var translation = await _searches.Translate(langs, text).ConfigureAwait(false);
await ctx.Channel.SendConfirmAsync(GetText("translation") + " " + langs, translation).ConfigureAwait(false);
}
catch
{
await ReplyErrorLocalizedAsync("bad_input_format").ConfigureAwait(false);
}
}
//[NadekoCommand, Usage, Description, Aliases]
//[OwnerOnly]
//public async Task Obfuscate([Leftover] string txt)
//{
// var lastItem = "en";
// foreach (var item in _google.Languages.Except(new[] { "en" }).Where(x => x.Length < 4))
// {
// var txt2 = await _searches.Translate(lastItem + ">" + item, txt);
// await ctx.Channel.EmbedAsync(new EmbedBuilder()
// .WithOkColor()
// .WithTitle(lastItem + ">" + item)
// .AddField("Input", txt)
// .AddField("Output", txt2));
// txt = txt2;
// await Task.Delay(500);
// lastItem = item;
// }
// txt = await _searches.Translate(lastItem + ">en", txt);
// await ctx.Channel.SendConfirmAsync("Final output:\n\n" + txt);
//}
public enum AutoDeleteAutoTranslate
{
Del,
Nodel
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task AutoTranslate(AutoDeleteAutoTranslate autoDelete = AutoDeleteAutoTranslate.Nodel)
{
var channel = (ITextChannel)ctx.Channel;
if (autoDelete == AutoDeleteAutoTranslate.Del)
{
_searches.TranslatedChannels.AddOrUpdate(channel.Id, true, (key, val) => true);
await ReplyConfirmLocalizedAsync("atl_ad_started").ConfigureAwait(false);
return;
}
if (_searches.TranslatedChannels.TryRemove(channel.Id, out _))
{
await ReplyConfirmLocalizedAsync("atl_stopped").ConfigureAwait(false);
return;
}
if (_searches.TranslatedChannels.TryAdd(channel.Id, autoDelete == AutoDeleteAutoTranslate.Del))
{
await ReplyConfirmLocalizedAsync("atl_started").ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task AutoTransLang([Leftover] string langs = null)
{
var ucp = (ctx.User.Id, ctx.Channel.Id);
if (string.IsNullOrWhiteSpace(langs))
{
if (_searches.UserLanguages.TryRemove(ucp, out langs))
await ReplyConfirmLocalizedAsync("atl_removed").ConfigureAwait(false);
return;
}
var langarr = langs.ToLowerInvariant().Split('>');
if (langarr.Length != 2)
return;
var from = langarr[0];
var to = langarr[1];
if (!_google.Languages.Contains(from) || !_google.Languages.Contains(to))
{
await ReplyErrorLocalizedAsync("invalid_lang").ConfigureAwait(false);
return;
}
_searches.UserLanguages.AddOrUpdate(ucp, langs, (key, val) => langs);
await ReplyConfirmLocalizedAsync("atl_set", from, to).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Translangs()
{
await ctx.Channel.SendTableAsync(_google.Languages, str => $"{str,-15}", 3).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,104 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class XkcdCommands : NadekoSubmodule
{
private const string _xkcdUrl = "https://xkcd.com";
private readonly IHttpClientFactory _httpFactory;
public XkcdCommands(IHttpClientFactory factory)
{
_httpFactory = factory;
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Xkcd(string arg = null)
{
if (arg?.ToLowerInvariant().Trim() == "latest")
{
try
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync($"{_xkcdUrl}/info.0.json").ConfigureAwait(false);
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
.WithImageUrl(comic.ImageLink)
.WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{comic.Num}").WithIconUrl("https://xkcd.com/s/919f27.ico"))
.AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("date")).WithValue($"{comic.Month}/{comic.Year}").WithIsInline(true));
var sent = await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
await Task.Delay(10000).ConfigureAwait(false);
await sent.ModifyAsync(m => m.Embed = embed.AddField(efb => efb.WithName("Alt").WithValue(comic.Alt.ToString()).WithIsInline(false)).Build()).ConfigureAwait(false);
}
}
catch (HttpRequestException)
{
await ReplyErrorLocalizedAsync("comic_not_found").ConfigureAwait(false);
}
return;
}
await Xkcd(new NadekoRandom().Next(1, 1750)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Xkcd(int num)
{
if (num < 1)
return;
try
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync($"{_xkcdUrl}/{num}/info.0.json").ConfigureAwait(false);
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
.WithImageUrl(comic.ImageLink)
.WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{num}").WithIconUrl("https://xkcd.com/s/919f27.ico"))
.AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("date")).WithValue($"{comic.Month}/{comic.Year}").WithIsInline(true));
var sent = await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
await Task.Delay(10000).ConfigureAwait(false);
await sent.ModifyAsync(m => m.Embed = embed.AddField(efb => efb.WithName("Alt").WithValue(comic.Alt.ToString()).WithIsInline(false)).Build()).ConfigureAwait(false);
}
}
catch (HttpRequestException)
{
await ReplyErrorLocalizedAsync("comic_not_found").ConfigureAwait(false);
}
}
}
public class XkcdComic
{
public int Num { get; set; }
public string Month { get; set; }
public string Year { get; set; }
[JsonProperty("safe_title")]
public string Title { get; set; }
[JsonProperty("img")]
public string ImageLink { get; set; }
public string Alt { get; set; }
}
}
}

View File

@@ -0,0 +1,58 @@
using Discord.Commands;
using NadekoBot.Core.Modules.Searches.Services;
using NadekoBot.Modules;
namespace NadekoBot.Core.Modules.Searches
{
public partial class Searches
{
// [Group]
// public class YtTrackCommands : NadekoSubmodule<YtTrackService>
// {
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task YtFollow(string ytChannelId, [Leftover] string uploadMessage = null)
// {
// var succ = await _service.ToggleChannelFollowAsync(Context.Guild.Id, Context.Channel.Id, ytChannelId, uploadMessage);
// if(succ)
// {
// await ReplyConfirmLocalizedAsync("yt_follow_added").ConfigureAwait(false);
// }
// else
// {
// await ReplyConfirmLocalizedAsync("yt_follow_fail").ConfigureAwait(false);
// }
// }
//
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task YtTrackRm(int index)
// {
// //var succ = await _service.ToggleChannelTrackingAsync(Context.Guild.Id, Context.Channel.Id, ytChannelId, uploadMessage);
// //if (succ)
// //{
// // await ReplyConfirmLocalizedAsync("yt_track_added").ConfigureAwait(false);
// //}
// //else
// //{
// // await ReplyConfirmLocalizedAsync("yt_track_fail").ConfigureAwait(false);
// //}
// }
//
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task YtTrackList()
// {
// //var succ = await _service.ToggleChannelTrackingAsync(Context.Guild.Id, Context.Channel.Id, ytChannelId, uploadMessage);
// //if (succ)
// //{
// // await ReplyConfirmLocalizedAsync("yt_track_added").ConfigureAwait(false);
// //}
// //else
// //{
// // await ReplyConfirmLocalizedAsync("yt_track_fail").ConfigureAwait(false);
// //}
// }
// }
}
}