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; LKeyValue: TArray; 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.