commit 58be680e4f1fd161b1eaaca93c36dc43b758f982 Author: jetsparrow Date: Sat Dec 15 00:43:47 2018 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9241fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover + +#secret config +karma.cfg.json diff --git a/JetKarmaBot.sln b/JetKarmaBot.sln new file mode 100644 index 0000000..9a514a0 --- /dev/null +++ b/JetKarmaBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JetKarmaBot", "JetKarmaBot\JetKarmaBot.csproj", "{729E88EE-BE5E-4D12-B83F-EDC5FC5E2D07}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {729E88EE-BE5E-4D12-B83F-EDC5FC5E2D07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {729E88EE-BE5E-4D12-B83F-EDC5FC5E2D07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {729E88EE-BE5E-4D12-B83F-EDC5FC5E2D07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {729E88EE-BE5E-4D12-B83F-EDC5FC5E2D07}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A780BD47-2B76-491D-AAF1-2A97B22A420C} + EndGlobalSection +EndGlobal diff --git a/JetKarmaBot/CommandRouter.cs b/JetKarmaBot/CommandRouter.cs new file mode 100644 index 0000000..7f944cf --- /dev/null +++ b/JetKarmaBot/CommandRouter.cs @@ -0,0 +1,36 @@ +using JetKarmaBot.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using Telegram.Bot.Args; + +namespace JetKarmaBot +{ + class ChatCommandRouter + { + Dictionary commands = new Dictionary(); + public bool Execute(object sender, MessageEventArgs args) + { + var text = args.Message.Text; + + if (CommandString.TryParse(text, out var cs)) + { + if (commands.ContainsKey(cs.Name)) + return commands[cs.Name].Execute(sender,args); + } + + return false; + } + + public void Add(IChatCommand c) + { + foreach (var name in c.Names) + { + if (commands.ContainsKey(name)) + throw new Exception($"command collision for name {name}, commands {commands[name].GetType()} and {c.GetType()}"); + commands[name] = c; + } + } + + } +} diff --git a/JetKarmaBot/Commands/AwardCommand.cs b/JetKarmaBot/Commands/AwardCommand.cs new file mode 100644 index 0000000..f35b9a4 --- /dev/null +++ b/JetKarmaBot/Commands/AwardCommand.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Telegram.Bot; +using Telegram.Bot.Args; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace JetKarmaBot.Commands +{ + class AwardCommand : IChatCommand + { + public IReadOnlyCollection Names => new[] { "award"}; + + public bool Execute(object sender, MessageEventArgs args) + { + //var mentions = args.Message.Entities.Where(e => e.Type == MessageEntityType.Mention).ToArray(); + if (args.Message.ReplyToMessage == null)// && !mentions.Any()) + { + Client.SendTextMessageAsync(args.Message.Chat.Id, "Please use this command in reply to another user, or use a mention."); + return true; + } + + var awarder = args.Message.From; + //var members = Client.get(,).Result; + //var recipient = mentions.FirstOrDefault()?.User ?? args.Message.ReplyToMessage.From; + var recipient = args.Message.ReplyToMessage.From; + + if (awarder.Id == recipient.Id) + { + Client.SendTextMessageAsync(args.Message.Chat.Id, "Please stop playing with yourself."); + return true; + } + + if (Me.Id == recipient.Id) + { + Client.SendTextMessageAsync(args.Message.Chat.Id, "I am a bot, and have no use for your foolish fake internet points."); + return true; + } + + var text = args.Message.Text; + var command = CommandString.Parse(text); + var awardTypeId = Db.GetAwardTypeId(command.Parameters.FirstOrDefault()); + var awardType = Db.AwardTypes[awardTypeId]; + Db.AddAward(awardTypeId, awarder.Id, recipient.Id, args.Message.Chat.Id); + var response = $"Awarded a {awardType.Name} to {recipient.Username}!\n" + + $"{recipient.Username} is at {Db.CountAwards(recipient.Id, awardTypeId)}{awardType.Symbol} now."; + Client.SendTextMessageAsync(args.Message.Chat.Id, response); + return true; + } + + Db Db { get; } + TelegramBotClient Client { get; } + User Me { get; } + + public AwardCommand(Db db, TelegramBotClient client, User me) + { + Db = db; + Client = client; + Me = me; + } + } +} diff --git a/JetKarmaBot/Commands/CommandString.cs b/JetKarmaBot/Commands/CommandString.cs new file mode 100644 index 0000000..fb2fd2f --- /dev/null +++ b/JetKarmaBot/Commands/CommandString.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JetKarmaBot.Commands +{ + public class CommandString + { + public CommandString(string name, params string[] parameters) + { + Name = name; + Parameters = parameters; + } + + public string Name { get; } + public string[] Parameters { get; } + + public static bool TryParse(string s, out CommandString result) + { + result = null; + if (string.IsNullOrWhiteSpace(s) || s[0] != '/') + return false; + + int space = s.IndexOf(' '); + if (space < 0) + result = new CommandString(s.Substring(1)); + else + result = new CommandString(s.Substring(1, space - 1), s.Substring(space).Split(' ', StringSplitOptions.RemoveEmptyEntries)); + return true; + } + + public static CommandString Parse(string s) + { + if (TryParse(s, out var c)) return c; + throw new ArgumentException($"\"{s}\" is not a command"); + } + } +} diff --git a/JetKarmaBot/Commands/DefineCommand.cs b/JetKarmaBot/Commands/DefineCommand.cs new file mode 100644 index 0000000..5659b13 --- /dev/null +++ b/JetKarmaBot/Commands/DefineCommand.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Args; +using Telegram.Bot.Types.Enums; + +namespace JetKarmaBot.Commands +{ + public class DefineCommand : IChatCommand + { + Dictionary m_Definitions = new Dictionary() + { + { "AbstractSingletonProxyFactoryBean", "*Convenient* superclass for FactoryBean types that produce singleton-scoped proxy objects." } + }; + + ITelegramBotClient m_client; + public DefineCommand(ITelegramBotClient client) + { + m_client = client; + } + + public IReadOnlyCollection Names => new[] {"define" }; + + public bool Execute(object sender, MessageEventArgs messageEventArgs) + { + var commandTerms = messageEventArgs.Message.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var chatId = messageEventArgs.Message.Chat.Id; + foreach (var term in commandTerms.Skip(1)) + { + if (m_Definitions.ContainsKey(term)) + { + m_client.SendTextMessageAsync(chatId, m_Definitions[term], parseMode: ParseMode.Markdown); + return true; + } + } + m_client.SendTextMessageAsync(chatId, "idk lol"); + return false; + } + } +} diff --git a/JetKarmaBot/Commands/EchoCommand.cs b/JetKarmaBot/Commands/EchoCommand.cs new file mode 100644 index 0000000..5e47b1f --- /dev/null +++ b/JetKarmaBot/Commands/EchoCommand.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Telegram.Bot; +using Telegram.Bot.Args; + +namespace JetKarmaBot.Commands +{ + public class EchoCommand : IChatCommand + { + ITelegramBotClient m_client; + public EchoCommand(ITelegramBotClient client) + { + m_client = client; + } + public IReadOnlyCollection Names => new[] { "echo" }; + + public bool Execute(object sender, MessageEventArgs args) + { + m_client.SendTextMessageAsync(args.Message.Chat.Id, args.Message.Text); + return true; + } + } +} diff --git a/JetKarmaBot/Commands/IChatCommand.cs b/JetKarmaBot/Commands/IChatCommand.cs new file mode 100644 index 0000000..5928e5a --- /dev/null +++ b/JetKarmaBot/Commands/IChatCommand.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Telegram.Bot.Args; + +namespace JetKarmaBot.Commands +{ + public interface IChatCommand + { + IReadOnlyCollection Names { get; } + bool Execute(object sender, MessageEventArgs messageEventArgs); + } + +} diff --git a/JetKarmaBot/Commands/StartCommand.cs b/JetKarmaBot/Commands/StartCommand.cs new file mode 100644 index 0000000..665dea8 --- /dev/null +++ b/JetKarmaBot/Commands/StartCommand.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Telegram.Bot.Args; + +namespace JetKarmaBot.Commands +{ + public class StartCommand : IChatCommand + { + Db m_db; + public StartCommand(Db db) + { + m_db = db; + } + public IReadOnlyCollection Names => new[] { "start" }; + + public bool Execute(object sender, MessageEventArgs args) + { + m_db.AddChat(new Db.Chat { ChatId = args.Message.Chat.Id }); + m_db.AddUser(new Db.User { UserId = args.Message.From.Id }); + return true; + } + } +} diff --git a/JetKarmaBot/Config.cs b/JetKarmaBot/Config.cs new file mode 100644 index 0000000..87898d8 --- /dev/null +++ b/JetKarmaBot/Config.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using JsonNet.PrivateSettersContractResolvers; +using Newtonsoft.Json.Linq; + +namespace JetKarmaBot +{ + public class Config : ConfigBase + { + public Config(string path) : base(path) { } + + public string ApiKey { get; private set; } + public string ConnectionString { get; private set; } + public string ProxyUrl { get; private set; } + public int ProxyPort { get; private set; } + public string ProxyLogin { get; private set; } + public string ProxyPassword { get; private set; } + } + + public abstract class ConfigBase + { + public ConfigBase(string path) + { + JObject configJson; + + if (File.Exists(path)) + { + configJson = JObject.Parse(File.ReadAllText(path)); + + using (var sr = configJson.CreateReader()) + { + var settings = new JsonSerializerSettings + { + ContractResolver = new PrivateSetterContractResolver() + }; + JsonSerializer.CreateDefault(settings).Populate(sr, this); + } + } + else configJson = new JObject(); + + configJson.Merge(JToken.FromObject(this), new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union + }); + + try // populate possible missing properties in file + { + File.WriteAllText(path, configJson.ToString(Formatting.Indented)); + } + catch (IOException e) + { + System.Diagnostics.Debug.WriteLine(e); + } + } + } +} + diff --git a/JetKarmaBot/Db.cs b/JetKarmaBot/Db.cs new file mode 100644 index 0000000..a32d44a --- /dev/null +++ b/JetKarmaBot/Db.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using MySql.Data.MySqlClient; + +namespace JetKarmaBot +{ + public class Db + { + Dictionary m_Chats; + public IReadOnlyDictionary Chats => m_Chats; + public void AddChat(Chat chat) + { + lock (m_SyncRoot) + if (!m_Chats.ContainsKey(chat.ChatId)) + { + Conn.Execute(@"INSERT INTO chat + (chatid) + VALUES + (@ChatId)", + chat); + m_Chats.Add(chat.ChatId, chat); + } + } + + Dictionary m_Users; + public IReadOnlyDictionary Users => m_Users; + public void AddUser(User user) + { + lock (m_SyncRoot) + if (!m_Users.ContainsKey(user.UserId)) + { + Conn.Execute(@"INSERT INTO user + (userid) + VALUES + (@UserId)", + user); + m_Users.Add(user.UserId, user); + } + } + Dictionary m_AwardTypes; + public byte DefaultAwardTypeId { get; } = 1; + public IReadOnlyDictionary AwardTypes => m_AwardTypes; + public IReadOnlyDictionary AwardTypesByCommandName { get; private set; } + + public int CountAwards(long userId, byte awardTypeId) + { + return Conn.QuerySingle + ( + "SELECT SUM(amount) FROM award WHERE toid = @userId AND awardtypeid = @awardTypeId", + new { userId, awardTypeId } + ) ?? 0; + } + + public byte GetAwardTypeId(string name) + => AwardTypesByCommandName.GetOrDefault(name)?.AwardTypeId ?? DefaultAwardTypeId; + + public bool AddAward(byte awardTypeId, long fromId, long toId, long chatId) + { + AddChat(new Chat() { ChatId = chatId }); + AddUser(new User() { UserId = fromId}); + AddUser(new User() { UserId = toId }); + + int affected = Conn.ExecuteScalar( + @"INSERT INTO award + (chatid, fromid, toid, awardtypeid, amount) + VALUES + (@chatId, @fromId, @toId, @awardTypeId, 1)", + new { awardTypeId, fromId, toId, chatId }); + return affected == 1; + } + + #region types + public class Chat + { + public long ChatId { get; set; } + } + + public class User + { + public long UserId { get; set; } + } + + public class AwardType + { + public byte AwardTypeId { get; set; } + public string CommandName { get; set; } + public string Name { get; set; } + public string Symbol { get; set; } + public string Description { get; set; } + } + + public class Award + { + public int AwardId { get; set; } + public byte AwardTypeId { get; set; } + public long FromId { get; set; } + public long ToId { get; set; } + public long ChatId { get; set; } + public byte Amount { get; set; } + } + + #endregion + + #region service + public Db(Config cfg) + { + Log("Initializing..."); + Conn = new MySqlConnection(cfg.ConnectionString); + Conn.ExecuteScalar("select 1"); + Load(); + Log("Initialized!"); + } + + object m_SyncRoot = new object(); + + IDbConnection Conn { get; } + void Load() + { + Log("Populating cache..."); + m_Chats = Conn.Query("SELECT * FROM chat").ToDictionary(u => u.ChatId); + m_Users = Conn.Query("SELECT * FROM user").ToDictionary(s => s.UserId); + m_AwardTypes = Conn.Query("SELECT * FROM awardtype").ToDictionary(c => c.AwardTypeId); + AwardTypesByCommandName = m_AwardTypes.Values.ToDictionary(kvp => kvp.CommandName); + Log("Cache populated!"); + } + #endregion + + void Log (string Message) => Console.WriteLine($"[{nameof(Db)}]: {Message}"); + } +} diff --git a/JetKarmaBot/Extensions/IReadOnlyDictionaryExtensions.cs b/JetKarmaBot/Extensions/IReadOnlyDictionaryExtensions.cs new file mode 100644 index 0000000..bb97ffe --- /dev/null +++ b/JetKarmaBot/Extensions/IReadOnlyDictionaryExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace JetKarmaBot +{ + public static class IReadOnlyDictionaryExtensions + { + public static TValue GetOrDefault(this IReadOnlyDictionary dict, TKey key) + { + TValue res = default(TValue); + if (key != null) + dict.TryGetValue(key, out res); + return res; + } + } +} diff --git a/JetKarmaBot/Extensions/PrivateSettersContractResolvers.cs b/JetKarmaBot/Extensions/PrivateSettersContractResolvers.cs new file mode 100644 index 0000000..2dc1cb0 --- /dev/null +++ b/JetKarmaBot/Extensions/PrivateSettersContractResolvers.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +// ReSharper disable once CheckNamespace +namespace JsonNet.PrivateSettersContractResolvers +{ + public class PrivateSetterContractResolver : DefaultContractResolver + { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var jProperty = base.CreateProperty(member, memberSerialization); + if (jProperty.Writable) + return jProperty; + + jProperty.Writable = member.IsPropertyWithSetter(); + + return jProperty; + } + } + + public class PrivateSetterCamelCasePropertyNamesContractResolver : CamelCasePropertyNamesContractResolver + { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var jProperty = base.CreateProperty(member, memberSerialization); + if (jProperty.Writable) + return jProperty; + + jProperty.Writable = member.IsPropertyWithSetter(); + + return jProperty; + } + } + + internal static class MemberInfoExtensions + { + internal static bool IsPropertyWithSetter(this MemberInfo member) + { + var property = member as PropertyInfo; + + return property?.GetSetMethod(true) != null; + } + } +} \ No newline at end of file diff --git a/JetKarmaBot/JetKarmaBot.cs b/JetKarmaBot/JetKarmaBot.cs new file mode 100644 index 0000000..9f72e27 --- /dev/null +++ b/JetKarmaBot/JetKarmaBot.cs @@ -0,0 +1,76 @@ +using JetKarmaBot.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +using Telegram.Bot; +using Telegram.Bot.Args; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace JetKarmaBot +{ + public class JetKarmaBot : IDisposable + { + public void Broadcast(string message) + { + foreach (var u in db.Chats) + client.SendTextMessageAsync(u.Value.ChatId, message); + } + + public JetKarmaBot(Config cfg, Db db) + { + this.db = db; + var httpProxy = new WebProxy($"{cfg.ProxyUrl}:{cfg.ProxyPort}") + { + Credentials = new NetworkCredential(cfg.ProxyLogin, cfg.ProxyPassword) + }; + var botClient = new TelegramBotClient(cfg.ApiKey, httpProxy); + var cred = new NetworkCredential(cfg.ProxyLogin, cfg.ProxyPassword); + client = new TelegramBotClient(cfg.ApiKey, httpProxy); + me = client.GetMeAsync().Result; + InitCommands(); + client.OnMessage += BotOnMessageReceived; + client.StartReceiving(); + } + + #region IDisposable + public void Dispose() + { + client.StopReceiving(); + } + #endregion + + #region service + Db db { get; } + TelegramBotClient client { get; } + User me { get; } + + ChatCommandRouter commands; + void BotOnMessageReceived(object sender, MessageEventArgs messageEventArgs) + { + var message = messageEventArgs.Message; + if (message == null || message.Type != MessageType.Text) + return; + + string s = message.Text; + long id = message.Chat.Id; + long from = message.From.Id; + Task.Run(() => commands.Execute(sender, messageEventArgs)); + } + void InitCommands() + { + commands = new ChatCommandRouter(); + commands.Add(new StartCommand(db)); + commands.Add(new EchoCommand(client)); + commands.Add(new DefineCommand(client)); + commands.Add(new AwardCommand(db, client, me)); + } + + #endregion + } +} diff --git a/JetKarmaBot/JetKarmaBot.csproj b/JetKarmaBot/JetKarmaBot.csproj new file mode 100644 index 0000000..067512e --- /dev/null +++ b/JetKarmaBot/JetKarmaBot.csproj @@ -0,0 +1,21 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + Always + + + + diff --git a/JetKarmaBot/Program.cs b/JetKarmaBot/Program.cs new file mode 100644 index 0000000..ba7aac3 --- /dev/null +++ b/JetKarmaBot/Program.cs @@ -0,0 +1,28 @@ +using System; + +namespace JetKarmaBot +{ + public class App + { + public static void Main(string[] args) + { + Current = new App(new Config("karma.cfg.json")); + + Console.ReadKey(); + } + + public static App Current { get; private set; } + + public App(Config cfg) + { + Config = cfg; + Db = new Db(Config); + Watcher = new JetKarmaBot(Config, Db); + Console.WriteLine("JatKarmaBot started!"); + } + + Config Config { get; } + Db Db { get; } + JetKarmaBot Watcher { get; } + } +}