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