We are writing a Skype bot in C # with a modular architecture

imagealert ('Hello Habr!');

For a long time I had the idea to make a kind of assistant tool that could bring me both the exchange rates and the weather and tell the anecdote to poison, but all my hands didn’t reach ... well, you know how it happens, right? In addition, on my endless list with funny ideas that it would be nice to someday implement - there was a “bot for Skype 4fun” item.

Hands reached. It's about writing a simple modular bot in C # with Skype integration. What happened in the end, and also why it is worth disconnecting the system unit from the network before climbing into it with a screwdriver - read under the cat.


Foreword


It would seem, where does the system unit and screwdriver? Well, so ... On one languid evening, I dismantled the system unit in order to lubricate the cooler on the power supply (aka beetle was noisy). Lubricated, checked that everything is spinning, spinning and pleasing to 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 bps player and ... a decision to finally write the first article in 4 years for my favorite hubr. Please do not kick much, the Chukchi author is not a writer.

For those who are too lazy to read the whole article: all the sorts here: https://github.com/Nigrimmist/HelloBot and instructions for
launch
Нужно лишь всё скомпилить и в \SkypeBotAdapterConsole\bin\Debug будет лежать готовая консолька, которую нужно запустить для тестирования (нужна регистрация библиотеки skyp4com в системе + старый скайп). Дальше в статье эти моменты расписаны более детально.


Introduction


This weekend, a little tired of sawing my brainchild - the next Facebook killer, I decided to pick up something for my soul and realize it. The choice fell on the bot for skype. I decided to write right away with the potential for extensibility, so that colleagues could add those bot modules that they needed directly.
By the way, I am in one Skype chat, which in turn consists of friends, acquaintances, colleagues and the famous Men's club. It was created during the collaboration on one of the projects, and somehow it has taken root in our contact lists, taking on the role of a male talker. It was for this chat that I wrote a bot in order to amuse the people a bit and to add a little zest.

We 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. The bot should be able to receive messages in chats and respond to them if they are addressed to it
- Ease of writing and connecting "modules" by third developers
- Ability to integrate with various clients

Subject research


I got into these your internet networks to look for information on how I could solve the main problem - interact with skype. The very first links threw me into the information that Microsoft has cut the API since December 2013 (this is easy to say, because 99% of the possibilities were "cut") and is not planning to develop this direction in any way.

Having made a thoughtful face, within half an hour I scribbled a code that every couple of seconds clicked on the chat, copied to the message buffer and thus interacted with the ui shell. Looking at this Frankenstein, my heart sank and I clamped the backspace for a good 10 seconds. “Yes, it cannot be that there is no better solution” - flashed through my head, and my hands themselves reached for the keyboard.

There was an idea to fasten the old api library to the old skype, but, as you know, Microsoft put a pink animal on us, forbidding us to use old versions of skype. After studying a number of articles, I came to the conclusion that there are some old portable versions, redone by craftsmen to a working state while maintaining the old functionality. And yet, 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, to implement what we have planned, we need:

- Skype4COM.dll - this is an ActiveX component that provides an API for communicating with Skype
- Interop.SKYPE4COMLib.dll - a proxy for interacting with Skype4COM.DLL from .net code
- Running Skype ( version 6.18 is suitable for example, I tried it 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 it there

regsvr32 Skype4COM.dll 

We put it next to the dll and run the batch file. We bite a cookie, drink it with kefir and rub our hands, because a tenth of the work is done.

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

Skype interaction


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

Code with comments
classProgram
    {
        //инициализируем объект класса Skype, с ним в дальнейшем и будем работатьprivatestatic Skype skype = new Skype();        
        staticvoidMain(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);
            }
        }
        //обработчик новых сообщенийprivatestaticvoidOnMessageReceived(ChatMessage pMessage, TChatMessageStatus status)
        {
            //суть такова, что для каждого сообщения меняется несколько статусов, поэтому мы ловим только те, у которых статус cmsReceived + это не позволит в будущем реагировать нашему боту на свои же сообщенияif (status == TChatMessageStatus.cmsReceived)
            {
                Console.WriteLine(pMessage.Body);
            }
        }  
    }


We start, we ask someone to write to us on Skype - the text of the interlocutor is displayed in the console. Win. We reach for another cookie and add kefir to the mug.

Writing modules


And so, just a little remains. 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 three interfaces there:

IActionHandler
Он будет отвечать за классы-обработчики сообщений.
publicinterfaceIActionHandler
    {
        List <string> CallCommandList { get;}
        string CommandDescription { get; }
        voidHandleMessage(string args, object clientData, Action<string> sendMessageFunc);
    }

где CallCommandList это список команд по которым будет вызван HandleMessage, CommandDescription нужен для вывода описания в команде !modules (об этом ниже) и HandleMessage — где модуль должен обработать входящие параметры (args), передав ответ в коллбек sendMessageFunc

IActionHandlerRegister
Он будет отвечать за регистрацию наших обработчиков.
publicinterfaceIActionHandlerRegister
    {
        List<IActionHandler> GetHandlers();
    }


ISkypeData
Он будет отвечать за дополнительную информацию о клиенте, в данном случае — о скайпе, если таковая необходима обработчику.
publicinterfaceISkypeData
    {
       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
Пример в виде модуля команды «скажи», который заставит бота сказать всё что будет после самой команды.
publicclassSay : IActionHandler
    {
        private Random r = new Random();
        private List<string> answers = new List<string>()
        {
            "Вот сам и скажи",           
            "Ищи дурака",
            "Зачем?",
            "5$",
            "Нет, спасибо",
        };
        public List<string> CallCommandList
        {
            get { returnnew List<string>() { "скажи", "say" }; }
        }
        publicstring CommandDescription { get { return@"Говорит что прикажете"; } }
        publicvoidHandleMessage(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 connect it all. Yes, it’s easy - you say, and run into the kitchen for a 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 necessary .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 forehead decision and is potentially a security hole. In a good way, you need to create a separate domain and give only the necessary rights when executing other people's modules, but we are naive people and assume that all the code is checked and the modules are written with good intentions. (The correct decision is not to write a bicycle, but to use it, for example, MEF )

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

HelloBot class
publicclassHelloBot
    {
        private  List<IActionHandler> handlers = new List<IActionHandler>();
        //за Tuple автора не пинать, ему как и вам хочется прокрастинировать, а не писать спецклассыprivate IDictionary<string, Tuple<string, Func<string>>> systemCommands;
        privatestring dllMask { get; set; }
        privatestring botCommandPrefix;
        privateint commandTimeoutSec;
        publicHelloBot(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();
        }
        privatevoidRegisterModules()
        {
            handlers = GetHandlers();
        }
        protectedvirtual 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 assemblyvar 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;
        }
        publicvoidHandleMessage(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);
                    }
                }
            }
        }
        publicdelegatevoidonErrorOccuredDelegate(Exception ex);
        publicevent onErrorOccuredDelegate OnErrorOccured;
        private List<IActionHandler> FindHandler(string command)
        {
            return handlers.Where(x => x.CallCommandList.Any(y=>y.Equals(command, StringComparison.OrdinalIgnoreCase))).ToList();
        }
        privatestringGetSystemCommands()
        {
            return String.Join(Environment.NewLine, systemCommands.Select(x => String.Format("!{0} - {1}", x.Key, x.Value.Item1)).ToList());
        }
        privatestringGetUserDefinedCommands()
        {
            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 last touch remains - to connect it with a console application that processes messages from Skype.

The final version of Program.cs for a console application
Комментарии выставлены только на новых участках кода.
classProgram
    {
        privatestatic Skype skype = new Skype();
        //объявляем нашего ботаprivatestatic HelloBot bot;
        staticvoidMain(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);
            }
        }
        staticvoidBotOnErrorOccured(Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
        privatestaticvoidOnMessageReceived(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});
            }
        }
        publicstaticobject _lock = newobject();
        privatestaticvoidSendMessage(string message, Chat toChat)
        {
                //во избежании конкурентных вызовов скайпа ставим все приходящие сообщения в очередь посредством lock'аlock (_lock)
                {
                    //отвечаем в чат, из которого пришло сообщение. Профит!
                    toChat.SendMessage(message);
                }
        }
    }



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

Written Modules
!bash выводит случайную цитату с баша
!ithap выводит случайную IT историю
! погода показывает текущую погоду в Минске
!say говорит то, что прикажете
!calc выполняет арифметические операции (через NCalc библиотеку)
18+ :)
! сиськи забирает рандомную фотку с тумблера. Ну а как же без них. К слову, одна из самых популярных команд в чате ))

! курс выводит текущие курсы и через параметры может детализировать вывод. Обмен евро на usd и тд.
и другие.


Known problems
Unfortunately, something in the protocol seems to have changed and the bot does not see new group chats. For some reason, he picks up the old with a bang, but with the new problem. I tried to dig, but could 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 a “warm-up”, after which it starts up and adequately responds to all subsequent messages.

Total


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

can poke a bot with a wand by skype name: mensclubbot . No authorization required. The list of modules can be viewed through "! Modules". Spins on hosting, so it works 24h.

Thank you for your attention, I hope the first pancake did not go lumpy and the material turned out to be useful.

Also popular now: