Общие принципы написания скриптов для программы RutonyChat - rutony/rutonychat-testscripts GitHub Wiki

Общая информация

В программе есть несколько мест где поддерживаются скрипты. В каждом из мест используются разные аргументы. Но общий принцип почти везде одиннаковый:

  • Бот
  • Удаленное управление
  • Кастомные метки
  • Автономные скрипты

Для скриптов используется кодировка UTF-8 без BOM

Шаблон для скриптов

Шаблон для отладки скриптов. Подойдет Visual Studio Community, Visual Studio Code, Xamarin Studio или другая среда разработки. Так же учитывайте, что в программе доступны некоторые компоненты по умолчанию, например, Newtonsoft json, поэтому он может быть добавлен в скрипте, и будет доступен при вызове скрипта. Но для тестов, его следует добавить в проект.

Список пакетов и библиотек доступных в программе:

  • CSScriptLibrary
  • Discord
  • HtmlAgilityPack
  • iTunesLib
  • KopiLua
  • NAudio
  • Newtonsoft.Json
  • NLua
  • OpenPop
  • SocketIoClientDotNet
  • SQLite-net
  • SuperSocket
  • TweetSharp
  • websocket-sharp
  • WebSocket4Net

Дополнительные методы

Все данные методы сделаны, что бы упростить однотипные действия в скриптах:

Все методы доступны в модуле RutonyBotFunctions

public static string GetScriptDirectory(string scriptname)

Получение каталога в котором размещен скрипт из Мастерской. Может быть полезен, если в скрипте используется какая то база, которая идет в комплекте, что бы получить относительный путь к ней из каталога Workshop.

public static bool FileHasString(string filename, string text)

Проверка наличия строки в текстовом файле.

public static void FileAddString(string filename, string text)

Добавить строку в текстовый файл.

public static void FileClear(string filename)

Очистить текстовый файл.

public static int FileLength(string filename)

Количество строк в текстовом файле.

public static string FileStringAt(string filename, int ind)

Строка из текстовая файла под номер ind.

Скрипты для бота

Для скриптов используется пространство имен - RutonyChat. Вызываемая функция имеет имя RunScript и заключена в класс Script. Аргументы:

  • site - имя сайта с которого был осуществлён вызов
  • username - имя вызвавшего пользователя
  • param - дополнительная информация для точной настройки скрипта пользователем

Важно. Запускаемый скрипт ничего не возвращает. Все вызовы по отсылке сообщений осуществляется методами:

RutonyBot.TwitchBot.Say(<string>); // чат твитча
RutonyBot.TwitchBot.SendCommand(<string>); // отправить команду в чат твитча
RutonyBot.GoodgameBot.Say(<string>); // чат гудгейма
RutonyBot.GoodgameBot.SendCommand(<string>); // отправить команду в чат гудгейма
RutonyBot.GoodgameBot.GetUserId(string username); // получить ID по нику
RutonyBot.GoodgameBot.SendBan(string username, string(int) seconds); // бан на гг
RutonyBot.YoutubeBot.Say(<string>); // чат ютуба
RutonyBot.SayToWindow(string _textParam); // сообщение в окно программы
RutonyBot.SayToWindow(string _textParam, Color color);
RutonyBot.SayToWindow(string _user, string _textParam);
RutonyBot.SayToWindow(string _user, string _textParam, Color color);
RutonyBot.BotSay(string text, List<ProgramProps.SiteEnum> targets); // отправка сообщения на список сайтов
RutonyBot.BotSay(string text, List<string> targets); // аналогично выше команде
RutonyBot.BotSay(string target, string text); // отправить сообщение на сайт target (универсальный метод, под любой сайт, пример, RutonyBot.BotSay("youtube", "123"); )

Важно. Вызов скрипта осуществляется в теле основной программе. А не в отдельном потоке. Поэтому доступны все классы и коллекции самой программы, для прямой работы с ними. Это следует учитывать при создании пауз, они должны быть реализованы отдельными потоками.

using System;
using System.Windows.Forms;      
namespace RutonyChat {
    public class Script {
        public void RunScript(string site, string username, string text, string param) {
            RutonyBot.TwitchBot.Say(string.Format("Param: {0}", param));
            /* Пример получения ранга пользователя и вывод его статистики
            RankControl.ChatterRank cr = RankControl.ListChatters.Find(r => r.Nickname == username);
            if (cr != null) {                    
                RutonyBot.TwitchBot.Say(
                    string.Format("{0} написал {1} сообщения(й)", username, cr.MessageQty)
                );
            }
            */
            // Вывод сообщения
            //MessageBox.Show("Это сообщение открыто из чата :D");
        }
    }
}

Скрипты для Удаленного управления

Для скриптов используется пространство имен - RutonyChat. Вызываемая функция имеет имя RunScript и заключена в класс Script. Аргументы:

  • username - имя вызвавшего пользователя, в случае триггеров по событию
  • param - дополнительная информация для точной настройки скрипта пользователем, в случае с донатом, там будет сумма

Важно. Вызов скрипта осуществляется в теле основной программе. А не в отдельном потоке. Поэтому доступны все классы и коллекции самой программы, для прямой работы с ними. Это следует учитывать при создании пауз, они должны быть реализованы отдельными потоками.

using System;
using System.Windows.Forms; 
     
namespace RutonyChat {
    public class Script {
        public void RunScript(string username, string param) {

             MessageBox.Show("Это сообщение открыто из чата :D");

        }
    }
}

Скрипты для кастомных меток

Для скриптов используется пространство имен - RutonyChat. Вызываемая функция имеет имя RunScript и заключена в класс Script. Аргументы:

  • param - дополнительная информация для точной настройки скрипта пользователем

Важно. Запускаемый скрипт возвращает значение которое будет помещено в метку. Вызов скрипта осуществляется в теле основной программе. А не в отдельном потоке. Поэтому доступны все классы и коллекции самой программы, для прямой работы с ними. Это следует учитывать при создании пауз, они должны быть реализованы отдельными потоками.

using System;
using System.Windows.Forms; 
     
namespace RutonyChat {
    public class Script {
        public string RunScript(string param) {

        }
    }
}

Автономные скрипты

Они отличаются от остальных скриптов одним очень важным моментом - они находятся в отдельных потоках и не выгружаются после завершения. То есть, они работают постоянно. Поэтому их можно использовать в случаях когда требуется накопление данных. Либо более тонкие и сложные условия для работы. Они универсальны в данным случае.

Примеры использования: Антикапс для чата (скрипт запоминает кто уже капсил и в следующий раз уже выдает бан вместо предупреждения), игра Травия (чаттеры отгадывают слова, если отгадали получают очки/баллы, можно посмотреть весь рейтинг, кто больше всего наотгадывал, база баллов сохраняется/загружается один раз), ну и другие примеры, их очень много.

using System;
using System.Threading;
     
namespace RutonyChat {
    public class Script {

        public void InitParams(string param) {
            RutonyBot.SayToWindow("Test script connected");
        }

        public void Closing() {
            RutonyBot.SayToWindow("Test script disconnected");
        }

        public void NewMessage(string site, string name, string text, bool system) {
            RutonyBot.SayToWindow(string.Format("site={0}, name={1}, text={2}", site, name, text));
        }
        public void NewMessageEx(string site, string name, string text, bool system, Dictionary<string, string> Params) {
        }

        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {
            RutonyBot.SayToWindow(string.Format("site={0} typeEvent={1}/{2}, name={3}, text={4}, donate={5}/{6}, qty={7}", 
                                                site, typeEvent, subplan, name, text, donate, currency, qty ));
        }
    }
}

Важно. Естественно такие скрипты так же поддерживают сложные параметры, поэтому у метода InitParams может быть перегрузка InitParams(Dictionary<string, string param).

NewMessageEx в Params, находятся дополнительные данные, например, id пользователя, является ли пользователь модератором и прочее.

Параметры для скриптов

Программа поддерживает 2 вида параметров: строкой и файлом описания параметров. В зависимости от параметров в скрипт при вызове передается либо строка, либо коллекция значений. Значения заполняются из пресета бота, либо кастомной метки. В зависимости от поддержки параметров скрипта, рядом с полем выбора скрипта будет либо текстовое поле, либо кнопка с вызовом окошка, где можно задать все параметры.

Параметры строкой

Не требуется дополнительно никаких настроек. В скрипт передается строковый параметр - param который содержит значение заполненное пользователем в программе. Далее уже скрипт сам волен делать и обрабатывать параметр как ему нужно.

Пример:

public void RunScript(string site, string username, string text, string param)

Файл описания параметров

Если в каталоге со скриптом лежит json файл с тем же именем, что и скрипт. То программа автоматически понимает что у скрипта есть описание параметров. Значение параметров, которые указал пользователь будет отдано в те же самые функции, с той лишь разницей что будет передан в скрипт не строковый параметр, а коллекция Dictionary<string, string>.

Пример: Есть скрипт testscript.cs и файл testscript.json. То будет использоваться перегрузка метода в

public void RunScript(string site, string username, string text, Dictionary<string, string> param)

Где ключи соответственно параметры, значения - значения.

Описание формата файла параметров

Формат аналогичен параметрам для тем оформлений. Пример использования - тема оформления - default-customs, файл params.json

// Структура файла параметров. Где массив параметры содержит описание параметров
{
  "params": [ ]    
}

// Цвет
    {
      "name": "NicknameColor",
      "desc": {
        "russian": "Цвет ника",
        "english": "Nickname color",
        "tchinese": "Nickname color"
      },
      "type": "color",
      "default": "#00A4F2"
    }

// Шрифт
{
      "name": "FontNickname",
      "desc": {
        "russian": "Шрифт никнейма",
        "english": "Font Nickname",
        "tchinese": "Font Nickname"
      },
      "type": "font",
      "default": {
		"name" : "Roboto",
		"size" : 16
	  }
    }

// Список
    {
      "name": "ShowEffect",
      "desc": {
        "russian": "Эффект появления сообщений",
        "english": "Show messages effect",
        "tchinese": "Show messages effect"
      },
      "type": "list",
      "default": "none",
      "enumerate": [
        {
          "value": "none"
        },	  
        {
          "value": "fadeIn"
        },
        {
          "value": "zoomIn"
        },
        {
          "value": "bounce"
        },
        {
          "value": "bounceInRight"
        },
        {
          "value": "bounceInLeft"
        },		
        {
          "value": "bounceIn"
        },
        {
          "value": "flipInX"
        },
        {
          "value": "flipInY"
        },
        {
          "value": "lightSpeedIn"
        },		
        {
          "value": "slideInRight"
        },	
        {
          "value": "slideInLeft"
        },			
      ]
    }

// Булево
    {
      "name": "HideMessages",
      "desc": {
        "russian": "Скрывать сообщения",
        "english": "Hide messages",
        "tchinese": "Hide messages"
      },
      "type": "bool",
      "default": false
    }

// Целое число со знаком
    {
      "name": "TimeNewMessages",
      "desc": {
        "russian": "Время показа сообщений",
        "english": "Time showing messages",
        "tchinese": "Time showing messages"
      },
      "type": "integer",
      "default": 10,
      "min":2,
      "max": 60
    }

// Вещественное число со знаком
{
      "name": "ShadowSize",
      "desc": {
        "russian": "Размер тени",
        "english": "Shadow size",
        "tchinese": "Shadow size"
      },
      "type": "float",
      "default": 1,
      "min": 1,
      "max": 100
    }

// Строка
    {
      "name": "StringExample",
      "desc": {
        "russian": "Пример строки (не используется)",
        "english": "String example (unabled)",
        "tchinese": "String example (unabled)"
      },
      "type": "string",
      "default": "Simple string text"
    }

Модуль Данные оповещений

Доступ к нему осуществаляется через ChatDB.ChatData и обращение к методам через ChatDB

Доступные коллекции

        public class FollowerClass {
            public int Index;
            public DateTime DT;
            public ProgramProps.SiteEnum Site;
            public string Name;
            public bool isTest = false;
        }
        public class SubscriberClass {
            public int Index;
            public DateTime DT;
            public ProgramProps.SiteEnum Site;
            public string Name;
            public string Text = "";
            public bool isTest = false;
        }
        public class DonateClass {
            public int Index;
            public DateTime DT;
            public ProgramProps.SiteEnum Site;
            public string Name;
            public float Sum;
            public string Currency;
            public string Text;
            public bool isTest = false;
        }
        public class DonateSumClass {
            public int Index;
            public ProgramProps.SiteEnum Site;
            public string Name;
            public float Sum;
            public string Currency;
            public bool isTest = false;
        }

        public class ChatDataClass {

            public List<FollowerClass> ListFollowers = new List<FollowerClass>();
            public List<SubscriberClass> ListSubscribers = new List<SubscriberClass>();
            public List<DonateClass> ListDonates = new List<DonateClass>();

            public List<DonateSumClass> ListDonateSum = new List<DonateSumClass>();

            public Dictionary<LabelBase.LabelType, int> ListCounters = new Dictionary<LabelBase.LabelType,int>();
        }

        public static ChatDataClass ChatData = new ChatDataClass();

Пример работы с меткой

Console.WriteLine(LabelBase.DictLabels[LabelBase.LabelType.Viewers_Youtube]);

LabelBase.DictLabels[LabelBase.LabelType.Viewers_Youtube].Params["QTY"] = qtyYT.ToString();
LabelBase.DictLabels[LabelBase.LabelType.Viewers_Youtube].Params["Like"] = intLike.ToString();
LabelBase.DictLabels[LabelBase.LabelType.Viewers_Youtube].Params["Dislike"] = intDislike.ToString();
LabelBase.DictLabels[LabelBase.LabelType.Viewers_Youtube].Save();

Работа с Рангами

Общий принцип

В программе есть база данных с рангами. Она храниться в файле /dataX/ranks.db в формате SQLite3 (программа для работы с базой DB Browser for SQLite). Программа использует файл базы SQLite3 только для первичной загрузки и сохранения данных в оперативную базу. Для оперативной же работы используется база RankControl.ListChatters. Итого есть 2 базы:

  • База SQLite3 (используемая для хранения)
  • База RankControl.ListChatters(используемая для оперативной работы, добавления, чтения, изменения)

Получается что работа вся происходит с RankControl.ListChatters, но для того что бы зафиксировать изменения нужно так же сделать изменения и в SQLite3 базе.

Такая организация вынужденная для ускорения работы программы. Просто пример, нужно внести изменения на 50000 пользователей. Проще внести их в оперативную базу, и добавить в список изменений в SQLite3 базу, которая постепенно будет их вносить. Это позволит и сохранить данные по рангам на случай падения программы, так и сократит скорость закрытия программы.

// База чаттеров, оперативная база, отсюда читаем ранги, вносим изменения, используется в первую очередь именно для чтения
RankControl.ListChatters<ChatterRank>

// Метод для обновления данных по чаттеру, по сути это список обьектов ChatterRank, которые в пакетном режиме вносятся в базу SQLite3, как есть возможность провести операцию
RankControl.ListUpdateSQL.Add(ChatterRank crItem);

Пример #1

По команде в чате !rank бот отправляет сообщение: Никнейм у тебя N кредитов

Файл скрипта: auto_simplerank.cs

using System;
using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json;
using System.Linq;
using System.Globalization;
using System.Drawing;

namespace RutonyChat {
    public class Script {

        public void InitParams(string param) {		

			
        }

        public void Closing() {

        }

        public void NewMessage(string site, string name, string text, bool system) {   
            
        }

		
		public void ShowAlert(string name, float sum, string text) {

		}
		
        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {


        }

        public void NewMessageEx(string site, string name, string text, bool system, Dictionary<string, string> Params) {

            if (text == "!rank") {
                RankControl.ChatterRank crItem = RankControl.ListChatters.FindLast(r => r.Nickname == "rutony");
                if (crItem != null) {
                    RutonyBot.BotSay(site, name + " у тебя " + crItem.CreditsQty.ToString() + " кредитов");
                }
            }

        }
    }
}

Пример #2

По команде в чате !цена или !price и название предмета, выдавать информацию о цене предмета из игры Escape from Tarkov. Предусмотреть возможность задать имя команды в программе.

Файл скрипта: auto_tarkovprices.cs

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Linq;

namespace RutonyChat {
    public class Script {

        public List<string> ListKeywords = new List<string>();

        public void InitParams(Dictionary<string, string> param) {

            // параметры скрипта 
            string Keywords = "";
            if (param.ContainsKey("Keywords")) {
                try {
                    Keywords = param["Keywords"];
                } catch { }
            }

            ListKeywords = Keywords.Split(',').ToList();

            RutonyBot.SayToWindow("Tarkov prices script connected");

        }

        public void Closing() {
            RutonyBot.SayToWindow("Tarkov prices script disconnected");
        }

        public void NewMessage(string site, string name, string text, bool system) {

            //RutonyBot.SayToWindow(string.Format("site={0}, name={1}, text={2}", site, name, text));

        }
        public class Ru {
            public string id;
            public string name;
            public string shortName;
            public int lastLowPrice;
        }

        public class Data {
            public List<Ru> ru = new List<Ru>();
        }
        public class RootItems {
            public Data data = new Data();
        }

        public async Task<string> GetProductsData(string _itemName) {

            //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            var client = new HttpClient();
            var request = new HttpRequestMessage(HttpMethod.Post, @"https://api.tarkov.dev/graphql");
            var content = new StringContent("{\"query\": \"{ru: items(lang: ru name: \\\"" + _itemName + "\\\") {id name shortName lastLowPrice} }\"}", null, "application/json");

            string res = "";

            try {
                request.Content = content;
                var response = await client.SendAsync(request);
                response.EnsureSuccessStatusCode();

                res = await response.Content.ReadAsStringAsync();
            } catch { }

            return res;

        }

        public void NewMessageEx(string site, string name, string text, bool system, Dictionary<string, string> Params) {

            foreach (string itemKeyword in ListKeywords) {
                if (text.ToLower().StartsWith(itemKeyword)) {

                    string[] arrString = text.Split(' ');

                    string itemName = text.Replace(itemKeyword, "").Trim();

                    string itemsDataRequest = GetProductsData(itemName).GetAwaiter().GetResult();

                    RootItems itemsData = JsonConvert.DeserializeObject<RootItems>(itemsDataRequest);

                    if (itemsData.data.ru.Count > 0) {
                        RutonyBot.BotSay(site, itemsData.data.ru[0].name + " стоит " + itemsData.data.ru[0].lastLowPrice + " рублей");
                    } else {
                        RutonyBot.BotSay(site, "Информации об " + itemName + " не обнаружено!");
                    }

                    return;
                }
            }

        }

        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {


        }
    }
}

Файл параметров: auto_tarkovprices.json

{
    "params": [
        {
            "name": "Keywords",
            "desc": {
              "russian": "Команда для отображения цены",
              "english": "Command",
              "tchinese": "Command "
            },
            "type": "string",
            "default": "!price,!цена"
          }
    ]
  }

Пример #3

Реализовать автоматический сбор ссылок из чата в которых содержится steamcommunity упоминание в текстовый файл.

Файл скрипта: auto_savelinks.cs

using System;
using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json;
using System.Linq;

namespace RutonyChat {
    public class Script {

        string SaveFilename = @"c:\links.txt";

        public void InitParams(Dictionary<string, string> param) {
            RutonyBot.SayToWindow("Сбор ссылок на steamcommunity включен");

            if (param.ContainsKey("SaveFile")) {
                try {
                    SaveFilename = param["SaveFile"];
                } catch { }
            }
        }

        public void Closing() {
            RutonyBot.SayToWindow("Сбор ссылок на steamcommunity отключен");
        }

        public void NewMessage(string site, string name, string text, bool system) {

            if (text.Contains("steamcommunity")) {
                RutonyBotFunctions.FileAddString(SaveFilename, string.Format("{0}> {1}: {2}", DateTime.Now.ToString(), name, text));
            }
        }

        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {

        }
    }
}

Пример #4

Автоматическое приветствие Новых зрителей, предусмотреть вариативность приветствия

Файл скрипта: auto_newviewer_hello.cs

using System;
using System.Threading;
using System.Collections.Generic;

namespace RutonyChat {
    public class Script {
        public void InitParams(string param) {            

          RutonyBot.SayToWindow("Приветствие подключено!");
        }

        public void Closing() {

        }


        public void NewMessage(string site, string name, string text, bool system) {

            if (system) {
                return;
            }
            
        }

        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {

            if (typeEvent == "new_viewer")  {
                List<string> ListHello = new List<string>();
                ListHello.Add("$name, привет! Как дела?");
                ListHello.Add("Вот это поворот сам $name зашел на огонек!");
                ListHello.Add("$name, привет!");
                ListHello.Add("Дай пятюню $name!");
                ListHello.Add("Всем выйти из сумрака $name в чате!");
                ListHello.Add("Здраствуй $name садись присаживайся");
                ListHello.Add("Я вижу тебя $name 🕵🏼");

                Random rnd = new Random();
                int ind = rnd.Next(ListHello.Count - 1);

                string msg = ListHello[ind].Replace("$name", name);
                RutonyBot.SayToWindow(msg);
            }
        }

    }
}

Пример #5

Реализовать автоматическую выдачу VIP для Твитч за баллы канала. VIP выдается на месяц. При запуске скрипта производится проверка на просроченность VIP и его снятие. Предусмотреть возможность настройки параметров скрипта.

Файл скрипта: auto_vip.cs

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.Threading;
using System.Drawing;
using System.IO;
using System.Xml.Linq;

namespace RutonyChat {
    public class Script {

        public int VipPrice = 15000;
        public int VipExpireDays = 30;

        public string VipText = "$name, выдана VIP, носи с гордостью и не позорь канал";
        public string VipTextExpire = "$name, подписка на VIP кончилась";

        private string FileName = "vip_expires.json";

        public class VipInfoItem {
            public string Nickname = string.Empty;
            public DateTime DT = DateTime.MinValue;
        }
        public List<VipInfoItem> ListVipInfo = new List<VipInfoItem>();

        public void InitParams(Dictionary<string, string> param) {
            
            RutonyBot.SayToWindow("Авто випки [Подключились]", Color.YellowGreen);

            if (param.ContainsKey("VipPrice")) {
                try {
                    VipPrice = int.Parse(param["VipPrice"]);
                } catch { }
            }

            if (param.ContainsKey("VipExpireDays")) {
                try {
                    VipExpireDays = int.Parse(param["VipExpireDays"]);
                } catch { }
            }

            if (File.Exists(ProgramProps.dir + FileName)) {

                try {
                    ListVipInfo = JsonConvert.DeserializeObject<List<VipInfoItem>>(ProgramProps.dir + FileName);
                } catch { 
                    ListVipInfo = new List<VipInfoItem>() { };
                }

                foreach (VipInfoItem itemVipInfo in ListVipInfo) {
                    if (itemVipInfo.DT.AddDays(VipExpireDays) > DateTime.Now) {
                        RutonyBot.SendTwitchCommand(RutonyBot.TwitchCommands.unvip, itemVipInfo.Nickname, itemVipInfo.Nickname, itemVipInfo.Nickname, itemVipInfo.Nickname);

                        RutonyBot.BotSay("twitch", VipTextExpire.Replace("$name", itemVipInfo.Nickname));
                    }
                }
                SaveData();
            }
        }
        public void Closing() {
            RutonyBot.SayToWindow("Авто випки [Отключились]", Color.Red);
            SaveData();
        }
        public void SaveData() {

            try {
                string textForSave = JsonConvert.SerializeObject(ListVipInfo, Formatting.Indented);

                File.WriteAllText(ProgramProps.dir + FileName, textForSave);

            } catch { }

        }

        public void NewMessage(string site, string name, string text, bool system) {

        }

        public void NewMessageEx(string site, string name, string text, bool system, Dictionary<string, string> Params) {

        }

        public void NewAlert(string site, string typeEvent, string subplan, string name, string text, float donate, string currency, int qty) {

            if (typeEvent == "TwitchPoints" && donate == VipPrice) {

                RutonyBot.SendTwitchCommand(RutonyBot.TwitchCommands.vip, name, name, name, name);
                ListVipInfo.Add(new VipInfoItem() {
                    Nickname = name,
                    DT = DateTime.Now,
                });
                RutonyBot.BotSay("twitch", VipText.Replace("$name", name));

                SaveData();

            }

        }
    }
}

Файл параметров auto_vip.json

{
    "params": [

        {
            "name": "VipPrice",
            "desc": {
              "russian": "Цена VIP",
              "english": "Цена VIP",
              "tchinese": "Цена VIP"
            },
            "type": "integer",
            "default": 15000,
            "min": 10,
            "max": 10000000
        },
        
        {
            "name": "VipExpireDays",
            "desc": {
              "russian": "Период выдачи VIP (дни)",
              "english": "Период выдачи VIP (дни)",
              "tchinese": "Период выдачи VIP (дни)"
            },
            "type": "integer",
            "default": 30,
            "min": 0,
            "max": 100000000
        },

        {
            "name": "VipText",
            "desc": {
              "russian": "Текст выдачи VIP",
              "english": "Текст выдачи VIP",
              "tchinese": "Текст выдачи VIP"
            },
            "type": "string",
            "default": "$name, выдана VIP, носи с гордостью и не позорь канал"
        },     
        {
            "name": "VipTextExpire",
            "desc": {
              "russian": "Текст отнятия VIP",
              "english": "Текст отнятия VIP",
              "tchinese": "Текст отнятия VIP"
            },
            "type": "string",
            "default": "$name, подписка на VIP кончилась"
        }
    ]
  }
⚠️ **GitHub.com Fallback** ⚠️