fsweet
Last changed: -75.102.39.212

.

Lightweight WPF Twitter client sample F# script

Change the username and password in the code, then simply run with F# interactive.

The client lets you both see your friend's timeline and post tweets, all in under 200 lines of F#!

Advertising degree | hr degree | Online International business degree | Online management degree | Marketing degree

    // fsweet - WPF Twitter client F# script by Phillip Trelford 2009


    #light


    #r "PresentationCore.dll" 
    #r "PresentationFramework.dll" 
    #r "WindowsBase.dll"


    open System
    open System.Diagnostics
    open System.IO
    open System.Net    
    open System.Text
    open System.Windows
    open System.Windows.Controls
    open System.Windows.Documents
    open System.Xml


    // Enter your username and password here               
    let username, password = "<your username>", "<your password>"


    /// Gets web response as an XmlDocument        
    let GetXmlWebResponse (request:WebRequest) =    
        try
            use response = request.GetResponse()    
            use stream = response.GetResponseStream()        
            let doc = XmlDocument() in doc.Load(stream)               
            Some(doc)
        with e -> e.ToString() |> printf "Error: %s";  None


    /// Gets Xml response from Web Url
    let Get (url:string) =    
        let request = WebRequest.Create(url)
        request.Credentials <- NetworkCredential(username, password)    
        GetXmlWebResponse request 


    /// Posts data to Web Url    
    let Post (url:string) user (data:string) =       
        let request = WebRequest.Create(url) :?> HttpWebRequest   
        request.Method <- "POST"        
        request.ServicePoint.Expect100Continue <- false
        request.Headers.Add("Authorization", "Basic " + user)
        let bytes = System.Text.Encoding.ASCII.GetBytes(data);
        request.ContentType <- "application/x-www-form-urlencoded"   
        request.ContentLength <- int64 bytes.Length
        request.GetRequestStream().Write(bytes, 0, bytes.Length)       
        GetXmlWebResponse request


    /// Posts user tweet                        
    let Tweet tweet =   
        let user = Convert.ToBase64String(Encoding.UTF8.GetBytes(username+":"+password));                        
        "status=" + tweet |> Post "http://twitter.com/statuses/update.xml" user         


    /// Gets user time line
    let GetUserTimeLine () =        
        sprintf @"http://twitter.com/statuses/user_timeline/%s.xml" username |> Get


    /// Gets friends time line
    let GetFriendsTimeLine () =
        "http://twitter.com/statuses/friends_timeline.xml" |> Get


    let NodeText (node:XmlNode) child = node.[child].InnerText
    let Date s = XmlConvert.ToDateTime(s, "ddd MMM dd HH:mm:ss zzzzz yyyy")


    /// Creates user tuple
    let CreateUser (user:XmlNode) =    
        let Text = NodeText user    // Curry NodeText function
        (Text "name", Text "screen_name", Text "profile_image_url")


    /// Creates status tuple
    let CreateStatus (status:XmlNode) =
        let Text = NodeText status  // Curry NodeText function
        (Text "created_at" |> Date, Text "text", CreateUser status.["user"])


    /// Gets statuses   
    let GetStatuses () =               
        match GetFriendsTimeLine () (*GetUserTimeLine()*) with         
        | Some doc ->         
            seq { for status in doc.DocumentElement.SelectNodes("status") do
                    yield CreateStatus status }        
        | None -> Seq.empty


    // Table layout panel type extends Grid for easier use in code                        
    type TableLayout (columnDefinitions:string seq,rowDefinitions:string seq) =
        inherit Grid ()   
        /// Converts grid length string to GridLength instance
        let ParseGridLength (s:string) =
            match s.Length, s.EndsWith("*") with
            | 0, _ -> GridLength()
            | n, false -> GridLength(Double.Parse(s))
            | 1, true -> GridLength(1.0, GridUnitType.Star)
            | n, true -> GridLength(Double.Parse(s.Substring(0, n-1)), GridUnitType.Star)                  
        do  columnDefinitions        
            |> Seq.map (fun text -> ColumnDefinition(Width=ParseGridLength text))
            |> Seq.iter base.ColumnDefinitions.Add    
        do  rowDefinitions       
            |> Seq.map (fun text -> RowDefinition(Height=ParseGridLength text))
            |> Seq.iter base.RowDefinitions.Add
        /// Adds item to layout, automatically setting grid column and row values                   
        member this.AddItem (item:#UIElement) =
            let span = match this.ColumnDefinitions.Count with | 0 -> 1 | n -> n
            let index = this.Children.Count
            item |> this.Children.Add |> ignore        
            Grid.SetColumn(item, index % span)
            Grid.SetRow(item, index / span)          


    /// Creates bitmap image
    let CreateBitmap uri =
        let img = Media.Imaging.BitmapImage()
        img.BeginInit(); img.UriSource <- uri; img.EndInit()
        img 


    /// Annotates hyperlinks converting text to an Inline array     
    let AnnotateLinks (text:string) =
        let rec GetUrlPositions (n:int) (acc) =
            match text.IndexOf("http", n) with
            | -1 -> acc
            | n -> n::(GetUrlPositions (n+1) acc)            
        GetUrlPositions 0 []
        |> Seq.map (fun first -> 
            first,            
                match text.IndexOfAny([|' ';'\t';'\r';'\n'|], first) with
                | -1 -> text.Length
                | n -> n        
        )       
        |> Seq.fold (fun (start,lines) (first, last) ->        
            let leadin = text.Substring(start, first - start)        
            let url = text.Substring(first, last - first)
            let uri = Uri(url, UriKind.Absolute)
            let link = Hyperlink(Run(url), NavigateUri=uri)      
            link.Click.Add (fun _ -> Process.Start(url) |> ignore )
            (last, (link :> Inline) :: (Run(leadin) :> Inline) :: lines) 
        ) (0, [])       
        |> (fun (start, lines) -> (Run(text.Substring(start)) :> Inline) :: lines)   
        |> Seq.to_array
        |> Array.rev   


    /// Renders statuses in specified control        
    let RenderStatuses (control:#ItemsControl) statuses =        
        control.Items.Clear()
        statuses
        |> Seq.iter (fun (createdAt, text, user) ->        
            let name, screen, url = user
            let across = new TableLayout(["40";"*"], [])        
            let bitmap = Uri(url, UriKind.Absolute) |> CreateBitmap        
            Image(Source=bitmap) |> across.AddItem
            let down = new TableLayout([], ["1*";"2*";"1*"])
            Label(Content=screen, FontWeight=FontWeights.Bold, ToolTip=name) |> down.AddItem              
            let block = TextBlock(TextWrapping=TextWrapping.Wrap)        
            AnnotateLinks text |> block.Inlines.AddRange
            down.AddItem block
            Label(Content=createdAt, FontStyle=FontStyles.Italic) |> down.AddItem        
            across.AddItem down
            control.Items.Add(across) |> ignore
        )


    [<STAThread>]
    do  let statusBox = new TextBox(TextWrapping=TextWrapping.Wrap)  
        let statusItems = new ListBox(Margin=Thickness(4.0))             
        ScrollViewer.SetHorizontalScrollBarVisibility(statusItems,ScrollBarVisibility.Disabled)               
        let characterCounter = new Label(FontSize=20.0,FontFamily=Media.FontFamily("Georgia"))
        let updateButton = new Button(Content="Update", Width=80.0, Height=24.0, IsEnabled=false)    
        let SetCharacterCount () =         
            let count = 140 - statusBox.Text.Length
            characterCounter.Content <- count
            characterCounter.Foreground <- 
                match count with
                | n when n >= 0 -> Media.SolidColorBrush(Media.Colors.Gray)
                | _ -> Media.SolidColorBrush(Media.Colors.Red)  
        SetCharacterCount ()      
        statusBox.TextChanged.Add (fun _ -> 
            SetCharacterCount ()
            updateButton.IsEnabled <- statusBox.Text.Length > 0 
        )            
        updateButton.Click.Add (fun _ -> 
            Tweet statusBox.Text |> ignore
            GetStatuses () |> RenderStatuses statusItems
            statusBox.Clear()
        )      
        let CreateStatusLayout () =
            let header = TableLayout(["*";"80"],[])
            Label(Content="What are you doing?", FontSize=16.0) |> header.AddItem
            characterCounter |> header.AddItem 
            let footer = TableLayout(["*";"80"],[])
            Label() |> footer.AddItem
            updateButton |> footer.AddItem
            let whatLayout = TableLayout([], ["1*";"2*";"1*"])
            whatLayout.Margin <- Thickness(4.0)
            whatLayout.AddItem header
            whatLayout.AddItem statusBox
            whatLayout.AddItem footer
            whatLayout               
        let windowLayout = TableLayout([], ["160";"*"])
        CreateStatusLayout () |> windowLayout.AddItem
        statusItems |> windowLayout.AddItem    
        let window = new Window(Title="fsweet", Width=320.0, Height=400.0)         
        //window.Icon <- Media.Imaging.BitmapFrame.Create(Uri(@"twitter_icon_16x16.bmp", UriKind.Relative))
        window.Content <- windowLayout
        window.Loaded.Add (fun _ -> GetStatuses () |> RenderStatuses statusItems)    
        (new Application()).Run(window) |> ignore