365 lines
9.7 KiB
Plaintext
365 lines
9.7 KiB
Plaintext
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; // <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 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;
|
||
|
||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||
LSpacePos := Pos(' ', s);
|
||
if LSpacePos = 0 then
|
||
Exit;
|
||
|
||
LParamStr := Copy(s, 1, LSpacePos - 1);
|
||
LRestStr := Copy(s, LSpacePos + 1, Length(s) - LSpacePos);
|
||
|
||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||
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 := '';
|
||
|
||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
|
||
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 := '';
|
||
|
||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
|
||
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.
|
||
|