📜 ⬆️ ⬇️

We write Skype bot on C # with modular architecture

image alert ('Hi Habr!');

It’s been a long time since the thought of making such a tool assistant, which could bring out currency rates and prompt the weather and anecdote to hunt, didn’t make it all up ... well, you know how it happens, right? In addition, in my endless list with funny ideas that it would be nice to someday realize - there was a “bot for Skype 4fun” item.

Hands reached. It will be about writing a simple modular bot on C # with integration into Skype. What happened in the end, as well as why you should turn off the system unit from the network before you climb into it with a screwdriver - read under the cut.

')

Foreword


It would seem, where does the system unit and the screwdriver? Well, then ... One languid evening I dismantled a system engineer in order to lubricate the cooler on the power supply unit (it roared like a beetle). Smeared, checked that everything turns, turns and pleases the ear. He began to collect everything in its original state and ... did not bother to disconnect it from the network. For what fate rewarded me with stars in my eyes, a waste of a certain amount on a new bpshnik and ... the decision to finally write the first article in 4 years for a favorite habr. Please do not kick much, the Chukchi author is not a writer.

For those who are too lazy to read the entire article: all sorts here: https://github.com/Nigrimmist/HelloBot and instructions for
launch
All you need is to compile everything and in \ SkypeBotAdapterConsole \ bin \ Debug there will be a ready-made console that you need to run for testing (you need to register skyp4com library in the system + old Skype). Further in the article these moments are described in more detail.


Introduction


On the next weekend, a little tired of sawing my brainchild - another Facebook killer, I decided to pick something up for the soul and realize. The choice fell on the bot for Skype. I decided to write right away with the foundation for extensibility, so that colleagues could add those bot modules that are needed directly by them.
By the way, I am in the same Skype chat, which in turn consists of friends, acquaintances, and colleagues, and the men's club. It was created during the time of joint work on one of the projects, and so somehow got accustomed to our contact lists, assuming the role of a male talker. It was for this chat that I wrote a bot, in order to have a little fun of the people, but to make a small highlight.

Set tasks


So. let's decide what we would like to have in the end:

- A separate bot module, the purpose of which is to process messages and return a response.
- Integration with Skype. A bot should be able to receive chat messages and respond to them if they are addressed to it.
- Ease of writing and connecting "modules" by third-party developers
- Ability to integrate with various clients

Case Study


I climbed into these your Internet to look for information about how I could solve the main problem - to interact with Skype. The first links I threw out on the information that Microsoft had cut the API since December 2013 (this is easy to say, because 99% of the opportunities were “reduced”) and so far it does not plan to develop this direction in any way.

Having made a thoughtful face, I for half an hour sketched the code, which every couple of seconds clicked on the chat, copied messages into the buffer and thus interacted with the ui shell. Looking at this Frankenstein, my heart sank and I squeezed my backspace for a good 10 seconds. "Yes, it can not be that there was no better solution" - flashed through my head, and my hands reached for the keyboard.

There was a thought to tie the old api library to the old skype, but, as you know, Microsoft and here we planted a pink animal, forbidding the use of old versions of Skype. Having studied a number of articles, I came to the conclusion that there are separate old portable versions that were converted by craftsmen to working condition while preserving the old functionality. And yes, by launching Skype on a virtual machine, I was convinced that the old api library still works with a slightly older Skype.

Implementation


And so, for the implementation of our plans we will need:

- Skype4COM.dll is an ActiveX component that provides an API for communicating with Skype
- Interop.SKYPE4COMLib.dll - proxy for interaction with Skype4COM.DLL from .net code
- Launched Skype (for example, version 6.18, tried on 4.2, but there was no chat support yet)
- Kefir and oatmeal cookies

The code was written in Visual Studio 2012 under 4.5. NET Framework.

Register Skype4COM.DLL in the system. The easiest way is to create a .bat file and enter there.

regsvr32 Skype4COM.dll 

Put it next to the dll and run the batch file. We bite a cookie, we wash it down with kefir and we rub our hands, because one tenth of the work is done.

Next, we need to somehow check whether it works at all.

Interaction with Skype


Create a console application, connect Interop.SKYPE4COMLib.dll and write the following simple code:

Code with comments
 class Program { //   Skype,        private static Skype skype = new Skype(); static void Main(string[] args) { // ,      Task.Run(delegate { try { //    skype.MessageStatus += OnMessageReceived; //   .     ,          . //5    ( -), true -         . skype.Attach(5, true); Console.WriteLine("skype attached"); } catch (Exception ex) { //  ,  -   Console.WriteLine("top lvl exception : " + ex.ToString()); } //   while (true) { Thread.Sleep(1000); } }); //    while (true) { Thread.Sleep(1000); } } //   private static void OnMessageReceived(ChatMessage pMessage, TChatMessageStatus status) { // ,       ,     ,    cmsReceived +             if (status == TChatMessageStatus.cmsReceived) { Console.WriteLine(pMessage.Body); } } } 


We start, we ask someone to write to us in Skype - the text of the interlocutor is displayed in the console. Win. Reach for another cookie and pour into a mug of yogurt.

We write modules


And so, it remains quite a bit. We need to implement the bot in such a way that connecting additional modules with commands for the bot was easier than lubricating the cooler in the power supply.

Create a library project and name it, say HelloBotCommunication. It will serve as a bridge between the modules and the bot. We put there three interfaces:

IActionHandler
He will be responsible for the message handler classes.
 public interface IActionHandler { List <string> CallCommandList { get;} string CommandDescription { get; } void HandleMessage(string args, object clientData, Action<string> sendMessageFunc); } 

where CallCommandList is a list of commands for which HandleMessage will be called, CommandDescription is needed to display the description in the! modules command (see below) and HandleMessage - where the module should process incoming parameters ( args ), passing the answer to the sendMessageFunc callback

IActionHandlerRegister
He will be responsible for registering our handlers.
 public interface IActionHandlerRegister { List<IActionHandler> GetHandlers(); } 


ISkypeData
He will be responsible for additional information about the client, in this case, about Skype, if the handler needs one.
 public interface ISkypeData { string FromName { get; set; } } 


The meaning of all this is this: the developer creates his .dll, connects our library for communication, inherits from IActionHandler and IActionHandlerRegister and implements the functionality he needs without thinking about everything that lies above.

Example
An example in the form of the command module "say", which will force the bot to say everything that happens after the command itself.
 public class Say : IActionHandler { private Random r = new Random(); private List<string> answers = new List<string>() { "   ", " ", "?", "5$", ", ", }; public List<string> CallCommandList { get { return new List<string>() { "", "say" }; } } public string CommandDescription { get { return @"  "; } } public void HandleMessage(string args, object clientData, Action<string> sendMessageFunc) { if (args.StartsWith("/")) { sendMessageFunc(answers[r.Next(0,answers.Count-1)]); } else { sendMessageFunc(args); } } } 



We write the body of the bot


There is a module, there is a library for communication, it remains to write the main hero of the occasion - Monsieur bot, and somehow link it all up. Yes, it is easy - you will say and run to the kitchen for the second package of kefir. And you will be right.

I called it HelloBot and created a separate library project. The essence of the class is to find the right .dll with modules and work with them. This is done through

 assembly.GetTypes().Where(x => i.IsAssignableFrom(x)) //  Activator.CreateInstance(type); 


Here I want to warn you a little. This is, by and large, a solution to the forehead and potentially a security hole. In an amicable way, you need to create a separate domain and give only the necessary rights when executing other modules, but we are naive people and assume that all the code is checked and the modules are written out of the best of intentions. (The correct decision is not to write a bicycle, but for example, MEF )

After registering the creation of an object, we will have at our disposal a command prefix (the default is "!") And a mask for searching for .dll modules. As well as the HandleMessage method in which all the magic is created.
The magic is to accept the incoming message, some specific data from the client (if any) and a callback to respond. A list of system commands (“help” and “modules”) is also entered, which allow you to see these commands in the first case and the list of all connected modules in the second.
The execution of the module is allocated in a separate thread and is limited in execution time (by default, 60 seconds), after which the thread simply ceases to exist.

HelloBot class
  public class HelloBot { private List<IActionHandler> handlers = new List<IActionHandler>(); // Tuple   ,      ,     private IDictionary<string, Tuple<string, Func<string>>> systemCommands; private string dllMask { get; set; } private string botCommandPrefix; private int commandTimeoutSec; public HelloBot(string dllMask = "*.dll", string botCommandPrefix = "!") { this.dllMask = dllMask; this.botCommandPrefix = botCommandPrefix; this.commandTimeoutSec = 60; systemCommands = new Dictionary<string, Tuple<string, Func<string>>>() { {"help", new Tuple<string, Func<string>>("  ", GetSystemCommands)}, {"modules", new Tuple<string, Func<string>>("  ", GetUserDefinedCommands)}, }; RegisterModules(); } private void RegisterModules() { handlers = GetHandlers(); } protected virtual List<IActionHandler> GetHandlers() { List<IActionHandler> toReturn = new List<IActionHandler>(); var dlls = Directory.GetFiles(".", dllMask); var i = typeof(IActionHandlerRegister); foreach (var dll in dlls) { var ass = Assembly.LoadFile(Environment.CurrentDirectory + dll); //get types from assembly var typesInAssembly = ass.GetTypes().Where(x => i.IsAssignableFrom(x)).ToList(); foreach (Type type in typesInAssembly) { object obj = Activator.CreateInstance(type); var clientHandlers = ((IActionHandlerRegister)obj).GetHandlers(); foreach (IActionHandler handler in clientHandlers) { if (handler.CallCommandList.Any()) { toReturn.Add(handler); } } } } return toReturn; } public void HandleMessage(string incomingMessage, Action<string> answerCallback, object data) { if (incomingMessage.StartsWith(botCommandPrefix)) { incomingMessage = incomingMessage.Substring(botCommandPrefix.Length); var argsSpl = incomingMessage.Split(' '); var command = argsSpl[0]; var systemCommandList = systemCommands.Where(x => x.Key.ToLower() == command.ToLower()).ToList(); if (systemCommandList.Any()) { var systemComand = systemCommandList.First(); answerCallback(systemComand.Value.Item2()); } else { var foundHandlers = FindHandler(command); foreach (IActionHandler handler in foundHandlers) { string args = incomingMessage.Substring((command).Length).Trim(); IActionHandler hnd = handler; var cts = new CancellationTokenSource(TimeSpan.FromSeconds(commandTimeoutSec)); var token = cts.Token; Task.Run(() => { using (cts.Token.Register(Thread.CurrentThread.Abort)) { try { hnd.HandleMessage(args, data, answerCallback); } catch (Exception ex) { if (OnErrorOccured != null) { OnErrorOccured(ex); } answerCallback(command + "    :("); } } },token); } } } } public delegate void onErrorOccuredDelegate(Exception ex); public event onErrorOccuredDelegate OnErrorOccured; private List<IActionHandler> FindHandler(string command) { return handlers.Where(x => x.CallCommandList.Any(y=>y.Equals(command, StringComparison.OrdinalIgnoreCase))).ToList(); } private string GetSystemCommands() { return String.Join(Environment.NewLine, systemCommands.Select(x => String.Format("!{0} - {1}", x.Key, x.Value.Item1)).ToList()); } private string GetUserDefinedCommands() { return String.Join(Environment.NewLine, handlers.Select(x => String.Format("{0} - {1}", string.Join(" / ", x.CallCommandList.Select(y => botCommandPrefix + y)), x.CommandDescription)).ToList()); } } 



The bot is ready, the final touch remains - to connect it with the console application that processes messages from Skype.

The final version of Program.cs for the console application
Comments are exposed only on new sections of the code.
 class Program { private static Skype skype = new Skype(); //   private static HelloBot bot; static void Main(string[] args) { bot = new HelloBot(); //   ,    bot.OnErrorOccured += BotOnErrorOccured; Task.Run(delegate { try { skype.MessageStatus += OnMessageReceived; skype.Attach(5, true); Console.WriteLine("skype attached"); } catch (Exception ex) { Console.WriteLine("top lvl exception : " + ex.ToString()); } while (true) { Thread.Sleep(1000); } }); while (true) { Thread.Sleep(1000); } } static void BotOnErrorOccured(Exception ex) { Console.WriteLine(ex.ToString()); } private static void OnMessageReceived(ChatMessage pMessage, TChatMessageStatus status) { Console.WriteLine(status + pMessage.Body); if (status == TChatMessageStatus.cmsReceived) { //            SendMessage,   ,    bot.HandleMessage(pMessage.Body, answer => SendMessage(answer,pMessage.Chat), new SkypeData(){FromName = pMessage.FromDisplayName}); } } public static object _lock = new object(); private static void SendMessage(string message, Chat toChat) { //            lock' lock (_lock) { //  ,    . ! toChat.SendMessage(message); } } } 



That's all. A couple of days my colleagues and I wrote a couple of modules. Examples under the cut.

Written modules
! bash displays a random quote from bash
! ithap displays random IT history
! weather shows the current weather in Minsk
! say says what you want
! calc performs arithmetic operations (via the NCalc library)
18+ :)
! Tits takes a random photo from the toggle switch. Well, what about without them. By the way, one of the most popular commands in the chat))

! the course displays the current rates and through the parameters can detail the output. Exchange euro to usd and so on.
other.


Known Issues
Unfortunately, something in the protocol seems to have changed and the bot does not see the new group chats. Old for some reason picks up with a bang, but with new problems. I tried to dig, but did not find a solution. If someone tells me how to overcome this sore, I will be grateful.
It also sometimes happens that messages are lost and Skype needs “warming up”, after which it starts up and responds adequately to all subsequent messages.

Total


As a result, we have what we have. The bot does not depend on the client, it supports the system of modules and all the source code of all this stuff is uploaded to the github: https://github.com/Nigrimmist/HelloBot . If someone has the desire and time - I am waiting for pull-requests of your useful modules :)

Bot can poke a stick on skype name: mensclubbot . Authorization is not required. The list of modules can be looked through "! Modules". It is spinning on the hosting, so it works 24h.

Thank you for your attention, I hope the first pancake did not go lumpy and the material was useful.

Source: https://habr.com/ru/post/238363/


All Articles