~technomancy/taverner

taverner/commands.fnl -rw-r--r-- 8.2 KiB
a86534a4Phil Hagelberg When notifying of nick changes, send user as well. 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
(local lume (require :lume))
(local channel (require :channel))
(local valid (require :valid))

(fn call-with-channel [state cstate channel-name f ...]
  (match (. state.channels channel-name)
    ch (f ch ...)
    nil (state:send cstate :403 channel-name ":No such channel")))

(fn call-with-user [state cstate nick f ...]
  (match (lume.match state.clients #(= nick $.nick))
    client (f client ...)
    nil (state:send cstate :401 nick ":No such nick")))

(fn NAMES [state cstate channel-name]
  (fn f [ch]
    (state:send cstate :353 "=" channel-name ":"
                (table.unpack (ch.member-names)))
    (state:send cstate :366 channel-name ":End of /NAMES list"))
  (call-with-channel state cstate channel-name f))

(fn MODE [state cstate target ?modestr ?args]
  (fn mode-channel [ch]
    (if (= nil ?modestr) (state:send cstate :221 (ch.get-modes))

        (and (channel.list-mode? ?modestr) (not ?args))
        (state:send cstate :324 ch.name ?modestr (ch.mode-string ?modestr))

        (not (ch.op? cstate.nick))
        (state:send cstate :482 ":You're not channel operator")

        (match (ch.set-mode ?modestr ?args)
          (_ cmd msg) (state:send cstate cmd msg)
          _ (ch.send nil cstate.nick :MODE ?modestr ?args))))
  (if (target:find "^#")
      (call-with-channel state cstate target mode-channel)
      (state:send cstate :501 ":Mode is only supported on channels.")))

(fn TOPIC [state cstate channel-name ?topic]
  (fn f [ch]
    (if ?topic
      (if (ch.may-change-topic? cstate.nick)
          (ch:set-topic cstate.nick ?topic)
          (state:send cstate :482 channel-name ":You're not channel operator"))
      (do (state:send cstate :332 channel-name
                      (.. ":" (or ch.topic "No topic set.")))
          (when ch.topic
            (state:send cstate :333 channel-name
                        ch.topic-set-by ch.topic-set-at)))))
  (call-with-channel state cstate channel-name f))

(fn JOIN [state cstate channel-name ?key]
  (state.log cstate.nick :joining channel-name ?key)
  (match-try (channel.get-or-make state cstate channel-name)
    ch (ch.allowed? cstate.nick (if (= ?key "") nil ?key))
    true (ch.join cstate.nick cstate.conn)
    _ (do (tset state.channels channel-name ch)
          (TOPIC state cstate channel-name)
          (NAMES state cstate channel-name))
    (catch
     (_ cmd msg) (state:send cstate cmd msg))))

(fn PART [state cstate channel-name]
  (state.log cstate.nick :parting channel-name)
  (fn f [ch]
    (if (ch.member? cstate.nick)
        (ch.part cstate.nick)
        (state:send cstate :422 ":You're not on that channel")))
  (call-with-channel state cstate channel-name f))

(fn msg-channel [state cstate channel-name command-name msg]
  (fn f [ch]
    (if (not (ch.member? cstate.nick))
        (state:send cstate :404 ":You aren't on that channel")
        (if (or (not (ch.mode :m)) (ch.voice? cstate.nick) (ch.op? cstate.nick))
            (ch.send cstate.nick (.. ":" cstate.nick)
                     command-name channel-name (.. ":" msg))
            (state:send cstate :404 ":Cannot send (+m)"))))
  (call-with-channel state cstate channel-name f))

(fn msg-user [state cstate to cmd msg]
  (fn f [{: conn}]
    (conn:send (.. ":" cstate.nick " " cmd " " cstate.nick " :" msg "\r\n")))
  (call-with-user state cstate to f))

(fn PRIVMSG [state cstate to msg]
  (if (to:match "^#")
      (msg-channel state cstate to :PRIVMSG msg)
      (msg-user state cstate to :PRIVMSG msg)))

(fn NOTICE [state cstate to msg]
  (if (to:match "^#")
      (msg-channel state cstate to :NOTICE msg)
      (msg-user state cstate to :NOTICE msg)))

(fn QUIT [state cstate ?reason]
  (tset state.clients cstate.conn nil)
  (cstate.conn:close)
  (each [_ ch (pairs state.channels)]
    (when (ch.member? cstate.nick)
      (ch.part cstate.nick)))
  (each [conn client (pairs state.clients)]
    (when client.nick
      (state:send client :QUIT cstate.nick (or ?reason "Quit")))))

(fn WHOIS [state cstate nick] ; just an empty reply for now
  (if (state:connected? nick)
      (state:send cstate :318 ":End of /WHOIS list")
      (state:send cstate :401 nick ":No such nick")))

(fn who-reply [state cstate nick channel]
  ;; technically supposed to be"<client> <channel> <username> <host> <server>
  ;; <nick> <flags> :<hopcount> <realname>" but who has the time
  (state:send cstate :352 channel nick))

(fn WHO [state cstate mask]
  (let [ch (. state.channels mask)]
    (if (and ch (not (ch.mode :s))) ; channel exists, not secret
        (each [_ nick (ipairs (ch.member-names))]
          (who-reply state cstate nick mask))
        (state:connected? mask)
        (who-reply state cstate mask "*")
        (each [_ {: nick} (pairs state.clients)]
          (when (and nick (string.find nick mask))
            (who-reply state cstate nick "*")))))
  (state:send cstate :315 ":End of WHO"))

(fn PING [state cstate token]
  (state:send cstate :PONG token))

(fn LIST [state cstate]
  ;; 321 is supposed to be optional but some clients don't work without it.
  (state:send cstate :321 "Channel :Users Name")
  (each [_ ch (pairs state.channels)]
    (when (or (not (ch.mode :s)) (ch.member? cstate.nick))
      (state:send cstate :322 ch.name
                  (tostring (lume.count (ch.member-names)))
                  (.. ":" (or ch.topic "No topic set.")))))
  (state:send cstate :323 ":End of /LIST"))

(fn KICK [state cstate channel-name nick]
  (fn f [ch]
    (let [nick (nick:match "^[a-z]+")]
      (state.log :kicking nick :from channel-name)
      (if (not (ch.op? cstate.nick))
          (state:send cstate :482 ":You're not channel operator")
          (not (ch.member? cstate.nick))
          (state:send cstate :442 ":You're not on that channel")
          (not (ch.member? nick))
          (state:send cstate :421 ":User isn't on that channel")
          (ch.part nick :KICK))))
  (call-with-channel state cstate channel-name f))

(fn KILL [state cstate target-reason]
  (fn f [client reason]
    (state:send client :KILL ":" reason)
    (QUIT state client (.. "Killed (" cstate.nick " (" reason "))"))
    (tset state.clients client.conn nil)
    (client.conn:close))
  (let [(target reason) (target-reason:match "([^ ]+) ?(.*)")]
    (if (not (lume.find state.ops cstate.nick))
        (state:send cstate :481 ":You're not a server operator")
        (or (= nil reason) (= "" reason))
        (state:send cstate :461 ":Provide a reason")
        (call-with-user state cstate target f reason))))

(fn ISON [state cstate ?nicks]
  (if ?nicks
      (let [connected (icollect [nick (?nicks:gmatch "([%S]+)")]
                        (if (state:connected? nick) nick))]
        (state:send cstate :303 ":" (table.concat connected " ")))
      (state:send cstate :461 ":Provide a list of nicks")))

(fn NICK [state cstate ?new-nick]
  (let [old-nick cstate.nick]
    (when (valid.set state cstate :nick ?new-nick)
      (each [_ ch (pairs state.channels)]
        (ch.rename old-nick ?new-nick cstate.user)))))

;; Don't touch this function without testing it locally! breaking reload sucks.
(fn REHASH [state cstate ?what]
  (state:send cstate :382 :clients (tostring (lume.count state.clients nil))
              :uptime (- (os.time) state.start-time))
  (if (= ?what :channels) (do (lume.hotswap :channel)
                              (each [_ ch (pairs state.channels)]
                                (ch:upgrade channel.make))
                              (state:send cstate :382 :Reloaded))
      (. package.loaded ?what) (match (pcall lume.hotswap ?what)
                                 true (state:send cstate :382 :Reloaded ?what)
                                 (_ msg) (state:send cstate :400 ":" msg))
      (= ?what :bots) (each [_ client (pairs state.clients)]
                        (when client.conn.upgrade
                          (client.conn:upgrade)
                          (state:send cstate :382 :Reloaded client.nick)))
      (state:send cstate :404 "Unknown target" ?what)))

(local commands {: JOIN : PART : PRIVMSG : QUIT : WHOIS : WHO : NOTICE : KILL
                 : PING : NAMES : LIST : TOPIC : MODE : KICK : REHASH : NICK
                 : ISON})

(fn get [command-name]
  (match (. commands (command-name:upper))
    command command
    _ (values nil (.. "Command " command-name " not found"))))

{: get}