unit uTTWEventSub; interface uses System.SysUtils, System.JSON, System.Types, System.UITypes, System.Classes, Winapi.WinInet, System.Win.ComObj, IdException, ipwcore, ipwtypes, ipwwsclient, ipwping, idhttp, IdSSLOpenSSL, uRecords, fmx.Types, System.Net.HttpClient, System.Net.HttpClientComponent; type TNotifyEvent = procedure(s: string) of object; TGetCustomRewardEvent = procedure(s: TCustomRewardEvent) of object; TGetFollowEvent = procedure(s: TFollowEvent) of object; TGetGiftEvent = procedure(s: TGiftEvent) of object; TGetSubEvent = procedure(s: TSubEvent) of object; TGetRaidEvent = procedure(s: TRaidEvent) of object; TOnLog = procedure(aModul: string; aMethod: string; aMessage: string; aLevel: integer) of object; TOnStatus = procedure(Sender: TObject; const ConnectionEvent: String; StatusCode: integer; const Description: String) of Object; type TTTW_ES = class(TObject) FTimer: ttimer; wss: TipwWSClient; private BroadcasterID: string; FAccessToken: string; FClientID: string; FOnError: TNotifyEvent; FOnMessage: TNotifyEvent; FOnSubOk: TNotifyEvent; FOnRAW: TNotifyEvent; FOnGetCustomReward: TGetCustomRewardEvent; FOnFollow: TGetFollowEvent; FOnGift: TGetGiftEvent; FOnSub: TGetSubEvent; FOnLog: TOnLog; FOnRaid: TGetRaidEvent; FOnStatus: TOnStatus; SW: TWelcomMessage; procedure HandleTimer(Sender: TObject); procedure ipwWSClient1DataIn(Sender: TObject; DataFormat: integer; const Text: string; const TextB: TBytes; EOM, EOL: Boolean); procedure ipwWSPing(Sender: TObject; const Payload: String; const PayloadB: TBytes; Response: Boolean); procedure ipwWSClient1ConnectionStatus(Sender: TObject; const ConnectionEvent: String; StatusCode: integer; const Description: String); procedure ipwWSClientError(Sender: TObject; ErrorCode: integer; const Description: string); procedure ipwWSClientDisconnected(Sender: TObject; StatusCode: integer; const Description: String); procedure ipwWSClientHeader(Sender: TObject; const Field: String; const Value: String); procedure ipwWSClientLog(Sender: TObject; LogLevel: integer; const aMessage, aLog: string); function subscribeTo(const EventType, Version: string; const Condition: string): Boolean; procedure subscribe(); // function ParseRewardRedeemed(const AJsonString: string): TRewardRedeemed; procedure EventMSG(const AText: string); function ParseWelcomMessage(const JSONString: string): TWelcomMessage; function ParseCustomRewardEvent(const JSONString: string) : TCustomRewardEvent; function ParseFollowEvent(const JSONString: string): TFollowEvent; function ParseSubEvent(const JSONString: string): TSubEvent; function ParseGiftEvent(const JSONString: string): TGiftEvent; function ParseRaidEvent(const JSONString: string): TRaidEvent; procedure toLog(aLevel: integer; aMethod: string; aMessage: string); function ParseMetadata(const JSONString: string): TMetadata; public constructor Create(Sender: TObject; aTokenWS, aClientID, aBroadcasterID: string); destructor Destroy; override; procedure Connect(); procedure Disconnect; property OnMessage: TNotifyEvent read FOnMessage write FOnMessage; property OnError: TNotifyEvent read FOnError write FOnError; property OnSubOk: TNotifyEvent read FOnSubOk write FOnSubOk; property OnRAW: TNotifyEvent read FOnRAW write FOnRAW; property OnGetCustomReward: TGetCustomRewardEvent read FOnGetCustomReward write FOnGetCustomReward; property OnStatus: TOnStatus read FOnStatus write FOnStatus; property OnFollow: TGetFollowEvent read FOnFollow write FOnFollow; property OnSub: TGetSubEvent read FOnSub write FOnSub; property OnGift: TGetGiftEvent read FOnGift write FOnGift; property OnRaid: TGetRaidEvent read FOnRaid write FOnRaid; property OnLog: TOnLog read FOnLog write FOnLog; end; implementation uses ugeneral; function SafeGetObj(Parent: TJSONObject; const Name: string): TJSONObject; begin Result := Parent.GetValue(Name); if not Assigned(Result) then raise Exception.CreateFmt('JSON object "%s" not found', [Name]); end; function GetStrDef(Obj: TJSONObject; const Name: string; Default: string = ''): string; begin if not Obj.TryGetValue(Name, Result) then Result := Default; end; function SafeGetInt(Parent: TJSONObject; const Name: string): integer; var V: TJSONValue; begin V := Parent.GetValue(Name); if Assigned(V) then Result := StrToIntDef(V.Value, 0) else Result := 0; end; function SafeGetBool(Parent: TJSONObject; const Name: string): Boolean; var V: TJSONValue; begin V := Parent.GetValue(Name); if Assigned(V) then Result := SameText(V.Value, 'true') else Result := False; end; procedure TTTW_ES.toLog(aLevel: integer; aMethod: string; aMessage: string); begin if Assigned(FOnLog) then FOnLog('uTTWEvenSub', aMethod, aMessage, aLevel); end; procedure TTTW_ES.Connect; begin if wss.Connected then wss.Disconnect; try wss.ConnectTo ('wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=60'); toLog(0, 'Connect', 'Подключение к WebSocket выполнено'); FTimer.Enabled := True; except on E: Exception do toLog(2, 'Connect', E.Message); end; end; constructor TTTW_ES.Create(Sender: TObject; aTokenWS, aClientID, aBroadcasterID: string); begin FAccessToken := aTokenWS; FClientID := aClientID; BroadcasterID := aBroadcasterID; wss := TipwWSClient.Create(nil); wss.Timeout := 30; wss.OnPing := ipwWSPing; wss.OnDataIn := ipwWSClient1DataIn; wss.OnConnectionStatus := ipwWSClient1ConnectionStatus; wss.OnError := ipwWSClientError; wss.OnLog := ipwWSClientLog; wss.OnDisconnected := ipwWSClientDisconnected; wss.OnHeader := ipwWSClientHeader; FTimer := ttimer.Create(nil); FTimer.Interval := 9000; FTimer.OnTimer := HandleTimer; FTimer.Enabled := False; toLog(0, 'Create', 'Инициализация EventSub'); end; destructor TTTW_ES.Destroy; begin toLog(0, 'Destroy', 'Завершение работы EventSub'); try if Assigned(FTimer) then FreeAndNil(FTimer); if Assigned(wss) then begin if wss.Connected then Disconnect; FreeAndNil(wss); end; finally inherited Destroy; end; end; procedure TTTW_ES.Disconnect; begin toLog(1, 'Disconnect', 'Отключение от WebSocket'); try if wss.Connected then wss.Disconnect; FTimer.Enabled := False; except on E: Exception do toLog(2, 'Disconnect', E.ClassName + ': ' + E.Message); end; end; procedure TTTW_ES.EventMSG(const AText: string); var md: TMetadata; begin TThread.Queue(nil, procedure begin if Assigned(FOnRAW) then FOnRAW(AText); md := ParseMetadata(AText); toLog(0, 'EventMSG', 'Тип сообщения: ' + md.message_type + ', Тип подписки: ' + md.subscription_type); if md.message_type = 'session_welcome' then begin toLog(0, 'EventMSG', 'Получен session_welcome'); SW := ParseWelcomMessage(AText); if Assigned(FOnMessage) then FOnMessage('Welcome message'); subscribe; end else if md.message_type = 'notification' then begin if Assigned(FOnGetCustomReward) and (md.subscription_type = 'channel.channel_points_custom_reward_redemption.add') then FOnGetCustomReward(ParseCustomRewardEvent(AText)); if Assigned(FOnFollow) and (md.subscription_type = 'channel.follow') then FOnFollow(ParseFollowEvent(AText)); if Assigned(FOnSub) and (md.subscription_type = 'channel.subscribe') then FOnSub(ParseSubEvent(AText)); if Assigned(FOnGift) and (md.subscription_type = 'channel.subscription.gift') then FOnGift(ParseGiftEvent(AText)); if Assigned(FOnRaid) and (md.subscription_type = 'channel.raid') then FOnRaid(ParseRaidEvent(AText)); end else if md.message_type = 'session_keepalive' then toLog(3, 'EventMSG', 'Получен keepalive'); end); end; procedure TTTW_ES.HandleTimer(Sender: TObject); begin if wss.Connected then begin toLog(3, 'HandleTimer', 'Отправка ping'); wss.Ping; end; end; procedure TTTW_ES.ipwWSClient1ConnectionStatus(Sender: TObject; const ConnectionEvent: String; StatusCode: integer; const Description: String); begin toLog(0, 'ConnectionStatus', Format('%s | %d | %s', [ConnectionEvent, StatusCode, Description])); if Assigned(FOnStatus) then FOnStatus(Sender, ConnectionEvent, StatusCode, Description); end; procedure TTTW_ES.ipwWSClient1DataIn(Sender: TObject; DataFormat: integer; const Text: string; const TextB: TBytes; EOM, EOL: Boolean); begin toLog(3, 'ipwWSClient1DataIn', Text); EventMSG(Text); end; procedure TTTW_ES.ipwWSClientDisconnected(Sender: TObject; StatusCode: integer; const Description: String); begin toLog(1, 'ipwWSClientDisconnected', Description); end; procedure TTTW_ES.ipwWSClientError(Sender: TObject; ErrorCode: integer; const Description: string); begin toLog(2, 'ipwWSClientError', Format('Код: %d | %s', [ErrorCode, Description])); if Assigned(FOnError) then FOnError(Description); end; procedure TTTW_ES.ipwWSClientHeader(Sender: TObject; const Field, Value: String); begin // toLog(3, 'ipwWSClientHeader', // 'Field: ' + Field + ' | Value: ' + Value); end; procedure TTTW_ES.ipwWSClientLog(Sender: TObject; LogLevel: integer; const aMessage, aLog: string); begin // toLog(3, 'ipwWSClientLog', 'Level: ' + IntToStr(LogLevel) // + ' | ' + aMessage + ' | ' + aLog); // form1.log(1, 'ipwWSClientLog', 'Level: ' + inttostr(LogLevel) + ' Message: ' + // aMessage + ' Log: ' + aLog); end; procedure TTTW_ES.ipwWSPing(Sender: TObject; const Payload: String; const PayloadB: TBytes; Response: Boolean); begin toLog(3, 'ipwWSPing', 'PING ' + Payload); end; function TTTW_ES.ParseMetadata(const JSONString: string): TMetadata; var Root, Metadata: TJSONObject; begin Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Metadata := SafeGetObj(Root, 'metadata'); Result.message_id := GetStrDef(Metadata, 'message_id'); Result.message_type := GetStrDef(Metadata, 'message_type'); Result.message_timestamp := GetStrDef(Metadata, 'message_timestamp'); Result.subscription_type := GetStrDef(Metadata, 'subscription_type'); finally Root.Free; end; end; function TTTW_ES.ParseWelcomMessage(const JSONString: string): TWelcomMessage; var Root, Payload, Session: TJSONObject; begin Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Session := SafeGetObj(Payload, 'session'); Result.Payload.Session.id := GetStrDef(Session, 'id'); Result.Payload.Session.status := GetStrDef(Session, 'status'); Result.Payload.Session.connected_at := GetStrDef(Session, 'connected_at'); Result.Payload.Session.keepalive_timeout_seconds := SafeGetInt(Session, 'keepalive_timeout_seconds'); Result.Payload.Session.reconnect_url := GetStrDef(Session, 'reconnect_url'); finally Root.Free; end; end; function TTTW_ES.ParseCustomRewardEvent(const JSONString: string) : TCustomRewardEvent; var Root, Payload, Subscription, mCondition, mTransport, Event, mReward: TJSONObject; begin toLog(3, 'ParseCustomRewardEvent', 'Начало парсинга награды'); Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Subscription := SafeGetObj(Payload, 'subscription'); with Result.Subscription do begin id := GetStrDef(Subscription, 'id'); subscription_type := GetStrDef(Subscription, 'type'); Version := GetStrDef(Subscription, 'version'); status := GetStrDef(Subscription, 'status'); cost := SafeGetInt(Subscription, 'cost'); created_at := GetStrDef(Subscription, 'created_at'); mCondition := SafeGetObj(Subscription, 'condition'); Condition.broadcaster_user_id := GetStrDef(mCondition, 'broadcaster_user_id'); Condition.reward_id := GetStrDef(mCondition, 'reward_id'); mTransport := SafeGetObj(Subscription, 'transport'); transport.method := GetStrDef(mTransport, 'method'); end; Event := SafeGetObj(Payload, 'event'); with Result.Event do begin id := GetStrDef(Event, 'id'); broadcaster_user_id := GetStrDef(Event, 'broadcaster_user_id'); broadcaster_user_login := GetStrDef(Event, 'broadcaster_user_login'); broadcaster_user_name := GetStrDef(Event, 'broadcaster_user_name'); user_id := GetStrDef(Event, 'user_id'); user_login := GetStrDef(Event, 'user_login'); user_name := GetStrDef(Event, 'user_name'); user_input := GetStrDef(Event, 'user_input'); mReward := SafeGetObj(Event, 'reward'); revard.id := GetStrDef(mReward, 'id'); revard.title := GetStrDef(mReward, 'title'); revard.cost := SafeGetInt(mReward, 'cost'); revard.prompt := GetStrDef(mReward, 'prompt'); end; finally Root.Free; end; end; function TTTW_ES.ParseFollowEvent(const JSONString: string): TFollowEvent; var Root, Payload, Subscription, mCondition, mTransport, Event: TJSONObject; begin toLog(3, 'ParseFollowEvent', 'Парсинг подписки'); Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Subscription := SafeGetObj(Payload, 'subscription'); with Result.Subscription do begin id := GetStrDef(Subscription, 'id'); subscription_type := GetStrDef(Subscription, 'type'); Version := GetStrDef(Subscription, 'version'); status := GetStrDef(Subscription, 'status'); cost := SafeGetInt(Subscription, 'cost'); created_at := GetStrDef(Subscription, 'created_at'); mCondition := SafeGetObj(Subscription, 'condition'); Condition.broadcaster_user_id := GetStrDef(mCondition, 'broadcaster_user_id'); mTransport := SafeGetObj(Subscription, 'transport'); transport.method := GetStrDef(mTransport, 'method'); end; Event := SafeGetObj(Payload, 'event'); with Result.Event do begin broadcaster_user_id := GetStrDef(Event, 'broadcaster_user_id'); broadcaster_user_login := GetStrDef(Event, 'broadcaster_user_login'); broadcaster_user_name := GetStrDef(Event, 'broadcaster_user_name'); user_id := GetStrDef(Event, 'user_id'); user_login := GetStrDef(Event, 'user_login'); user_name := GetStrDef(Event, 'user_name'); followed_at := GetStrDef(Event, 'followed_at'); end; finally Root.Free; end; end; function TTTW_ES.ParseGiftEvent(const JSONString: string): TGiftEvent; var Root, Payload, Subscription, mCondition, mTransport, Event: TJSONObject; begin toLog(3, 'ParseGiftEvent', 'Парсинг подарочной подписки'); Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Subscription := SafeGetObj(Payload, 'subscription'); with Result.Subscription do begin id := GetStrDef(Subscription, 'id'); subscription_type := GetStrDef(Subscription, 'type'); Version := GetStrDef(Subscription, 'version'); status := GetStrDef(Subscription, 'status'); cost := SafeGetInt(Subscription, 'cost'); created_at := GetStrDef(Subscription, 'created_at'); mCondition := SafeGetObj(Subscription, 'condition'); Condition.broadcaster_user_id := GetStrDef(mCondition, 'broadcaster_user_id'); mTransport := SafeGetObj(Subscription, 'transport'); transport.method := GetStrDef(mTransport, 'method'); end; Event := SafeGetObj(Payload, 'event'); with Result.Event do begin broadcaster_user_id := GetStrDef(Event, 'broadcaster_user_id'); broadcaster_user_login := GetStrDef(Event, 'broadcaster_user_login'); broadcaster_user_name := GetStrDef(Event, 'broadcaster_user_name'); user_id := GetStrDef(Event, 'user_id'); user_login := GetStrDef(Event, 'user_login'); user_name := GetStrDef(Event, 'user_name'); total := SafeGetInt(Event, 'total'); tier := GetStrDef(Event, 'tier'); cumulative_total := SafeGetInt(Event, 'cumulative_total'); is_anonymous := SafeGetBool(Event, 'anonymous'); end; finally Root.Free; end; end; function TTTW_ES.ParseRaidEvent(const JSONString: string): TRaidEvent; var Root, Payload, Subscription, mCondition, mTransport, Event: TJSONObject; begin toLog(3, 'ParseRaidEvent', 'Парсинг рейда'); Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Subscription := SafeGetObj(Payload, 'subscription'); with Result.Subscription do begin id := GetStrDef(Subscription, 'id'); subscription_type := GetStrDef(Subscription, 'type'); Version := GetStrDef(Subscription, 'version'); status := GetStrDef(Subscription, 'status'); cost := SafeGetInt(Subscription, 'cost'); created_at := GetStrDef(Subscription, 'created_at'); mCondition := SafeGetObj(Subscription, 'condition'); Condition.broadcaster_user_id := GetStrDef(mCondition, 'to_broadcaster_user_id'); mTransport := SafeGetObj(Subscription, 'transport'); transport.method := GetStrDef(mTransport, 'method'); end; Event := SafeGetObj(Payload, 'event'); with Result.Event do begin from_broadcaster_user_id := GetStrDef(Event, 'from_broadcaster_user_id'); from_broadcaster_user_login := GetStrDef(Event, 'from_broadcaster_user_login'); from_broadcaster_user_name := GetStrDef(Event, 'from_broadcaster_user_name'); to_broadcaster_user_id := GetStrDef(Event, 'to_broadcaster_user_id'); to_broadcaster_user_login := GetStrDef(Event, 'to_broadcaster_user_login'); to_broadcaster_user_name := GetStrDef(Event, 'to_broadcaster_user_name'); viewers := SafeGetInt(Event, 'viewers'); end; finally Root.Free; end; end; function TTTW_ES.ParseSubEvent(const JSONString: string): TSubEvent; var Root, Payload, Subscription, mCondition, mTransport, Event: TJSONObject; begin toLog(3, 'ParseSubEvent', 'Парсинг подписки'); Root := TJSONObject.ParseJSONValue(JSONString) as TJSONObject; if not Assigned(Root) then raise Exception.Create('Invalid JSON'); try Payload := SafeGetObj(Root, 'payload'); Subscription := SafeGetObj(Payload, 'subscription'); with Result.Subscription do begin id := GetStrDef(Subscription, 'id'); subscription_type := GetStrDef(Subscription, 'type'); Version := GetStrDef(Subscription, 'version'); status := GetStrDef(Subscription, 'status'); cost := SafeGetInt(Subscription, 'cost'); created_at := GetStrDef(Subscription, 'created_at'); mCondition := SafeGetObj(Subscription, 'condition'); Condition.broadcaster_user_id := GetStrDef(mCondition, 'broadcaster_user_id'); mTransport := SafeGetObj(Subscription, 'transport'); transport.method := GetStrDef(mTransport, 'method'); end; Event := SafeGetObj(Payload, 'event'); with Result.Event do begin broadcaster_user_id := GetStrDef(Event, 'broadcaster_user_id'); broadcaster_user_login := GetStrDef(Event, 'broadcaster_user_login'); broadcaster_user_name := GetStrDef(Event, 'broadcaster_user_name'); user_id := GetStrDef(Event, 'user_id'); user_login := GetStrDef(Event, 'user_login'); user_name := GetStrDef(Event, 'user_name'); tier := GetStrDef(Event, 'tier'); is_gift := SafeGetBool(Event, 'is_gift'); end; finally Root.Free; end; end; function TTTW_ES.subscribeTo(const EventType, Version: string; const Condition: string): Boolean; var JSON: TStringStream; Resp: string; HTTP: TNetHTTPClient; begin Result := False; toLog(0, 'subscribeTo', 'Подписка на ' + EventType); try HTTP := TNetHTTPClient.Create(nil); try HTTP.ContentType := 'application/json'; HTTP.CustomHeaders['Authorization'] := 'Bearer ' + FAccessToken; HTTP.CustomHeaders['Client-Id'] := FClientID; JSON := TStringStream.Create(TJSONObject.Create.AddPair('type', EventType) .AddPair('version', Version).AddPair('condition', TJSONObject.ParseJSONValue(Condition) as TJSONObject) .AddPair('transport', TJSONObject.Create.AddPair('method', 'websocket') .AddPair('session_id', SW.Payload.Session.id)).ToJSON, TEncoding.UTF8); try Resp := HTTP.Post('https://api.twitch.tv/helix/eventsub/subscriptions', JSON).ContentAsString(); toLog(3, 'subscribeTo', 'Ответ Twitch: ' + Resp); if Pos('"status":"enabled"', Resp) > 0 then begin toLog(0, 'subscribeTo', 'Подписка успешна'); Result := True; end else toLog(1, 'subscribeTo', 'Подписка не подтверждена: ' + Resp); finally JSON.Free; end; finally HTTP.Free; end; except on E: Exception do toLog(2, 'subscribeTo', 'Ошибка подписки: ' + E.Message); end; end; procedure TTTW_ES.subscribe; begin // channel.channel_points_custom_reward.add (1) // channel.follow (2) moderator:read:followers // channel.subscribe (1) channel:read:subscriptions // channel.subscription.gift (1) channel:read:subscriptions // channel.raid (1) if subscribeTo('channel.channel_points_custom_reward_redemption.add', '1', '{"broadcaster_user_id":"' + BroadcasterID + '"}') then toLog(0, 'subscribe', 'channel.channel_points_custom_reward_redemption.add OK') else toLog(2, 'subscribe', 'channel.channel_points_custom_reward_redemption.add'); if subscribeTo('channel.raid', '1', '{"to_broadcaster_user_id":"' + BroadcasterID + '"}') then toLog(0, 'subscribe', 'channel.raid OK') else toLog(2, 'subscribe', 'channel.raid'); if subscribeTo('channel.follow', '2', '{"broadcaster_user_id":"' + BroadcasterID + '","moderator_user_id":"' + BroadcasterID + '"}') then toLog(0, 'subscribe', 'channel.follow OK') else toLog(2, 'subscribe', 'channel.follow'); if subscribeTo('channel.subscribe', '1', '{"broadcaster_user_id":"' + BroadcasterID + '"}') then toLog(0, 'subscribe', 'channel.subscribe OK') else toLog(2, 'subscribe', 'channel.subscribe'); if subscribeTo('channel.subscription.gift', '1', '{"broadcaster_user_id":"' + BroadcasterID + '"}') then toLog(0, 'subscribe', 'channel.subscription.gift OK') else toLog(2, 'subscribe', 'channel.subscription.gift'); end; end.