// OneScript: ./OInt/core/Modules/OPI_Twitter.os
// Lib: Twitter
// CLI: twitter

// MIT License

// Copyright (c) 2023 Anton Tsitavets

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// https://github.com/Bayselonarrend/OpenIntegrations
// If you don't know where to start, you should find the GetStandardParameters method()
// and read comments

// BSLLS:Typo-off
// BSLLS:LatinAndCyrillicSymbolInWord-off
// BSLLS:IncorrectLineBreak-off
// BSLLS:NumberOfOptionalParams-off
// BSLLS:UsingServiceTag-off

//@skip-check method-too-many-params
//@skip-check module-structure-top-region
//@skip-check module-structure-method-in-regions
//@skip-check wrong-string-literal-content

// Uncomment if OneScript is executed
// #Use "../../tools"

#Region Public

#Region DataAndSettings

// Get authorization link
// Forms a link for authorization via the browser
//
// Parameters:
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// String - URL for browser transition
Function GetAuthorizationLink(Parameters = "") Export

    Parameters_ = GetStandardParameters(Parameters);

    URLParameters = New Structure;

    URLParameters.Insert("response_type"        , "code");
    URLParameters.Insert("client_id"            , Parameters_["client_id"]);
    URLParameters.Insert("redirect_uri"         , Parameters_["redirect_uri"]);
    URLParameters.Insert("scope"                , Parameters_["scope"]);
    URLParameters.Insert("state"                , "state");
    URLParameters.Insert("code_challenge"       , "challenge");
    URLParameters.Insert("code_challenge_method", "plain");

    URLParameters = OPI_Tools.RequestParametersToString(URLParameters);
    Link          = "https://twitter.com/i/oauth2/authorize" + URLParameters;

    Return Link;

EndFunction

// Get token
// Gets the token by the code received when authorizing using the link from GetAuthorizationLink
//
// Parameters:
// Code - String - Code obtained from authorization See GetAuthorizationLink - code
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function GetToken(Val Code, Val Parameters = "") Export

    OPI_TypeConversion.GetLine(Code);

    Parameters_ = GetStandardParameters(Parameters);

    RequestParameters = New Structure;
    RequestParameters.Insert("code"         , Code);
    RequestParameters.Insert("grant_type"   , "authorization_code");
    RequestParameters.Insert("client_id"    , Parameters_["client_id"]);
    RequestParameters.Insert("redirect_uri" , Parameters_["redirect_uri"]);
    RequestParameters.Insert("code_verifier", "challenge");

    Response = OPI_Tools.Post("https://api.twitter.com/2/oauth2/token"
        , RequestParameters, , False);

    Return Response;

EndFunction

// Refresh token
// Updates the v2 token using the refresh_token
//
// Parameters:
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function RefreshToken(Val Parameters = "") Export

    Parameters_ = GetStandardParameters(Parameters);
    Refresh     = "refresh_token";

    RequestParameters = New Structure;
    RequestParameters.Insert(Refresh      , Parameters_[Refresh]);
    RequestParameters.Insert("grant_type" , Refresh);
    RequestParameters.Insert("client_id"  , Parameters_["client_id"]);

    Response = OPI_Tools.Post("https://api.twitter.com/2/oauth2/token"
    , RequestParameters, , False);

    Return Response;

EndFunction

// !NOCLI
// Method for insertion into an http service, the address of which is specified in redirect_uri
// Calls the token acquisition method, as for obtaining a token from the code received
// on redirect_uri after authorization via the browser is only 30 seconds
//
// Parameters:
// Request - HTTPServiceRequest - Request coming to the http service
//
// Returns:
// HTTPResponse, Arbitrary, BinaryData - Result of reading the JSON response from the server
Function HandleIncomingRequestAfterAuthorization(Request) Export

    Code          = Request.RequestParameters["code"];
    TokenResponse = GetToken(Code);

    // BSLLS:CommentedCode-off
    // Preferred token storage
    // Constants.TwitterRefresh.Set(ResponseToken["refresh_token"]);
    // Constants.TwitterToken.Set(ResponseToken["access_token"]);
    // BSLLS:CommentedCode-on

    Return TokenResponse;

EndFunction

#EndRegion

#Region Tweets

// !NOCLI
// Create custom tweet
//
// Parameters:
// Text - String - Tweet text
// MediaArray - Array of String, BinaryData - Array of binary data or file paths
// PollOptionsArray - Array of String - Array of poll options, if necessary
// PollDuration - String, Number - Poll duration if necessary (poll without duration is not created)
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreateCustomTweet(Val Text = ""
    , Val MediaArray = ""
    , Val PollOptionsArray = ""
    , Val PollDuration = ""
    , Val Parameters = "") Export

    OPI_TypeConversion.GetLine(Text);
    OPI_TypeConversion.GetLine(PollDuration);

    If ValueIsFilled(MediaArray) Then
        OPI_TypeConversion.GetCollection(MediaArray);
    EndIf;

    If ValueIsFilled(PollOptionsArray) Then
        OPI_TypeConversion.GetCollection(PollOptionsArray);
    EndIf;

    Parameters_ = GetStandardParameters(Parameters);
    URL         = "https://api.twitter.com/2/tweets";
    Array       = "Array";
    Fields      = New Map;

    If ValueIsFilled(Text) Then
        Fields.Insert("text", Text);
    EndIf;

    If TypeOf(PollOptionsArray) = Type(Array) And ValueIsFilled(PollDuration) Then

        PollDuration = Number(PollDuration);

        If PollOptionsArray.Count() > 0 Then

            OptionStructure = New Structure("options,duration_minutes", PollOptionsArray, PollDuration);
            Fields.Insert("poll", OptionStructure);

        EndIf;

    EndIf;

    If TypeOf(MediaArray) = Type(Array) Then
        If MediaArray.Count() > 0 Then
            Fields.Insert("media", New Structure("media_ids", MediaArray));
        EndIf;
    EndIf;

    Authorization = CreateAuthorizationHeaderV2(Parameters_);
    Response      = OPI_Tools.Post(URL, Fields, Authorization);

    Return Response;

EndFunction

// Create text tweet
// Creates a tweet without attachments
//
// Parameters:
// Text - String - Tweet text - text
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreateTextTweet(Val Text, Val Parameters = "") Export
    Return CreateCustomTweet(Text, , , , Parameters);
EndFunction

// Create image tweet
// Creates a tweet with an image attachment
//
// Parameters:
// Text - String - Tweet text - text
// ImageArray - Array of String, BinaryData - Image files array - pictures
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreateImageTweet(Val Text, Val ImageArray, Val Parameters = "") Export

    MediaArray = UploadAttachmentsArray(ImageArray, "photo", Parameters);
    Return CreateCustomTweet(Text, MediaArray, , , Parameters);

EndFunction

// Create gif tweet
// Creates a tweet with a gif attachment
//
// Parameters:
// Text - String - Tweet text - text
// GifsArray - Array of String, BinaryData - Gif files array - gifs
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreateGifTweet(Val Text, Val GifsArray, Val Parameters = "") Export

    MediaArray = UploadAttachmentsArray(GifsArray, "animated_gif", Parameters);
    Return CreateCustomTweet(Text, MediaArray, , , Parameters);

EndFunction

// Create video tweet
// Creates a tweet with a video attachment
//
// Parameters:
// Text - String - Tweet text - text
// VideosArray - Array of String, BinaryData - Video files array - videos
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreateVideoTweet(Val Text, Val VideosArray, Val Parameters = "") Export

    MediaArray = UploadAttachmentsArray(VideosArray, "video", Parameters);
    Return CreateCustomTweet(Text, MediaArray, , , Parameters);

EndFunction

// Create poll tweet
// Creates a tweet with a poll
//
// Parameters:
// Text - String - Tweet text - text
// OptionArray - Array of String - Poll options array - options
// Duration - String, Number - Poll duration - duration
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Map Of KeyAndValue - serialized JSON response from Twitter
Function CreatePollTweet(Val Text, Val OptionArray, Val Duration, Val Parameters = "") Export

    Return CreateCustomTweet(Text, , OptionArray, Duration, Parameters);

EndFunction

// Upload attachments array !NOCLI
// Uploads files to the server and returns their IDs
//
// Parameters:
// ArrayOfFiles - Array of String, BinaryData - Files array
// AttachmentsType - String - Attachments type
// Parameters - Structure Of String - See GetStandardParameters - auth - Authorization JSON or path to .json
//
// Returns:
// Array Of String - Media ID array
Function UploadAttachmentsArray(Val ArrayOfFiles, Val AttachmentsType, Val Parameters = "") Export

    OPI_TypeConversion.GetLine(AttachmentsType);
    OPI_TypeConversion.GetCollection(ArrayOfFiles);

    MediaArray  = New Array;
    Parameters_ = GetStandardParameters(Parameters);
    MIS         = "media_id_string";

    If ValueIsFilled(ArrayOfFiles) Then

        For Each SendingFile In ArrayOfFiles Do

            OPI_TypeConversion.GetBinaryData(SendingFile);

            Response = UploadMediaFile(SendingFile, AttachmentsType, Parameters_);
            MediaID  = Response[MIS];

            If Not ValueIsFilled(MediaID) Then
                Return Response;
            EndIf;

            MediaArray.Add(MediaID);

        EndDo;

    EndIf;

    Return MediaArray;

EndFunction

#EndRegion

#EndRegion

#Region Private

Function UploadMediaFile(Val File, Val Type, Val Parameters)

    OPI_TypeConversion.GetBinaryData(File);

    RequestType = "POST";
    Parameters_ = GetStandardParameters(Parameters);
    URL         = "https://upload.twitter.com/1.1/media/upload.json";

    If Type = "photo" Then

        Fields = New Structure;
        Fields.Insert("media_data"    , Base64String(File));
        Fields.Insert("media_category", Type);

        Authorization = CreateAuthorizationHeaderV1(Parameters_, Fields, RequestType, URL);
        Response      = OPI_Tools.Post(URL, Fields, Authorization, False);

    Else

        Response = UploadMediaInParts(File, Type, RequestType, URL, Parameters_);

    EndIf;

    Return Response;

EndFunction

Function UploadMediaInParts(Val File, Val Type, Val RequestType, Val URL, Parameters)

    Unit     = 1024;
    Count    = 4;
    MediaKey = "media_key";
    MIS      = "media_id_string";
    Command  = "command";
    Size     = File.Size();

    MIMETypeMapping = New Map;
    MIMETypeMapping.Insert("photo"       , "image/jpeg");
    MIMETypeMapping.Insert("video"       , "video/mp4");
    MIMETypeMapping.Insert("animated_gif", "image/gif");

    ChunkSize    = Count * Unit * Unit;
    ArrayReading = SplitBinaryData(File, ChunkSize);

    Fields = New Structure;
    Fields.Insert(Command          , "INIT");
    Fields.Insert("total_bytes"    , OPI_Tools.NumberToString(Size));
    Fields.Insert("media_type"     , MIMETypeMapping.Get(Type));
    Fields.Insert("media_category" , Type);

    Authorization = CreateAuthorizationHeaderV1(Parameters, Fields, RequestType, URL);

    InitializationResponse = OPI_Tools.Post(URL, Fields, Authorization, False);
    InitializationKey      = InitializationResponse[MediaKey];
    InitializationID       = InitializationResponse[MIS];

    If Not ValueIsFilled(InitializationKey) Or Not ValueIsFilled(InitializationID) Then
        Return InitializationResponse;
    EndIf;

    Counter = 0;

    For Each Part In ArrayReading Do

        Fields = New Structure;
        Fields.Insert(Command         , "APPEND");
        Fields.Insert("media_key"     , InitializationKey);
        Fields.Insert("segment_index" , OPI_Tools.NumberToString(Counter));
        Fields.Insert("media"         , Part);

        Authorization = CreateAuthorizationHeaderV1(Parameters, New Structure, RequestType, URL);

        OPI_Tools.PostMultipart(URL, Fields, , , Authorization);

        Counter = Counter + 1;

    EndDo;

    Fields = New Structure;
    Fields.Insert(Command   , "FINALIZE");
    Fields.Insert("media_id", InitializationID);

    ProcessingStatus = GetProcessingStatus(Parameters, Fields, URL);

    If Not TypeOf(ProcessingStatus) = Type("String") Then
        Return ProcessingStatus;
    EndIf;

    Response = WaitForProcessingCompletion(ProcessingStatus, InitializationID, URL, Parameters);

    Return Response;

EndFunction

Function WaitForProcessingCompletion(Val ProcessingStatus, Val InitializationID, Val URL, Val Parameters)

    ProcessingInfo = "processing_info";
    Command        = "command";
    Fields         = New Structure;

    Fields.Insert(Command   , "STATUS");
    Fields.Insert("media_id", InitializationID);

    WHile String(ProcessingStatus) = "pending" Or String(ProcessingStatus) = "in_progress" Do

        Authorization = CreateAuthorizationHeaderV1(Parameters, Fields, "GET", URL);
        Response      = OPI_Tools.Get(URL, Fields, Authorization);
        Information   = Response[ProcessingInfo];

        If Not ValueIsFilled(Information) Then
            Return Response;
        EndIf;

        ProcessingStatus = Information["state"];

        If Not ValueIsFilled(ProcessingStatus) Then
            Return Response;
        EndIf;

    EndDo;

    If ProcessingStatus = "failed" Then
        Raise "Twitter could not process the video you uploaded";
    EndIf;

    Return Response;

EndFunction

Function GetStandardParameters(Val Parameters = "")

    // The definition of the data required for the work is collected here.
    // For Twitter, this is quite a significant set, which is due to the presence of 2 APIs at once,
    // which, at the same time, are not created for different tasks, but are simply versions of each other.
    // The current version of the API is v2 and it requires obtaining temporary tokens. Despite the fact,
    // that Twitter insists on using this latest version, they somehow managed not to transfer
    // file upload mechanism and some others from the old version - v1.1. Therefore, something needs to be done
    // on version 1.1, and something on 2: up to the point that they removed the ability to post tweets from v1.1,
    // but only through it you can add a picture to the tweet. At the same time, their authentication methods and tokens are different

    // The world gigacorporation of Elon Musk, by the way, a reminder ;)

    // P.S The following is often referred to as the "Twitter Developer settings page" - this
    // https://developer.twitter.com/en/portal/dashboard and chosing of project from list (key icon)

    Parameters_ = New Map;
    Permissions = "tweet.read tweet.write tweet.moderate.write users.read "
        + "follows.read follows.write offline.access space.read mute.read "
        + "mute.write like.read like.write list.read list.write block.read "
        + "block.write bookmark.read bookmark.write";

    // Data for API v2

    // redirect_uri - URL of your http service (or other request handler) for authorization
    // scope - a set of permissions for the received key. Can be any, but offline.access is mandatory
    // client_id - From OAuth 2.0 Client ID and Client Secret settings page of Twitter Developer
    // client_secret - From OAuth 2.0 Client ID and Client Secret settings page of Twitter Developer
    // access_token - GetAuthorizationLink() -> Browser -> code will come to redirect_uri -> GetToken(code)
    // refresh_token - Comes together with access_token and is used to refresh it (access_token lifetime - 2 hr)
    // The update is done using the UpdateToken method with new access_token and refresh_token.
    // For the next update, you need to use a new refresh_token, so hardcode
    // won't work (access_token won't work either)

    // |--> RefreshToken() ->|access_token --> 2 hrs. lifetime
    // | |refresh_token --|
    // |--------[after 2 hrs.]-------------------|

    // Data for API v1.1

    // oauth_token - From Authentication Tokens -> Access Token and Secret settings page of Twitter Developer
    // oauth_token_secret - From Authentication Tokens -> Access Token and Secret settings page of Twitter Developer
    // oauth_consumer_key - From Consumer Keys -> Access Token and Secret settings page of Twitter Developer
    // oauth_consumer_secret - From Consumer Keys -> Access Token and Secret settings page of Twitter Developer

    // These tokens do not need to be updated

    Parameters_.Insert("redirect_uri"         , "");
    Parameters_.Insert("scope"                , Permissions);
    Parameters_.Insert("client_id"            , "");
    Parameters_.Insert("client_secret"        , "");
    Parameters_.Insert("access_token"         , ""); // Should be something like Constants.TwitterToken.Get()
    Parameters_.Insert("refresh_token"        , ""); // Should be something like Constants.TwitterRefresh.Get()
    Parameters_.Insert("oauth_token"          , "");
    Parameters_.Insert("oauth_token_secret"   , "");
    Parameters_.Insert("oauth_consumer_key"   , "");
    Parameters_.Insert("oauth_consumer_secret", "");

    OPI_TypeConversion.GetCollection(Parameters);

    If TypeOf(Parameters) = Type("Structure") Or TypeOf(Parameters) = Type("Map") Then
        For Each PassedParameter In Parameters Do
            Parameters_.Insert(PassedParameter.Key, OPI_Tools.NumberToString(PassedParameter.Value));
        EndDo;
    EndIf;

    Return Parameters_;

EndFunction

Function CreateAuthorizationHeaderV1(Val Parameters, Val Fields, Val RequestType, Val URL)

    CurrentDate         = OPI_Tools.GetCurrentDate();
    AuthorizationHeader = "";
    HashingMethod       = "HMAC-SHA1";
    APIVersion          = "1.0";
    SignatureString     = "";
    Signature           = "";
    OCK = "oauth_consumer_key";
    OTK = "oauth_token";
    CurrentUNIXDate     = OPI_Tools.UNIXTime(CurrentDate);
    CurrentUNIXDate     = OPI_Tools.NumberToString(CurrentUNIXDate);
    ParametersTable     = New ValueTable;
    ParametersTable.Columns.Add("Key");
    ParametersTable.Columns.Add("Value");

    For Each Field In Fields Do

        NewLine       = ParametersTable.Add();
        NewLine.Key   = Field.Key;
        NewLine.Value = Field.Value;

    EndDo;

    NewLine       = ParametersTable.Add();
    NewLine.Key   = OCK;
    NewLine.Value = Parameters[OCK];

    NewLine       = ParametersTable.Add();
    NewLine.Key   = OTK;
    NewLine.Value = Parameters[OTK];

    NewLine       = ParametersTable.Add();
    NewLine.Key   = "oauth_version";
    NewLine.Value = APIVersion;

    NewLine       = ParametersTable.Add();
    NewLine.Key   = "oauth_signature_method";
    NewLine.Value = HashingMethod;

    NewLine       = ParametersTable.Add();
    NewLine.Key   = "oauth_timestamp";
    NewLine.Value = CurrentUNIXDate;

    NewLine       = ParametersTable.Add();
    NewLine.Key   = "oauth_nonce";
    NewLine.Value = CurrentUNIXDate;

    For Each TableRow In ParametersTable Do

        TableRow.Key   = EncodeString(TableRow.Key, StringEncodingMethod.URLencoding);
        TableRow.Value = EncodeString(TableRow.Value, StringEncodingMethod.URLencoding);

    EndDo;

    ParametersTable.Sort("Key");

    For Each TableRow In ParametersTable Do

        SignatureString = SignatureString
            + TableRow.Key
            + " ="
            + TableRow.Value
            + "&";

    EndDo;

    SignatureString = Left(SignatureString, StrLen(SignatureString) - 1);
    SignatureString = Upper(RequestType)
        + "&"
        + EncodeString(URL            , StringEncodingMethod.URLencoding)
        + "&"
        + EncodeString(SignatureString, StringEncodingMethod.URLencoding);

    Signature = EncodeString(Parameters["oauth_consumer_secret"], StringEncodingMethod.URLencoding)
        + "&"
        + EncodeString(Parameters["oauth_token_secret"], StringEncodingMethod.URLencoding);

    Signature = OPI_Cryptography.HMAC(GetBinaryDataFromString(Signature)
        , GetBinaryDataFromString(SignatureString)
        , HashFunction.SHA1
        , 64);

    Signature = EncodeString(Base64String(Signature), StringEncodingMethod.URLencoding);

    Delimiter           = """,";
    AuthorizationHeader = AuthorizationHeader
        + "OAuth "
        + "oauth_consumer_key =""" + Parameters[OCK] + Delimiter
        + "oauth_token =""" + Parameters[OTK] + Delimiter
        + "oauth_signature_method=""" + HashingMethod + Delimiter
        + "oauth_timestamp =""" + CurrentUNIXDate + Delimiter
        + "oauth_nonce =""" + CurrentUNIXDate + Delimiter
        + "oauth_version =""" + APIVersion + Delimiter
        + "oauth_signature =""" + Signature;

        HeaderMapping = New Map;
        HeaderMapping.Insert("authorization", AuthorizationHeader);

    Return HeaderMapping;

EndFunction

Function CreateAuthorizationHeaderV2(Val Parameters)

    ReturnMapping = New Map;
    ReturnMapping.Insert("Authorization", "Bearer " + Parameters["access_token"]);

    Return ReturnMapping;

EndFunction

Function GetProcessingStatus(Val Parameters, Val Fields, Val URL)

    ProcessingInfo = "processing_info";
    Authorization  = CreateAuthorizationHeaderV1(Parameters, Fields, "POST", URL);

    Response    = OPI_Tools.Post(URL, Fields, Authorization, False);
    Information = Response[ProcessingInfo];

    If Not ValueIsFilled(Information) Then
        Return Response;
    EndIf;

    ProcessingStatus = Information["state"];

    If Not ValueIsFilled(ProcessingStatus) Then
        Return Response;
    Else
        Return ProcessingStatus;
    EndIf;

EndFunction

#EndRegion
