﻿unit uTTWIRC;

interface

uses
  System.Classes, System.SysUtils, IdIRC, IdSSLOpenSSL, IdContext,
  FMX.Forms, IdGlobal, IdComponent, System.StrUtils, uRecords;



type
  TNotifyEvent = procedure(s: string) of object;
  TJoinEvent = procedure(aNick: string) of object;
  TMyStatusEvent = procedure(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string) of object;
  tOnMessageRecord = procedure(aRecord: TTwitchChatMessage) of object;
  TOnLog = procedure(aModul: string; aMethod: string; aMessage: string; aLevel: integer) of object;

  TTTW = class
  private
    ws: TIdIRC;
    ssl: TIdSSLIOHandlerSocketOpenSSL;
    FOnLog: TOnLog;
    FOnStatus: TMyStatusEvent;
    FOnDisConnect: TNotifyEvent;
    FOnJoin: TJoinEvent;
    FOnMessage: TNotifyEvent;
    FOnMessageRecord: tOnMessageRecord;
    channel_name: string;
    room_id: string;
    channel_id: string;
    procedure wsConnected(Sender: TObject);
    procedure wsStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string);
    procedure wsDisconnected(Sender: TObject);
    procedure wsDataIn(ASender: TIdContext; AIn: boolean; const AMessage: string);
    procedure Join(ASender: TIdContext; const ANickname, AHost, AChannel: string);
    procedure se(ASender: TIdContext; AErrorCode: Integer; const AErrorMessage: String);
    procedure RAW(text: string);
    procedure toLog(aLevel: integer; aMethod: string; aMessage: string);
    procedure toParse(t: string);
 public
    constructor Create(Sender: TObject);
    destructor Destroy; override;
    procedure Init(a_oauth, a_channel, a_username: string);
    procedure Connect;
    procedure Disconnect;
    procedure sendMessage(text: string);
    function ParseTwitchChatMessage(const AMessage: string): TTwitchChatMessage;
    function GetRoom_ID: string;
    function Pars(T_, text, _T: string): string;
    property OnLog: TOnLog read FOnLog write FOnLog;
    property OnStatus: TMyStatusEvent read FOnStatus write FOnStatus;
    property OnDisConnect: TNotifyEvent read FOnDisConnect write FOnDisConnect;
    property OnJoin: TJoinEvent read FOnJoin write FOnJoin;
    property OnMessage: TNotifyEvent read FOnMessage write FOnMessage;
    property OnMessageRecord: tOnMessageRecord read FOnMessageRecord write FOnMessageRecord;
  end;

implementation

uses uGeneral; // ��� ������� � ��������� Log

const
  LOG_INFO = 0;
  LOG_WARNING = 1;
  LOG_ERROR = 2;
  LOG_DEBUG = 3;

procedure TTTW.toLog(aLevel: integer; aMethod: string; aMessage: string);
begin
  if aLevel < 0 then
    aLevel := LOG_INFO
  else if aLevel > LOG_DEBUG then
    aLevel := LOG_DEBUG;

  if Assigned(FOnLog) then
    FOnLog('uTTWIRC', aMethod, aMessage, aLevel);
end;

constructor TTTW.Create(Sender: TObject);
begin
  try
    ws := TIdIRC.Create;
    ssl := TIdSSLIOHandlerSocketOpenSSL.Create;
    ws.IOHandler := ssl;
    ws.OnConnected := wsConnected;
    ws.OnDisconnected := wsDisconnected;
    ws.OnStatus := wsStatus;
    ws.OnRaw := wsDataIn;
    ws.OnJoin := Join;
    ws.OnServerError := se;
  except
    on E: Exception do
      toLog(LOG_ERROR, 'Create', E.Message);
  end;
end;

destructor TTTW.Destroy;
begin
  try
    if Assigned(ws) then
    begin
      ws.OnConnected := nil;
      ws.OnDisconnected := nil;
      ws.OnStatus := nil;
      ws.OnRaw := nil;
      ws.OnJoin := nil;
      ws.OnServerError := nil;
      ws.IOHandler := nil;
      ws.Free;
    end;
    if Assigned(ssl) then
      ssl.Free;
  except
    on E: Exception do
      ;
  end;
  inherited;
end;

function TTTW.ParseTwitchChatMessage(const AMessage: string): TTwitchChatMessage;
var
  s: string;
  LSpacePos: Integer;
  LParamStr, LRestStr: string;
  LParams: TArray<string>;
  I: Integer;
  LKeyValue: TArray<string>;
  LUsernamePart: string;
  LMessagePos: Integer;
begin
  Result := Default(TTwitchChatMessage);
  s := AMessage;

  // �������� ����������� ��������
  LSpacePos := Pos(' ', s);
  if LSpacePos = 0 then
    Exit;

  LParamStr := Copy(s, 1, LSpacePos - 1);
  LRestStr := Copy(s, LSpacePos + 1, Length(s) - LSpacePos);

  // ������������ ���������
  LParams := LParamStr.Split([';']);
  for I := 0 to High(LParams) do
  begin
    LKeyValue := LParams[I].Split(['=']);
    if Length(LKeyValue) = 2 then
    begin
      case AnsiIndexStr(LKeyValue[0], [
        '@badge-info', 'badges', 'client-nonce', 'color', 'display-name', 'emotes',
        'first-msg', 'id', 'mod', 'returning-chatter', 'room-id', 'subscriber',
        'tmi-sent-ts', 'turbo', 'user-id', 'user-type', 'vip'
      ]) of
        0: Result.BadgeInfo := LKeyValue[1];
        1: Result.Badges := LKeyValue[1];
        2: Result.ClientNonce := LKeyValue[1];
        3: Result.Color := LKeyValue[1];
        4: Result.DisplayName := LKeyValue[1];
        5: Result.Emotes := LKeyValue[1];
        6: Result.FirstMsg := StrToIntDef(LKeyValue[1], 0);
        7: Result.Id := LKeyValue[1];
        8: Result.Moder := StrToIntDef(LKeyValue[1], 0);
        9: Result.ReturningChatter := StrToIntDef(LKeyValue[1], 0);
        10: Result.RoomId := LKeyValue[1];
        11: Result.Subscriber := StrToIntDef(LKeyValue[1], 0);
        12: Result.TmiSentTs := StrToInt64Def(LKeyValue[1], 0);
        13: Result.Turbo := StrToIntDef(LKeyValue[1], 0);
        14: Result.UserId := LKeyValue[1];
        15: Result.UserType := LKeyValue[1];
        16: Result.Vip := StrToIntDef(LKeyValue[1], 0);
      end;
    end;
  end;

  if LRestStr.StartsWith(':') then
  begin
    LUsernamePart := Copy(LRestStr, 1, Pos('!', LRestStr) - 1);
    Result.Username := LUsernamePart.Substring(1);
  end
  else
    Result.Username := '';

  // ��������� �����
  LMessagePos := Pos('PRIVMSG #', LRestStr);
  if LMessagePos > 0 then
  begin
    Inc(LMessagePos, Length('PRIVMSG #'));
    Result.Channel := Copy(LRestStr, LMessagePos, PosEx(' ', LRestStr, LMessagePos) - LMessagePos);
  end
  else
    Result.Channel := '';

  // �������� �����
  LMessagePos := Pos(' :', LRestStr);
  if LMessagePos > 0 then
    Result.Message := Copy(LRestStr, LMessagePos + 2, Length(LRestStr) - LMessagePos - 1)
  else
    Result.Message := '';
end;

procedure TTTW.Init(a_oauth, a_channel, a_username : string);
begin
  try
    channel_name := a_channel;
    ws.Host := 'irc.chat.twitch.tv';
    ws.Port := 6697;
    ssl.SSLOptions.SSLVersions := [sslvSSLv23];
    ws.Password := 'oauth:' + a_oauth;
    ws.Nickname := a_username;
    channel_name := a_channel;
 //   Token := a_oauth;
  except
    on E: Exception do
      toLog(LOG_ERROR, 'Init', E.Message);
  end;
end;

procedure TTTW.Connect;
begin
  try
    if not ws.Connected then
    begin
      ws.Connect;
    ws.Raw('CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands');
    ws.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
    end;
  except
    on E: Exception do
      toLog(LOG_ERROR, 'Connect', E.Message);
  end;
end;

procedure TTTW.Disconnect;
begin
  try
    if ws.Connected then
    begin
      ws.Disconnect;
    end;
  except
    on E: Exception do
      toLog(LOG_ERROR,  'Disconnect', E.ClassName + ': ' + E.Message);
  end;
end;

function TTTW.GetRoom_ID: string;
begin
result:=room_id;
end;

procedure TTTW.sendMessage(text: string);
begin
  try
    ws.Say('#' + channel_name, text);
  except
    on E: Exception do
      toLog(LOG_ERROR, 'sendMessage', E.Message);
  end;
end;

procedure TTTW.RAW(text: string);
begin
  try
    ws.Raw(text);
  except
    on E: Exception do
      toLog(LOG_ERROR, 'RAW', E.Message);
  end;
end;

procedure TTTW.wsConnected(Sender: TObject);
begin
  if Assigned(FOnStatus) then
    FOnStatus(ws, TIdStatus.hsConnected, 'Connected to Twitch IRC');
  toLog(LOG_INFO, 'wsConnected', 'Connected to Twitch IRC');
end;

procedure TTTW.wsStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string);
begin
  if Assigned(FOnStatus) then
    FOnStatus(ASender, AStatus, AStatusText);
end;

procedure TTTW.wsDisconnected(Sender: TObject);
begin
  if Assigned(FOnDisConnect) then
    FOnDisConnect('Disconnected');
  toLog(LOG_WARNING, 'wsDisconnected', 'Disconnected from Twitch IRC');
end;

procedure TTTW.wsDataIn(ASender: TIdContext; AIn: boolean; const AMessage: string);
begin

  toLog(LOG_DEBUG, 'wsDataIn', AMessage);

  if Pos('CAP * ACK', AMessage) <> 0 then
  begin
    Sleep(200);
    ws.Raw('JOIN #' + channel_name);
  end;

   toParse(AMessage);
end;

procedure TTTW.toParse(t: string);
var
 LTwitchChatMessage:tTwitchChatMessage;
begin
  try
    if (Pos('room-id=', t) <> 0) and (Pos('ROOMSTATE', t) <> 0) then
      room_id := Pars('room-id=', t, ';');

    if Pos('NOTICE * :Login authentication failed', t) <> 0 then
    begin

      toLog(2, 'toParse', 'Токен бота просрочен');
      Disconnect;
      Exit;
    end;

    if Pos('PRIVMSG', t) <> 0 then
    begin
    LTwitchChatMessage := ParseTwitchChatMessage(t);
      if Assigned(FOnMessageRecord) then
        FOnMessageRecord(LTwitchChatMessage);
    end;
  except
    on E: Exception do
     toLog(2, 'toParse', E.Message);
  end;
end;

procedure TTTW.Join(ASender: TIdContext; const ANickname, AHost, AChannel: string);
begin
  if Assigned(FOnJoin) then
    FOnJoin(ANickname);
  toLog(LOG_INFO, 'Join', ANickname + ' joined ' + AChannel);
end;

procedure TTTW.se(ASender: TIdContext; AErrorCode: Integer; const AErrorMessage: String);
begin
  toLog(LOG_ERROR, 'se', AErrorMessage);
end;

function TTTW.Pars(T_, text, _T: string): string;
var
  a, b: Integer;
begin
  Result := '';
  if (T_ = '') or (text = '') or (_T = '') then
    Exit;
  a := Pos(T_, text);
  if a = 0 then
    Exit
  else
    a := a + Length(T_);
  text := Copy(text, a, Length(text) - a + 1);
  b := Pos(_T, text);
  if b > 0 then
    Result := Copy(text, 1, b - 1);
end;

end.

