tii

Tcl-based suite for working with ii/idec protocol
git clone git://git.luxferre.top/tii.git
Log | Files | Refs | README

commit 8981e6cfc81a5e4bb10c19b908a9152634c53822
parent c026d357531e952367e19631d7859e86ccca45a7
Author: Luxferre <lux@ferre>
Date:   Mon, 21 Oct 2024 22:10:54 +0300

Added the cli echo viewer and rudimentary README

Diffstat:
AREADME | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtiifetch.tcl | 38++++++++++++++++++++++++++++++++------
Atiiview.tcl | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 313 insertions(+), 6 deletions(-)

diff --git a/README b/README @@ -0,0 +1,134 @@ +tii: a Tcl-based suite for working with ii/IDEC protocol +======================================================== +This suite implements the client side of ii and (partially) IDEC protocols of +distributed, cross-platform, text-based communication (a FIDOnet successor, so +to speak). Protocol documentation can be found here for both ii and IDEC: +https://github.com/IDEC-net/new-docs/blob/master/protocol-en.md +(there will be an effort to write a more concise version of that doc) + +The tii suite requires at least Tcl 8.6 to run. Running it inside starpacks is +possible but not recommended. + +The tii repo consists of the following parts: + +* tiifetch.tcl: the core ii/IDEC message fetching library and CLI utility +* tiipost.tcl: the core ii/IDEC message posting library and CLI utility +* tiiview.tcl: the CLI viewer of the fetched ii/IDEC messages and conferences +* tiidb: the (overridable) directory that contains all messages and echo lists +* stations.txt: the list of stations to be auto-fetched by tiifetch when none + of its command-line parameters is passed +* auth.txt: the list of station/authstring mappings to be used by tiipost when + posting messages to a particular station +* tiix.tcl: the GUI ii/IDEC viewer that also leverages tiifetch and tiipost to + provide fetching and posting functionality + +Readiness status +---------------- +* tiifetch.tcl: ready/tested +* tiipost.tcl: planned +* tiiview.tcl: ready/tested +* tiidb (format): ready/tested +* stations.txt (format): ready/tested +* auth.txt (format): ready to be implemented +* tiix.tcl: planned +* Overall status: work in progress + +Usage +----- +This section is a work in progress and will be updated as more components are +developed. + +### Fetching the messages (tiifetch.tcl): ### + +tclsh tiifetch.tcl [station_url] [echos] [dbdir] + +This command will fetch all messages into the dbdir ("tiidb" in the script dir +by default) from the station_url (can be empty, see below) based on the echo +conference names (can be delimited with slash /, comma (,) or semicolon (;)) +and create the corresponding file structure if it's missing. + +Fetching is supported for the following station URL schemes and protocols: + +* HTTP (http://) +* HTTPS (https://) +* Gopher (gopher://) +* Gopher over TLS (gophers://) +* Finger (finger://) +* Nex (nex://) +* Spartan (spartan://) +* Gemini (gemini://) + +If the station_url parameter is empty or no parameters are passed at all, +tiifetch.tcl will look for a file called stations.txt that lists (each on a +new line) all the station URLs to sync from. Messages from all listed stations +will be merged into the same echo conference database. + +### Viewing the messages from CLI (tiiview.tcl): ### + +tclsh tiiview.tcl [echo_name] [filter_string] [line_width] [dbdir] + +If the echo_name parameter is passed, this command will write all formatted +messages from the coresponding echo conference to the standard output. +You should pipe this message stream to your $PAGER terminal application, like +less or most. The messages will be formatted according to the filter_string +format (see below). If the filter string is empty or omitted, the messages +will appear in the exact order their IDs appear in the echo conference file. + +If the echo_name parameter is empty or no parameters are passed at all, this +command will output the list of echo conferences registered in the dbdir. +The conference list will be alphabetically ordered and the filter_string will +have no effect at all if you pass it. + +If the line_width parameter is omitted, the text reflows to 80 chars per line. + +If the dbdir is ommitted, it defaults to "tiidb" in the script directory. + +This component is fully offline and can only work with a compatible message +database that tiifetch.tcl can generate (see "Message database format"). + +The filter string can take one of the following basic forms: + +* h[number]: only take [number] messages from the head (start) of the list +* t[number]: only take [number] messages from the tail (end) of the list +* rh[number]: same as h but output messages from newest to oldest in the list +* rt[number]: same as t but output messages from newest to oldest in the list + +If [number] is 0 then it means no message limit. +The reverse operation is always done after limiting the results. + +e.g. rt50 will output 50 newest messages in the conference, starting from the +most recent one. The default basic value is h0, so no filter applied will mean +outputting all messages from the oldest to the newest. + +You can extend the basic form by appending a search regular expression to it, +like this: [form]/[regex]. Note that the regex always applies AFTER the basic +filtering has been done. Also note that the search is done within all fields +of the message. E.g. h100/retro will find the retro-themed messages among the +first 100 of them. + +Message database format +----------------------- +The tiidb format is based upon the official ii/IDEC developer recommendations +and is fully plaintext-based, portable and very simple: + +* Message contents are stored decoded in the "msg/" subdirectory. The file + names are their plain 20-character hash IDs. +* Echo contents are stored in the "echo/" subdirectory. The file names are the + conference names verbatim, containing newline-separated message IDs that + belong to those conferences, in order they were published there. + Every echo file ends with a blank line. + +FAQ +--- + +- Does tii implement any IDEC extensions? + +Only one: fetching list.txt from the station to get the entire list of echo +conferences served by this station. This is something that the original ii +spec did not support. + + +Credits +------- +Created by Luxferre in 2024, released into public domain with no warranties. + diff --git a/tiifetch.tcl b/tiifetch.tcl @@ -1,13 +1,12 @@ #!/usr/bin/env tclsh # tiifetch: fetch all data from an ii/idec station into the local text db # (see https://github.com/idec-net/new-docs/blob/master/protocol-en.md) -# Usage: tiifetch station_http_url [echos] [db_dir] +# Usage: tiifetch.tcl [station_url] [echos] [db_dir] # The echo list should be delimited with slash (/), comma (,) or semicolon (;) # if no echos are specified (or "" is passed), then list.txt will be fetched # and then all missing echo content from it will be downloaded # If db_dir isn't specified, it's fetched and merged into the -# tiidb directory in the program root -# with echoconfs and messages respectively +# tiidb directory in the program root with echoconfs and messages respectively # This component only fetches the messages, doesn't parse or display them # Supported protocols: HTTP, HTTPS, Gemini, Spartan, Gopher/Finger/Nex # Depends on Tcllib for URI parsing @@ -268,6 +267,24 @@ proc fetchiidb {url echos dbdir dolog} { } } +proc massfetch {echos dbdir dolog} { + global appdir + if {$dolog eq 1} {puts "No ii/idec station URL specified, using stations.txt"} + set stfile [file join $appdir "stations.txt"] + if {[file exists $stfile]} { + set stlist [split [readfile $stfile] "\n"] + foreach station $stlist { + set station [string trim $station] + if {$station ne ""} { + if {$dolog eq 1} {puts "Fetching from $station"} + fetchiidb $station $echos $dbdir $dolog + } + } + } else { + if {$dolog eq 1} {puts "No stations.txt found, bailing out!"} + } +} + # end of procs, start the entrypoint if {![info exists argv0] || [file tail [info script]] ne [file tail $argv0]} {return} @@ -278,13 +295,22 @@ if [string match *app-tiifetch $appdir] { set appdir [file normalize [file join $appdir ".." ".." ".." ]] } +set localdbdir [file join $appdir "tiidb"] if {$argc > 0} { - set localdbdir [file join $appdir "tiidb"] if {$argc > 2} { set localdbdir [lindex $argv 2] } puts "Fetching messages, please wait..." - fetchiidb [lindex $argv 0] [lindex $argv 1] $localdbdir 1 + set sturl [string trim [lindex $argv 0]] + if {$sturl eq ""} { + massfetch [lindex $argv 1] $localdbdir 1 + } else { + fetchiidb $sturl [lindex $argv 1] $localdbdir 1 + } puts "Messages fetched" -} else {puts "No ii/idec station URL specified!"} +} else { + puts "Fetching messages, please wait..." + massfetch "" $localdbdir 1 + puts "Messages fetched" +} diff --git a/tiiview.tcl b/tiiview.tcl @@ -0,0 +1,147 @@ +#!/usr/bin/env tclsh +# tiiview: view ii/idec messages from the local text db +# Usage: tiiview.tcl [echo_name] [filter_string] [termwidth] [dbdir] +# Created by Luxferre in 2024, released into public domain + +# file read helper +proc readfile {fname} { + set fp [open $fname r] + fconfigure $fp -encoding utf-8 + set data [read $fp] + close $fp + return $data +} + +# basic text reflow helper +# list in, string out +proc tiiflow {lines width} { + set outtext {} + foreach inl $lines { + set l [string length $inl] + if {$l <= $width} { + append outtext "$inl\n" + } else { + set wordset "" + set words [split $inl " "] + foreach w $words { + set candidate "$wordset $w" + if {[string length $candidate] <= $width} { + set wordset $candidate + } else { + append outtext "$wordset\n" + set wordset $w + } + } + append outtext "$wordset\n" + } + } + return $outtext +} + +# parse and pretty-print the found message +proc formatmessage {msgdata msgid globalwidth} { + set globalline [string repeat = $globalwidth] + set hdrline [string repeat - $globalwidth] + set msglines [lmap s [split $msgdata "\n"] {string trimright $s}] + # parsing according to the spec, first 7 lines are: + # tags, echoarea, timestamp, msgfrom, msgfrom_addr, msgto, subj + # and then an empty line and the message body follows + set tags [split [lindex $msglines 0] "/"] + if {[dict exists $tags repto]} { + set replyto [dict get $tags repto] + } else {set replyto ""} + set echoarea [lindex $msglines 1] + set timestamp [lindex $msglines 2] + set msgfrom [lindex $msglines 3] + set msgfromaddr [lindex $msglines 4] + set msgto [lindex $msglines 5] + set subj [lindex $msglines 6] + set msgbody [tiiflow [lrange $msglines 8 end] $globalwidth] + set tz "" + set renderedts [clock format $timestamp -format {%Y-%m-%d %H:%M:%S} -timezone $tz] + catch { # because pipe can be broken anytime + puts "\[$renderedts\] ii://$msgid" + puts "$echoarea - $msgfrom ($msgfromaddr) to $msgto" + if {$replyto ne ""} { + puts "Replied to: ii://$replyto" + } + puts "Subj: $subj" + puts $hdrline + puts "$msgbody$globalline\n" + } +} + +# entry point +set scriptpath [file normalize [info script]] +set appdir [file dirname $scriptpath] +# check if we're running from a starpack +if [string match *app-tiiview $appdir] { + set appdir [file normalize [file join $appdir ".." ".." ".." ]] +} +set localdbdir [file join $appdir "tiidb"] +set echoname "" +set filterstr "" +set twidth 80 +if {$argc > 0} { + if {$argc > 1} { + set filterstr [string trim [lindex $argv 1]] + } + if {$argc > 2} { + set twidth [expr {int([lindex $argv 2])}] + } + if {$argc > 3} { + set localdbdir [lindex $argv 3] + } + set echoname [string trim [lindex $argv 0]] +} +if {$twidth < 20} {set twidth 80} +if {$filterstr eq ""} {set filterstr "h0"} +set msgdir [file join $localdbdir "msg"] +set echodir [file join $localdbdir "echo"] +if {$echoname eq ""} { # list the echodir + set echos [glob -tails -directory $echodir -nocomplain -types f "*.*"] + puts [join [lsort $echos] "\n"] +} else { # fetch the actual contents + set echofile [file join $echodir $echoname] + if {[file exists $echofile]} { + set msglist [split [readfile $echofile] "\n"] + set filters [split $filterstr "/"] + set basicmod [string trim [lindex $filters 0]] + set filterregex {} + if {[llength $filters] > 1} { + set filterregex [string trim [lindex $filters 1]] + } + set doreverse 0 + if {[string first r $basicmod] > -1} {set doreverse 1} + set dotail 0 + if {[string first t $basicmod] > -1} {set dotail 1} + set numitems 0 + if {[regexp {\d+} $basicmod foundnum]} {set numitems $foundnum} + # perform the element filtering + if {$numitems > 0} { + incr numitems -1 + if {$dotail eq 1} { + set msglist [lrange $msglist end-$numitems end] + } else { + set msglist [lrange $msglist 0 $numitems] + } + } + if {$doreverse eq 1} { + set msglist [lreverse $msglist] + } + foreach msgid $msglist { # iterate over the list after filtering + set msgid [string trim $msgid] + if {$msgid ne ""} { + set msgfile [file join $msgdir $msgid] + if {[file exists $msgfile]} { + set msgdata [readfile $msgfile] + set pass 1 + if {$filterregex ne {}} { + set pass [regexp -line -nocase -- $filterregex $msgdata] + } + if {$pass eq 1} {formatmessage $msgdata $msgid $twidth} + } + } + } + } else {puts "This echo conference doesn't exist in the local DB!"} +}