365 lines
9.7 KiB
ObjectPascal
365 lines
9.7 KiB
ObjectPascal
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.
|
|
|