view tj.tcl @ 613:ea6ebcf42b47

TJ: Initial commit of the TJ2.0 refactoring effort. Instead of using Eggdrop XTRA user flags for storing data, we now employ a SQLite3 database. The database schema is probably not yet "final", so maybe beware of deploying this script yet. Also add example configuration file and script for creating the empty database tables.
author Matti Hamalainen <ccr@tnsp.org>
date Wed, 10 Feb 2021 11:12:13 +0200
parents 14dfb925a64a
children 8c61b4032648
line wrap: on
line source

##########################################################################
#
# TJ v2.0 by ccr/TNSP <ccr@tnsp.org>
# (C) Copyright 2021 Tecnic Software productions (TNSP)
#
# This script is freely distributable under GNU GPL (version 2) license.
#
##########################################################################

### The configuration should be in config.tj in same directory
### as this script. Or change the line below to point where ever
### you wish. See "config.tj.example" for an example config file.
source [file dirname [info script]]/config.tj

### Required utillib.tcl
source [file dirname [info script]]/utillib.tcl


##########################################################################
# No need to look below this line
##########################################################################
package require sqlite3
package require textutil::split

set tj_name "TJ"
set tj_message "$tj_name v2.0 (C) 2021 ccr/TNSP"


# ------------------------------------------------------------------------
### Utility functions
proc tj_log {umsg} {
  global tj_log_enable tj_name
  if {$tj_log_enable != 0} {
    putlog "${tj_name}: $umsg"
  }
}


proc tj_qm {uid} {
  global tj_messages

  if {[info exists tj_messages($uid)]} {
    return $tj_messages($uid)
  } else {
    return $uid
  }
}


proc tj_smsg {apublic anick achan amsg {aargs {}}} {
  global tj_preferredmsg tj_cmd_name
  set amsg [string map [list "@cmd@" $tj_cmd_name] $amsg]
  utl_msg_args $tj_preferredmsg $apublic $anick $achan $amsg $aargs
}


proc tj_msg {apublic anick achan aid {aargs {}}} {
  tj_smsg $apublic $anick $achan [tj_qm $aid] $aargs
}


proc tj_correct_handle {uhand} {
  set ulower [string tolower $uhand]
  foreach uuser [userlist] {
    if {[string tolower $uuser] == $ulower} {
      return $uuser
    }
  }
  return ""
}


proc tj_cmd_match {uid ustr} {
  global tj_commands
  return [utl_cmd_match tj_commands $uid $ustr]
}


proc tj_arg_rest {rarglist rindex rstr} {
  upvar $rstr dstr

  if {$rindex < [llength $rarglist]} {
    set dstr [join [lrange $rarglist $rindex end] " "]
    return 1
  } else {
    return 0
  }
}


proc tj_get_id {rstr rindex rid} {
  global tj_default_id
  upvar $rindex uindex
  upvar $rid uid

  if {[string index $rstr 0] == "#"} {
    set uid [string tolower [string range $rstr 1 end]]
    incr uindex
    return 1
  } else {
    set uid $tj_default_id
    return 0
  }
}


proc tj_ctime {ustamp} {
  return [clock format $ustamp -format [tj_qm "datefmt"]]
}


# ------------------------------------------------------------------------
proc tj_str_append { qlist qvalue qsingular qplural } {
  upvar $qlist ulist
  if {$qvalue > 0} {
    if {$qvalue > 1} {
      set qfmt $qplural
    } else {
      set qfmt $qsingular
    }
    lappend ulist [utl_str_map_values [tj_qm "tj_str_${qfmt}"] [list $qvalue]]
  }
}


proc tj_get_tj_str {udelta} {

  if {$udelta < 0} {
    set utmp [expr -$udelta]
  } else {
    set utmp $udelta
  }

  set uyears [expr ($utmp / (365*24*60*60))]
  set urem [expr ($utmp % (365*24*60*60))]
  set udays [expr ($urem / (24*60*60))]
  set urem [expr ($urem % (24*60*60))]
  set uhours [expr ($urem / (60*60))]
  set urem [expr ($urem % (60*60))]
  set uminutes [expr ($urem / 60)]

  set ulist {}
  tj_str_append ulist $uyears "year" "years"
  tj_str_append ulist $udays "day" "days"
  tj_str_append ulist $uhours "hour" "hours"
  tj_str_append ulist $uminutes "minute" "minutes"

  set ustr [join [lrange $ulist 0 end-1] [tj_qm "tj_str_sep"]]
  if {[llength $ulist] > 1} {
    append ustr [tj_qm "tj_str_sep_last"]
  }
  append ustr [lindex $ulist end]

  return $ustr
}


proc tj_get_tj_str_delta { ustamp } {
  set udelta [expr $ustamp - [clock seconds]]

  if {$udelta < 0} {
    set ufmt "tj_past"
  } else {
    set ufmt "tj_remaining"
  }

#  return [utl_str_map_values [tj_qm $ufmt] [list ]]
  return [tj_get_tj_str $udelta]
}


proc tj_display_tjs {upublic unick uchan uname uid} {
  set uclock [clock seconds]
  set nresults 0
  set usql "SELECT * FROM tj WHERE uuser='[utl_escape $uname]' AND utype=0 AND uid='[utl_escape $uid]'"
  tj_dbh eval $usql {
    incr nresults
    set ustamp [utl_sql_datetime_to_stamp $utarget]
    set udelta [expr $ustamp - $uclock]

    if {$udelta < 0} {
      set ufmt "tj_past"
    } else {
      set ufmt "tj_remaining"
    }

    tj_msg $upublic $unick $uchan $ufmt [list $uname $uid [tj_get_tj_str $udelta] [tj_ctime $ustamp]]
  }

  if {!$nresults} {
    tj_msg $upublic $unick $uchan "tj_not_set" [list $uname $uid]
  }
}


# ------------------------------------------------------------------------
proc tj_cmd {unick $uhost uhand uchan uargs upublic} {
  global tj_messages tj_default_id tj_default_time tj_default_desc

  # Check if we have a valid user
  if {![utl_valid_user $uhand]} {
    tj_msg $upublic $unick $uchan "err_invalid_user" [list $uhand]
    return 1
  }

  set uhand [tj_correct_handle $uhand]

  # Check and handle arguments
  set rarglist [::textutil::split::splitx $uargs {\s+}]
  set rcmd [lindex $rarglist 0]

  if {[tj_cmd_match "help" $rcmd]} {
    # Show help
    foreach ukey $tj_messages(help_full) {
      tj_msg $upublic $unick $uchan $ukey
    }
    return 1
  } elseif {[tj_cmd_match "set" $rcmd]} {
    # !tj set [#id] <[-/+]days [[+/-]<hours>] | dd.mm.yyyy [hh:mm]> [<desc>]
    set rindex 1
    if {![utl_arg_get $rarglist rindex rarg 0]} {
      tj_msg $upublic $unick $uchan "help_set"
      return 1
    }

    # Check for #id prefix
    if {[tj_get_id $rarg rindex uid]} {
      set rdesc ""
    } else {
      set rdesc $tj_default_desc
    }

    if {![utl_arg_get $rarglist rindex rarg 1]} {
      tj_msg $upublic $unick $uchan "help_set"
      return 1
    }

    # Is it a time stamp?
    if {[regexp {\d+\.\d+\.\d\d\d\d} $rarg rdate]} {
      # Seems so .. check for hours
      set rtime $tj_default_time
      if {[utl_arg_get $rarglist rindex rarg 0]} {
        if {[regexp {\d+:\d+} $rarg rtime]} {
          incr rindex
        } else {
          set rtime $tj_default_time
        }
      }

      tj_arg_rest $rarglist $rindex rdesc

      # Check the timestamp validity
      if {[catch {set rstamp [clock scan "${rdate} ${rtime}" -format "%d.%m.%Y %H:%M"]} uerrmsg]} {
        tj_msg $upublic $unick $uchan "err_timestamp" [list $uerrmsg $rdate $rtime]
        return 1
      }

    } elseif {[regexp {(\+|\-)?(\d+)} $rarg -> dsign rdays]} {
      # Check for days
      if {$dsign == "-"} { set rdays [expr -$rdays] }

      set rhours 0
      if {[utl_arg_get $rarglist rindex rarg 0]} {
        if {[regexp {(\+|\-)?(\d+)} $rarg -> hsign rhours]} {
          if {$hsign == "-"} { set rhours [expr -$rhours] }
          incr rindex
        }

        tj_arg_rest $rarglist $rindex rdesc

        if {$rhours < -24 || $rhours > 24} {
          tj_msg $upublic $unick $uchan "err_invalid_hours" [list $rhours]
          return 1
        }
      }

      # Compute target timestamp
      set rstamp [expr [clock seconds] + ($rdays * 24 * 60 * 60) + ($rhours * 60 * 60)]
    } else {
      # Only description, if any
      if {![tj_arg_rest $rarglist 1 rdesc]} {
        tj_msg $upublic $unick $uchan "err_invalid_format" [list]
        return 1
      }
      set rstamp "invalid"
    }

    # Check if ID exists
    set umode 1
    set nres 0
    set usql "SELECT * FROM tj WHERE uid='[utl_escape $uid]'"
    tj_dbh eval $usql {
      set umode 0
      set cid $id
      set ctarget $utarget
      incr nres
    }

    # Check for DB sanity
    if {$nres > 1} {
      tj_msg $upublic $unick $uchan "err_db_corrupt" [list $nres]
      tj_log "too many $nres fatal error piip"
      return 1
    }

    # Check if we are trying to add a new entry without valid timestamp
    if {$rstamp == "invalid"} {
      if {$umode} {
        tj_msg $upublic $unick $uchan "err_missing_timestamp"
        return 1
      }

      set rdate $ctarget
      set ustamp [utl_sql_datetime_to_stamp $ctarget]
    } else {
      set rdate [utl_sql_stamp_to_datetime $rstamp]
      set ustamp $rstamp
    }

    set utjstr [tj_get_tj_str_delta $rstamp]
    set udate [tj_ctime $ustamp]

    # Max reminders ..
    set usql "SELECT COUNT(*) FROM tj WHERE uuser='[utl_escape $uhand]'"
    if {[catch {set nreminders [tj_dbh onecolumn $usql]} uerrmsg]} {
      tj_msg $upublic $unick $uchan "err_sql" [list $uerrmsg]
      tj_log "$uerrmsg on SQL:\n$usql"
      return 1
    }

    # Check for maximum reminders
    if {$nreminders > 5} {
      tj_msg $upublic $unick $uchan "err_too_many" [list $nreminders]
      return 1
    }

    if {$umode} {
      # Insert new entry
      set usql "INSERT INTO tj (uid,uuser,ucreated,udesc,utype,utarget) VALUES ('[utl_escape $uid]', '[utl_escape $uhand]', [unixtime], '[utl_escape $rdesc]', 0, '$rdate')"
      if {[catch {tj_dbh eval $usql} uerrmsg]} {
        tj_msg $upublic $unick $uchan "err_sql" [list $uerrmsg]
        tj_log "$uerrmsg on SQL:\n$usql"
        return 1
      }

      set cid [tj_dbh last_insert_rowid]
      tj_msg $upublic $unick $uchan "tj_set" [list $cid $uid $uhand $rdesc $udate $utjstr]
      tj_log "tj_set $cid:$uid:$uhand:$rdesc:$rdate:$udate"
    } else {
      # Update existing entry
      set usqlargs {}
      if {[string length $rdesc] > 0} {
        lappend usqlargs "udesc='[utl_escape $rdesc]'"
      }
      if {$rstamp != "invalid"} {
        lappend usqlargs "utarget='$rdate'"
      }

      if {[llength $usqlargs] > 0} {
        set usql "UPDATE tj SET [join $usqlargs ,] WHERE uid='[utl_escape $uid]'"
        if {[catch {tj_dbh eval $usql} uerrmsg]} {
          tj_msg $upublic $unick $uchan "err_sql" [list $uerrmsg]
          tj_log "$uerrmsg on SQL:\n$usql"
          return 1
        }
      }

      tj_msg $upublic $unick $uchan "tj_updated" [list $cid $uid $uhand $rdesc $udate $utjstr]
      tj_log "tj_updated $cid:$uid:$uhand:$rdesc:$rdate:$udate"
    }

  } elseif {[tj_cmd_match "remind" $rcmd]} {
    # !tj remind <#id> <<dd.mm.yyyy [hh:mm]>|<message>>
    # !tj remind <#id> <<dd.mm [hh:mm]>|<message>>
    if {[llength $rarglist] < 3} {
      tj_msg $upublic $unick $uchan "help_add"
      return 1
    }

  } elseif {[tj_cmd_match "list" $rcmd]} {
    # !tj list
    # List reminders
    set uclock [clock seconds]
    set nresults 0
    set usql "SELECT * FROM tj WHERE uuser='[utl_escape $uhand]' ORDER BY ucreated"
    tj_dbh eval $usql {
      incr nresults
      tj_msg $upublic $unick $uchan "#${nresults}: $utype : $utarget : $uid : $udesc"
    }

    if {!$nresults} {
      tj_msg $upublic $unick $uchan "no_results" [list $uname]
    }

  } elseif {[tj_cmd_match "delete" $rcmd]} {
    # !tj delete <id>
    tj_msg $upublic $unick $uchan "help_delete"
  } elseif {[tj_cmd_match "show" $rcmd]} {
    # !tj show [#id] <name>
    # Check for #id prefix
    set rindex 1
    if {[utl_arg_get $rarglist rindex rarg 0]} {
      tj_get_id $rarg rindex uid
    }

    if {![utl_arg_get $rarglist rindex rarg 0]} {
      tj_msg $upublic $unick $uchan "help_show"
      return 1
    }

    set uname [tj_correct_handle $rarg]
    if {$uname == ""} {
      tj_msg $upublic $unick $uchan "err_unknown_user" [list $rcmd]
      return 1
    }

    tj_display_tjs $upublic $unick $uchan $uname $uid
  } else {
    # !tj [#id] [name]
    set uname $uhand
    set rindex 0
    if {[utl_arg_get $rarglist rindex rarg 0]} {
      # Check for #id prefix
      tj_get_id $rarg rindex uid

      if {[utl_arg_get $rarglist rindex rarg 0]} {
        set uname [tj_correct_handle $rarg]
        if {$uname == ""} {
          tj_msg $upublic $unick $uchan "err_unknown_user" [list $rcmd]
          return 1
        }
      }
    } else {
      set uid $tj_default_id
    }

    tj_display_tjs $upublic $unick $uchan $uname $uid
  }

  return 1
}


#-------------------------------------------------------------------------
# Script initialization
#-------------------------------------------------------------------------
putlog "$tj_message"

if {[catch {sqlite3 tj_dbh $tj_db_file} uerrmsg]} {
  putlog "Could not open SQLite3 database '${tj_db_file}': ${uerrmsg}"
  exit 2
}

# end of script