unit uTTWAPI;

interface

uses
  System.Classes, System.SysUtils, IdHTTP, IdSSLOpenSSL, System.JSON, FMX.Forms,
  IdMultipartFormData, DateUtils, uDataBase, System.Generics.Collections,
  uRecords;

type
  TOnLog = procedure(aModul: string; aMethod: string; aMessage: string; aLevel: integer) of object;

type
  TTTW_API = class(TObject)
  private
    Token_api: string;
    Token_api_streamer: string;
    ClientID: string;
    channel_name_api: string;
    BotName_api: string;
    FChatBadges: TList<TChatBadge>;
    FOnLog: TOnLog;
    function GetFollowedAtFromJson(jsonString: string): string;

    function getTTW(method: string; ClientID: string;
      isStreamer: boolean = false): string;
    function DelTTW(method: string; ClientID: string;
      isStreamer: boolean = false): string;
    function postTTW(method: string; ClientID: string;
      params: TIdMultipartFormDataStream; isStreamer: boolean = false)
      : string; overload;
    function postTTW(method: string; ClientID: string; params: TStringStream;
      isStreamer: boolean = false): string; overload;

    function patchTTW(method: string; ClientID: string; params: TStringStream;
      isStreamer: boolean = false): string; overload;

    procedure toLog(alevel: integer; amethod: string; amessage: string);
  public
    constructor Create(Sender: TObject);
    destructor Destroy; override;
    procedure Init(myClient, myToken, streamerToken, Channel,
      myBotName: string);
    procedure getTTWStat(Channel: string; var avg_viewers: integer;
      var max_viewers: integer; var hours_watched: integer;
      var followers: integer; var followers_total: integer);
    procedure shoutouts(id: string);
    procedure SendNotify(text: string);
    procedure banUser(id: string);
    procedure banUserTime(id: string; aTime: integer);
    procedure unBanUser(id: string);
    procedure warnUser(id: string);
    procedure raid(id: string);
    procedure unRaid();

    procedure setModerator(id: string);
    procedure delModerator(id: string);
    procedure setVIP(id: string);
    procedure delVIP(id: string);
    procedure getCustomReward(var acr: TList<TCustomRevards>);
    function createCustomReward(title: string; cost: string;
      promt: string = ''; isUserInput: boolean = false): TCustomRevards;
    procedure UpdateCustomReward(ahr: TCustomRevards);
    procedure UpdateRedemptionStatus(ahr: TCustomRewardEvent);
    procedure deleteCustomReward(id: string);
    function getRoomAndBot(): string;
    function getUserbyLogin(login: string): TUser;
    function getFollow(id: string): TDateTime;
    function GetRoomID: string;
    procedure getGlobalChatBadges(var gcb: TList<TChatBadge>);
    procedure getCustomChatBadges(var ccb: TList<TChatBadge>);
    procedure GetChannelEmotes(var ce: TList<TEmotes>);
    procedure GetGlobalEmotes(var ge: TList<TEmotes>);
    function ValidateTwitchToken(const TokenName, TokenValue: string; var DayOfLive: integer): Boolean;
    property OnLog: TOnLog read FOnLog write FOnLog;
  end;

var
  room_id: string = '';
  bot_id: string = '';

implementation

uses uGeneral;

constructor TTTW_API.Create(Sender: TObject);
begin
  FChatBadges := TList<TChatBadge>.Create;
end;

function TTTW_API.createCustomReward(title: string; cost: string;
  promt: string = ''; isUserInput: boolean = false): TCustomRevards;
var
  RequestData: TStringStream;
  s, s1, json: string;
  cr: TCustomRevards;
  JSONData: TJSONObject;
  JSONArray: TJSONArray;
begin
  Result := Default(TCustomRevards);
  try
    if room_id = '' then
      room_id := GetRoomID;
    s := '';
    s1 := '';
    if isUserInput then
      s := ' "is_user_input_required":true,';
    if promt <> '' then
      s1 := ' "prompt":"' + promt + '",';
    RequestData := TStringStream.Create('{  "title":"' + title + '", ' + s + s1
      + ' "cost":' + cost + ' }', TEncoding.UTF8);
    try
      json := postTTW('channel_points/custom_rewards?broadcaster_id=' + room_id, ClientID,
        RequestData, true);
      if json = '' then
      begin
        toLog(1, 'createCustomReward', 'Награда не создалась, запрос вернул пустой ответ');
        Exit;
      end;

      JSONData := TJSONObject.ParseJSONValue(json) as TJSONObject;
      if not Assigned(JSONData) then
      begin
        toLog(2, 'createCustomReward', 'Ошибка парсинга JSON');
        Exit;
      end;

      try
        JSONArray := JSONData.GetValue<TJSONArray>('data');
        if JSONArray.Count > 0 then
        begin
          cr.id := JSONArray.Items[0].GetValue<string>('id');
          cr.title := JSONArray.Items[0].GetValue<string>('title');
          cr.promt := JSONArray.Items[0].GetValue<string>('prompt');
          cr.cost := JSONArray.Items[0].GetValue<integer>('cost');
          cr.is_user_input_required := JSONArray.Items[0].GetValue<boolean>
            ('is_user_input_required');
          Result := cr;
        end;
      finally
        JSONData.Free;
      end;
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'createCustomReward', E.Message);
  end;
end;

destructor TTTW_API.Destroy;
begin
  FChatBadges.Free;
  inherited;
end;

procedure TTTW_API.Init(myClient, myToken, streamerToken, Channel,
  myBotName: string);
begin
  ClientID := myClient;
  Token_api := myToken;
  Token_api_streamer := streamerToken;
  channel_name_api := Channel;
  BotName_api := myBotName;
end;

procedure TTTW_API.banUser(id: string);
var
  RequestData: TStringStream;
begin
  try
    if bot_id = '' then
      bot_id := getRoomAndBot;
    if (bot_id = '') or (room_id = '') then
      Exit;

    RequestData := TStringStream.Create('{"data": {"user_id":"' + id +
      '","reason":"no reason"}}', TEncoding.UTF8);
    try
      postTTW('moderation/bans?broadcaster_id=' + room_id + '&moderator_id=' +
        bot_id, ClientID, RequestData);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'banUser', E.Message);
  end;
end;

procedure TTTW_API.banUserTime(id: string; aTime: integer);
var
  RequestData: TStringStream;
begin
  try
    if bot_id = '' then
      bot_id := getRoomAndBot;
    if (bot_id = '') or (room_id = '') then
      Exit;

    RequestData := TStringStream.Create('{"data": {"user_id":"' + id +
      '","duration":' + IntToStr(aTime) + '}}', TEncoding.UTF8);
    try
      postTTW('moderation/bans?broadcaster_id=' + room_id + '&moderator_id=' +
        bot_id, ClientID, RequestData);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'banUserTime', E.Message);
  end;
end;

procedure TTTW_API.deleteCustomReward(id: string);
begin
  try
    if Token_api_streamer = '' then
      Exit;
    if room_id = '' then
      Exit;
    DelTTW('channel_points/custom_rewards?broadcaster_id=' + room_id + '&id=' +
      id, ClientID, true);
  except
    on E: Exception do
      toLog(2, 'deleteCustomReward', E.Message);
  end;
end;

procedure TTTW_API.delModerator(id: string);
begin
  try
    if Token_api_streamer = '' then
      Exit;
    if room_id = '' then
      Exit;
    DelTTW('moderation/moderators?broadcaster_id=' + room_id + '&user_id=' + id,
      ClientID, true);
  except
    on E: Exception do
      toLog(2, 'delModerator', E.Message);
  end;
end;

function TTTW_API.DelTTW(method, ClientID: string; isStreamer: boolean): string;
var
  url: string;
  response1, Token: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      if isStreamer then
        Token := Token_api_streamer
      else
        Token := Token_api;

      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      http.Request.CustomHeaders.AddValue('Client-ID', ClientID);
      http.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + Token);
      http.Request.ContentType := 'application/json';
      url := 'https://api.twitch.tv/helix/' + method;
      response1 := http.Delete(url);
      Result := response1;
    except
      on E: Exception do
        toLog(2, 'DelTTW', E.Message + ' [' + method + ']');
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

procedure TTTW_API.delVIP(id: string);
begin
  try
    if Token_api_streamer = '' then
      Exit;
    if room_id = '' then
      Exit;
    DelTTW('channels/vips?broadcaster_id=' + room_id + '&user_id=' + id,
      ClientID, true);
  except
    on E: Exception do
      toLog(2, 'delVIP', E.Message);
  end;
end;

procedure TTTW_API.getCustomReward(var acr: TList<TCustomRevards>);
var
  JSON: string;
  sl: TCustomRevards;
  JSONData: TJSONObject;
  JSONArray: TJSONArray;
  i: integer;
begin
  try
    if Token_api_streamer = '' then
    begin
      toLog(1, 'getCustomReward', 'Token_api_streamer пуст');
      Exit;
    end;
    if room_id = '' then
      Exit;

    JSON := getTTW('channel_points/custom_rewards?broadcaster_id=' + room_id +
      '&only_manageable_rewards=true', ClientID, true);
    if JSON = '' then
    begin
      toLog(1, 'getCustomReward', 'Пустой ответ от API');
      Exit;
    end;

    JSONData := TJSONObject.ParseJSONValue(JSON) as TJSONObject;
    if not Assigned(JSONData) then
    begin
      toLog(2, 'getCustomReward', 'Ошибка парсинга JSON');
      Exit;
    end;

    try
      JSONArray := JSONData.GetValue<TJSONArray>('data');
      for i := 0 to JSONArray.Count - 1 do
      begin
        sl.id := JSONArray.Items[i].GetValue<string>('id');
        sl.title := JSONArray.Items[i].GetValue<string>('title');
        sl.promt := JSONArray.Items[i].GetValue<string>('prompt');
        sl.cost := JSONArray.Items[i].GetValue<integer>('cost');
        sl.is_user_input_required := JSONArray.Items[i].GetValue<boolean>
          ('is_user_input_required');
        acr.Add(sl);
      end;
    finally
      JSONData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'getCustomReward', E.Message);
  end;
end;

function TTTW_API.getFollow(id: string): TDateTime;
var
  s: string;
  followedAt: string;
begin
  Result := 0;
  s := getTTW('channels/followers?user_id=' + id + '&broadcaster_id=' + room_id,
    ClientID);
  followedAt := GetFollowedAtFromJson(s);
  if followedAt <> '' then
    Result := StrToDate(followedAt);
end;

function TTTW_API.GetFollowedAtFromJson(jsonString: string): string;
var
  JSON: TJSONObject;
  dataArray: TJSONArray;
begin
  Result := '';
  if jsonString = '' then Exit;

  JSON := TJSONObject.ParseJSONValue(jsonString) as TJSONObject;
  if not Assigned(JSON) then Exit;

  try
    dataArray := JSON.GetValue('data') as TJSONArray;
    if Assigned(dataArray) and (dataArray.Count > 0) then
      Result := DateToStr(ISO8601ToDate(dataArray.Items[0].GetValue<string>('followed_at')));
  finally
    JSON.Free;
  end;
end;

function TTTW_API.getTTW(method, ClientID: string; isStreamer: boolean): string;
var
  url: string;
  response1, Token: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      if isStreamer then
        Token := Token_api_streamer
      else
        Token := Token_api;

      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      http.Request.CustomHeaders.AddValue('Client-ID', ClientID);
      http.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + Token);
      url := 'https://api.twitch.tv/helix/' + method;
      response1 := http.Get(url);
      Result := response1;
    except
      on E: Exception do
        toLog(2, 'getTTW', E.Message + ' [' + method + ']');
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

procedure TTTW_API.getTTWStat(Channel: string; var avg_viewers, max_viewers,
  hours_watched, followers, followers_total: integer);
var
  jsonObject: TJSONObject;
  url: string;
  response1: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      url := 'https://twitchtracker.com/api/channels/summary/' + Channel;
      response1 := http.Get(url);
      jsonObject := TJSONObject.ParseJSONValue(response1) as TJSONObject;
      if not Assigned(jsonObject) then
      begin
        toLog(2, 'getTTWStat', 'Ошибка парсинга JSON');
        Exit;
      end;

      try
        avg_viewers := jsonObject.GetValue<integer>('avg_viewers');
        max_viewers := jsonObject.GetValue<integer>('max_viewers');
        hours_watched := jsonObject.GetValue<integer>('hours_watched');
        followers := jsonObject.GetValue<integer>('followers');
        followers_total := jsonObject.GetValue<integer>('followers_total');
      finally
        jsonObject.Free;
      end;
    except
      on E: Exception do
        toLog(2, 'getTTWStat', E.Message);
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

function ParseBadgeVersion(VersionObj: TJSONObject): TBadgeVersion;
begin
  Result.Id := VersionObj.GetValue('id').Value;
  Result.ImageUrl1x := VersionObj.GetValue('image_url_1x').Value;
  Result.ImageUrl2x := VersionObj.GetValue('image_url_2x').Value;
  Result.ImageUrl4x := VersionObj.GetValue('image_url_4x').Value;
  Result.Title := VersionObj.GetValue('title').Value;
  Result.Description := VersionObj.GetValue('description').Value;
  Result.ClickAction := VersionObj.GetValue('click_action').Value;
  Result.ClickUrl := VersionObj.GetValue('click_url').Value;
end;

function ParseChatBadge(BadgeObj: TJSONObject): TChatBadge;
var
  VersionsArray: TJSONArray;
  I: Integer;
begin
  Result.SetId := BadgeObj.GetValue('set_id').Value;
  VersionsArray := BadgeObj.GetValue('versions') as TJSONArray;
  SetLength(Result.Versions, VersionsArray.Count);
  for I := 0 to VersionsArray.Count - 1 do
    Result.Versions[I] := ParseBadgeVersion(VersionsArray.Items[I] as TJSONObject);
end;

procedure ParseBadgesFromApi(const JSONResponse: string; Badges: TList<TChatBadge>);
var
  RootObj: TJSONObject;
  DataArray: TJSONArray;
  I: Integer;
begin
  if JSONResponse = '' then Exit;

  RootObj := TJSONObject.ParseJSONValue(JSONResponse) as TJSONObject;
  if not Assigned(RootObj) then Exit;

  try
    DataArray := RootObj.GetValue('data') as TJSONArray;
    for I := 0 to DataArray.Count - 1 do
      Badges.Add(ParseChatBadge(DataArray.Items[I] as TJSONObject));
  finally
    RootObj.Free;
  end;
end;

procedure TTTW_API.getGlobalChatBadges(var gcb: TList<TChatBadge>);
var
  jsonString: string;
begin
  jsonString := getTTW('chat/badges/global', ClientID, false);
  ParseBadgesFromApi(jsonString, gcb);
end;

procedure TTTW_API.getCustomChatBadges(var ccb: TList<TChatBadge>);
var
  jsonString: string;
begin
  if room_id = '' then Exit;
  jsonString := getTTW('chat/badges?broadcaster_id=' + room_id, ClientID, false);
  ParseBadgesFromApi(jsonString, ccb);
end;

function TTTW_API.getRoomAndBot(): string;
var
  jsonString: string;
  JSON: TJSONObject;
  dataArray: TJSONArray;
begin
  try
    jsonString := getTTW('users?login=' + LowerCase(BotName_api), ClientID);
    JSON := TJSONObject.ParseJSONValue(jsonString) as TJSONObject;
    if not Assigned(JSON) then
    begin
      toLog(2, 'getRoomAndBot.GetBotID', 'Ошибка парсинга JSON');
      Exit;
    end;

    try
      dataArray := JSON.GetValue('data') as TJSONArray;
      if Assigned(dataArray) and (dataArray.Count > 0) then
        bot_id := dataArray.Items[0].GetValue<string>('id');
    finally
      JSON.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'getRoomAndBot.GetBotID', E.Message);
  end;

  try
    jsonString := getTTW('users?login=' + LowerCase(channel_name_api), ClientID);
    JSON := TJSONObject.ParseJSONValue(jsonString) as TJSONObject;
    if not Assigned(JSON) then
    begin
      toLog(2, 'getRoomAndBot.GetRoomID', 'Ошибка парсинга JSON');
      Exit;
    end;

    try
      dataArray := JSON.GetValue('data') as TJSONArray;
      if Assigned(dataArray) and (dataArray.Count > 0) then
        room_id := dataArray.Items[0].GetValue<string>('id');
    finally
      JSON.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'getRoomAndBot.GetRoomID', E.Message);
  end;
  Result := room_id;
end;

function TTTW_API.GetRoomID: string;
begin
  Result := room_id;
end;

function TTTW_API.getUserbyLogin(login: string): TUser;
var
  u: TUser;
  JSON: TJSONObject;
  dataArray: TJSONArray;
  jsonString: string;
begin
  Result := Default(TUser);
  jsonString := getTTW('users?login=' + LowerCase(login), ClientID);
  if jsonString = '' then Exit;

  JSON := TJSONObject.ParseJSONValue(jsonString) as TJSONObject;
  if not Assigned(JSON) then Exit;

  try
    dataArray := JSON.GetValue('data') as TJSONArray;
    if Assigned(dataArray) and (dataArray.Count > 0) then
    begin
      u.id := dataArray.Items[0].GetValue<string>('id');
      u.login := dataArray.Items[0].GetValue<string>('login');
      u.DisplayName := dataArray.Items[0].GetValue<string>('display_name');
      u.created_at := ISO8601ToDate(dataArray.Items[0].GetValue<string>('created_at'));
      Result := u;
    end;
  finally
    JSON.Free;
  end;
end;

function TTTW_API.postTTW(method, ClientID: string;
  params: TIdMultipartFormDataStream; isStreamer: boolean): string;
var
  url: string;
  response1, Token: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      if isStreamer then
        Token := Token_api_streamer
      else
        Token := Token_api;

      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      http.Request.CustomHeaders.AddValue('Client-ID', ClientID);
      http.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + Token);
      http.Request.ContentType := 'application/json';
      url := 'https://api.twitch.tv/helix/' + method;
      response1 := http.Post(url, params);
      Result := response1;
    except
      on E: Exception do
        toLog(2, 'postTTW', E.Message + ' [' + method + ']');
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

function TTTW_API.patchTTW(method, ClientID: string; params: TStringStream;
  isStreamer: boolean): string;
var
  url: string;
  response1, Token: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      if isStreamer then
        Token := Token_api_streamer
      else
        Token := Token_api;

      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      http.Request.CustomHeaders.AddValue('Client-ID', ClientID);
      http.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + Token);
      http.Request.ContentType := 'application/json';
      url := 'https://api.twitch.tv/helix/' + method;
      response1 := http.Patch(url, params);
      Result := response1;
    except
      on E: Exception do
        toLog(2, 'patchTTW', E.Message + ' [' + method + ']');
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

function TTTW_API.postTTW(method, ClientID: string; params: TStringStream;
  isStreamer: boolean): string;
var
  url: string;
  response1, Token: string;
  http: TIdHTTP;
  ssl: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';
  http := TIdHTTP.Create(nil);
  ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    try
      if isStreamer then
        Token := Token_api_streamer
      else
        Token := Token_api;

      http.IOHandler := ssl;
      ssl.SSLOptions.method := sslvSSLv23;
      http.Request.UserAgent :=
        'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      http.Request.CustomHeaders.AddValue('Client-ID', ClientID);
      http.Request.CustomHeaders.AddValue('Authorization', 'Bearer ' + Token);
      http.Request.ContentType := 'application/json';
      url := 'https://api.twitch.tv/helix/' + method;
      response1 := http.Post(url, params);
      Result := response1;
    except
      on E: Exception do
        toLog(2, 'postTTW', E.Message + ' [' + method + ']');
    end;
  finally
    http.Free;
    ssl.Free;
  end;
end;

procedure TTTW_API.raid(id: string);
var
  p: TIdMultipartFormDataStream;
begin
  try
    if room_id = '' then
      Exit;
    p := TIdMultipartFormDataStream.Create;
    try
      postTTW('raids?from_broadcaster_id=' + room_id + '&to_broadcaster_id=' + id, ClientID, p, true);
    finally
      p.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'raid', E.Message);
  end;
end;

procedure TTTW_API.SendNotify(text: string);
var
  p: TStringStream;
begin
  try
    if bot_id = '' then
    begin
      toLog(1, 'SendNotify', 'bot_id пуст. Исправляю');
      getRoomAndBot;
      if bot_id = '' then
      begin
        toLog(2, 'SendNotify', 'bot_id все равно пуст');
        Exit;
      end;
    end;
    if room_id = '' then
    begin
      toLog(2, 'SendNotify', 'room_id пуст');
      Exit;
    end;

    p := TStringStream.Create('{"message":"' + text + '","color":"primary"}', TEncoding.UTF8);
    try
      postTTW('chat/announcements?broadcaster_id=' + room_id + '&moderator_id=' + bot_id, ClientID, p);
    finally
      p.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'SendNotify', E.Message);
  end;
end;

procedure TTTW_API.setModerator(id: string);
var
  RequestData: TStringStream;
begin
  try
    if Token_api_streamer = '' then Exit;
    if room_id = '' then Exit;

    RequestData := TStringStream.Create('', TEncoding.UTF8);
    try
      postTTW('moderation/moderators?broadcaster_id=' + room_id + '&user_id=' + id, ClientID, RequestData, true);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'setModerator', E.Message);
  end;
end;

procedure TTTW_API.setVIP(id: string);
var
  RequestData: TStringStream;
begin
  try
    if Token_api_streamer = '' then Exit;
    if room_id = '' then Exit;

    RequestData := TStringStream.Create('', TEncoding.UTF8);
    try
      postTTW('channels/vips?broadcaster_id=' + room_id + '&user_id=' + id, ClientID, RequestData, true);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'setVIP', E.Message);
  end;
end;

procedure TTTW_API.shoutouts(id: string);
var
  p: TIdMultipartFormDataStream;
begin
  try
    if (bot_id = '') or (room_id = '') then
      Exit;
    p := TIdMultipartFormDataStream.Create;
    try
      postTTW('chat/shoutouts?from_broadcaster_id=' + room_id + '&to_broadcaster_id=' + id + '&moderator_id=' + bot_id, ClientID, p);
    finally
      p.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'shoutouts', E.Message);
  end;
end;

procedure TTTW_API.toLog(alevel: integer; amethod, amessage: string);
begin
  if Assigned(FOnLog) then
    FOnLog('uTTWAPI', amethod, amessage, alevel);
end;

procedure TTTW_API.unBanUser(id: string);
begin
  try
    if bot_id = '' then bot_id := getRoomAndBot;
    if (bot_id = '') or (room_id = '') then Exit;
    DelTTW('moderation/bans?broadcaster_id=' + room_id + '&moderator_id=' + bot_id + '&user_id=' + id, ClientID);
  except
    on E: Exception do
      toLog(2, 'unBanUser', E.Message);
  end;
end;

procedure TTTW_API.unRaid;
begin
  try
    if room_id = '' then Exit;
    DelTTW('raids?broadcaster_id=' + room_id, ClientID);
  except
    on E: Exception do
      toLog(2, 'unRaid', E.Message);
  end;
end;

procedure TTTW_API.UpdateCustomReward(ahr: TCustomRevards);
var
  RequestData: TStringStream;
  qid: string;
begin
  try
    if room_id = '' then Exit;
    qid := ahr.id;
    RequestData := TStringStream.Create('{"cost": ' + IntToStr(ahr.cost) + '}', TEncoding.UTF8);
    try
      patchTTW('channel_points/custom_rewards?broadcaster_id=' + room_id + '&id=' + qid, ClientID, RequestData, true);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'UpdateCustomReward', E.Message);
  end;
end;

procedure TTTW_API.UpdateRedemptionStatus(ahr: TCustomRewardEvent);
var
  qbid, qrid, qid: string;
  RequestData: TStringStream;
begin
  try
    if (bot_id = '') or (room_id = '') then
      Exit;
    qbid := ahr.event.broadcaster_user_id;
    qrid := ahr.event.revard.id;
    qid := ahr.event.id;
    toLog(0, 'UpdateRedemptionStatus', 'ChannelId: ' + qbid + '; Reward.id: ' + qrid + '; Redemption.id: ' + qid);

    RequestData := TStringStream.Create('{"status":"CANCELED"}', TEncoding.UTF8);
    try
      patchTTW('channel_points/custom_rewards/redemptions?broadcaster_id=' + qbid + '&reward_id=' + qrid + '&id=' + qid, ClientID, RequestData, true);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'UpdateRedemptionStatus', E.Message);
  end;
end;

function TTTW_API.ValidateTwitchToken(const TokenName, TokenValue: string; var DayOfLive: integer): Boolean;
var
  HTTP: TIdHTTP;
  SSLHandler: TIdSSLIOHandlerSocketOpenSSL;
  ResponseStream: TStringStream;
  ResponseJSON: TJSONObject;
  StatusCode: Integer;
  ResponseText: string;
begin
  Result := False;
  DayOfLive := 0;
  if Trim(TokenValue) = '' then
  begin
    toLog(1, 'ValidateTwitchToken', 'Пустой токен');
    Exit;
  end;

  HTTP := TIdHTTP.Create(nil);
  SSLHandler := TIdSSLIOHandlerSocketOpenSSL.Create(HTTP);
  ResponseStream := TStringStream.Create;
  try
    HTTP.IOHandler := SSLHandler;
    HTTP.Request.CustomHeaders.Values['Authorization'] := 'OAuth ' + TokenValue;
    HTTP.Request.UserAgent := 'YourApp/1.0';
    HTTP.Request.Accept := 'application/json';
    HTTP.HTTPOptions := [hoKeepOrigProtocol];

    try
      HTTP.Get('https://id.twitch.tv/oauth2/validate', ResponseStream);
      StatusCode := HTTP.ResponseCode;
      ResponseText := ResponseStream.DataString;
    except
      on E: EIdHTTPProtocolException do
      begin
        StatusCode := E.ErrorCode;
        ResponseText := E.ErrorMessage;
      end;
      on E: Exception do
      begin
        toLog(2, 'ValidateTwitchToken', E.Message);
        Exit;
      end;
    end;

    if StatusCode = 200 then
    begin
      try
        ResponseJSON := TJSONObject.ParseJSONValue(ResponseText) as TJSONObject;
        if not Assigned(ResponseJSON) then
        begin
          toLog(2, 'ValidateTwitchToken', 'Ошибка парсинга JSON');
          Exit;
        end;

        try
          if ResponseJSON.GetValue('expires_in') <> nil then
          begin
            DayOfLive := Round(ResponseJSON.GetValue('expires_in').Value.ToInteger / 86400);
            toLog(0, 'ValidateTwitchToken', Format('Токен действителен. Осталось: %d дней', [DayOfLive]));
          end;
          Result := True;
        finally
          ResponseJSON.Free;
        end;
      except
        on E: Exception do
          toLog(2, 'ValidateTwitchToken', E.Message);
      end;
    end
    else if StatusCode = 401 then
    begin
      toLog(2, 'ValidateTwitchToken', 'Invalid token');
    end
    else
    begin
      toLog(2, 'ValidateTwitchToken', Format('HTTP %d: %s', [StatusCode, ResponseText]));
    end;
  finally
    ResponseStream.Free;
    SSLHandler.Free;
    HTTP.Free;
  end;
end;

procedure TTTW_API.warnUser(id: string);
var
  RequestData: TStringStream;
begin
  try
    if bot_id = '' then bot_id := getRoomAndBot;
    if (bot_id = '') or (room_id = '') then Exit;

    RequestData := TStringStream.Create('{"data": {"user_id":"' + id + '","reason":"Вам вынесено предупреждение!"}}', TEncoding.UTF8);
    try
      postTTW('moderation/warnings?broadcaster_id=' + room_id + '&moderator_id=' + bot_id, ClientID, RequestData);
    finally
      RequestData.Free;
    end;
  except
    on E: Exception do
      toLog(2, 'warnUser', E.Message);
  end;
end;

function GetStringValue(JSONObj: TJSONObject; const Name: string): string;
var
  Val: TJSONValue;
begin
  Val := JSONObj.GetValue(Name);
  if Assigned(Val) then
    Result := Val.Value
  else
    Result := '';
end;

function GetStringArray(JSONObj: TJSONObject; const Name: string): TArray<string>;
var
  Arr: TJSONArray;
  I: Integer;
begin
  SetLength(Result, 0);
  if not JSONObj.TryGetValue<TJSONArray>(Name, Arr) then Exit;
  SetLength(Result, Arr.Count);
  for I := 0 to Arr.Count - 1 do
    Result[I] := Arr.Items[I].Value;
end;

procedure ParseEmotes(const JSONString: string; EmotesList: TList<TEmotes>);
var
  RootObj: TJSONObject;
  DataArr: TJSONArray;
  EmoteObj: TJSONObject;
  ImagesObj: TJSONObject;
  Emote: TEmotes;
  I: Integer;
  chosenFormat, chosenTheme, chosenScale: string;
  foundAnimated,  foundDark: Boolean;
  scaleVal, maxScale: Double;
  s: string;
begin
  if JSONString = '' then Exit;

  RootObj := TJSONObject.ParseJSONValue(JSONString) as TJSONObject;
  if not Assigned(RootObj) then Exit;

  try
    if not RootObj.TryGetValue<TJSONArray>('data', DataArr) then Exit;
    for I := 0 to DataArr.Count - 1 do
    begin
      EmoteObj := DataArr.Items[I] as TJSONObject;
      Emote.id := GetStringValue(EmoteObj, 'id');
      Emote.name := GetStringValue(EmoteObj, 'name');
      Emote.tier := GetStringValue(EmoteObj, 'tier');
      Emote.emote_type := GetStringValue(EmoteObj, 'emote_type');
      Emote.emote_set_id := GetStringValue(EmoteObj, 'emote_set_id');

      if EmoteObj.TryGetValue<TJSONObject>('images', ImagesObj) then
      begin
        Emote.images.Url1x := GetStringValue(ImagesObj, 'url_1x');
        Emote.images.Url2x := GetStringValue(ImagesObj, 'url_2x');
        Emote.images.Url4x := GetStringValue(ImagesObj, 'url_4x');
      end;

      Emote.format := GetStringArray(EmoteObj, 'format');
      Emote.scale := GetStringArray(EmoteObj, 'scale');
      Emote.theme_mode := GetStringArray(EmoteObj, 'theme_mode');

      // Выбор формата (format)
      foundAnimated := False;
      chosenFormat := 'static'; // значение по умолчанию

      // Проверка наличия "animated"
      for s in Emote.format do
        if s = 'animated' then
        begin
          chosenFormat := 'animated';
          foundAnimated := True;
          Break;
        end;

      // Если не найден "animated", ищем любой не-"static"
      if not foundAnimated then
        for s in Emote.format do
          if s <> 'static' then
          begin
            chosenFormat := s;
            Break;
          end;

      // Выбор темы (theme_mode)
      foundDark := False;
      for s in Emote.theme_mode do
        if s = 'dark' then
        begin
          chosenTheme := 'dark';
          foundDark := True;
          Break;
        end;
      if not foundDark then
        chosenTheme := 'light';

      // Выбор масштаба (scale)
      maxScale := 0;
      for s in Emote.scale do
      begin
        if TryStrToFloat(s, scaleVal, TFormatSettings.Invariant) then
          if scaleVal > maxScale then
            maxScale := scaleVal;
      end;
      // Если масштабы отсутствуют, используем 1.0
      if maxScale = 0 then maxScale := 1.0;
      chosenScale := Format('%.1f', [maxScale], TFormatSettings.Invariant);

      // Формирование ссылки
      Emote.topImage := 'https://static-cdn.jtvnw.net/emoticons/v2/' +
                        Emote.id + '/' +
                        chosenFormat + '/' +
                        chosenTheme + '/' +
                        chosenScale;

      EmotesList.Add(Emote);
    end;
  finally
    RootObj.Free;
  end;

end;

procedure TTTW_API.GetChannelEmotes(var ce: TList<TEmotes>);
var
  jsonres: string;
begin
  if room_id = '' then Exit;
  jsonres := getTTW('chat/emotes?broadcaster_id=' + room_id, ClientID, false);
  ParseEmotes(jsonres, ce);
end;

procedure TTTW_API.GetGlobalEmotes(var ge: TList<TEmotes>);
var
  jsonres: string;
begin
  jsonres := getTTW('chat/emotes/global', ClientID, false);
  ParseEmotes(jsonres, ge);
end;

end.
