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;

type
  TTTW = class(TObject)
  private
    ws: TIdIRC;
    ssl: TIdSSLIOHandlerSocketOpenSSL;
    channel_name: string;
    Token: string;

    FOnConnect: TNotifyEvent;
    FOnDisConnect: TNotifyEvent;

    FOnJoin: TJoinEvent;
    FOnStatus: TMyStatusEvent;
    FOnMessageRecord:tOnMessageRecord;
    FOnLog: TOnLog;
    room_id: string;

    procedure wsConnected(Sender: TObject);
    procedure wsDisconnected(Sender: TObject);
    procedure wsDataIn(ASender: TIdContext; AIn: boolean; const AMessage: string);
    procedure toParse(t: string);
    procedure Join(ASender: TIdContext; const ANickname, AHost, AChannel: string);
    procedure se(ASender: TIdContext; AErrorCode: Integer; const AErrorMessage: String);
    function Pars(T_, text, _T: string): string;
    function ParseTwitchChatMessage(const AMessage: string): TTwitchChatMessage;
    procedure toLog(aLevel: integer; aMethod: string; aMessage: string);
  public
    constructor Create(Sender: TObject);
    destructor Destroy; override;
    procedure Init(myToken, Channel, Bot_Name: string);
    procedure sendMessage(text: string);
    procedure RAW(text: string);
    procedure Connect;
    procedure Disconnect;

    function GetRoom_ID:string;
    property FRoom_ID:string read GetRoom_ID;
    property OnConnect: TNotifyEvent read FOnConnect write FOnConnect;
    property OnDisConnect: TNotifyEvent read FOnDisConnect write FOnDisConnect;

    property OnMessageRecord: tOnMessageRecord read FOnMessageRecord write FOnMessageRecord;
    property OnLog: TOnLog read FOnLog write FOnLog;
    property OnJoin: TJoinEvent read FOnJoin write FOnJoin;
    property OnStatus: TMyStatusEvent read FOnStatus write FOnStatus;
  end;




implementation

uses uGeneral; //     Log

procedure TTTW.toLog(aLevel: integer; aMethod: string; aMessage: string);
begin
  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:= FOnStatus;
    ws.OnRaw := wsDataIn;
    ws.OnJoin := Join;
    ws.OnServerError := se;
  except
    on E: Exception do
      toLog(2, 'Create', E.Message);
  end;
end;

destructor TTTW.Destroy;
begin
  ws.Free;
  inherited;
end;

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

  //       
  LSpacePos := Pos(' ', s);
  if LSpacePos > 0 then
  begin
    LParamStr := Copy(s, 1, LSpacePos - 1);
    LRestStr := Copy(s, LSpacePos + 1, Length(s) - LSpacePos);
  end
  else
  begin
    LParamStr := s;
    LRestStr := '';
  end;

  //  
  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, MaxInt).Trim
  else
    Result.Message := '';
end;

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

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

procedure TTTW.Disconnect;
begin
  try
    if ws.Connected then //   
    begin
      ws.Disconnect;
    end;
  except
    on E: Exception do
      toLog(2,  '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(2, 'sendMessage', E.Message);
  end;
end;

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

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

procedure TTTW.wsDisconnected(Sender: TObject);
begin
 if Assigned(FOnStatus) then
   FOnStatus(ws, TIdStatus.hsDisconnected, 'Disconnected.');
  if Assigned(FOnDisConnect) then
    FOnDisConnect('Disconnected');
  toLog(1, 'wsDisconnected', 'Disconnected from Twitch IRC');
end;

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

  toLog(3, '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(0, 'Join', ANickname + ' joined ' + AChannel);
end;

procedure TTTW.se(ASender: TIdContext; AErrorCode: Integer; const AErrorMessage: String);
begin
  toLog(2, '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.
