Search code examples
.netvb.netfile-uploadimgur

Accessing imgUr thru OAuth (uploading to user account)


To start doing this "simple" task I've researched for a procedure that I've taken as an example here to follow and reproduce the steps, the procedure can upload an image "anonymously":

Private ReadOnly ClientId As String = "My Client ID" ' => "..............."
Private ReadOnly ClientSecret As String = "My Client Secret" ' => "........................................"

' Usage:
' Dim url As String = UploadImage("C:\Image.jpg") : MessageBox.Show(url)
Public Function UploadImage(ByVal image As String)

    Dim w As New WebClient()
    w.Headers.Add("Authorization", "Client-ID " & ClientId)
    Dim Keys As New System.Collections.Specialized.NameValueCollection

    Try

        Keys.Add("image", Convert.ToBase64String(File.ReadAllBytes(image)))
        Dim responseArray As Byte() = w.UploadValues("https://api.imgur.com/3/image", Keys)
        Dim result = Encoding.ASCII.GetString(responseArray)
        Dim reg As New System.Text.RegularExpressions.Regex("link"":""(.*?)""")
        Dim match As Match = reg.Match(result)
        Dim url As String = match.ToString.Replace("link"":""", "").Replace("""", "").Replace("\/", "/")
        Return url

    Catch s As Exception

        MessageBox.Show("Something went wrong. " & s.Message)
        Return "Failed!"

    End Try

End Function

But what I really would like to do is upload the image into my user account, which is http://elektrostudios.imgur.com.

I've found this question but what him said in the answer is not clear for my (due to my newbie knowledges), anyways I've tried to use the function above but just sending the BEARER header with my ClientSecret ID 'cause If I understood good what the oauth 2 api documentation says the token could also be the ClientSecret Id?, but I don't get the expected result.

So searching the way to get a proper acces token I've seen this else question which helped me to discover RestSharp library and to know how to send the request, I did some modifications to use it with Imgur API but I'm getting this error-response:

{"data":{"error":"client_id and response_type are required","request":"\/oauth2\/authorize","method":"POST"},"success":false,"status":400}

This is what I have:

Public Sub GetAccessToken()

    Dim xrc As RestClient = New RestClient
    Dim grant_type As String = "authorization_code"
    Dim request As New RestRequest(Method.POST)
    Dim strBody As String
    Dim response As RestResponse
    Dim strResponse As String

    request.Method = Method.POST
    request.RequestFormat = DataFormat.Xml

    'Base URL
    xrc.BaseUrl = "https://api.imgur.com"

    'Resource
    request.Resource = "oauth2/authorize"

    'Format body
    strBody = String.Format("client_id={0}&response_type={1}", ClientId, ClientSecret)

    'Add body to request
    request.AddBody("Authorization", strBody)

    'Execute
    response = xrc.Execute(request)

    'Parse Response
    strResponse = response.Content

    MessageBox.Show(response.Content.ToString)

End Sub

So my question are 2 in 1:

How I can upload an Image into an Imgur user account using the required things such as the access token?.

PS: Please remember that even getting the access token I don't know how to use it after stored it.

UPDATE:

I'm trying to use @Plutonix solution but when I try to request the Token it throws an exception "Need a valid PIN first", I'm using a valid ClientId and ClientSecret, I'm missing something more?, here is the code:

Private imgUR As New imgurAPI("my client id", "my client secret")

Private Sub Button1_Click() Handles Button1.Click

    Dim wb As New WebBrowser
    imgUR.RequestPinBrowser(wb)

    ' The instruction below throws an exception:
    ' "Need a valid PIN first"
    Dim result As imgurAPI.imgUrResults = imgUR.RequestToken
    wb.Dispose()

    ' check result
    If result = imgurAPI.imgUrResults.OK Then

        ' assumes the file exists
        imgUR.UploadImage("C:\Test.jpg", False)

        Clipboard.SetText(imgUR.LastImageLink)
        MessageBox.Show(imgUR.LastImageLink)

    Else
        MessageBox.Show(String.Format("Error getting access token. Status:{0}",
            result.ToString))
    End If

End Sub

Solution

  • This is long, because it is more or less a compleat API:

    Public Class imgurAPI
        ' combination of this API and imgUR server responses
        Public Enum imgUrResults
            OK = 200                        ' AKA Status200 
    
            ' errors WE return
            OtherAPIError = -1              ' so far, just missing ImgLink
            InvalidToken = -2
            InvalidPIN = -3                 ' pins expire
            InvalidRequest = -4
            TokenParseError = -5
    
            ' results we get from server
            BadRequestFormat = 400          ' Status400   
            AuthorizationError = 401        ' Status401  
    
            Forbidden = 403                 ' Status403   
            NotFound = 404                  ' Status404   ' bad URL Endpoint
            RateLimitError = 429            ' Status429   ' RateLimit Error
            ServerError = 500               ' Status500   ' internal server error
    
            UknownStatus = 700              ' We havent accounted for it (yet), 
                                            '   may be trivial or new
        End Enum
    
        ' container for the cool stuff they send us
        Friend Class Token
            Public Property AcctUserName As String
            Public Property AccessToken As String
            Public Property RefreshToken As String
            Public Property Expiry As DateTime
    
            Public Sub New()
                AcctUserName = ""
                AccessToken = ""
                RefreshToken = ""
                Expiry = DateTime.MinValue
            End Sub
    
            Friend Function IsExpired() As Boolean
    
                If (Expiry > DateTime.Now) Then
                    Return False
                Else
                    ' if expired reset everything so some moron doesnt
                    ' expose AccessToken and test for ""
                    AcctUserName = ""
                    AccessToken = ""
                    RefreshToken = ""
                    Expiry = DateTime.MinValue
                    Return True
                End If
            End Function
    
        End Class
    
        ' NO simple ctor!!!
        ' constructor initialized with ClientID and SecretID
        Public Sub New(clID As String, secret As String)
            clientID = clID
            clientSecret = secret
            myPin = ""
            imgToken = New Token
            LastImageLink = ""
            UseClipboard = True
            AnonOnly = False
        End Sub
    
        ' constructor initialized with ClientID and SecretID
        Public Sub New(clID As String)
            clientID = clID
            clientSecret = ""
            myPin = ""
            imgToken = New Token
            LastImageLink = ""
            UseClipboard = True
            AnonOnly = True
        End Sub
    
    
        Private clientID As String
        Private clientSecret As String
    
        Private AnonOnly As Boolean = True
    
        ' tokens are not public
        Private imgToken As Token
    
        Public Property LastImageLink As String
    
        Public Property UseClipboard As Boolean
    
        ' precise moment when it expires for use in code
        Public ReadOnly Property TokenExpiry As DateTime
            Get
                If imgToken IsNot Nothing Then
                    Return imgToken.Expiry
                Else
                    Return Nothing
                End If
            End Get
        End Property
    
        Public Function GetExpiryCountdown() As String
            Return String.Format("{0:hh\:mm\:ss}", GetExpiryTimeRemaining)
        End Function
    
        ' time left as a TimeSpan
        Public Function GetExpiryTimeRemaining() As TimeSpan
            Dim ts As New TimeSpan(0)
    
            If imgToken Is Nothing Then
                Return ts
            End If
    
            If DateTime.Now > imgToken.Expiry Then
                Return ts
            Else
                ts = imgToken.Expiry - DateTime.Now
                Return ts
            End If
    
        End Function
    
        Public Function IsTokenValid() As Boolean
    
            If imgToken Is Nothing Then
                Return False
            End If
    
            If String.IsNullOrEmpty(imgToken.AcctUserName) Then
                Return False
            End If
    
            If imgToken.IsExpired Then
                Return False
            End If
    
            Return True
    
        End Function
    
        ' Currently, the PIN is set from a calling App.  Might be possible
        ' to feed the log in to imgUr to get a PIN
        Private myPin As String
        Public WriteOnly Property Pin As String
            Set(value As String)
                myPin = value
            End Set
        End Property
    
    
        ' Navigates to the web page.
        ' see wb_DocumentCompleted for code to 
        ' parse the PIN from the document
        Public Sub RequestPinBrowser(BrowserCtl As WebBrowser)
    
            If AnonOnly Then
                ' you do not need a PIN for Anon
                Throw New ApplicationException("A PIN is not needed for ANON Uploads")
                Exit Sub
            End If
    
            If BrowserCtl Is Nothing Then
                Throw New ArgumentException("Missing a valid WebBrowser reference")
                Exit Sub
            End If
    
            ' imgur API format
            ' https://api.imgur.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=REQUESTED_RESPONSE_TYPE&state=APPLICATION_STATE
    
            Dim OAuthUrlTemplate = "https://api.imgur.com/oauth2/authorize?client_id={0}&response_type={1}&state={2}"
            Dim ReqURL As String = String.Format(OAuthUrlTemplate, clientID, "pin", "ziggy")
    
            BrowserCtl.Url = New Uri(ReqURL)
        End Sub
    
    
        Public Function GetAccessToken() As imgUrResults
            ' there are different types of token requests
            ' which vary only by the data submitted
    
            Dim sReq As String = String.Format("client_id={0}&client_secret={1}&grant_type=pin&pin={2}",
                                                clientID, clientSecret, myPin)
            If myPin = String.Empty Then
                Return imgUrResults.InvalidPIN
            End If
    
            If AnonOnly Then Return imgUrResults.InvalidRequest
    
            ' call generic token processor
            Return RequestToken(sReq)
    
        End Function
    
        ' request a Token 
        Private Function RequestToken(sRequest As String) As imgUrResults
            Dim url As String = "https://api.imgur.com/oauth2/token/"
    
            Dim myResult As imgUrResults = imgUrResults.OK
    
            ' create request for the URL, using POST method
            Dim request As WebRequest = WebRequest.Create(url)
            request.Method = "POST"
    
            ' convert the request, set content format, length
            Dim data As Byte() = System.Text.Encoding.UTF8.GetBytes(sRequest)
            request.ContentType = "application/x-www-form-urlencoded"
            request.ContentLength = data.Length
    
            ' write the date to request stream
            Dim dstream As Stream = request.GetRequestStream
            dstream.Write(data, 0, data.Length)
            dstream.Close()
    
            ' json used on the response and potential WebException
            Dim json As New JavaScriptSerializer()
    
            ' prepare for a response
            Dim response As WebResponse = Nothing
            Dim SvrResponses As Dictionary(Of String, Object)
    
            Try
                response = request.GetResponse
                ' convert status code to programmatic result
                myResult = GetResultFromStatus(CType(response, HttpWebResponse).StatusCode)
    
            Catch ex As WebException
                ' a bad/used pin will throw an exception
                Dim resp As String = New StreamReader(ex.Response.GetResponseStream()).ReadToEnd()
    
                SvrResponses = CType(json.DeserializeObject(resp.ToString), 
                                        Dictionary(Of String, Object))
                myResult = GetResultFromStatus(Convert.ToString(SvrResponses("status")))
    
            End Try
    
            'Console.WriteLine(CType(response, HttpWebResponse).StatusDescription)
            'Console.WriteLine(CType(response, HttpWebResponse).StatusCode)
    
            ' premature evacuation
            If myResult <> imgUrResults.OK Then
                If dstream IsNot Nothing Then
                    dstream.Close()
                    dstream.Dispose()
                End If
                If response IsNot Nothing Then
                    response.Close()
                End If
    
                Return myResult
            End If
    
            ' read the response stream
            dstream = response.GetResponseStream
            Dim SvrResponseStr As String
            Using sr As StreamReader = New StreamReader(dstream)
                ' stream to string
                SvrResponseStr = sr.ReadToEnd
                'Console.WriteLine(SvrResponse)
            End Using
    
            ' close streams
            dstream.Close()
            dstream.Dispose()
            response.Close()
    
            Try
                ' use json serialier to parse the result(s)
                ' convert SvrRsponse to Dictionary
                SvrResponses = CType(json.DeserializeObject(SvrResponseStr), 
                                        Dictionary(Of String, Object))
    
                ' get stuff from Dictionary
                imgToken.AccessToken = Convert.ToString(SvrResponses("access_token"))
                imgToken.RefreshToken = Convert.ToString(SvrResponses("refresh_token"))
                imgToken.AcctUserName = Convert.ToString(SvrResponses("account_username"))
    
                ' convert expires_in to a point in time
                Dim nExp As Integer = Convert.ToInt32(Convert.ToString(SvrResponses("expires_in")))
                imgToken.Expiry = Date.Now.Add(New TimeSpan(0, 0, nExp))
    
                ' Pins are single use
                ' throw it away since it is no longer valid 
                myPin = ""
    
            Catch ex As Exception
                'MessageBox.Show(ex.Message)
                myResult = imgUrResults.TokenParseError
            End Try
    
            Return myResult
    
    
        End Function
    
        ' public interface to check params before trying to upload
        Public Function UploadImage(filename As String, Optional Anon As Boolean = False) As imgUrResults
    
            If AnonOnly Then
                Return DoImageUpLoad(filename, AnonOnly)
            Else
                If IsTokenValid() = False Then
                    Return imgUrResults.InvalidToken
                End If
            End If
    
            ' should be the job of the calling app to test for FileExist
            Return DoImageUpLoad(filename, Anon)
    
        End Function
    
        ' actual file uploader
        Private Function DoImageUpLoad(fileName As String, Optional Anon As Boolean = False) As imgUrResults
            Dim result As imgUrResults = imgUrResults.OK
            LastImageLink = ""
    
            Try
                ' create a WebClient 
                Using wc = New Net.WebClient()
                    ' read image
                    Dim values = New NameValueCollection() From
                            {
                                {"image", Convert.ToBase64String(File.ReadAllBytes(fileName))}
                            }
                    ' type of headers depends on whether this is an ANON or ACCOUNT upload
                    If Anon Then
                        wc.Headers.Add("Authorization", "Client-ID " + clientID)
                    Else
                        wc.Headers.Add("Authorization", "Bearer " & imgToken.AccessToken)
                    End If
    
                    ' upload, get response
                    Dim response = wc.UploadValues("https://api.imgur.com/3/upload.xml", values)
    
                    ' read response converting byte array to stream
                    Using sr As New StreamReader(New MemoryStream(response))
                        Dim uplStatus As String
                        Dim SvrResponse As String = sr.ReadToEnd
    
                        Dim xdoc As XDocument = XDocument.Parse(SvrResponse)
                        ' get the status of the request
                        uplStatus = xdoc.Root.Attribute("status").Value
                        result = GetResultFromStatus(uplStatus)
    
                        If result = imgUrResults.OK Then
                            LastImageLink = xdoc.Descendants("link").Value
    
                            ' only overwrite the server result status
                            If String.IsNullOrEmpty(LastImageLink) Then
                                ' avoid NRE elsewhere
                                LastImageLink = ""
                                ' we did something wrong parsing the result
                                ' but this one is kind of minor
                                result = imgUrResults.OtherAPIError
                            End If
                        End If
    
                    End Using
    
                    If UseClipboard AndAlso (result = imgUrResults.OK) Then
                        Clipboard.SetText(LastImageLink)
                    End If
    
                End Using
            Catch ex As Exception
                Dim errMsg As String = ex.Message
    
                ' rate limit
                If ex.Message.Contains("429") Then
                    result = imgUrResults.RateLimitError
    
                    ' internal error
                ElseIf ex.Message.Contains("500") Then
                    result = imgUrResults.ServerError
    
                End If
            End Try
    
            Return result
        End Function
    
        Private Function GetResultFromStatus(status As String) As imgUrResults
    
            Select Case status.Trim
                Case "200"
                    Return imgUrResults.OK
                Case "400"
                    Return imgUrResults.BadRequestFormat
                Case "401"
                    Return imgUrResults.AuthorizationError
                Case "403"
                    Return imgUrResults.Forbidden
                Case "404"
                    Return imgUrResults.NotFound
                Case "429"
                    Return imgUrResults.RateLimitError
                Case "500"
                    Return imgUrResults.ServerError
                Case Else
                    ' Stop - work out other returns
                    Return imgUrResults.UknownStatus
            End Select
        End Function
    
        Private Function GetResultFromStatus(status As Int32) As imgUrResults
            ' some places we get a string, others an integer
            Return GetResultFromStatus(status.ToString)
        End Function
    
    End Class
    

    HOW TO USE IT

    The process requires a web browser for the user to navigate and request a PIN. For testing/development, I used a WebBrowser control and snagged the PIN from the returned page.

    Note: for testing, my imgUR account was setup as DESKTOP, since we are sending from a DESKTOP app. Also, this is for YOU sending images to YOUR account. There is not a way for OTHERS to upload to YOUR account without giving out your secret ID and/or embedding your master ImgUR Login and password in the App. That is how ImgUR designed it.

    A. Create an imgUR object:

    Friend imgUR As imgurAPI
    imgUR = New imgurAPI(<your Client ID>,<your secret code>)
    

    B. Get a Pin - Method One

    ' pass the app's WebBrowser Control
    imgUR.RequestPinBrowser(wb)
    

    This will take you to a imgur page where you must authorize the issue of a PIN for uploading to your account. Enter your Account Name, Password, Click ALLOW. A new page with the PIN will be displayed. Copy the PIN from the webpage to some other control which can feed it to the imgurAPI Class.

    There is code below to parse the PIN page and get it into another control.

    Method Two

    • Using your own external browser, go to

    https://api.imgur.com/oauth2/authorize? client_id=YOUR_CLIENT_ID&response_type=pin&state=ziggy

    • Log In
    • Copy the PIN you receive into a TextBox or something to send it to the imgurAPI:
    • Set pin: imgUR.Pin = <<PIN YOU RECEIVED>>

    The process is the same either way, just a matter of whether you want to have to include a WebBrowser control in your form. PINs are only good for a short time, so you must use it to get an access token right away.

    C. Get Access token

    ' imgUrResults is an enum exposed by the class
    Dim result As imgurAPI.imgUrResults = imgUR.RequestToken
    

    Notes:

    • the imgUR class will retain the token
    • tokens currently expire in 1 hour (3600 seconds)

    D: Upload a File
    Upload using imgUR.UploadImage(filename, boolAnon)

    Filename - the file to upload

    boolAnon - Boolean flag. False = upload this file to your account vs the Anon general pool method.

    Example:

    ' get token
    Dim result As imgurAPI.imgUrResults = imgUR.RequestToken
    
    ' check result
    If result = imgurAPI.imgUrResults.OK Then
        ' assumes the file exists
        imgUR.UploadImage("C:\Temp\London.jpg", False)
    Else
        MessageBox.Show(String.Format("Error getting access token. Status:{0}",
            result.ToString))
    End If
    

    After the file uploads, the procedure looks for the link in the response. If the link can be parsed, it will be available from the LastImageLink property and pasted to the ClipBoard as well.

    Misc Properties, Settings and Methods

    LastImageLink (String) - URL of the last image uploaded

    UseClipBoard (Bool) - When true, imgurAPI class posts the link to the uploaded image to the Clipboard

    TokenExpiry (Date) - The DateTime that the current token expires

    GetTokenTimeRemaining() As TimeSpan - A TimeSpan representing how long before the current token expires

    Public Function GetTokenCountdown() As String - Formatted string of TimeRemaining

    Public WriteOnly Property Pin As String - the PIN required to get an access token

    Public Function IsTokenValid() As Boolean - is the current token valid

    Public Function IsTokenExpired() As Boolean - simple Boolean version of TimeRemaining vs DateTime.Now

    Notes

    • Tokens can be renewed or extended. But since they last for an hour, this seems plenty.
    • PINS are only good for a short time. Once a PIN is exchanged for a token the imgurAPI (this class) clears the PIN. If there is a problem getting the Token, you will have to get a new PIN first (or paste the last one if you just got it a few minutes ago).
    • Uploaded images are not visible to the world at large unless/until you change the setting on your account.
    • You can reset your SecretID (Settings -> Applications). If you do, you will need to also reset it for apps using this API class, and recompile (or read it from a config file).

    If you use a WebBrowser control to get a PIN, you can add this code to the DocumentCompleted event to scrape the PIN from the HTML:

    ' wb is the control
    Dim htmlDoc As System.Windows.Forms.HtmlDocument = wb.Document
    Dim elP As System.Windows.Forms.HtmlElement = htmlDoc.GetElementById("pin")
    
    If elP IsNot Nothing Then
        sPin = elP.GetAttribute("value")
        If String.IsNullOrEmpty(sPin) = False Then
           ' user has to push the button for `imgUR.Pin = tbPIN.Text`
           ' this is in case the HTML changes, the user can override
           ' and input the correct PIN
           Me.tbPIN.Text = sPin
        End If
    
    End If
    

    About the OAuth Model

    this is unofficial - info learned from reading the docs and working with the API. Applies to imgur API v3 as of this date.

    There is nothing automated about getting a PIN. One way or another you must navigate to a URL in a browser and enter your account name and Password to get a PIN. This is by design so that you, yourself, personally are authorizing some external app to access your account content.

    Method One above uses a .NET WebBrowser control to do this. With this method we can be sure that both you and the imgur class are using the same Endpoint/URL because it sends you there via the browser control.

    Method Two is just you go there in some browser, any browser. Log in, get a PIN, and paste it to the imgurAPI class.

    No matter the method, the correct Endpoint/URL to use is:

    https://api.imgur.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=pin&state=foobar

    Using a browser on the form using the imgurAPI class, we obviously we can be sure that both you and the class are using the same URL and ClientID. The code for DocumentComplete fetches the PIN into the TextBox only you still need to set it in the class:

    myimgUR.PIN = tbPinCode.Text
    

    PINS are single use, and expire.

    So when developing especially, and you stop the code, add some stuff then rerun naturally, the code will no longer have the old Token or PIN. If the last PIN was recent and not submitted, you might not have to get a new one, but I find it is hard to remember if that is the case.

    The class treats PINS as single use. Once a Token has been received, it clears out the variable since they have been used and are no longer valid.


    Final Edit

    • Added an Anon Only mode

    To use the class to upload ONLY in Anon mode (to the general site, not your account), the SecretID is not needed. For this, use the new constructor overload:

    Public Sub New(clientID As String)
    

    This sets the class to work an Anon ONLY and certain methods will return an error or throe an exception when using Account based methods such as GetToken. If you initialize it with just ClientID, it remains in AnonOnly mode until you recreate the object with the both the ClientID and SecretID.

    There is no real reason to use it as AnonOnly (unless you do not have an account) since the UploadImage method allows you to specify it as an Anon upload by file:

    Function UploadImage(filename As String, 
                         Optional Anon As Boolean = False) As imgUrResults
    
    • Revised/clarified the imgUrResults Enum

    This is meant to be all-encompassing: some returns indicate a problem detected by the class, others are server responses which are simply passed along.

    • Removed IsTokenExpired

    IsTokenValid is more thorough. There are other methods to get the time remaining or the actual Expiry.

    • Added assorted error trapping/handling
      • Check for a valid WebBrowser control when requesting a PIN
      • Refined the method used to get the server status code after an image is uploaded
      • Reworked some handling to give prioritize remote server status over class returns

    .