{
  получение доната
  procedure TfrSettings.HandleWSDonate(aNick, aMessage, aSum: string);

  получение сообщения
  procedure TTTW_Bot.ttwIRCOnMessageRecord(aRecord: TTwitchChatMessage);

  получение событий
  ttw_ES.OnFollow := frOBS1.toEventWebServer;
  ttw_ES.OnSub := frOBS1.toEventWebServer;
  ttw_ES.OnGift := frOBS1.toEventWebServer;
  ttw_ES.OnRaid := frOBS1.toEventWebServer;
}

unit uGeneral;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, DateUtils,
  System.Variants, uTTWIRC, uTTWEventSub, uTTWAPI, uSoundManager, uKeyEvent,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.TabControl,
  FMX.Controls.Presentation, FMX.StdCtrls, System.ImageList, FMX.ImgList,
  FMX.Styles, ShellAPI, StrUtils, IdComponent, uRegExpr, uCustomEmoties,
  fSettings, fAI, fNotify, fAutoActions, FMX.ListBox, fLog, fEvents,
  System.Generics.Collections, utts, uGigaChat, uChatAPI, uMyTimer, uRecords,
  System.IOUtils, fCommands, uDataBase, FMX.Edit, FMX.Colors, FMX.SpinBox,
  windows, System.Skia, FMX.Skia, uCreateChat, uCreateNotify, fOBS, fTTS,
  fPlayerWeb, uWebServerKandinsky, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo,
  fRevards;

type
  TTTW_Bot = class(TForm)
    V: TTabControl;
    TabItem1: TTabItem;
    TabItem2: TTabItem;
    TabItem3: TTabItem;
    TabItem4: TTabItem;
    frSettings1: TfrSettings;
    ImageList1: TImageList;
    TabItem5: TTabItem;
    Panel1: TPanel;
    btnConnecting: TButton;
    Label2: TLabel;
    Label3: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Label7: TLabel;
    Label8: TLabel;
    aiConnecting: TAniIndicator;
    Label9: TLabel;
    Label10: TLabel;
    Label11: TLabel;
    Label12: TLabel;
    frAI1: TfrAI;
    TabItem6: TTabItem;
    TabItem7: TTabItem;
    TabItem8: TTabItem;
    TabItem9: TTabItem;
    frNotify1: TfrNotify;
    Label1: TLabel;
    frAutoActions1: TfrAutoActions;
    frOBS1: TfrOBS;
    frLog1: TfrLog;
    cbTheme: TComboBox;
    Label15: TLabel;
    frCommands1: TfrCommands;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    SpeedButton3: TSpeedButton;
    btnCreateChat: TButton;
    frTTS1: TfrTTS;
    GroupBox1: TGroupBox;
    frPlayerWeb1: TfrPlayerWeb;
    frEvents1: TfrEvents;
    GroupBox2: TGroupBox;
    GroupBox3: TGroupBox;
    frRevards1: TfrRevards;
    procedure cbThemeChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure SpeedButton3Click(Sender: TObject);
    procedure SpeedButton2Click(Sender: TObject);
    procedure frSettings1btnDAStartClick(Sender: TObject);
    procedure frCommands1btnRandAddClick(Sender: TObject);
    procedure frOBS1btnDeleteeChatClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);

    procedure frTTS1btnSendClick(Sender: TObject);
    procedure btnConnectingClick(Sender: TObject);
    procedure frAutoActions1btnCounterAddClick(Sender: TObject);
    procedure frAutoActions1btnCounterEditClick(Sender: TObject);
    procedure frAutoActions1btnCounterDeleteClick(Sender: TObject);
  private
    { Private declarations }
    procedure toSpeech(aText: string);
    procedure OnTTWStatus(ASender: TObject; const AStatus: TIdStatus;
      const AStatusText: string);
    procedure ttwIRCOnMessageRecord(aRecord: TTwitchChatMessage);
    procedure PlayNotifySound(aMod, aVip, aSub: Boolean);
    procedure CheckBannedWords(const aText, dname, aID: string);
    procedure ReadDB();
    function PreprocessMessageText(const aText: string): string;
    procedure UpdateWordCounters(const aText: string);
    procedure decodeResponseSendToTTW(aResponseText, aCommandText,
      adName: string);
    function Pars(T_, text, _T: string): string;
    procedure gptError(Msg: string);
    function ResponsParserGroups(inMess: string): string;
    function ResponsParserRandoms(inMess: string): string;
    function ResponsParserCounters(inMess: string): string;
    function ResponsParserStatic(inMess, adName, aCommandText: string): string;
    function ResponsParserAPI(inMess, adName: string): string;
    function ResponsParserGPT(inMess, aCommandText: string): string;
    function ResponsParserAIGen(inMess, aCommandText: string): string;
    function ResponsParserAIPic(inMess, aCommandText, aNick: string): string;
    function ResponsParserSounds(inMess: string): string;
    function ResponsParserText(inMess: string): string;
    function ValidateInput: Boolean;
    procedure ConnectProcedure;
    procedure DisconnectProcedure;
    procedure HandleTimers(Start: Boolean);
    procedure ESError(aMsg: string);
    procedure ESStatus(Sender: TObject; const ConnectionEvent: String;
      StatusCode: Integer; const Description: String);
    procedure ESOnSubOk(s: string);
  public
    { Public declarations }
    procedure toLog(aModule, aMethod, aMessage: string; aCode: Integer);
    function FindUserRow(const aNick: string): Integer;
    procedure GlobalExceptionHandler(Sender: TObject; E: Exception);
  end;

var
  TTW_Bot: TTTW_Bot;
  myConst: TConst;
  db: TSettingsDatabase;
  appconst: TBotAppCfg;
  ttw_IRS: TTTW;
  ttw_ES: TTTW_ES;
  ttw_API: TTTW_API;
  userlist: TList<TUser>;
  Kandinsky: TKandinsky_Web;
  kePoints: TKE;

implementation

{$R *.fmx}

procedure TTTW_Bot.HandleTimers(Start: Boolean);
var
  I: Integer;
begin
  for I := 0 to high(frAutoActions1.listTimer) do
  begin
    if Assigned(frAutoActions1.FTimerList[I]) then
    begin
      if Start and (frAutoActions1.listTimer[I].Enable = 1) then
        frAutoActions1.FTimerList[I].StartT
      else
        frAutoActions1.FTimerList[I].StopT;
    end;
  end;
end;

procedure TTTW_Bot.toSpeech(aText: string);
var
  tts: ttts;
  s, s1: string;
begin
  s := ExtractFilePath(ParamStr(0)) + 'piper\piper.exe';
  s1 := ExtractFilePath(ParamStr(0)) + 'piper\voices';
  if frTTS1.cbVoices.ItemIndex = -1 then
    exit;
  tts := ttts.Create(s, s1);
  try
    tts.SetModel(frTTS1.cbVoices.text);
    tts.TextToSpeech(aText, true);
  finally
    tts.Free;
  end;
end;

procedure TTTW_Bot.GlobalExceptionHandler(Sender: TObject; E: Exception);
begin
  try
    TTW_Bot.toLog('GlobalException', E.ClassName, E.Message, 2);
  except
    // на случай, если логгер сам кинет исключение
  end;
end;

procedure TTTW_Bot.ConnectProcedure;
var
  rid: string;
  BotToken, StreamerToken: string;
  dol: Integer;
begin
  BotToken := frSettings1.edtBotToken.text;
  StreamerToken := frSettings1.edtBotTokenStreamer.text;

  // Проверка бот-токена
  if not ttw_API.ValidateTwitchToken('Бот', BotToken, dol) then
    raise Exception.Create('Недействительный бот-токен');
  Label12.text := inttostr(dol);
  // Проверка стример-токена (если отличается)
  if (BotToken <> StreamerToken) and not ttw_API.ValidateTwitchToken('Стример',
    StreamerToken, dol) then
    raise Exception.Create('Недействительный стример-токен');
  Label11.text := inttostr(dol);
  // Инициализация и подключение основного сервиса
  ttw_IRS.Init(frSettings1.edtBotToken.text, frSettings1.edtChannel.text,
    frSettings1.edtBotName.text);

  ttw_IRS.Connect;

  // Инициализация API
  try
    ttw_API.Init(frSettings1.edtBotClientID.text, frSettings1.edtBotToken.text,
      IfThen(frSettings1.edtBotTokenStreamer.text = '',
      frSettings1.edtBotToken.text, frSettings1.edtBotTokenStreamer.text),
      frSettings1.edtChannel.text, frSettings1.edtBotName.text);
  except
    on E: Exception do
    begin
      toLog('uGeneral', 'ConnectProcedure.ttw_API.Init', E.Message, 2);

    end;
  end;

  // Обработка ролей

  rid := ttw_API.getRoomAndBot;
  if rid = '' then
    raise Exception.Create('Не удалось получить Room ID');
  try
    frRevards1.LoadCustomRevards;
  except
    on E: Exception do
    begin
      toLog('uGeneral', 'ConnectProcedure.LoadCustomRevards', E.Message, 2);
    end;
  end;
  // Загрузка эмодзи и бейджей
  try
    frOBS1.ChatBadges.Clear;
    ttw_API.getCustomChatBadges(frOBS1.ChatBadges);
    ttw_API.getGlobalChatBadges(frOBS1.ChatBadges);

    frOBS1.ChatEmotes.Clear;
    ttw_API.GetChannelEmotes(frOBS1.ChatEmotes);
    ttw_API.GetGlobalEmotes(frOBS1.ChatEmotes);

    frOBS1.BTTV.getGlobal;
    frOBS1.BTTV.getCustom(rid);

    frOBS1.m7tv.getGlobal;
    frOBS1.m7tv.getCustom(rid);
  except
    on E: Exception do
    begin
      toLog('uGeneral', 'ConnectProcedure.Emotes', E.Message, 2);

    end;
  end;

  // Инициализация EventSub

  try
    if Assigned(ttw_ES) then
      FreeAndNil(ttw_ES);

    ttw_ES := TTTW_ES.Create(Self, frSettings1.edtBotTokenStreamer.text,
      frSettings1.edtBotClientID.text, rid);

    // Назначение обработчиков событий
    ttw_ES.OnLog := toLog;
    ttw_ES.OnError := ESError;
    ttw_ES.OnGetCustomReward := frEvents1.ESOnGetCustomReward;
    ttw_ES.OnStatus := ESStatus;
    // ttw_ES.OnRAW := fRewards.ESOnRAW;
    ttw_ES.OnSubOk := ESOnSubOk;
    ttw_ES.OnFollow := frOBS1.toEventWebServer;
    ttw_ES.OnSub := frOBS1.toEventWebServer;
    ttw_ES.OnGift := frOBS1.toEventWebServer;
    ttw_ES.OnRaid := frOBS1.toEventWebServer;
    toLog('uGeneral', 'ConnectProcedure.ttw_ES', 'Создан', 0);
    ttw_ES.Connect;
  except
    on E: Exception do
    begin
      toLog('uGeneral', 'ConnectProcedure.ttw_ES', E.Message, 2);

    end;
  end;

  HandleTimers(true);

  if frSettings1.cbDAAutoLogin.IsChecked then
  begin
    frSettings1.btnDAGetCodeClick(Self);
  end;

end;

procedure TTTW_Bot.DisconnectProcedure;

begin
  try
    toLog('DisconnectProcedure', 'Start', 'Начало процедуры отключения', 3);
    // 1. Отключаем IRS
    toLog('DisconnectProcedure', 'ttw_IRS.Disconnect',
      'Попытка отключения IRS', 3);
    ttw_IRS.Disconnect;
    // 2. Обрабатываем EventSub
    if Assigned(ttw_ES) then
    begin
      toLog('DisconnectProcedure', 'ttw_ES.Disconnect',
        'Попытка отключения ES', 3);
      ttw_ES.Disconnect;

      // Даем время на корректное завершение
      Sleep(150);
      toLog('DisconnectProcedure', 'ttw_ES.Free', 'Освобождение ES', 3);
      FreeAndNil(ttw_ES);
    end;
    // 3. Очищаем интерфейс
    { if Assigned(fChatFrame) then
      begin
      fChatFrame.ChatBadges.Clear;
      fChatFrame.ChatEmotes.Clear;
      { for I := 0 to fChatFrame.ChatWebServers.Count - 1 do
      begin
      fChatFrame.ChatWebServers[I].WebServerChat.ActiveServer(false);
      end;
      end; }
    // 4. Останавливаем таймеры
    HandleTimers(false);

    frSettings1.FWSClient.Disconnect;

    Label3.text := 'Disconnected';
    Label6.text := 'Disconnected';
  except
    on E: Exception do
      toLog('DisconnectProcedure', 'General', E.ClassName + ': ' +
        E.Message, 2);
  end;
end;

procedure TTTW_Bot.ESError(aMsg: string);
begin
  toLog('uGeneral', 'ESError', aMsg, 2);
end;

procedure TTTW_Bot.ESOnSubOk(s: string);
begin
  toLog('uGeneral', 'ESOnSubOk', s, 0);
end;

procedure TTTW_Bot.ESStatus(Sender: TObject; const ConnectionEvent: String;
  StatusCode: Integer; const Description: String);
begin

  Label6.text := ConnectionEvent;
end;

function TTTW_Bot.ValidateInput: Boolean;
begin
  Result := false;

  if Trim(frSettings1.edtBotToken.text) = '' then
  begin
    ShowMessage('Токен бота обязателен для заполнения!');
    exit;
  end;

  if Trim(frSettings1.edtChannel.text) = '' then
  begin
    ShowMessage('Название канала обязательно для заполнения!');
    exit;
  end;

  if (Trim(frSettings1.edtBotClientID.text) = '') then
  begin
    ShowMessage('Client ID обязателен!');
    exit;
  end;

  Result := true;
end;

procedure TTTW_Bot.btnConnectingClick(Sender: TObject);

begin
  if not ValidateInput then
    exit;

  btnConnecting.Enabled := false;
  aiConnecting.Enabled := true;
  aiConnecting.Visible := true;

  try
    if btnConnecting.text = 'Подключиться' then
      ConnectProcedure
    else
      DisconnectProcedure;

    TThread.Synchronize(nil,
      procedure
      begin
        btnConnecting.text := IfThen(btnConnecting.text = 'Подключиться',
          'Отключиться', 'Подключиться');
      end);
  except
    on E: Exception do

      ShowMessage('Ошибка подключения: ' + E.Message);
  end;

  aiConnecting.Enabled := false;
  aiConnecting.Visible := false;
  btnConnecting.Enabled := true;

end;

procedure TTTW_Bot.cbThemeChange(Sender: TObject);
begin
  cbTheme.ItemIndex := cbTheme.Items.IndexOf(cbTheme.text);
  if cbTheme.ItemIndex <> -1 then
    TStyleManager.SetStyleFromFile(myConst.stlPath + cbTheme.text);
  db.WriteSetting('cbTheme', inttostr(cbTheme.ItemIndex));
end;

procedure TTTW_Bot.FormCreate(Sender: TObject);
var
  Path: string;

  function GetPathToTestExe: string; // вернет папку romaming
  begin
    Result := GetEnvironmentVariable('APPDATA');
    if Result <> '' then
      Result := IncludeTrailingPathDelimiter(Result);
  end;

begin

  myConst.GeneralPath := ExtractFilePath(ParamStr(0));
  myConst.AppDataPath := GetPathToTestExe + 'TTW_Bot\';

  if not DirectoryExists(myConst.AppDataPath) then
    CreateDir(myConst.AppDataPath);

  myConst.DBPath := myConst.AppDataPath + 'settings.db';

  if not DirectoryExists(myConst.AppDataPath + 'fonts') then
    CreateDir(myConst.AppDataPath + 'fonts');
  myConst.fontsPath := myConst.AppDataPath + 'fonts\';

  if not DirectoryExists(myConst.AppDataPath + 'imgs') then
    CreateDir(myConst.AppDataPath + 'imgs');
  myConst.imgsPath := myConst.AppDataPath + 'imgs\';

  if not DirectoryExists(myConst.AppDataPath + 'sounds') then
    CreateDir(myConst.AppDataPath + 'sounds');
  myConst.soundsPath := myConst.AppDataPath + 'sounds\';

  if not DirectoryExists(myConst.AppDataPath + 'stl') then
    CreateDir(myConst.AppDataPath + 'stl');
  myConst.stlPath := myConst.AppDataPath + 'stl\';

  if not DirectoryExists(myConst.AppDataPath + 'ytSongs') then
    CreateDir(myConst.AppDataPath + 'ytSongs');
  myConst.ytSongsPath := myConst.AppDataPath + 'ytSongs\';

  myConst.SilentPlay := myConst.GeneralPath + 'SilentPlayer.exe';
  myConst.ytPlay := myConst.GeneralPath + 'Player.exe';
  myConst.cfg1 := myConst.GeneralPath + 'botapp.cfg';

  db := TSettingsDatabase.Create(myConst.DBPath);
  frAutoActions1.FTimerList := TObjectList<TMyTimerThread>.Create(false);
  ReadDB;
  frCommands1.frsgSounds.ObjectRecord := frCommands1.listSounds;
  frCommands1.frsgSounds.TableName := 'listSounds';
  frCommands1.frsgSounds.UpdateGrid;
  frCommands1.frsgFiles.ObjectRecord := frCommands1.listFiles;
  frCommands1.frsgFiles.TableName := 'listFiles';
  frCommands1.frsgFiles.UpdateGrid;
  frCommands1.frsgNeiro.ObjectRecord := frCommands1.listNeiro;
  frCommands1.frsgNeiro.TableName := 'listNeiro';
  frCommands1.frsgNeiro.UpdateGrid;

  for Path in TDirectory.GetFiles(myConst.stlPath) do
    cbTheme.Items.Add(ExtractFileName(Path));
  cbTheme.ItemIndex := strtoint(db.ReadSetting('cbTheme', '-1'));
  frLog1.FLogList := TList<TRLog>.Create;

  frPlayerWeb1.Init;

  userlist := TList<TUser>.Create;
  ttw_IRS := TTTW.Create(Self);
  ttw_IRS.OnMessageRecord := ttwIRCOnMessageRecord;
  ttw_IRS.OnLog := toLog;
  ttw_IRS.OnStatus := OnTTWStatus;

  ttw_API := TTTW_API.Create(Self);

end;

procedure TTTW_Bot.OnTTWStatus(ASender: TObject; const AStatus: TIdStatus;
const AStatusText: string);
begin
  Label3.text := AStatusText;
end;

procedure TTTW_Bot.FormDestroy(Sender: TObject);
begin
   FreeAndNil(frOBS1.ChatBadges);
  FreeAndNil(frOBS1.ChatEmotes);
  FreeAndNil(frOBS1.ChatWebServers);
FreeAndNil(frOBS1.EventWebServers);
FreeAndNil(frOBS1.KandinskyWebServers);
FreeAndNil(frRevards1.CustomRewards);

  frOBS1.BTTV.Free;
  frOBS1.m7tv.Free;
  userlist.Free;
  kePoints.Free;

  DisconnectProcedure;
  FreeAndNil(ttw_IRS);
  FreeAndNil(ttw_ES);
  FreeAndNil(ttw_API);
  if Assigned(Kandinsky) then
    Kandinsky.Free;
  //frSettings1.Destroy;
  FreeAndNil(db);
  FreeAndNil(frAutoActions1.FTimerList);
  FreeAndNil(frLog1.FLogList);
  inherited;
end;

function TTTW_Bot.FindUserRow(const aNick: string): Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := 0 to userlist.Count - 1 do
    if userlist[I].login = LowerCase(aNick) then
    begin
      Result := I;
      Break;
    end;
end;

function TTTW_Bot.PreprocessMessageText(const aText: string): string;
begin
  Result := LowerCase(aText);
  Result := StringReplace(Result, #$D, '', [rfReplaceAll]);
  Result := StringReplace(Result, '󠀀', '', [rfReplaceAll]);
  Result := Trim(Result) + ' ';
end;

procedure TTTW_Bot.UpdateWordCounters(const aText: string);
var
  I: Integer;
  Words: TArray<string>;
  mode: Boolean;

  procedure IncrementCounter(ARow: Integer);
  var
    Count: Integer;
  begin
    Count := frAutoActions1.listCounters[ARow].Count + 1;
    frAutoActions1.listCounters[ARow].Count := Count;
    db.SaveRecordArray<TCounter>('listCounters', frAutoActions1.listCounters);
    frAutoActions1.UpdateGridFromArray;
  end;

  function StringToArray(const input: string): TArray<string>;
  var
    Delimiter: char;
    Words: TArray<string>;
    I: Integer;
  begin
    Delimiter := ',';
    Words := input.Split([Delimiter]);
    SetLength(Result, Length(Words));
    for I := 0 to High(Words) do
      Result[I] := Words[I].Trim;
  end;

  function ContainsAnyWord(const str: string;
  const Words: array of string): Boolean;
  var
    I: Integer;
  begin
    for I := Low(Words) to High(Words) do
    begin
      if Pos(AnsiLowerCase(Words[I]), AnsiLowerCase(str)) > 0 then
      begin
        Result := true;
        exit;
      end;
    end;
    Result := false;
  end;

begin
  for I := 0 to high(frAutoActions1.listCounters) do
  begin
    mode := frAutoActions1.listCounters[I].auto = 1;
    if mode then
    begin
      Words := StringToArray(frAutoActions1.listCounters[I].trigger);
      if ContainsAnyWord(aText, Words) then
        IncrementCounter(I);
    end;
  end;
end;

procedure TTTW_Bot.CheckBannedWords(const aText, dname, aID: string);
var
  I: Integer;
  rx: TRegExpr;
begin
  rx := TRegExpr.Create;
  try
    for I := 0 to high(frAutoActions1.listBanWords) do
    begin
      rx.Expression := frAutoActions1.listBanWords[I].regexp;
      if rx.Exec(aText) then
      begin
        if aID = '0' then
          ttw_API.banUserTime(dname, 86400)
        else
          ttw_API.banUserTime(aID, 86400);
        Break;
      end;
    end;
  finally
    rx.Free;
  end;
end;

procedure TTTW_Bot.ttwIRCOnMessageRecord(aRecord: TTwitchChatMessage);
// обработка нового сообщения
var
  processedText: string;
  firstWord: string;
  commandText: string;
  responseText, s: string;
  u: TUser;
  I: Integer;

  function ExtractFirstWord(var aText: string): string;
  var
    posSpace: Integer;
  begin
    posSpace := Pos(' ', aText);
    if posSpace > 0 then
    begin
      Result := Copy(aText, 1, posSpace - 1);
      Result := StringReplace(Result, ',', '', [rfReplaceAll]);
      Result := StringReplace(Result, '.', '', [rfReplaceAll]);
      Result := StringReplace(Result, '?', '', [rfReplaceAll]);
    end
    else
      Result := aText;
  end;
  function IsCommand(const aWord: string): Boolean;
  begin
    Result := (Length(aWord) > 0) and (aWord[1] = '!');
  end;

  function ExtractCommandText(const aText, aFirstWord: string): string;
  begin
    Result := Copy(aText, Length(aFirstWord) + 1, Length(aText));
  end;

  function ProcessCommand(const aCommand: string): string;
  var
    I: Integer;
  begin
    Result := '';
    for I := 0 to high(frCommands1.listCommands) do
    begin
      if ContainsText(aCommand, frCommands1.listCommands[I].R1) then
      begin
        Result := frCommands1.listCommands[I].R2;
        Break;
      end;
    end;
  end;

begin
  if aRecord.Username = '' then
    exit;
  I := FindUserRow(aRecord.Username);
  if I <> -1 then
  begin // user есть
    u := userlist[I];
  end
  else
  begin
    u.ID := aRecord.UserId;
    u.login := aRecord.Username;
    u.isVIP := false;
    u.isModer := false;
    u.isO := false;
    u.created_at := StrToDate('01.01.1990');
    u.follow_at := StrToDate('01.01.1990');
    u.isO_today := false;
    userlist.Add(u);
  end;
  I := FindUserRow(aRecord.Username);
  if (u.isO) and (NOT u.isO_today) then
  begin
    ttw_API.shoutouts(u.ID);
    u.isO_today := true;
  end;
  userlist[I] := u;

  processedText := PreprocessMessageText(aRecord.Message);
  UpdateWordCounters(processedText);
  CheckBannedWords(processedText, aRecord.DisplayName, aRecord.UserId);

  if (frCommands1.frContruct1.cbTextToSpeach.IsChecked) and (processedText[1] = '!') and
    (processedText[2] = '!') and (processedText[3] = '!') then
  begin
    s := StringReplace(processedText, '!!!', '', [rfReplaceAll]);
    s := Trim(s);
    toSpeech(s);
    exit;
  end;

  if (frCommands1.frContruct1.cbHelloTTS.IsChecked) and (aRecord.FirstMsg = 1) then
    toSpeech('приветствую, ' + IfThen(aRecord.DisplayName <> '',
      aRecord.DisplayName, aRecord.Username));

  firstWord := ExtractFirstWord(processedText);
  if IsCommand(firstWord) then
  begin
    commandText := ExtractCommandText(processedText, firstWord);
    responseText := ProcessCommand(firstWord);
    decodeResponseSendToTTW(responseText, commandText, aRecord.DisplayName);
  end;
  TThread.Queue(nil,
    procedure
    begin
      frOBS1.MsgToWebServer(aRecord);
    end);
  PlayNotifySound((aRecord.Moder = 1), (aRecord.Vip = 1),
    (aRecord.Subscriber = 1));
end;

procedure TTTW_Bot.PlayNotifySound(aMod, aVip, aSub: Boolean);
var
  s: string;
  sm: TSongMachine;
begin

  sm := TSongMachine.Create;
  try
    if (aMod) and (frNotify1.chEnNotifyMod.IsChecked) and
      (frNotify1.edtNotifyFileNameMod.text <> '') then
    begin
      if frNotify1.cbNotifyFileAgain1.IsChecked then
        s := frNotify1.edtNotifyFileName.text
      else
        s := frNotify1.edtNotifyFileNameMod.text;

      sm.PlaySilent(s, inttostr(round(frNotify1.tbNotifyVolumeMod.Value)));
      exit;
    end;
    if (aVip) and (frNotify1.chEnNotifyVip.IsChecked) and
      (frNotify1.edtNotifyFileNameVip.text <> '') then
    begin
      if frNotify1.cbNotifyFileAgain2.IsChecked then
        s := frNotify1.edtNotifyFileName.text
      else
        s := frNotify1.edtNotifyFileNameVip.text;
      sm.PlaySilent(s, inttostr(round(frNotify1.tbNotifyVolumeVip.Value)));
      exit;
    end;
    if (aSub) and (frNotify1.chEnNotifySub.IsChecked) and
      (frNotify1.edtNotifyFileNameSub.text <> '') then
    begin
      if frNotify1.cbNotifyFileAgain3.IsChecked then
        s := frNotify1.edtNotifyFileName.text
      else
        s := frNotify1.edtNotifyFileNameSub.text;
      sm.PlaySilent(s, inttostr(round(frNotify1.tbNotifyVolumeSub.Value)));
      exit;
    end;
    if (frNotify1.chEnNotify.IsChecked) and
      (frNotify1.edtNotifyFileName.text <> '') then
    begin
      s := frNotify1.edtNotifyFileName.text;
      sm.PlaySilent(s, inttostr(round(frNotify1.tbNotifyVolume.Value)));
      exit;
    end;
  finally
    sm.Free;
  end;
end;

procedure TTTW_Bot.frAutoActions1btnCounterAddClick(Sender: TObject);
var i:integer;
begin
  frAutoActions1.btnCounterAddClick(Sender);
  frCommands1.frContruct1.cbCounterName.Items.Clear;
  for i:=0 to high(frAutoActions1.listCounters) do
  frCommands1.frContruct1.cbCounterName.Items.Add(frAutoActions1.listCounters[i].counterName);
end;

procedure TTTW_Bot.frAutoActions1btnCounterDeleteClick(Sender: TObject);
var i:integer;
begin
  frAutoActions1.btnCounterDeleteClick(Sender);
    frCommands1.frContruct1.cbCounterName.Items.Clear;
  for i:=0 to high(frAutoActions1.listCounters) do
  frCommands1.frContruct1.cbCounterName.Items.Add(frAutoActions1.listCounters[i].counterName);
end;

procedure TTTW_Bot.frAutoActions1btnCounterEditClick(Sender: TObject);
var i:integer;
begin
  frAutoActions1.btnCounterEditClick(Sender);
    frCommands1.frContruct1.cbCounterName.Items.Clear;
  for i:=0 to high(frAutoActions1.listCounters) do
  frCommands1.frContruct1.cbCounterName.Items.Add(frAutoActions1.listCounters[i].counterName);
end;

procedure TTTW_Bot.frCommands1btnRandAddClick(Sender: TObject);
begin
  frCommands1.btnRandAddClick(Sender);

end;

procedure TTTW_Bot.frOBS1btnDeleteeChatClick(Sender: TObject);
begin
  frOBS1.btnDeleteeChatClick(Sender);

end;

procedure TTTW_Bot.frSettings1btnDAStartClick(Sender: TObject);
begin
  frSettings1.btnDAStartClick(Sender);

end;

procedure TTTW_Bot.frTTS1btnSendClick(Sender: TObject);
begin
  frTTS1.btnSendClick(Sender);

end;

procedure TTTW_Bot.ReadDB;

  function XorDecryptToStrings(const InputFile, Key: string): TStrings;
  var
    InStream: TFileStream;
    MemStream: TMemoryStream;
    KeyBytes: TBytes;
    KeyLen, KeyIndex: Integer;
    B: Byte;
  begin
    KeyBytes := TEncoding.ANSI.GetBytes(Key);
    KeyLen := Length(KeyBytes);
    if KeyLen = 0 then
      raise Exception.Create('Ключ не может быть пустым');

    InStream := TFileStream.Create(InputFile, fmOpenRead);
    try
      MemStream := TMemoryStream.Create;
      try
        KeyIndex := 0;
        while InStream.Position < InStream.Size do
        begin
          InStream.ReadBuffer(B, 1);
          B := B xor KeyBytes[KeyIndex];
          MemStream.WriteBuffer(B, 1);
          KeyIndex := (KeyIndex + 1) mod KeyLen;
        end;
        MemStream.Position := 0;
        Result := TStringList.Create;
        try
          Result.LoadFromStream(MemStream, TEncoding.ANSI);
        except
          Result.Free; // Освобождаем при ошибке загрузки
          raise;
        end;
      finally
        MemStream.Free;
      end;
    finally
      InStream.Free;
    end;
  end;
// Загрузка компонентов настроек (TEdit, TCheckBox)
  procedure LoadSettingsComponents;
  var
    I: Integer;
    c: TComponent;
  begin
    for I := 0 to frSettings1.ComponentCount - 1 do
    begin
      c := frSettings1.Components[I];
      if c is TEdit then
        TEdit(c).text := db.ReadSetting(TEdit(c).Name)
      else if c is TCheckBox then
        TCheckBox(c).IsChecked := (db.ReadSetting(TCheckBox(c).Name) = 'True');
    end;
    db.FChannel := frSettings1.edtChannel.text;
  end;

// Загрузка данных в гриды команд
  procedure LoadGridsData;
  begin
    db.LoadRecordArray<TRandomCounters>('RandomCounters',
      frCommands1.RandomCounters);
    db.LoadRecordArray<TListCommands>('listSounds', frCommands1.listSounds);
    db.LoadRecordArray<TListCommands>('listFiles', frCommands1.listFiles);
    db.LoadRecordArray<TListCommands>('listNeiro', frCommands1.listNeiro);
    db.LoadRecordArray<TListCommands>('listCommands', frCommands1.listCommands);
    frCommands1.UpdateGridFromArray;

  end;

// Загрузка списка групп
  procedure LoadGroupNames;
  begin
    db.getGroupName(frCommands1.frGroupsRequest1.lbRandomGroup.Items);
  end;

// Загрузка зашифрованного конфига
  procedure LoadEncryptedConfig;
  var
    tempList: TStrings; // Временный список для результата
    I: Integer;
  begin
    if not FileExists(myConst.cfg1) then
      exit;

    tempList := nil; // Инициализация
    try
      tempList := XorDecryptToStrings(myConst.cfg1, 'fgvasrgEFAXFAFAS');

      for I := 0 to tempList.Count - 1 do
      begin
        var
        eqPos := Pos('=', tempList[I]);
        if eqPos > 0 then
        begin
          var
          Key := Trim(Copy(tempList[I], 1, eqPos - 1));
          var
          Value := Trim(Copy(tempList[I], eqPos + 1, MaxInt));

          if Key = 'k1' then
            appconst.TTV_ClientID := Value
          else if Key = 'k2' then
            appconst.AI_GigaChat_AC := Value
          else if Key = 'k3' then
            appconst.AI_GigaChat_ClientID := Value
          else if Key = 'k4' then
            appconst.AI_ChatGPT_Token := Value
          else if Key = 'k5' then
            appconst.AI_DeepSeec_Token := Value
          else if Key = 'k6' then
            appconst.DA_ClientID := Value
          else if Key = 'k7' then
            appconst.DA_Sicret := Value
          else if Key = 'k8' then
            appconst.DA_URL := Value;
        end;
      end;

      frSettings1.btnGetClientID.Visible := (appconst.TTV_ClientID <> '');
      frAI1.btnGetAIDef.Visible := ((appconst.AI_GigaChat_AC <> '') and
        (appconst.AI_GigaChat_ClientID <> '')) or
        (appconst.AI_ChatGPT_Token <> '') or (appconst.AI_DeepSeec_Token <> '');
      frSettings1.btnGetDADef.Visible := (appconst.DA_ClientID <> '') and
        (appconst.DA_Sicret <> '') and (appconst.DA_URL <> '');
    finally
      tempList.Free; // Важно: освобождаем временный список!
    end;
  end;
// Загрузка настроек уведомлений
  procedure LoadNotifySettings;
  var
    I: Integer;
    c: TComponent;
  begin
    for I := 0 to frNotify1.ComponentCount - 1 do
    begin
      c := frNotify1.Components[I];
      if c is TEdit then
        TEdit(c).text := db.ReadSetting(TEdit(c).Name)
      else if c is TCheckBox then
        TCheckBox(c).IsChecked := (db.ReadSetting(TCheckBox(c).Name) = 'True')
      else if c is TSwitch then
        TSwitch(c).IsChecked := (db.ReadSetting(TSwitch(c).Name) = 'True')
      else if c is TTrackBar then
        TTrackBar(c).Value :=
          strtoint(db.ReadSetting(TTrackBar(c).Name, '100'));
    end;
  end;

// Загрузка настроек ИИ
  procedure LoadAISettings;
  var
    I: Integer;
    c: TComponent;
    ii: Integer;

    // Настройки GigaChat
    procedure SetupGigaChatSettings;
    begin
      frAI1.rbGC.IsChecked := true;
      frAI1.Label45.text := 'ClientID';
      frAI1.Label47.text := 'Autorization Code';
      frAI1.Label1.Visible := false;
      frAI1.edtAIP2.Visible := true;
      frAI1.edtAIP2.Password := true;
      frAI1.edtAIP3.Visible := false;
      frAI1.cbOllama.Visible := false;
    end;

  // Настройки DeepSeek
    procedure SetupDeepSeekSettings;
    begin
      frAI1.rbDS.IsChecked := true;
      frAI1.Label45.text := 'API Token';
      frAI1.Label47.text := '';
      frAI1.Label1.Visible := false;
      frAI1.edtAIP2.Visible := false;
      frAI1.edtAIP3.Visible := false;
      frAI1.cbOllama.Visible := false;
    end;

  // Настройки ChatGPT
    procedure SetupChatGPTSettings;
    begin
      frAI1.rbCG.IsChecked := true;
      frAI1.Label45.text := 'API Token';
      frAI1.Label47.text := '';
      frAI1.Label1.Visible := false;
      frAI1.edtAIP2.Visible := false;
      frAI1.edtAIP3.Visible := false;
      frAI1.cbOllama.Visible := false;
    end;

  // Настройки кастомного ИИ
    procedure SetupCustomAISettings;
    begin
      frAI1.RBCustom.IsChecked := true;
      frAI1.Label45.text := 'API Token';
      frAI1.Label47.text := 'URL';
      frAI1.Label1.Visible := true;
      frAI1.edtAIP2.Visible := true;
      frAI1.edtAIP2.Password := false;
      frAI1.edtAIP3.Visible := true;
      frAI1.cbOllama.Visible := true;
      frAI1.cbOllama.IsChecked := db.ReadSetting(frAI1.cbOllama.Name) = '1';
    end;

  begin
    for I := 0 to frAI1.ComponentCount - 1 do
    begin
      c := frAI1.Components[I];
      if c is TEdit then
        TEdit(c).text := db.ReadSetting(TEdit(c).Name)
      else if c is TCheckBox then
        TCheckBox(c).IsChecked := db.ReadSetting(TCheckBox(c).Name) = '1';
    end;

    ii := strtoint(db.ReadSetting('aiIndex', '0'));
    case ii of
      0:
        SetupGigaChatSettings;
      1:
        SetupDeepSeekSettings;
      2:
        SetupChatGPTSettings;
      3:
        SetupCustomAISettings;
    end;

    frSettings1.Init;

  end;

// Загрузка гридов автоматических действий
  procedure LoadAutoActionsGrids;
var i:integer;
  begin
    db.LoadRecordArray<TListTimer>('listTimer', frAutoActions1.listTimer);
    db.LoadRecordArray<TBanWord>('listBanWords', frAutoActions1.listBanWords);
    db.LoadRecordArray<TCounter>('listCounters', frAutoActions1.listCounters);
     for I := 0 to High(frAutoActions1.listCounters) do
      frCommands1.frContruct1.cbCounterName.items.Add(frAutoActions1.listCounters[i].counterName);
    frCommands1.frContruct1.cbTextToSpeach.IsChecked:=db.ReadSetting('cbTextToSpeach', 'False') = 'True';
    frCommands1.frContruct1.cbHelloTTS.IsChecked:= db.ReadSetting('cbHelloTTS', 'False') = 'True';
    frAutoActions1.initTimers;
    frAutoActions1.UpdateGridFromArray;
  end;

// Загрузка интеграций с ОБС
  procedure LoadOBSGrids;
  var
    I: Integer;
  begin
    db.LoadRecordArray<TOBSChat>('listChats', frOBS1.listChats);

    frOBS1.BTTV := TBTTV.Create;
    frOBS1.m7tv := t7tv.Create;
    frOBS1.ChatBadges := TList<TChatBadge>.Create;
    frOBS1.ChatEmotes := TList<TEmotes>.Create;
    frOBS1.ChatWebServers := TList<TChatWebServers>.Create;

    for I := 0 to High(frOBS1.listChats) do
    begin
      frOBS1.CreateWebChat(frOBS1.listChats[I]);
    end;

    db.LoadRecordArray<TOBSNotify>('listNotify', frOBS1.listNotify);
    frOBS1.EventWebServers := TList<TEventWebServers>.Create;
    for I := 0 to High(frOBS1.listNotify) do
    begin

      frOBS1.CreateWebEvents(frOBS1.listNotify[I]);
    end;
    db.LoadRecordArray<TOBSKandinsky>('listKandinsky', frOBS1.listKandinsky);
    frOBS1.KandinskyWebServers := TList<TKandinskyWebServers>.Create;
    if (frAI1.edtKandiKey.text <> '') and (frAI1.edtKandiSecret.text <> '') then
    begin
      for I := 0 to High(frOBS1.listKandinsky) do
      begin
        frOBS1.CreateWebKandinsky(frOBS1.listKandinsky[I]);
      end;

    end;

    frOBS1.UpdateGridFromArray;
  end;

// Загрузка навыков
  procedure LoadSkills;
  begin
    db.LoadRecordArray<TEventGlobal>('ListEvents', frEvents1.ListEvents);
    frEvents1.UpdateGrid;
    frRevards1.CustomRewards := TList<TCustomRevards>.Create;

    frEvents1.CustomRewardEvents := TList<TCustomRewardEvent>.Create;
    kePoints := TKE.Create(frEvents1.edtParams, frEvents1.cbKey1,
      frEvents1.cbKey2, frEvents1.cbKey3);
    frTTS1.btnUpdateVoicesClick(Self);
    frTTS1.cbVoices.ItemIndex := strtoint(db.ReadSetting('cbVoices', '0'));
    frTTS1.cbOutput.ItemIndex := strtoint(db.ReadSetting('cbOutput', '0'));

  end;

begin
  LoadAISettings;
  LoadSkills;
  LoadSettingsComponents;
  LoadGridsData;
  LoadGroupNames;
  LoadEncryptedConfig;

  LoadNotifySettings;

  LoadOBSGrids;
  LoadAutoActionsGrids;
end;

procedure TTTW_Bot.SpeedButton1Click(Sender: TObject);
begin
  ShellExecute(0, 'open', pwidechar('https://www.twitch.tv/incadence'),
    nil, nil, 1);
end;

procedure TTTW_Bot.SpeedButton2Click(Sender: TObject);
begin
  // https://www.twitch.tv/kuznecogr
  ShellExecute(0, 'open', pwidechar('https://www.twitch.tv/kuznecogr'),
    nil, nil, 1);
end;

procedure TTTW_Bot.SpeedButton3Click(Sender: TObject);
begin
  // https://www.flaticon.com/ru/authors/karacis
  ShellExecute(0, 'open',
    pwidechar('https://www.flaticon.com/ru/authors/karacis'), nil, nil, 1);
end;

procedure TTTW_Bot.toLog(aModule, aMethod, aMessage: string; aCode: Integer);
begin
  TThread.Synchronize(nil,
    procedure
    var
      ml: TRLog;
    begin
      // Инициализация всех полей записи
      ml.rTime := Now;
      case aCode of
        0:
          ml.rType := 'INFO';
        1:
          ml.rType := 'WARNING';
        2:
          ml.rType := 'ERROR';
        3:
          ml.rType := 'DEBUG';
      else
        ml.rType := 'UNKNOWN';
      end;
      ml.rModule := aModule; // string
      ml.rMethod := aMethod; // string
      ml.rMessage := aMessage; // string
      // Добавляем запись в список
      frLog1.FLogList.Add(ml);
      // Обновляем грид
      frLog1.UpdateGridFilters;
    end);
end;

procedure TTTW_Bot.decodeResponseSendToTTW(aResponseText, aCommandText,
  adName: string);
var
  res: string;

  ID: string;
  RowIndex: Integer;
begin

  res := '';
  res := aResponseText;
  RowIndex := FindUserRow(adName);
  ID := userlist[RowIndex].ID;
  if ID = '' then
    ID := '0';
  // ----------------------------------------Группы ответов
  res := ResponsParserGroups(res);
  // ----------------------------------------рандомы
  res := ResponsParserRandoms(res);
  // ----------------------------------------счетчики
  res := ResponsParserCounters(res);
  // ----------------------------------------константы
  res := ResponsParserStatic(res, adName, aCommandText);
  // ----------------------------------------апи команды
  res := ResponsParserAPI(res, adName);
  // ----------------------------------------GPT
  res := ResponsParserGPT(res, aCommandText);
  // ----------------------------------------звуки
  res := ResponsParserSounds(res);
  // ----------------------------------------text
  res := ResponsParserText(res);
  // ----------------------------------------AIGen
  res := ResponsParserAIGen(res, aCommandText);
  // ----------------------------------------AIPic
  res := ResponsParserAIPic(res, aCommandText, adName);
  if res <> '' then
    ttw_IRS.sendMessage(res);
end;

function TTTW_Bot.Pars(T_, text, _T: string): string;
var
  A, B: Integer;
begin
  Result := '';
  A := Pos(T_, text);
  if A = 0 then
    exit;
  A := A + Length(T_);
  B := Pos(_T, text, A);
  if B > 0 then
    Result := Copy(text, A, B - A);
end;

function TTTW_Bot.ResponsParserGroups(inMess: string): string;
var
  ss, ss2, res: string;

  sl: TStringList;
begin
  res := inMess;
  while Pos('{{', res) <> 0 do
  begin
    ss := Pars('{{', res, '}}');
    sl := TStringList.Create;
    randomize;
    db.getGroupResponse(ss, sl);
    ss2 := sl[random(sl.Count)];
    res := StringReplace(res, '{{' + ss + '}}', ss2, [rfReplaceAll]);
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserSounds(inMess: string): string;
var
  ss, res: string;
  I, p: Integer;
  sm: TSongMachine;
begin
  res := inMess;
  sm := TSongMachine.Create;
  try
    if ContainsText(res, '||') then
    begin
      // Находим позицию первого '||'
      p := Pos('||', res);
      // Копируем все, что идет после первого '||'
      ss := Copy(res, p + 2, Length(res) - p - 1);
      // +2 чтобы пропустить первый '||'
      // Находим позицию второго '||'
      p := Pos('||', ss);
      if p > 0 then
      begin
        // Копируем текст между '||'
        ss := Copy(ss, 1, p - 1);
        if ss <> '' then
        begin
          // Ищем значение в sgSAFiles
          for I := 0 to high(frCommands1.listSounds) do
          begin
            if frCommands1.listSounds[I].R1 = ss then
            begin
              if FileExists(frCommands1.listSounds[I].R2) then
              begin
                { PlaySong(sgSAFiles.Cells[1, i],
                  inttostr(round(tbSoundVolume.Value)), True); }
                sm.PlayPublic(frCommands1.listSounds[I].R2,
                  inttostr(round(100)));
              end
              else
              begin
                toLog('uGeneral', 'ResponsParserSounds',
                  'Файл "' + frCommands1.listSounds[I].R1 + '" не найден: ' +
                  frCommands1.listSounds[I].R2, 2);
              end;
              Break;
            end;
          end;
          // Удаляем обработанный фрагмент из исходной строки
          res := StringReplace(res, '||' + ss + '||', '', [rfReplaceAll]);
        end;
      end;
    end;
  finally
    sm.Free;
  end;

  Result := res;
end;

function TTTW_Bot.ResponsParserText(inMess: string): string;
var
  ss, res, fn, rres: string;
  sll: TStringList;
  I, p: Integer;
begin
  res := inMess;
  if ContainsText(res, '|(') then
  begin
    p := Pos('|(', res);
    ss := Copy(res, p, Length(res) - p);
    p := Pos('|(', ss, 2);
    ss := Copy(ss, 1, p + 1);
    ss := StringReplace(ss, '|(', '', [rfReplaceAll]);
    if ss <> '' then
    begin
      for I := 0 to high(frCommands1.listFiles) do
      begin
        if frCommands1.listFiles[I].R1 = ss then
        begin
          fn := frCommands1.listFiles[I].R2;
          Break;
        end;
      end;
      sll := TStringList.Create;
      try
        sll.LoadFromFile(fn, TEncoding.UTF8);
        rres := sll.text;
      finally
        sll.Free;
      end;
      if rres <> '' then
      begin
        if Length(rres) > 450 then
        begin
          rres := Copy(rres, 1, 450);
          rres := rres + '...';
        end;
        rres := StringReplace(rres, #13#10, ' ', [rfReplaceAll]);
        res := StringReplace(res, '|(' + ss + '|(', rres, [rfReplaceAll]);
      end;
    end;
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserRandoms(inMess: string): string;
var
  r, res: string;
  I: Integer;

  function RandomInRange(MinValue, MaxValue: Integer): Integer;
  begin
    randomize;
    Result := random(MaxValue - MinValue + 1) + MinValue;
  end;

begin
  res := inMess;
  while ContainsText(res, '[[') do
  begin
    r := Pars('[[', res, ']]');
    for I := 0 to high(frCommands1.RandomCounters) do
    begin
      if frCommands1.RandomCounters[I].rndName = r then
      begin
        res := StringReplace(res, '[[' + r + ']]',
          inttostr(RandomInRange(frCommands1.RandomCounters[I].Ot,
          frCommands1.RandomCounters[I].ToValue)), [rfReplaceAll]);
        Break;
      end;
    end;
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserCounters(inMess: string): string;
var
  r, res: string;
  I: Integer;
begin
  res := inMess;
  while ContainsText(res, '``') do
  begin
    r := Pars('``', res, '``');
    for I := 0 to high(frAutoActions1.listCounters) do
    begin
      if frAutoActions1.listCounters[I].counterName = r then
      begin
        res := StringReplace(res, '``' + r + '``',
          inttostr(frAutoActions1.listCounters[I].Count), [rfReplaceAll]);
        Break;
      end;
    end;
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserStatic(inMess, adName,
  aCommandText: string): string;
var
  res, RandomUserName: string;
begin
  res := inMess;
  res := StringReplace(res, '[USERNAME]', '@' + adName, [rfReplaceAll]);
  res := StringReplace(res, '[TO]', aCommandText, [rfReplaceAll]);
  if ContainsText(res, '[RANDOMUSER]') then
  begin
    if Pos('@', aCommandText) <> 0 then
    begin
      RandomUserName := aCommandText;
    end
    else
    begin
      randomize;
      RandomUserName := '@' + userlist[random(userlist.Count)].login;
    end;
    res := StringReplace(res, '[RANDOMUSER]', RandomUserName, [rfReplaceAll]);
  end;
  Result := res;
end;

procedure TTTW_Bot.gptError(Msg: string);
begin
  toLog('GPT', 'gptError', Msg, 2);
end;

function TTTW_Bot.ResponsParserAIGen(inMess, aCommandText: string): string;
var
  res, GPTRequest: string;
  GigaChat: TGigaChat;
  ChatAPI: TChatAPI;
  mystr: string;
  p, I: Integer;
  ss: string;
begin
  res := inMess;
  if ContainsText(res, '<|') then
  begin
    // Находим позицию первого '||'
    p := Pos('<|', res);
    // Копируем все, что идет после первого '<|'
    ss := Copy(res, p + 2, Length(res) - p - 1);
    // +2 чтобы пропустить первый '<|'
    // Находим позицию второго '<|'
    p := Pos('<|', ss);
    if p > 0 then
    begin
      // Копируем текст между '<|'
      ss := Copy(ss, 1, p - 1);
      if ss <> '' then
      begin
        // Ищем значение в sgSAFiles
        for I := 0 to high(frCommands1.listNeiro) do
        begin
          if frCommands1.listNeiro[I].R1 = ss then
          begin
            mystr := frCommands1.listNeiro[I].R2;
            Break;
          end;
        end;
        // Удаляем обработанный фрагмент из исходной строки
        // res := StringReplace(res, '<|' + ss + '<|', mystr, [rfReplaceAll]);
      end;
    end;
  end;
  mystr := StringReplace(mystr, '[UT]', aCommandText, [rfReplaceAll]);

  mystr := StringReplace(mystr, '[AI]', '', [rfReplaceAll]);
  if (frAI1.edtAIP1.text = '') then
  begin
    GPTRequest :=
      'тут должен быть ответ нейросети, но стример зажал логиниться';
  end
  else
  begin
    if frAI1.rbGC.IsChecked then // GigaChat
    begin
      GigaChat := TGigaChat.Create(Self, frAI1.edtAIP1.text, frAI1.edtAIP2.text,
        frAI1.edtGPTPrefix.text);
      try
        try
          GPTRequest := GigaChat.GetGPTRequest
            ('https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
            'GigaChat', mystr);
        except
          on E: Exception do
            toLog('uGeneral', 'ResponsParserAIGen.gigachat', E.Message, 2);
        end;
      finally
        GigaChat.Destroy;
      end;
    end;
    if frAI1.rbDS.IsChecked then // DeepSeek
    begin
      ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
        frAI1.edtGPTPrefix.text);
      try
        ChatAPI.OnError := gptError;
        try
          GPTRequest := ChatAPI.GetGPTRequest
            ('https://api.deepseek.com/chat/completions',
            'deepseek-chat', mystr);
        except
          on E: Exception do
            toLog('uGeneral', 'ResponsParserAIGen.deepseek', E.Message, 2);
        end;
      finally
        ChatAPI.Destroy;
      end;
    end;
    if frAI1.rbCG.IsChecked then // ChatGPT
    begin
      ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
        frAI1.edtGPTPrefix.text);
      try
        try
          GPTRequest := ChatAPI.GetGPTRequest
            ('https://api.openai.com/v1/chat/completions',
            'gpt-3.5-turbo', mystr);
        except
          on E: Exception do
            toLog('uGeneral', 'ResponsParserAIGen.openai', E.Message, 2);
        end;
      finally
        ChatAPI.Destroy;
      end;
    end;
    if frAI1.RBCustom.IsChecked then // Custom
    begin
      ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
        frAI1.edtGPTPrefix.text);
      try
        try
          GPTRequest := ChatAPI.GetGPTRequest(frAI1.edtAIP2.text,
            frAI1.edtAIP3.text, mystr);
        except
          on E: Exception do
            toLog('uGeneral', 'ResponsParserAIGen.custom', E.Message, 2);
        end;
      finally
        ChatAPI.Destroy;
      end;
    end;

    GPTRequest := StringReplace(GPTRequest, 'nn', ' ', [rfReplaceAll]);
    GPTRequest := StringReplace(GPTRequest, 'nn', ' ', [rfReplaceAll]);
    if GPTRequest = '' then
      GPTRequest := 'ой, кажется нейронка пока не доступна';

  end;
  res := StringReplace(res, '<|' + ss + '<|', GPTRequest, [rfReplaceAll]);
  Result := res;
end;

function TTTW_Bot.ResponsParserAIPic(inMess, aCommandText,
  aNick: string): string;
var
  res: string;
  I: Integer;
begin
  res := inMess;
  if ContainsText(res, '[Kandinsky]') then
  begin
    for I := 0 to frOBS1.KandinskyWebServers.Count - 1 do
      frOBS1.KandinskyWebServers[I].WebServerKandinsky.generate
        (aCommandText, aNick);
    res := StringReplace(res, '[Kandinsky]', '', [rfReplaceAll]);
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserAPI(inMess, adName: string): string;
var
  res, follow, age, ID: string;
  RowIndex: Integer;
  u: TUser;
  function GetPeriodEnding(n, r: Integer): string;
  var
    res: array [0 .. 3, 0 .. 2] of string;
  begin
    res[0, 0] := 'год';
    res[0, 1] := 'года';
    res[0, 2] := 'лет';

    res[1, 0] := 'месяц';
    res[1, 1] := 'месяца';
    res[1, 2] := 'месяцев';

    res[2, 0] := 'день';
    res[2, 1] := 'дня';
    res[2, 2] := 'дней';

    res[3, 0] := 'раз';
    res[3, 1] := 'раза';
    res[3, 2] := 'раз';

    if (n mod 10 = 1) and (n mod 100 <> 11) then
      Result := res[r, 0]
    else if (n mod 10 >= 2) and (n mod 10 <= 4) and
      ((n mod 100 < 10) or (n mod 100 >= 20)) then
      Result := res[r, 1]
    else
      Result := res[r, 2];
  end;
  function GetDateDifference(const inputDate: string): string;
  var
    currentDate, targetDate: TDateTime;
    years, months, days: Integer;
  begin
    try
      targetDate := StrToDate(inputDate);
      currentDate := Now;

      years := YearsBetween(currentDate, targetDate);
      targetDate := IncYear(targetDate, years);

      months := MonthsBetween(currentDate, targetDate);
      targetDate := IncMonth(targetDate, months);

      days := DaysBetween(currentDate, targetDate);

      Result := inttostr(years) + ' ' + GetPeriodEnding(years, 0) + ' ' +
        inttostr(months) + ' ' + GetPeriodEnding(months, 1) + ' ' +
        inttostr(days) + ' ' + GetPeriodEnding(days, 2);
    except
      on E: Exception do
      begin
        toLog('uGeneral', 'ResponsParserAPI.GetDateDifference', E.Message, 2);
        Result := '';
      end;
    end;
  end;

begin
  res := inMess;
  RowIndex := FindUserRow(adName);
  u := userlist[RowIndex];
  ID := u.ID;
  if ContainsText(res, '[FOLLOW]') then
  begin
    if YearOf(u.follow_at) < 2000 then
      u.follow_at := ttw_API.getFollow(ID);
    follow := GetDateDifference(DateToStr(u.follow_at));
    res := StringReplace(res, '[FOLLOW]', follow, [rfReplaceAll]);
  end;
  if ContainsText(res, '[AGE]') then
  begin
    if YearOf(userlist[RowIndex].created_at) < 2000 then
      u := ttw_API.getUserbyLogin(u.login);
    age := GetDateDifference(DateToStr(u.created_at));
    res := StringReplace(res, '[AGE]', age, [rfReplaceAll]);
  end;
  userlist[RowIndex] := u;
  if ContainsText(res, '[STAT]') then
  begin
    var
      avg_viewers: Integer;
    var
      max_viewers: Integer;
    var
      hours_watched: Integer;
    var
      followers: Integer;
    var
      followers_total: Integer;
    ttw_API.getTTWStat(db.FChannel, avg_viewers, max_viewers, hours_watched,
      followers, followers_total);
    var
      resultStat: string;
    resultStat := 'Статистика канала за месяц: Средний онлайн: ' +
      inttostr(avg_viewers) + '; Максимальный онлайн: ' + inttostr(max_viewers)
      + '; Часов просмотра: ' + inttostr(hours_watched) +
      '; Подписчиков за месяц: ' + inttostr(followers) + '; Всего подписчиков: '
      + inttostr(followers_total);
    res := StringReplace(res, '[STAT]', resultStat, [rfReplaceAll]);
  end;
  Result := res;
end;

function TTTW_Bot.ResponsParserGPT(inMess, aCommandText: string): string;
var
  res, GPTRequest: string;
  GigaChat: TGigaChat;
  ChatAPI: TChatAPI;
begin
  res := inMess;

  if ContainsText(res, '[AI]') then
  begin

    begin
      if frAI1.rbGC.IsChecked then // GigaChat
      begin
        GigaChat := TGigaChat.Create(Self, frAI1.edtAIP1.text,
          frAI1.edtAIP2.text, frAI1.edtGPTPrefix.text);
        try
          try
            GPTRequest := GigaChat.GetGPTRequest
              ('https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
              'GigaChat', aCommandText);
          except
            on E: Exception do
              toLog('uGeneral', 'ResponsParserGPT.gigachat', E.Message, 2);
          end;
        finally
          GigaChat.Destroy;
        end;
      end;
      if frAI1.rbDS.IsChecked then // DeepSeek
      begin
        ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
          frAI1.edtGPTPrefix.text);
        try
          ChatAPI.OnError := gptError;
          try
            GPTRequest := ChatAPI.GetGPTRequest
              ('https://api.deepseek.com/chat/completions', 'deepseek-chat',
              aCommandText);
          except
            on E: Exception do
              toLog('uGeneral', 'ResponsParserGPT.deepseek', E.Message, 2);
          end;
        finally
          ChatAPI.Destroy;
        end;
      end;
      if frAI1.rbCG.IsChecked then // ChatGPT
      begin
        ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
          frAI1.edtGPTPrefix.text);
        try
          try
            GPTRequest := ChatAPI.GetGPTRequest
              ('https://api.openai.com/v1/chat/completions', 'gpt-3.5-turbo',
              aCommandText);
          except
            on E: Exception do
              toLog('uGeneral', 'ResponsParserGPT.openai', E.Message, 2);
          end;
        finally
          ChatAPI.Destroy;
        end;
      end;
      if frAI1.RBCustom.IsChecked then // Custom
      begin
        ChatAPI := TChatAPI.Create(Self, frAI1.edtAIP1.text,
          frAI1.edtGPTPrefix.text);
        try
          try
            GPTRequest := ChatAPI.GetGPTRequest(frAI1.edtAIP2.text,
              frAI1.edtAIP3.text, aCommandText, frAI1.cbOllama.IsChecked);
          except
            on E: Exception do
              toLog('uGeneral', 'ResponsParserGPT.custom', E.Message, 2);
          end;
        finally
          ChatAPI.Destroy;
        end;
      end;

      GPTRequest := StringReplace(GPTRequest, 'nn', ' ', [rfReplaceAll]);
      GPTRequest := StringReplace(GPTRequest, 'nn', ' ', [rfReplaceAll]);
      if GPTRequest = '' then
        GPTRequest := 'ой, кажется нейронка пока не доступна';
    end;
    res := StringReplace(res, '[AI]', GPTRequest, [rfReplaceAll]);
  end;
  Result := res;
end;

end.
