Twitter is going to stop supporting basic authentication and will use only OAuth. That's why I tried again how to connect to Twitter via OAuth and found out that there were some changes during last year.

After some minor changes in code OAuth via PowerShell is working again. Check my updated article How to use OAuth to connect to Twitter in PowerShell.

Meta: 2010-07-10, Pepa

Update 2010-07-10: added support for PIN and link for library download, because there is no new release at DevDefined home page.

When I was working on a Microblog reader for Twitter and Identi.ca, I was thinking about using OAuth for authorization purposes. Recently I have started again. Now I can show you full working example from how to register an application up to how to get the data.

Register your application

If you have an application that will request some data from a service and want to use OAuth for authorization, you have to register it first, so that the service knows about the application. In my case service will be Twitter and the application (consumer in OAuth terminology) will be PowerShell.

First go to Twitter and log in using your standard credentials and browse to Settings–>Connections. In the right column go to the Developers section and click on the link that points to http://twitter.com/oauth_clients. On this page you can see all applications you have registered so far. To create new application (probably your first one), click on Register a new application and fill the info.

First part of registration Second part of registration

Registered After you submit the form, you will receive your applications key and secret (consumer key / consumer secret). This two hashes are used by your application when trying to get authorization key from the service (Twitter). Ok, it's time to play with them.

Authorize and request data

You can implement OAuth protocol on your own or you can use existing implementations. First time I tried an implementation by Shannon Whitley. It didn't work as expected (some problems with token expiration). Then I downloaded DevDefinedOauthPreview1-ReleaseBing.zip from Google code / DevDefined, that worked without problems.

Update 2010-07-10: there is no new release available, although the code is still alive. You have to either download the project and compile it or download the library I compiled for you. The old version worked fine, but some time later Twitter introduced PIN. The source code reflects new changes, but no release has been issued so far..

[3] Add-Type -Path C:\OAuthDevDefined\DevDefined.OAuth.dll
[4] $cons = New-Object devdefined.oauth.consumer.oauthconsumercontext

Use the keys provided by Twitter and set the signature method.

[5] $cons.ConsumerKey = '6NoGCtBEDdZGZtKe7JWdw'
[6] $cons.ConsumerSecret = 'lPkVk1PUCdNe7yXrGBI5fGO1UNjyU4rXOUzHt2SdvE'
[7] $cons.SignatureMethod = [devdefined.oauth.framework.signaturemethod]::HmacSha1

Create an OAuth session and request for authorization. PowerShell has to redirect you to Twitter page where you will allow the access for our application.

[8] $session = new-object DevDefined.OAuth.Consumer.OAuthSession `
 $cons,"http://twitter.com/oauth/request_token", `
 "http://twitter.com/oauth/authorize","http://twitter.com/oauth/access_token"
[9] $rtoken = $session.GetRequestToken()  #unique token just for authorization
[10] $authLink = $session.GetUserAuthorizationUrlForToken($rtoken, 'anything'); $authLink
http://twitter.com/oauth/authorize?oauth_token=RMmhALQzA1glYlR....&oauth_callback=anything
[11] [diagnostics.process]::start($authLink)  #redirection to Twitter

Prompt to allow access Access granted, PIN issued. A browser window should appear and you are requested to allow the access. After that you will see a PIN that you have to pass as the request parameter. We will request for access token that will identify our powershell client and then we will download last 5 statuses and parse user names from them.

[12] $pin = read-host -prompt 'Enter PIN that you have seen at Twitter page'
[13] $accessToken = $session.ExchangeRequestTokenForAccessToken($rtoken, $pin)
[14] $accessToken | Export-CliXml c:\temp\myTwitterAccessToken.clixml

Now you got your access token that contains keys for later use. The token is stored in a xml file. Currently the Twitter's access token doesn't expire, so you just need to store it and use later.
Ok, "use", but how? It is very similar to what you have seen so far:

[1] Add-Type -Path C:\OAuthDevDefined\DevDefined.OAuth.dll
# create context (provide correct keys)
[2] $cons = New-Object devdefined.oauth.consumer.oauthconsumercontext
[3] $cons.ConsumerKey = '6NoGCtBEDdZGZtKe7JWdw'
[4] $cons.ConsumerSecret = 'lPkVk1PUCdNe7yXrGBI5fGO1UNjyU4rXOUzHt2SdvE'
[5] $cons.SignatureMethod = [devdefined.oauth.framework.signaturemethod]::HmacSha1
# create session; I pass $null and not full urls. It looks weird, maybe 
# there is a more elegant way how to create the session
[6] $session = new-object DevDefined.OAuth.Consumer.OAuthSession $cons, $null, $null, $null
# create access token and fill its data
[7] $accessToken = new-object DevDefined.OAuth.Framework.TokenBase
[8] $at = import-cliXml C:\temp\myTwitterAccessToken.clixml
[9] $accessToken.ConsumerKey, $accessToken.Realm, $accessToken.Token, $accessToken.TokenSecret = `
  $at.ConsumerKey, $at.Realm, $at.Token, $at.TokenSecret
# finally, create request and read response
[10] $req = $session.Request($accessToken)
[11] $req.Context.RequestMethod = 'GET'
[12] $req.Context.RawUri = [Uri]'http://api.twitter.com/1/statuses/friends_timeline.xml?count=5'
[13] $res = [xml][DevDefined.OAuth.Consumer.ConsumerRequestExtensions]::ReadBody($req)
[14] $res.statuses.status | % { $_.user.Name }
Michal Těhník
Jeffery Hicks
Jon Skeet
Martin Hassman
David Grudl

Download

More info

Meta: 2010-07-10, Pepa

Pozn: nejlepší při použití prohlížeče podporujícího :hover – FF, Opera, Chrome, Safari. Ne IE.

Zřejmě mnozí z vás víte, že jsem fanouškem a aktivním uživatelem twitteru. Je docela dobře možné, že většina z vás sem přišla z něj, protože jsem se vás sem snažil dostat. Proč? Abych vám dal malý dáreček, o který jste nikdy nestáli.

Jsem jeden z mála, kteří si vytvořili svou vlastní twittří čtečku. Důvody tu zmiňovat nebudu; podstatné je, že mám přistup k datům. A že jsem si všechny utwittky (vlastní i cizí) bez jakýchkoliv úmyslů zálohoval. Ano, mám takový malý archív.

Linky

Co vám tedy nabízím? Pokud zrovna tebe konkrétně sleduji, můžeš se mrknout na přehled svých výplodů.

Neboli @adent, @AugiCZ, @Bajtos, @borekb, @bretik, @cermak, @codinghorror, @CZMilka, @DavidGrudl, @fczbkk, @gorline, @hakaval, @hassmanm, @ch9, @jakubvrana, @jankorbel, @jantichy, @jaredpar, @jiravanet, @jiritvrdek, @jirkavagner, @jonskeet, @jsnover, @karmiq, @kolman, @marekl, @MarekP, @mashable, @meap, @migueldeicaza, @michalblaha, @nikdo, @OdeToCode, @PetrKaleta, @petrkou, @pixycz, @plavacek, @RadekHulan, @rarous, @renestein, @RickStrahl, @scottgu, @ScriptingGuys, @shanselman, @ShayLevy, @sibiranka, @synopsi, @tangero, @twitterapi, @webexpo, @zdrojak

Zajímavosti

Pokud byste se chtěli podívat na někoho, kdo je velice aktivní v převlékání avatarů, zkuste: @hassmanm, @jiritvrdek, @radekhulan, @rarous, @shanselman

Jinak mí nejaktivnější přispivatelé jsou @mashable a @shanselman.

Jen tak se můžete mrknout i na @pixycz a jeho opuštění twitthřiště.

A u @webexpo je zajímavá aktivita – proč byla asi tak vysoká právě v říjnu? :) A teď už se pochopitelně zase dostává na průměr. A aktivita @zdrojak už po odchodu @hassmanm taky není, co bývala.

Pár poznámek

  • Tvoje utwittky jsou veřejná věc. Není ale problém zrovna tebe ze seznamu odstranit.
  • U žádného uživatele není pravděpodobně kompletní seznam toho, co vypotil. Pokud jsi odpovídal někomu, koho nesleduji, tuto odpověď nevidím a tedy v archívu nebude.
  • Pokud nejsi v seznamu a myslíš, že bys tam měl být, kontaktuj mě.
  • Pokud máš zájem o své utwittky v jiné formě (např. xml), není nic jednoduššího.
  • Čas je zasazen do UTC, tj. není tam žádný posun, ani není zohledněn letní čas. Používá se časové pásmo Praha GMT +01:00
  • Snažil jsem se zahrnout jen veřejné účty. Pokud je tvůj účet neveřejný a jsi v seznamu, neváhej a zavolej!

Meta: 2009-11-26, Pepa

In the previous post about Microblog reader in PowerShell I wrote about a little bit complex application made in PowerShell and PowerBoots. It was quite a long reading, however if you don't need multiaccount support and other features, but preview would be ok for you, have a look at the following code. simple microblog reader

# calls the passed url with given credentials
function GetFriendsStatuses {
    param(
        [Parameter(Position=0, Mandatory=$true)] [string]$userName,
        [Parameter(Position=1, Mandatory=$true)] [string]$password,
        [Parameter(Position=2, Mandatory=$true)] [string]$max,
        [Parameter(Position=3, Mandatory=$true)] [string]$lastId
    )
    $url = "http://twitter.com/statuses/friends_timeline.xml?since_id=$lastId&count=$max"
    Write-Debug "url to fetch: $url"
    $request  = [Net.WebRequest]::Create($url)
    $request.Timeout = $request.ReadWriteTimeout = -1
    $request.Credentials = New-Object System.Net.NetworkCredential($userName, $password)
    $request.Method = "GET"
    $response = $request.GetResponse()
    $reader   = [System.IO.StreamReader]$response.GetResponseStream()
    $ret      = $reader.ReadToEnd()
    $reader.close()
    Write-Debug "Length of response: $($ret.Length)"
    [xml]$ret
}

# gets all new statuses
function GetNewStatuses {
    param(
        [Parameter(Position=0, Mandatory=$true)] [string]$userName,
        [Parameter(Position=1, Mandatory=$true)] [string]$password
    )
    if (!$global:statuses) {
        $global:statuses = @()
        Write-Debug "`$statuses created"
    }
    Write-Debug "going to read statuses for $userName"
    if (!$global:lastStatusId) {
        # first time we only ghet the last status id and store it, nothing is downloaded
        Write-Debug "first last id check"
        try {
            $global:lastStatusId = @(
                (GetFriendsStatuses $userName $password 1 1).statuses.status)[0].id
            Write-Debug "new status id set: $($global:lastStatusId)"
        }catch{
            Write-Warning "Error when getting first status id"
            Write-Warning "Exception: $_"
            $global:lastStatusId = 1
        }
        return
    }
    try {
        $global:currentSt = @(
            (GetFriendsStatuses $userName $password 100 $global:lastStatusId).statuses.status)
        if ($global:currentSt.Count -eq 0) {
            Write-Debug "nothing new"
            return
        }
        $global:lastStatusId = @($currentSt)[0].id
        $currentSt | sort id | % { $global:statuses += $_ }
        Write-Debug "last status id set: $global:lastStatusId"
    }catch {
        Write-Warning "Error when getting statuses: $_"
    }
}

function CreateDeleagate {
  try {     [Void][GetEmptyDelegate] }
  catch {
    Add-Type -TypeDefinition @"
      public class GetEmptyDelegate{
          public delegate void EmptyDelegateD();
        public static EmptyDelegateD Get { 
            get { return Nothing; } }
        public static void Nothing(){}}
"@
  }
}
function Refresh([object[]]$obj) {
  $null = $obj | % { 
      $_.Dispatcher.Invoke(
        [System.Windows.Threading.DispatcherPriority]::Render, [getemptydelegate]::get); }
}
function SetOpacity { param([float]$opacity, [bool]$store)
    if ($store) { $global:MBSTimerBorder.Tag = $global:MBSTimerBorder.Opacity;  }
    $global:MBSTimerBorder.Opacity = $opacity
}
function SetGeneralLabelContent { param([Object]$label, [string]$s) 
    $label.Content = $s
    Refresh $label
}
function MBSRefreshImages {
    $global:MBSCurrentImages.Children.Clear()
    $global:statuses | % {
        if (!$_) { return }
        Write-Debug "Adding info for status $($_.id)"
        $img = $_.user.profile_image_url
        $text = $_.text
        $date = $_.created_at
        $user = $_.user.screen_name
        Write-Debug "Adding image at $img"
        $global:MBSCurrentImages.Children.Add(
            (Image -source $img -margin 2 -width 30 -height 30 -tooltip ("{0} {1}`n{2}" -f $user,$date,$text))
        )
    }
    $sqrt = [int][Math]::Sqrt($global:statuses.Count) + 1 # +1 at je to o neco malo sirsi
    $global:MBSCurrentImages.MinWidth = [Math]::Max((30+4)*$sqrt, 20);
    Refresh $global:MBSCurrentImages
}

function Start-MbSPreview {
    param([int]$delay=10, [string]$userName, [string]$password)
    $global:MBSTimerWindow = boots -WindowStyle None -AllowsTransparency $true -Topmost $true {
        border -minheight 20 -borderBrush Green -borderThickness 2 `
            -cornerRadius 10 -background 'white' -opacity 0.4 {
            stackpanel -orientation vertical -margin 5 {
                WrapPanel -maxWidth 120 { Label '' -padding 0 -margin 0 } | % { 
                    $global:MBSCurrentImages = $_; $_ }
                Label "" -FontSize 10 -FontFamily Arial -padding 1 -margin 0 -foreground '#aaa' | % { 
                    $global:MBSInfo = $_; $_ }
                Button "Exit" -Name Close -On_Click { `
                    write-host "closing"
                    $global:MBSTimerWindow.Close()
                }
            }
        } | % { $global:MBSTimerBorder = $_; $_ }
    }  `
    -On_MouseLeftButtonDown { $this.DragMove() } `
    -On_MouseEnter { SetOpacity 1 $true } `
    -On_MouseLeave { SetOpacity 0.5 $true }  `
    -On_Close { $global:MBSTimer.Stop(); $global:MBSTimerWindow | Remove-BootsWindow } `
    -Async -Passthru -Background Transparent -ShowInTaskbar $true `
    -Tag @{"delay"=$delay; "userName"=$userName; "password"=$password} `
    -Title 'Twitter preview'
    
    $null = Invoke-BootsWindow $global:MBSTimerWindow {
        $global:MBSTimer = new-object System.Windows.Threading.DispatcherTimer
        $MBSTimer.Interval = [TimeSpan]"0:$($delay):0"
        $MBSTimer.Add_Tick( { 
            Write-Debug "tick"
            SetGeneralLabelContent $global:MBSInfo 'Working...'
            GetNewStatuses $global:MBSTimerWindow.Tag["userName"] $global:MBSTimerWindow.Tag["password"]
            SetGeneralLabelContent $global:MBSInfo `
                ("Next: " + (Get-Date).AddMinutes($global:MBSTimerWindow.Tag["delay"]).ToString("HH:mm:ss"))
            MBSRefreshImages
        } )
        SetGeneralLabelContent $global:MBSInfo 'First check...'
        if (!$global:lastStatusId) { GetNewStatuses $userName $password }
        SetGeneralLabelContent $global:MBSInfo `
            ("Next: " + (Get-Date).AddMinutes($global:MBSTimerWindow.Tag["delay"]).ToString("HH:mm:ss"))
        $MBSTimer.Start()
    }
    $global:statuses = @()
}

CreateDeleagate

How to run it – either copy the code or download the file and save it somewhere. Then run the following commands. After Start-MbSPreview is executed id of your last status (~tweet) is downloaded. Because we specified delay of 10 minutes, it will sleep for 10 mins. After that new statuses are downloaded and displayed (if there are any).
You have to provide valid credentials of course.
Enjoy

[1] . .\check-microblogsSimple.ps1
[2] Start-MbSPreview -delay 10 -userName stejcz -password password

Meta: 2009-05-21, Pepa

Why

MicroReader is written in PowerShell. If you are not interested in PowerShell or you are not willing to learn it, you can stop reading right now.

My first microblogging service was Jaiku. My friend nikdo brought me there. It was a great time there until they started cutting features and Jaiku was more down then working. So we decided to move to Twitter. It is completely other world. One of features that I missed very much were replies that are bound to the root "question". It's much more difficult to follow the original idea than on Jaiku. This was the first reason why I started working on a reader that is capable to display replies as expected.

Features

Grouping replies

replies bound to original question, see the indentation extra downloaded statuses with lower opacity As mentioned in the intro, replies are grouped together to its original question. Visually you can display them with a left margin. If there is a new status that is bound to a parent and you have read the parent already (so it is not returned in new statuses list), the parent status is downloaded as well so that the reader knows the original question. The extra-downloaded status is shown in 0.33 opacity (see the second image on the left).

More accounts support

After I started on Twitter I added many people to my "following" list. E.g. Scott Hanselman was one of them. You know that Scott tweets quite often, and it was hard to follow him because his tweets include personal things as well that I'm not interested in. Therefore I created new account at Twitter only for reading tweets from some people only when I have time for that.

That's why I needed multiaccount support. Besided that the reader supports Identi.ca as well, because Identi.ca offers the same API as Twitter.

Removing tweets from some people

It's possible from any reason to remove some people. For example I follow halr9000 on Twitter and Identi.ca. He posts the same to both services. So currently I simply remove his posts to Identi.ca.

Tweets backup

After tweets are downloaded, they are stored to disk for possible later processing. E.g. you might want to make a statistics or just store them for better search based on your needs.

How to use it

First, I assume that you know what PowerShell is and you have PowerShell on your machine installed.

Second, if you want to use the MicroReader fully (to display tweets, not just for downloading), you need to download PowerBoots provided by Jaykul. It's a great PowerShell module to bring WPF into PowerShell world. There are of course some other ways, but PowerBoots simplify working with WPF quite noticeably.

MicroReader is a PowerShell module as well. So the functionality is wrapped inside and some objects can not be used. If you would like to see the internals from console as well, just rename it to *.ps1 and remove the last line with Export-ModuleMember. Note: I refer to tweets as statuses (general term not bound only to Twitter)

Initialization

Download the zip file and unpack it into its own directory. In further examples let's assume you unpacked it into directory d:\MicroReader.
We import the module and PowerBoots (I assume you have installed PowerBoots already).

[1] Import-Module d:\MicroReader\check-microblogs.psm1
[2] Import-Module powerboots

We create a new session. The first parameter is root directory where some files are stored:

  • images identifying Twitter and Identi.ca
  • last fetched status id for each service
  • directory 'statuses' for the downloaded statuses
  • directory 'images' for cached avatars of statuses authors
  • last xml response from servers with prefix currStatuses and directMessages

The second directory is optional and it will be needed when you will backup statuses (pack them into a zip archive). It should be directory with 7zip packer. Then we add your credentials. The color there is just for function that displays the statuses, it helps to distinguish the service the status is bound to.

[3] New-MbSession D:\MicroReader\ G:\Programs\7zip\ # we initialize the session
[4] Add-MbTwitter stejcz password Green                            
[5] Add-MbTwitter leporeloeu password    # uses default color - white
[6] Add-MbIdentica stej password Red

Getting new statuses

Now we can try to get some statuses. When we use MicroReader for the first time, up to 100 statuses per account will be downloaded. Command Start-MbPreview opens new PowerBoots window in its own thread (thx Jaykul).

[7] Start-MbPreview

statuses downloaded the first time you run Start-MbPreview You can see the result on the left. Downloaded statuses are stored in global variable $mbStatuses. The reader checks for new statuses every 10 minutes. If you want to display them, just click on Finish – after that replies will be bound to their reply parents (i.e. original questions), so some more statuses will be probably downloaded. To display the statuses just try the following command. Warning: the more statuses to display the more slow are PowerBoots. If you will try it for the first one, be really patient.

[8] $global:mbStatuses | Show-MbStatuses

After you are done, you can just open new preview window again: [9] Start-MbPreview. You can specify -treshold parameter. If there are less statuses downloaded than the treshold, the window's opacity is 0.4. Parameter -delay denotes amount of minutes between attempts to download new statuses.

The Exit button on the preview window only closes the window, it means that reply parents are not downloaded and no status is marked as hidden.

Backup services

When time is up and you have to switch your computer off, you might want to backup your services, so that you don't have to add them via Add-MbTwitter/Identica next time.

[10] Export-MbServices

To be more secure you have to provide a password.
If you have the services stored on disk, it makes sense to import them (next time you switch on the computer). To import them just try:

[11] Import-MbServices 

Internal functions

There is a function called Check-Microblog. It simply downloads all the new statuses, removes the unwated ones and adds reply parents. It is defined like this:

  function Check-Microblog {   
    param(
        [Parameter(Position=0, Mandatory=$false)][object[]]$itemsToRemove
    )
    ($global:mbStatuses = Get-MbStatuses | 
        Remove-MbStatuses | 
        Add-MbReplyParents
    ) | Show-MbStatuses 
  }

From the code above you can see some helpful functions:

Get-MbStatuses
Is used for downloading of new statuses. It internally loads files with last statuses id for each service and checks the appropriate url with credentials you passed to Add-MbTwitter. You can pass as in statuses already returned by Get-MbStatuses (only new statuses are added) or pass in $null.
Remove-MbStatuses
Marks some statuses as hidden. It depends on the displaying function how it handles hidden statuses. Currently Show-MbStatuses shows them in grey color.
Add-MbReplyParents
This function adds the reply parent for each status whose ReplyId refers to other status.

Removing statuses

As I said earlier it is possible to remove (just download and optionally not display) some statuses. To setup the default filter that is used by Show-MbPreview use this command:

[12] Set-MbDefaultRemoveFilters @{'Account'='Identica'; 'User'='halr9000'}
[13] Get-MbDefaultRemoveFilters
Name                           Value
----                           -----
Account                        Identica
User                           halr9000

If you don't specify any default filter, you can pass it in later to the Remove-MbStatuses function explicitly. For more examples check the module source and look for Remove-MbStatuses.

Simplify startup

I grouped some commands together, put them into a function and included into my profile file:

  function Start-MbSession { param([switch]$powerBoots)
    Import-Module d:\MicroReader\check-microblogs.psm1
    Import-MBSession d:\MicroReader c:\prgs\7zip
      if ($powerBoots) {
          Import-Module powerboots
      }
      Set-MBDefaultRemoveFilters @(@{'App'='Identica';'User'='halr9000'}, 'webdesigndev', `
        'fczbkk', 'emilylewis', 'NETTUTS')
  }

Some TODOs

There are still some possible features to add, that wait only in my mind.

  • Now it's needed to close the window opened from Start-MbPreview and after that to open new window from command line (by Show-MbStatuses). It would be much better to add a button to the preview window that would open the statuses for reading. Currently I had some problems caused probably by inproper use of PowerBoots.
  • Possibility to pass a script block to Remove-MbStatuses. Then I could hide all RT statuses easily.
  • The functionality can be ported to .NET and used on web site or in exe file, so it's not needed to have PowerShell installed on the computer.
  • Make WPF layout prettier. It's priority is not high, but why not ;) I think that I need to review the controls used to make loading of the window with statuses faster.
  • Use background thread for downloading statuses, because the preview windows is frozen when downloading new statuses. However, priority of this TODO is quite low.
  • Remove scrollbars when not needd.
  • Remove "Drag me" button and enable drag & drop on the whole window.

The psm1 file is quite big, it grew very much since the first versions. I'm C# programmer, so it's possible that some constructions could be done more elegantly in PowerShell. In this case don't hesitate and contact me.

Download and contact

In case you have something to tell me, contact me on Twitter, Identi.ca, mail me at stej{-at-}leporelo{#dot#}eu or fill a Contact form.

In the zip file there is a powershell module and twitter and identica images that are used for statuses background.

Meta: 2009-05-19, Pepa