//Handles all Twilio RestAPI calls, so any file that that needs to retrieve
// info from twilio uses this file
// Authors:
// Cameron Hayes
// Elias Sarraf

unit Twilio.Data.Module;

interface

uses
  System.SysUtils, System.Classes, Data.DB, MemDS, DBAccess, Uni, UniProvider,
  PostgreSQLUniProvider, REST.Client, REST.Types, System.JSON, Winapi.Windows,
  Winapi.Messages, System.Variants, System.Generics.Collections, System.IniFiles,
  Vcl.Forms;

type
  TTwilioDataModule = class(TDataModule)
    ucEnvoy: TUniConnection;
    PostgreSQLUniProvider1: TPostgreSQLUniProvider;
    UniQuery1: TUniQuery;
    uqLocations: TUniQuery;
    uqLocationslocation: TStringField;
    uqLocationsphone_number: TStringField;
    uqRecordings: TUniQuery;
    uqRecordingsaccount_sid: TStringField;
    uqRecordingsapi_version: TStringField;
    uqRecordingscall_sid: TStringField;
    uqRecordingsconference_sid: TStringField;
    uqRecordingsdate_created: TDateTimeField;
    uqRecordingsdate_updated: TDateTimeField;
    uqRecordingsstart_time: TDateTimeField;
    uqRecordingsduration: TStringField;
    uqRecordingssid: TStringField;
    uqRecordingsprice: TStringField;
    uqRecordingsprice_unit: TStringField;
    uqRecordingsstatus: TStringField;
    uqRecordingschannels: TStringField;
    uqRecordingssource: TStringField;
    uqRecordingserror_code: TStringField;
    uqRecordingsuri: TStringField;
    uqRecordingsencryption_details: TStringField;
    uqRecordingsmedia_url: TStringField;
    uqRecordingstranscription: TMemoField;
    uqCalls: TUniQuery;
    uqCallsdate_updated: TDateTimeField;
    uqCallsprice_unit: TStringField;
    uqCallsparent_call_sid: TStringField;
    uqCallscaller_name: TStringField;
    uqCallsduration: TStringField;
    uqCallsannotation: TMemoField;
    uqCallsanswered_by: TStringField;
    uqCallssid: TStringField;
    uqCallsqueue_time: TStringField;
    uqCallsprice: TStringField;
    uqCallsapi_version: TStringField;
    uqCallsstatus: TStringField;
    uqCallsdirection: TStringField;
    uqCallsstart_time: TDateTimeField;
    uqCallsdate_created: TDateTimeField;
    uqCallsfrom_formatted: TStringField;
    uqCallsgroup_sid: TStringField;
    uqCallstrunk_sid: TStringField;
    uqCallsuri: TStringField;
    uqCallsaccount_sid: TStringField;
    uqCallsend_time: TDateTimeField;
    uqCallsto_formatted: TStringField;
    uqCallsphone_number_sid: TStringField;
    uqCallsforwarded_from: TStringField;
    uqLocationsphone_number_formatted: TStringField;
    procedure DataModuleCreate(Sender: TObject);
    procedure DataModuleDestroy(Sender: TObject);
    function GetRecordings(count: integer; offset: integer): integer;
    function GetCalls(phoneNum: string; count: integer; offset: integer): integer;
    procedure UpdateDB();
    procedure ShowCalls(PhoneNum: string);
    function GetTranscription(uri: string): string;
    function toDateTime(date: string): TDateTime;
    function FullUpdate(): string;
  private
    { Private declarations }
    accountSID: string;
    authHeader: string;
  public
    { Public declarations }
  end;

var
  TwilioDataModule: TTwilioDataModule;

implementation

uses
  Common.Logging,
  Common.Config;

{%CLASSGROUP 'Vcl.Controls.TControl'}

{$R *.dfm}

function TTwilioDataModule.GetCalls(phoneNum: string; count: integer; offset: integer): integer;
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  res: string;
  sid: string;
  jsValue: TJSONValue;
  jsObj: TJSONObject;
  jaCalls: TJSONArray;
  joCall: TJSONObject;
  i: integer;
  uri: string;
  sql: string;
  addedCount: Integer;
begin
  restClient := TRESTClient.Create(nil);
  restClient.BaseURL := 'https://api.twilio.com';

  restRequest := TRESTRequest.Create(nil);
  restRequest.Client := restClient;

  restResponse := TRESTResponse.Create(nil);
  restRequest.Response := restResponse;

  restRequest.Method := rmGET;
  res := '/2010-04-01/Accounts/' + accountSID + '/Calls.json?Status=completed&To=+' + phoneNum + '&PageSize=' + IntToStr(count);
  restRequest.Resource := res;

  param := restRequest.Params.AddItem;
  param.Name := 'Authorization';
  param.Kind := pkHTTPHEADER;
  param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
  param.Value := authHeader;

  restRequest.Execute;

  jsValue := restResponse.JSONValue;
  jsObj := TJSONObject(jsValue);
  jaCalls := TJSONArray(jsObj.GetValue('calls'));

  addedCount := 0;

  for i := offset to (jaCalls.Count - 1) do
  begin
    joCall := TJSONObject(jaCalls.Items[i]);
    sql := 'select * from calls where sid = ' + QuotedStr(joCall.Get('sid').JsonValue.Value);
    uqCalls.Close;
    uqCalls.SQL.Text := sql;
    uqCalls.Open;

    if uqCalls.IsEmpty then
    begin
      uqCalls.Append;
      uqCallsdate_updated.AsDateTime := toDateTime(joCall.Get('date_updated').JsonValue.Value);
      uqCallsprice_unit.AsString := joCall.Get('price_unit').JsonValue.Value;
      uqCallsparent_call_sid.AsString := joCall.Get('parent_call_sid').JsonValue.Value;
      uqCallsduration.AsString := joCall.Get('duration').JsonValue.Value;
      uqCallsannotation.AsString := joCall.Get('annotation').JsonValue.Value;
      uqCallsanswered_by.AsString := joCall.Get('answered_by').JsonValue.Value;
      uqCallssid.AsString := joCall.Get('sid').JsonValue.Value;
      uqCallsqueue_time.AsString := joCall.Get('queue_time').JsonValue.Value;
      uqCallsprice.AsString := joCall.Get('price').JsonValue.Value;
      uqCallsdirection.AsString := joCall.Get('direction').JsonValue.Value;
      uqCallsstart_time.AsDateTime := toDateTime(joCall.Get('start_time').JsonValue.Value);
      uqCallsdate_created.AsDateTime := toDateTime(joCall.Get('date_created').JsonValue.Value);
      uqCallsfrom_formatted.AsString := joCall.Get('from_formatted').JsonValue.Value;
      uqCallsgroup_sid.AsString := joCall.Get('group_sid').JsonValue.Value;
      uqCallstrunk_sid.AsString := joCall.Get('trunk_sid').JsonValue.Value;
      uqCallsforwarded_from.AsString := joCall.Get('forwarded_from').JsonValue.Value;
      uqCallsuri.AsString := joCall.Get('uri').JsonValue.Value;
      uqCallsaccount_sid.AsString := joCall.Get('account_sid').JsonValue.Value;
      uqCallsend_time.AsDateTime := toDateTime(joCall.Get('end_time').JsonValue.Value);
      uqCallsto_formatted.AsString := joCall.Get('to_formatted').JsonValue.Value;
      uqCallsphone_number_sid.AsString := joCall.Get('phone_number_sid').JsonValue.Value;
      uqCallscaller_name.AsString := joCall.Get('caller_name').JsonValue.Value;
      uqCallsapi_version.AsString := joCall.Get('api_version').JsonValue.Value;
      uqCallsStatus.AsString := joCall.Get('status').JsonValue.Value;
      uqCalls.Post;

      Inc(addedCount);
    end
    else
      continue;
  end;

  restClient.Free;
  restRequest.Free;
  restResponse.Free;

  Result := addedCount;
end;


function TTwilioDataModule.GetRecordings(count: integer; offset: integer): integer;
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  jsValue: TJSONValue;
  jsObj: TJSONObject;
  jaRecordings: TJSONArray;
  joRec: TJSONObject;
  joTrans: TJSONObject;
  i: integer;
  sql: string;
  turi: string;
  addedCount: Integer;
begin
  restClient := TRESTClient.Create(nil);
  restClient.BaseURL := 'https://api.twilio.com';

  restRequest := TRESTRequest.Create(nil);
  restRequest.Client := restClient;

  restResponse := TRESTResponse.Create(nil);
  restRequest.Response := restResponse;

  restRequest.Method := rmGET;
  restRequest.Resource := '/2010-04-01/Accounts/' + accountSID + '/Recordings.json?PageSize=' + IntToStr(count);

  param := restRequest.Params.AddItem;
  param.Name := 'Authorization';
  param.Kind := pkHTTPHEADER;
  param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
  param.Value := authHeader;

  restRequest.Execute;

  jsValue := restResponse.JSONValue;
  jsObj := TJSONObject(jsValue);
  jaRecordings := TJSONArray(jsObj.GetValue('recordings'));

  addedCount := 0;

  for i := offset to jaRecordings.Count - 1 do
  begin
    joRec := TJSONObject(jaRecordings.Items[i]);
    sql := 'select * from recordings where sid = ' + QuotedStr(joRec.Get('sid').JsonValue.Value);
    uqRecordings.Close;
    uqRecordings.SQL.Text := sql;
    uqRecordings.Open;

    if not uqRecordings.IsEmpty then
      continue;

    uqRecordings.Append;
    uqRecordingsaccount_sid.AsString := joRec.Get('account_sid').JsonValue.Value;
    uqRecordingscall_sid.AsString := joRec.Get('call_sid').JsonValue.Value;
    uqRecordingsdate_created.AsDateTime := toDateTime(joRec.Get('date_created').JsonValue.Value);
    uqRecordingsstatus.AsString := joRec.Get('status').JsonValue.Value;
    uqRecordingsduration.AsString := joRec.Get('duration').JsonValue.Value;
    uqRecordingsapi_version.AsString := joRec.Get('api_version').JsonValue.Value;
    uqRecordingsconference_sid.AsString := joRec.Get('conference_sid').JsonValue.Value;
    uqRecordingsdate_updated.AsDateTime := toDateTime(joRec.Get('date_updated').JsonValue.Value);
    uqRecordingsstart_time.AsDateTime := toDateTime(joRec.Get('start_time').JsonValue.Value);
    uqRecordingssid.AsString := joRec.Get('sid').JsonValue.Value;
    uqRecordingsprice.AsString := joRec.Get('price').JsonValue.Value;
    uqRecordingsprice_unit.AsString := joRec.Get('price_unit').JsonValue.Value;
    uqRecordingschannels.AsString := joRec.Get('channels').JsonValue.Value;
    uqRecordingssource.AsString := joRec.Get('source').JsonValue.Value;
    uqRecordingserror_code.AsString := joRec.Get('error_code').JsonValue.Value;
    uqRecordingsuri.AsString := joRec.Get('uri').JsonValue.Value;
    uqRecordingsencryption_details.AsString := joRec.Get('encryption_details').JsonValue.Value;
    uqRecordingsmedia_url.AsString := joRec.Get('media_url').JsonValue.Value;

    if joRec.Get('duration').JsonValue.Value = '-1' then
    begin
      uqRecordingstranscription.AsString := 'Empty';
    end
    else
    begin
      joTrans := TJSONObject(joRec.Get('subresource_uris').JsonValue);
      turi := joTrans.Get('transcriptions').JsonValue.Value;
      uqRecordingstranscription.AsString := GetTranscription(turi);
    end;

    uqRecordings.Post;
    Inc(addedCount);
  end;

  restClient.Free;
  restRequest.Free;
  restResponse.Free;

  Result := addedCount;
end;


function TTwilioDataModule.GetTranscription(uri: string): string;
// Retrieves a JSONArray of 1 transcription then adds that one transcription
// into the database. The RestAPI call is directly linked to Recording sid so
// this is only called when the recording has an assoiciated transcription. Bulk
// adding transcriptions is currently not doable.
var
restClient: TRESTClient;
restRequest: TRESTRequest;
restResponse: TRESTResponse;
param: TRESTRequestParameter;
jsValue: TJSONValue;
jsObj: TJSONObject;
joTrans: TJSONObject;
jaTrans: TJSONArray;
i: integer;
trans_text: string;
begin
  restClient := TRESTClient.Create(nil);
  restClient.BaseURL := 'https://api.twilio.com';

  restRequest := TRESTRequest.Create(nil);
  restRequest.Client := restClient;

  restResponse := TRESTResponse.Create(nil);
  restRequest.Response := restResponse;

  restRequest.Method := rmGET;
  restRequest.Resource := uri;

  param := restRequest.Params.AddItem;
  param.Name := 'Authorization';
  param.Kind := pkHTTPHEADER;
  param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
  param.Value := authHeader;

  restRequest.Execute;

  jsValue := restResponse.JSONValue;

  jsObj := TJSONObject(jsValue);
  jaTrans := TJSONArray(jsObj.Get('transcriptions').JsonValue);
  if jaTrans.IsEmpty then
  begin
    trans_text := 'Empty';
  end
  else
  begin
  joTrans := TJSONObject(jaTrans.Items[0]);
  trans_text := joTrans.Get('transcription_text').JsonValue.Value;
  end;
  if trans_text = 'null' then
  begin
    trans_text := 'Empty';
  end;

  restClient.Free;
  restRequest.Free;
  restResponse.Free;
  Result := trans_text;
end;


procedure TTwilioDataModule.UpdateDB();
// Updates the calls for all 4 locations and gets the 50 most recent recordings
// Activates on a timer in Main set for every 5 minutes.
// phoneDict: Dictionary containing all the locations which are linked to a
// phone number.
var
  sql: string;
  phoneNum: string;
begin
  sql := 'select * from locations';
  uqLocations.Close;
  uqLocations.SQL.Text := sql;
  uqLocations.Open;
  GetRecordings(50, 0);
  while not uqLocations.Eof do
  begin
    phoneNum := uqLocations.FieldByName('phone_number').AsString;
    GetCalls(phoneNum, 50, 0);
    uqLocations.Next;
  end;
end;


function TTwilioDataModule.toDateTime(date: string): TDateTime;
// Converts the string we were given from twilio into a datetime
// date: string - formatted ddd, dd mmm yyyy hh:nn:ss+zzzz
// Returns a DateTime Object with formating specified below
var
  fs: TFormatSettings;
  dt: TDateTime;
  dateString: String;
begin
  // Removes the very first day(ddd, )
  date := Copy(date, Pos(',', date) + 2, MaxInt);
  // Removes the timezone(+zzzz) as it is always +0000
  date := Copy(date, 1, Pos(' +', date)-1);
  fs.Create;
  fs.DateSeparator := ' ';
  fs.TimeSeparator := ':';
  fs.ShortDateFormat := 'mm/dd/yyyy';
  fs.ShortTimeFormat := 'hh:nn:ss';

  dt := VarToDateTime(date);
  Result := dt;
end;


procedure TTwilioDataModule.ShowCalls(PhoneNum: string);
var
SQL: string;
begin
  SQL := 'select * ' + ' from calls where to_formatted = '
          + QuotedStr(PhoneNum) + ' order by calls.date_created desc';

  uqCalls.SQL.Text := SQL;
  uqCalls.Open;
end;


procedure TTwilioDataModule.DataModuleCreate(Sender: TObject);
var
  IniFile: TIniFile;
  iniStr: string;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
  Logger.Log(5, 'Twilio Data Module Created');
  try
    iniStr := IniFile.ReadString('Database', 'Server', '');
    if iniStr <> '' then
      ucEnvoy.Server := iniStr;

    iniStr := IniFile.ReadString('Database', 'Database', '');
    if iniStr <> '' then
      ucEnvoy.Database := iniStr;

    iniStr := IniFile.ReadString('Database', 'Username', '');
    if iniStr <> '' then
      ucEnvoy.Username := iniStr;

    iniStr := IniFile.ReadString('Database', 'Password', '');
    if iniStr <> '' then
      ucEnvoy.Password := iniStr;

    // Same logic as before for Twilio...
    iniStr := IniFile.ReadString('Twilio', 'AccountSID', '');
    if iniStr <> '' then
      accountSID := iniStr;

    iniStr := IniFile.ReadString('Twilio', 'AuthHeader', '');
    if iniStr <> '' then
      authHeader := iniStr;

  finally
    IniFile.Free;
  end;

  uqLocations.Open;
end;


procedure TTwilioDataModule.DataModuleDestroy(Sender: TObject);
begin
  Logger.Log(5, 'Twilio Data Module Destroyed');
  uqLocations.Close;
end;


function TTwilioDataModule.FullUpdate: String;
var
  sql, phoneNum: string;
  count, offset, added: integer;
  call_count, rec_count: integer;
  resultString: string;
begin
  call_count := 0;
  rec_count := 0;

  // Update calls for each location
  sql := 'select * from locations';
  uqLocations.Close;
  uqLocations.SQL.Text := sql;
  uqLocations.Open;

  while not uqLocations.Eof do
  begin
    phoneNum := uqLocations.FieldByName('phone_number').AsString;
    offset := 0;
    count := 50;

    repeat
      added := GetCalls(phoneNum, count, offset);
      if added > 0 then
        Inc(call_count, added);
      Inc(offset, count);
    until added = 0;

    uqLocations.Next;
  end;

  // Update recordings
  offset := 0;
  count := 50;

  repeat
    added := GetRecordings(count, offset);
    if added > 0 then
      Inc(rec_count, added);
    Inc(offset, count);
  until added = 0;

  resultString := 'Added ~' + IntToStr(call_count) + ' calls to database' + sLineBreak +
                  'Added ~' + IntToStr(rec_count) + ' recordings to database';
  Result := resultString;
end;


end.
