~aleteoryx/tclircc

ref: 3d47eb23315b24f4a33ba1e1b0e75c69070a0c53 tclircc/irc.tcl -rwxr-xr-x 7.7 KiB
3d47eb23Aleteoryx Getting started a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#!/bin/tclsh

namespace eval ::irc {
  proc ::irc::is {type value {cap {}}} {
    # validation helper.
    # cap is a list of negotiated capabilities.
    switch -- $type {
      command { regexp {^([a-zA-Z]+|[0-9]{3})$} $value }
      command::named { regexp {^[a-zA-Z]+$} $value }
      command::numeric { regexp {^[0-9]{3}$} $value }

      tags { regexp {^\+?([^\r\n\0 ;/]/)?[a-zA-Z0-9\-]+(=[^\r\n\0; ]*)?(;\+?([^\r\n\0 ;/]/)?[a-zA-Z0-9\-]+(=[^\r\n\0; ]*)?)*$} $value }
      tags::tag { regexp {^\+?([^\r\n\0 ;/]/)?[a-zA-Z0-9\-]+(=[^\r\n\0; ]*)?$} $value }
      tags::key { regexp {^\+?([^\r\n\0 ;/]/)?[a-zA-Z0-9\-]+$} $value }
      tags::value { regexp {^[^\r\n\0; ]*$} $value }

      misc::dict { expr {![string is list $value] || [llength $value] % 2 == 1}}

      default { return -code error "unknown subcommand \"$subcommand\": must be command(|::named|::numeric), tags(|::tag|::key|::value), or misc::dict" }
    }
  }

  proc ::irc::esc {type value} {
    # for escaping specific things
    switch -- $type {
      tags::value {
        string map {"\\"  "\\\\"
                    "\n" {\n}
                    "\r" {\r}
                    {;}  {\:}
                    { }  {\s}} $value
      }

      default { return -code error "unknown subcommand \"$subcommand\": must be tags::value" }
    }
  }

  proc ::irc::unesc {type value} {
    # for unescaping specific things
    # needs to be handled manually due to Quirkiness
    switch -- $type {
      tags::value {
        set ret ""
        set backslash false
        foreach char [split $value ""] {
          if !$backslash {
            if {$char == "\\"} {
              set backslash true
            } else {
              set ret [string cat $ret $char]
            }
          } else {
            switch $char {
              n { set ret [string cat $ret "\n"] }
              r { set ret [string cat $ret "\r"] }
              : { set ret [string cat $ret ";"] }
              s { set ret [string cat $ret " "] }

              # covers backslash too
              default { set ret [string cat $ret $char] }
            }
            set backslash false
          }
        }

        return $ret
      }

      default { return -code error "unknown subcommand \"$subcommand\": must be tags::value" }
    }
  }

  proc ::irc::connect {hostname port {usetls 1}} {
    if $usetls {
      if {[info commands ::tls::socket] == ""} { package require tls }
      ::tls::socket $hostname $port
    } else {
      socket hostname port
    }
  }

  proc ::irc::tags {subcommand args} {
    switch -- $subcommand {
      exists {
        if {[llength $args] != 2} { return -code error "wrong # args: should be \"irc::tags exists tags key\"" }
        set tags [lindex $args 0]
        set key [lindex $args 1]

        foreach tag [split $tags ";"] {
          if { ![string first "$key=" $tag] || $tag == $key } { return true }
        }
        return false
      }
      remove {
        if {[llength $args] != 2} { return -code error "wrong # args: should be \"irc::tags remove tags key\"" }
        set tags [lindex $args 0]
        set ret ""
        set key [lindex $args 1]

        foreach tag [split $tags ";"] {
          if { [string first "$key=" $tag] && $tag != $key } {
            set ret [string cat $ret $tag ";"]
          }
        }

        string range $ret 0 end-1
      }
      get {
        if {[llength $args] ni {1 2}} { return -code error "wrong # args: should be \"irc::tags get tags ?key?\"" }
        set tags [lindex $args 0]
        set key [if {[llength $args] == 2} {lindex $args 1}]

        if {$key == ""} {
          set ret ""
          foreach tag [split $tags ";"] {
            set split [string first = $tag]
            set key [string range $tag 0 $split-1]
            set value [::irc::unesc tags::value [string range $tag $split+1 end]]
            lappend ret [list $key $value]
          }
          return $ret
        }

        foreach tag [split $tags ";"] {
          if {![string first "$key=" $tag]} {
            return [::irc::unesc tags::value [string range $tag [string first = $tag]+1 end]]
          } elseif { $tag == $key } { return "" }
        }

        return -code error "key \"$key\" not known in tags"
      }
      set {
        if {[llength $args] ni {2 3}} { return -code error "wrong # args: should be \"irc::tags set tags key ?value?\"" }
        set tags [lindex $args 0]
        set ret ""
        set key [lindex $args 1]
        set value [if {[llength $args] == 3} { lindex $args 2 }]
        set found false

        foreach tag [split $tags ";"] {
          if { !$found && ![string first "$key=" $tag] || $tag == $key } {
            if {$value != ""} {
              set ret [string cat $ret $key = [::irc::esc tags::value $value] ";"]
            } else {
              set ret [string cat $ret $key ";"]
            }
            set found true
          } else {
            set ret [string cat $ret $tag ";"]
          }
        }

        if !$found {
          if {$value != ""} {
            set ret [string cat $ret $key = [::irc::esc tags::value $value] ";"]
          } else {
            set ret [string cat $ret $key ";"]
          }
        }

        string range $ret 0 end-1
      }

      create {
        set ret ""
        dict for {key value} $args {
          set ret [::irc::tags set $ret $key $value]
        }
        return $ret
      }
      merge {
        set ret ""

        foreach tags $args {
          if [::irc::is tags $tags] {
            foreach tag [split $tags ";"] {
              set split [string first = $tag]
              set key [string range $tag 0 $split-1]
              set value [::irc::unesc tags::value [string range $tag $split+1 end]]
              set ret [::irc::tags set $ret $key $value]
            }
          } elseif [::irc::is misc::dict $tags] {
            dict for {key value} $tags {
              set ret [::irc::tags set $ret $key $value]
            }
          } else {
            return -code error "argument is not tags or a dict"
          }
        }
        return $ret
      }
      dict {
        if {[llength $args] != 1} { return -code error "wrong # args: should be \"irc::tags dict tags\"" }
        set tags [lindex $args 0]

        set ret ""
        foreach tag [split $tags ";"] {
          set split [string first = $tag]
          set key [string range $tag 0 $split-1]
          set value [::irc::unesc tags::value [string range $tag $split+1 end]]
          dict set ret $key $value
        }
        return $ret
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be create, dict, exists, get, merge, remove, or set" }
    }
  }

  proc ::irc::cmd {subcommand args} {
    switch -- $subcommand {
      fmt {
        if [llength $args] {
          set tags ""
          if {[lindex $args 0] == "-tags"} {
            if {[llength $args] < 3} { return -code error "missing tags dictionary argument to -tags option" }
            set tags [::irc::tags merge [lindex $args 0]]
            set args [lrange $args 2 end]
          }
          # TODO: source generation/parsing, actual command formatting
          set source ""
          if {[lindex $args 0] == "-tags"} {
            if {[llength $args] < 3} { return -code error "missing tags map argument to -tags option" }
            set tags [lindex $args 0]
            set args [lrange $args 2 end]
          }

          set cmd [lindex $args 0]
          if ![::irc::is command $cmd] { return -code error "invalid irc command \"$cmd\"" }
          set cmd [string toupper $cmd]
        } else { return -code error "wrong # args: should be \"irc::cmd fmt ?-tags tags? ?-source source? command ?arg ...? \"" }
      }
      send { ... }
      default { return -code error "unknown subcommand \"$subcommand\": must be fmt or send" }
    }
  }
}