A Simple Wiki with Outlook backend

MJ - While contemplating ideas to use Tcl and tcom as a Getting Things Done frontend with all the data in Outlook, it occured to me that it's fairly trivial to build a basic wiki which stores the pages as Outlook notes. The code below is an initial version. A starpack can be downloaded from [1 ], this starpack contains a sample css file and a file with all the outlook constants.

This version is probably not very efficient and it is missing search, which is an essential part of any wiki. But it does allow wiki syntax in your Outlook notes and displaying it in a browser with links between the notes.

Note that it requires the Redemption COM server [2 ] for bypassing outlook security pop-ups. However if you don't mind the popups, changing the store to use Application.Outlook instead is trivial. Just change:

 set rdo [tcom::ref createobject "Redemption.RDOSession"] 
 $rdo Logon
 set notesFolder [$rdo GetDefaultFolder $c(OlFolderNotes)]

to

 set rdo [tcom::ref createobject "Outlook.Application"] 
 set notesFolder [[$rdo GetNamespace "MAPI"] GetDefaultFolder $c(OlFolderNotes)]

Current todos are:

  • Adding search, probably naive search at first (will be slow) and later maybe use outlook support for search folders to speed it up.
  • More extensive formatting
  • Efficiency improvements (caching?)
  • Clean up web server part or use some existing server (Wub?)
  • Add error checking :-)
  • Add Unicode support
  • Possibly change wiki-links to CamelCase because that's easier to type on a PDA

Code for the application:

 package require tcom
 package require snit

 set appdir [pwd]

 proc decodeUrl data {
    regsub -all {\+} $data { } data
    regsub -all {([][$\\])} $data {\\\1} data
    regsub -all {%([0-9a-fA-F][0-9a-fA-F])} $data {[format %c 0x\1]} data
    set data [subst $data]
 }

 snit::type WikiPage {
    option -id
    option -title
    option -body
    option -lastupdate
    variable header
    variable footer
    variable raw

    constructor args {
        $self configurelist $args
        set header       [list "'[$self cget -title]"]
        lappend header   "\[Homepage | /\] |"  
        lappend header   "\[Today | [clock format [clock seconds] -format {/view/Logbook_%Y_%m_%d}]\]"
        lappend header          "----" 
        set footer       [list ----] 
        lappend footer   "\[Edit | /edit/[$self cget -title]\]"
        lappend footer         "<form method='get' action=\"/cmd/search\">"
        lappend footer         "<input type =\"text\" name=text size=20>"
        lappend footer   "<input type=\"hidden\" name=\"show\" value=1>"
        lappend footer   "<input type=\"submit\" value =\"Find\"></form>"   
    }
    method id {} {
        $self cget -id
    }
    method asHTML {} {
        set result {}
        set raw [list {*}$header {*}[$self cget -body] {*}$footer]
        set inUL 0
        foreach line $raw {
            set line [regsub -all  {\[(.*?) \| (.*?)\]} $line {<a href='\2'>\1</A>}]
            set line [regsub -all  {\[(.*?)\]} $line {<a href='/view/\1'>\1</A>}]
            if {[string trimright $line] eq "----"} {
                set line "<hr/>"
            }
            if {[regexp {^('+)(.*)$} $line -> ticks text]} {
                set numTicks [string length $ticks]
                set line "<h$numTicks>$text</h$numTicks>"
            }

            if {$line eq {}} {
                set line "<p>"
            }
            if {[string index $line 0] eq {*}} {
                if {!$inUL} {
                    lappend result <ul>
                    set inUL 1
                }
                set line <li>[string range $line 1 end]</li>
            } elseif {$inUL} {
                lappend result </ul>
                set inUL 0
            }
            lappend result $line
        }
        join $result \r\n
    }
 }

 #=========================================

 snit::type OutlookStore {
    # Outlook by default creates notes where the Body has the Subject
    # as first line unless the title is not unique.
    # Hence the Subject field is used to identify the page
    typevariable c
    typevariable rdo
    typevariable notesFolder
    typeconstructor {
        set constantsFile [open [file join $::appdir constants.txt]]
        foreach line [split [read $constantsFile] \n] {
            set c([lindex $line 0]) [lindex $line 1]
        }
        close $constantsFile
        set rdo [tcom::ref createobject "Redemption.RDOSession"]
        $rdo Logon
        set notesFolder [$rdo GetDefaultFolder $c(OlFolderNotes)]
    }
  
    method getPage {title} {
            tcom::foreach note [$notesFolder Items] {
            if {$title eq [string trim [$note Subject]]} {
                return [list [lrange [split [string map [list \r {}] [$note Body]] \n] 1 end] $note]
            }
        }
        return "{} 0"
    }

    method savePage {title body} {
        lassign [$self getPage $title] _ id
        if {$id != 0} {
            $id Body $title\r\n$body
        } else {
            set id [[$notesFolder Items] Add]
            $id Subject $title
            $id Body $title\r\n$body
        }    
        $id Save
    }
    
 }

 #=========================================

 snit::type Server {
    option -port 5151
    option -store
    variable socket
    variable css

    constructor args {
        $self configurelist $args
        set cssFile [open [file join $::appdir wiki.css]]
        set css [read $cssFile]
        close $cssFile
        set socket [socket -server [list $self handle] [$self cget -port]]       
    }
    
    method handle {chan add port} {
        fconfigure $chan -translation crlf -buffering line
        set request [string trimleft [lindex [gets $chan] 1] /]
        lassign [split $request /] action page
        set page [decodeUrl $page]
        if {$action eq {}} {
            set action view
            set page HomePage
        }
        puts "Action: $action for page: $page"
        

     
        switch $action {
            view {
                puts $chan "HTTP/1.0 200 OK"
                puts $chan "Content-Type: text/html"
                puts $chan ""                
                puts $chan [$self viewPage $page]
            }
            edit {
                puts $chan "HTTP/1.0 200 OK"
                puts $chan "Content-Type: text/html"
                puts $chan ""                
                lassign [[$self cget -store] getPage $page] content id
                puts $content
                set content [join $content \n]
                puts $chan "<HTML><HEAD><TITLE>Edit $page</TITLE><LINK REL=\"stylesheet\" HREF=\"/css/wiki\">"
                puts $chan "</HEAD><BODY BGCOLOR=\"#FFF8F8\"><H1>Edit $page</H1>" 
                puts $chan "<form method=\"post\" action=\"/save/$page\">" 
                puts $chan "<textarea name='content' cols=80 rows=30>$content</textarea>"
                puts $chan "<BR><input type=\"submit\" value='Save'></form> </BODY></HTML>"
            }
            save {
                # assume save
                while {[gets $chan line]!=-1} {
                    lassign [split $line :] key val
                    puts "$key: $val"
                    if {$key eq "Content-Length"} {
                        set length [string trim $val]
                        incr length
                        break
                    }
                }
                set request [read $chan $length]
                set data [join [lrange [split $request =] 1 end] =]
                set data [decodeUrl $data]


                set store [$self cget -store]
                $store savePage $page $data

                # redirect to page
                puts $chan "HTTP/1.1 303 See other"
                puts $chan "Location: /view/$page"
                puts $chan ""
            }
            css {
                puts $chan "HTTP/1.0 200 OK"
                puts $chan "Content-Type: text/html"
                puts $chan ""                
                puts $chan $css
            }
            default {
                puts $chan "HTTP/1.0 200 OK"
                puts $chan "Content-Type: text/html"
                puts $chan ""                
                puts $chan "<h1>Unsupported request: $request</h1>" 
                puts stderr "Unsupported request: $request"
            }
        }
        close $chan
    }        


    method viewPage page {
        set store [$self cget -store]
        lassign [$store getPage $page]  body id
        set view [WikiPage %AUTO% -title $page -body $body -id $id]
        set body [$view asHTML]
        $view destroy

        set result "
        <html>
        <title>$page</title>
        <head>
        <link rel=\"stylesheet\" href=\"/css/wiki\" type=\"text/css\" />
        </head>
        <body>
            $body
        </body>
        </html>
        "
    }
 }

 OutlookStore store
 Server %AUTO% -store store
 vwait forever