rivescript 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,986 @@
1
+ # RiveScript Ruby port, https://jvmlab.org/, MIT License
2
+
3
+ # Brain logic for RiveScript
4
+
5
+ require_relative "utils"
6
+ require_relative "inheritance"
7
+
8
+ class RiveScript
9
+ class Brain
10
+ # Reply weights above this are clamped to prevent trivially-crafted
11
+ # {weight=N} tags from ballooning the random-choice bucket.
12
+ MAX_REPLY_WEIGHT = 10_000
13
+
14
+ # Private-use-area token used to shield a fully-processed inner reply
15
+ # from being tag-processed a second time when substituted into a BEGIN
16
+ # block's {ok} placeholder.
17
+ BEGIN_OK_TOKEN = "\uE000RIVE_OK\uE000"
18
+
19
+ # Simple text-transform tags that are allowed to wrap {ok} in a BEGIN
20
+ # block (e.g. "{uppercase}{ok}{/uppercase}"). These are resolved against
21
+ # the already-processed inner reply directly (bypassing the tag engine)
22
+ # so the wrapping author's intent is preserved without re-running the
23
+ # full tag processor (with its side-effecting tags) over user-influenced
24
+ # reply text a second time.
25
+ BEGIN_OK_FORMAT_TAGS = %w[person formal sentence uppercase lowercase].freeze
26
+
27
+ def self.parse_int_js(str)
28
+ m = str.to_s.strip.match(/\A[-+]?\d+/)
29
+ m ? m[0].to_i : nil
30
+ end
31
+
32
+ # Splits "name=value" assignment data on the first "=" only, so that
33
+ # values which themselves contain "=" are preserved intact.
34
+ def self.split_assignment(data)
35
+ data.to_s.split("=", 2)
36
+ end
37
+
38
+ TAGS = {
39
+ "bot" => {
40
+ "self_closing" => true,
41
+ "handle" => lambda { |rive, data, _user, _scope|
42
+ vars = rive._var
43
+ split = Brain.split_assignment(data)
44
+ if split.length > 1
45
+ vars[split[0].strip] = split[1]
46
+ ""
47
+ elsif split.length == 1
48
+ val = vars[split[0].strip]
49
+ val = "undefined" if val.nil?
50
+ val
51
+ else
52
+ "undefined"
53
+ end
54
+ }
55
+ },
56
+ "env" => {
57
+ "self_closing" => true,
58
+ "handle" => lambda { |rive, data, _user, _scope|
59
+ globals = rive._global
60
+ split = Brain.split_assignment(data)
61
+ if split.length > 1
62
+ globals[split[0].strip] = split[1]
63
+ ""
64
+ elsif split.length == 1
65
+ val = globals[split[0].strip]
66
+ val = "undefined" if val.nil?
67
+ val
68
+ else
69
+ "undefined"
70
+ end
71
+ }
72
+ },
73
+ "set" => {
74
+ "self_closing" => true,
75
+ "handle" => lambda { |rive, data, user, _scope|
76
+ split = Brain.split_assignment(data)
77
+ rive.set_uservar(user, split[0].strip, split[1]) if split.length > 1
78
+ ""
79
+ }
80
+ },
81
+ "get" => {
82
+ "self_closing" => true,
83
+ "handle" => lambda { |rive, data, user, _scope|
84
+ rive.get_uservar(user, data.strip)
85
+ }
86
+ },
87
+ "add" => {
88
+ "self_closing" => true,
89
+ "handle" => lambda { |rive, data, user, _scope|
90
+ split = Brain.split_assignment(data)
91
+ name = split[0].strip
92
+ raw_value = split[1]
93
+ existing_value = rive.get_uservar(user, name) || 0
94
+ existing_value = 0 if existing_value == "undefined"
95
+ value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip)
96
+ existing_number = Brain.parse_int_js(existing_value.to_s)
97
+ if value.nil?
98
+ return "[ERR: Math can't 'add' non-numeric value '#{raw_value}']"
99
+ elsif existing_number.nil?
100
+ return "[ERR: Math can't 'add' non-numeric user variable '#{name}']"
101
+ else
102
+ result = existing_number + value
103
+ rive.set_uservar(user, name, result)
104
+ end
105
+ ""
106
+ }
107
+ },
108
+ "sub" => {
109
+ "self_closing" => true,
110
+ "handle" => lambda { |rive, data, user, _scope|
111
+ split = Brain.split_assignment(data)
112
+ name = split[0].strip
113
+ raw_value = split[1]
114
+ existing_value = rive.get_uservar(user, name) || 0
115
+ value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip)
116
+ existing_value = 0 if existing_value == "undefined"
117
+ existing_number = Brain.parse_int_js(existing_value.to_s)
118
+ if value.nil?
119
+ return "[ERR: Math can't 'sub' non-numeric value '#{raw_value}']"
120
+ elsif existing_number.nil?
121
+ return "[ERR: Math can't 'sub' non-numeric user variable '#{name}']"
122
+ else
123
+ result = existing_number - value
124
+ rive.set_uservar(user, name, result)
125
+ end
126
+ ""
127
+ }
128
+ },
129
+ "mult" => {
130
+ "self_closing" => true,
131
+ "handle" => lambda { |rive, data, user, _scope|
132
+ split = Brain.split_assignment(data)
133
+ name = split[0].strip
134
+ raw_value = split[1]
135
+ existing_value = rive.get_uservar(user, name) || 0
136
+ value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip)
137
+ existing_value = 0 if existing_value == "undefined"
138
+ existing_number = Brain.parse_int_js(existing_value.to_s)
139
+ if value.nil?
140
+ return "[ERR: Math can't 'mult' non-numeric value '#{raw_value}']"
141
+ elsif existing_number.nil?
142
+ return "[ERR: Math can't 'mult' non-numeric user variable '#{name}']"
143
+ else
144
+ result = existing_number * value
145
+ rive.set_uservar(user, name, result)
146
+ end
147
+ ""
148
+ }
149
+ },
150
+ "div" => {
151
+ "self_closing" => true,
152
+ "handle" => lambda { |rive, data, user, _scope|
153
+ split = Brain.split_assignment(data)
154
+ name = split[0].strip
155
+ raw_value = split[1]
156
+ existing_value = rive.get_uservar(user, name) || 0
157
+ value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip)
158
+ existing_value = 0 if existing_value == "undefined"
159
+ existing_number = Brain.parse_int_js(existing_value.to_s)
160
+ if value.nil?
161
+ return "[ERR: Math can't 'div' non-numeric value '#{raw_value}']"
162
+ elsif existing_number.nil?
163
+ return "[ERR: Math can't 'div' non-numeric user variable '#{name}']"
164
+ elsif value == 0
165
+ return "[ERR: Can't Divide By Zero]"
166
+ else
167
+ result = existing_number.fdiv(value)
168
+ result = result.to_i if result == result.to_i
169
+ rive.set_uservar(user, name, result)
170
+ end
171
+ ""
172
+ }
173
+ },
174
+ "call" => {
175
+ "self_closing" => false,
176
+ "handle" => lambda { |rive, data, _user, scope|
177
+ trimmed = Utils.trim(data)
178
+ m = trimmed.match(/\A(\S+)(?:\s+(.*))?\z/m)
179
+ output = rive.errors["objectNotFound"]
180
+ return output unless m
181
+
182
+ obj = m[1]
183
+ args = m[2] ? Utils.parse_call_args(m[2]) : []
184
+ objlangs = rive._objlangs
185
+ handlers = rive._handlers
186
+
187
+ if objlangs.key?(obj)
188
+ lang = objlangs[obj]
189
+ if handlers[lang]
190
+ begin
191
+ output = handlers[lang].call(rive, obj, args, scope)
192
+ rescue StandardError => e
193
+ rive.brain.warn(e.message) unless e.nil?
194
+ output = "[ERR: Error raised by object macro: #{e.message}]"
195
+ end
196
+ else
197
+ output = "[ERR: No Object Handler]"
198
+ end
199
+ end
200
+ output
201
+ }
202
+ }
203
+ }.freeze
204
+
205
+ def initialize(master)
206
+ @master = master
207
+ @strict = master._strict
208
+ @utf8 = master._utf8
209
+ @mutex = Mutex.new
210
+ end
211
+
212
+ # The user ID currently being processed by #reply (only meaningful from
213
+ # within object macros invoked during a reply). Stored per-thread so
214
+ # concurrent calls to #reply from different threads don't clobber
215
+ # each other's notion of "the current user".
216
+ def current_user
217
+ Thread.current[:rivescript_current_user]
218
+ end
219
+
220
+ def say(message)
221
+ @master.send(:say, message)
222
+ end
223
+
224
+ def warn(message, filename = nil, lineno = nil)
225
+ @master.warn(message, filename, lineno)
226
+ end
227
+
228
+ def reply(user, msg, scope = nil)
229
+ @mutex.synchronize do
230
+ say("Asked to reply to [#{user}] #{msg}")
231
+
232
+ Thread.current[:rivescript_current_user] = user
233
+ msg = format_message(msg)
234
+ reply = ""
235
+
236
+ bot_session.set(user, { "__initialmatch__" => nil })
237
+
238
+ if bot_topics["__begin__"]
239
+ begin_reply = get_reply(user, "request", "begin", 0, scope)
240
+
241
+ if begin_reply.include?("{ok}")
242
+ inner_reply = get_reply(user, msg, "normal", 0, scope)
243
+ ok_replacement = inner_reply
244
+
245
+ BEGIN_OK_FORMAT_TAGS.each do |type|
246
+ wrap_pattern = /\{#{type}\}\{ok\}\{\/#{type}\}/i
247
+ next unless begin_reply.match?(wrap_pattern)
248
+
249
+ ok_replacement = type == "person" ? substitute(inner_reply, "person") : Utils.string_format(type, inner_reply)
250
+ begin_reply = begin_reply.gsub(wrap_pattern, BEGIN_OK_TOKEN)
251
+ end
252
+
253
+ begin_reply = begin_reply.gsub("{ok}", BEGIN_OK_TOKEN)
254
+ reply = process_tags(user, msg, begin_reply, [], [], 0, scope)
255
+ reply = reply.gsub(BEGIN_OK_TOKEN, ok_replacement)
256
+ else
257
+ reply = process_tags(user, msg, begin_reply, [], [], 0, scope)
258
+ end
259
+ else
260
+ reply = get_reply(user, msg, "normal", 0, scope)
261
+ end
262
+
263
+ history = bot_session.get(user, "__history__")
264
+ history = new_history if history == "undefined"
265
+ begin
266
+ history["input"].pop
267
+ history["input"].unshift(msg)
268
+ history["reply"].pop
269
+ history["reply"].unshift(reply)
270
+ rescue StandardError
271
+ history = new_history
272
+ end
273
+ bot_session.set(user, { "__history__" => history })
274
+
275
+ reply
276
+ end
277
+ ensure
278
+ Thread.current[:rivescript_current_user] = nil
279
+ end
280
+
281
+ def format_message(msg, botreply = nil)
282
+ msg = msg.to_s
283
+ msg = msg.downcase unless case_sensitive?
284
+
285
+ msg = substitute(msg, "sub")
286
+
287
+ if @utf8
288
+ msg = msg.gsub(/[\\<>]+/, "")
289
+
290
+ if !@master.unicode_punctuation.nil?
291
+ msg = msg.gsub(@master.unicode_punctuation, "")
292
+ end
293
+
294
+ if !botreply.nil?
295
+ msg = msg.gsub(/[.?,!;:@#$%^&*()]/, "")
296
+ end
297
+ else
298
+ msg = Utils.strip_nasties(msg, @utf8)
299
+ end
300
+
301
+ msg.strip.gsub(/\s+/, " ")
302
+ end
303
+
304
+ def trigger_regexp(user, regexp)
305
+ regexp = regexp.gsub(/^\*$/, "<zerowidthstar>")
306
+ regexp = regexp.gsub("*", "(.+?)")
307
+ regexp = regexp.gsub("#", "(\\d+?)")
308
+ regexp = regexp.gsub("_", "(\\w+?)")
309
+ regexp = regexp.gsub(/\s*\{weight=\d+\}\s*/i, "")
310
+ regexp = regexp.gsub("<zerowidthstar>", "(.*?)")
311
+ regexp = regexp.gsub(/\|{2,}/, "|")
312
+ regexp = regexp.gsub(/(\(|\[)\|/, '\1')
313
+ regexp = regexp.gsub(/\|(\)|\])/, '\1')
314
+
315
+ if @utf8
316
+ regexp = regexp.gsub("\\@", "\\u0040")
317
+ end
318
+
319
+ giveup = 0
320
+ while (match = regexp.match(/\[(.+?)\]/))
321
+ if (giveup += 1) > 50
322
+ warn("Infinite loop when trying to process optionals in a trigger!")
323
+ return ""
324
+ end
325
+
326
+ parts = match[1].split("|")
327
+ opts = parts.map { |p| "(?:\\s|\\b)+#{p}(?:\\s|\\b)+" }
328
+
329
+ pipes = opts.join("|")
330
+ pipes = pipes.gsub(Regexp.new(Regexp.escape("(.+?)")), "(?:.+?)")
331
+ pipes = pipes.gsub(Regexp.new(Regexp.escape("(\\d+?)")), "(?:\\d+?)")
332
+ pipes = pipes.gsub(Regexp.new(Regexp.escape("(\\w+?)")), "(?:\\w+?)")
333
+ pipes = pipes.gsub("[", "__lb__").gsub("]", "__rb__")
334
+ regexp = regexp.sub(
335
+ Regexp.new("\\s*\\[#{Regexp.escape(match[1])}\\]\\s*"),
336
+ "(?:#{pipes}|(?:\\b|\\s)+)"
337
+ )
338
+ end
339
+
340
+ regexp = regexp.gsub("__lb__", "[").gsub("__rb__", "]")
341
+ regexp = regexp.gsub("\\w", "[^\\s\\d]")
342
+
343
+ giveup = 0
344
+ while regexp.include?("@")
345
+ if (giveup += 1) > 50
346
+ break
347
+ end
348
+ if (match = regexp.match(/@(.+?)\b/))
349
+ name = match[1]
350
+ rep = ""
351
+ arrays = bot_array
352
+ if arrays[name] && !arrays[name].empty?
353
+ rep = "(?:" + arrays[name].map { |item| Utils.quotemeta(item) }.join("|") + ")"
354
+ end
355
+ regexp = regexp.sub(/@#{Regexp.escape(name)}\b/, rep)
356
+ end
357
+ end
358
+
359
+ giveup = 0
360
+ while regexp.include?("<bot")
361
+ if (giveup += 1) > 50
362
+ break
363
+ end
364
+ if (match = regexp.match(/<bot (.+?)>/i))
365
+ name = match[1]
366
+ rep = ""
367
+ vars = bot_var
368
+ rep = Utils.quotemeta(Utils.strip_nasties(vars[name], @utf8).downcase) if vars[name]
369
+ regexp = regexp.sub(/<bot #{Regexp.escape(name)}>/i, rep)
370
+ end
371
+ end
372
+
373
+ giveup = 0
374
+ while regexp.include?("<get")
375
+ if (giveup += 1) > 50
376
+ break
377
+ end
378
+ if (match = regexp.match(/<get (.+?)>/i))
379
+ name = match[1]
380
+ rep = @master.get_uservar(user, name)
381
+ regexp = regexp.sub(/<get #{Regexp.escape(name)}>/i, Utils.quotemeta(rep.to_s.downcase))
382
+ end
383
+ end
384
+
385
+ giveup = 0
386
+ regexp = regexp.gsub(/<input>/i, "<input1>")
387
+ regexp = regexp.gsub(/<reply>/i, "<reply1>")
388
+ history = bot_session.get(user, "__history__")
389
+ history = new_history if history == "undefined"
390
+ while regexp.include?("<input") || regexp.include?("<reply")
391
+ if (giveup += 1) > 50
392
+ break
393
+ end
394
+ %w[input reply].each do |type|
395
+ (1..9).each do |i|
396
+ tag = "<#{type}#{i}>"
397
+ next unless regexp.include?(tag)
398
+
399
+ value = Utils.quotemeta(format_message(history[type][i - 1], type == "reply"))
400
+ regexp = regexp.gsub(tag, value)
401
+ end
402
+ end
403
+ end
404
+
405
+ if @utf8 && regexp.include?("\\u")
406
+ regexp = regexp.gsub(/\\u([A-Fa-f0-9]{4})/) { Regexp.last_match(1).to_i(16).chr(Encoding::UTF_8) }
407
+ end
408
+
409
+ regexp.gsub(/\|{2,}/m, "|")
410
+ end
411
+
412
+ def handle_tag(rive, user, content, scope, depth)
413
+ tag = ""
414
+ reminder = ""
415
+ i = 0
416
+ while i < content.length
417
+ if TAGS.key?(tag)
418
+ reminder = content[(i + 1)..]
419
+ break
420
+ elsif content[i] == " "
421
+ reminder = content[(i + 1)..]
422
+ break
423
+ elsif content[i] == ">"
424
+ reminder = content[(i + 1)..]
425
+ return { "response" => "<#{tag}>", "reminder" => reminder }
426
+ end
427
+ tag += content[i]
428
+ i += 1
429
+ end
430
+
431
+ tag_def = TAGS[tag]
432
+ self_closing = tag_def ? tag_def["self_closing"] : true
433
+ end_tag = self_closing ? ">" : "</#{tag}>"
434
+ result = parse_complex_tags(rive, user, reminder, scope, depth, end_tag)
435
+ reminder = result["reminder"]
436
+
437
+ response = if tag_def && tag_def["handle"]
438
+ tag_def["handle"].call(rive, result["response"], user, scope)
439
+ else
440
+ "<#{tag} #{result["response"]}>"
441
+ end
442
+ { "response" => response, "reminder" => reminder }
443
+ end
444
+
445
+ def parse_complex_tags(rive, user, content, scope, depth, end_tag = "")
446
+ return { "response" => content, "reminder" => "" } if depth > 50
447
+
448
+ response = ""
449
+ reminder = content
450
+ next_tag = reminder.index("<")
451
+ next_end = end_tag.empty? ? reminder.length : (reminder.index(end_tag) || reminder.length)
452
+
453
+ while !reminder.empty? && next_tag && next_tag < next_end
454
+ response += reminder[0...next_tag]
455
+ reminder = reminder[(next_tag + 1)..]
456
+ result = handle_tag(rive, user, reminder, scope, depth + 1)
457
+ response += result["response"].to_s
458
+ reminder = result["reminder"].to_s
459
+ next_tag = reminder.index("<")
460
+ next_end = end_tag.empty? ? reminder.length : (reminder.index(end_tag) || reminder.length)
461
+ end
462
+
463
+ response += reminder[0...next_end].to_s
464
+ reminder = reminder[(next_end + end_tag.length)..] || ""
465
+
466
+ { "response" => response, "reminder" => reminder }
467
+ end
468
+
469
+ def process_tags(user, msg, reply, st, bst, step, scope)
470
+ stars = [""]
471
+ stars.concat(st)
472
+ botstars = [""]
473
+ botstars.concat(bst)
474
+ stars.push("undefined") if stars.length == 1
475
+ botstars.push("undefined") if botstars.length == 1
476
+
477
+ giveup = 0
478
+ while (match = reply.match(/\(@([A-Za-z0-9_]+)\)/i))
479
+ if (giveup += 1) > bot_depth
480
+ warn("Infinite loop looking for arrays in reply!")
481
+ break
482
+ end
483
+
484
+ name = match[1]
485
+ arrays = bot_array
486
+ result = if arrays[name]
487
+ "{random}#{arrays[name].join("|")}{/random}"
488
+ else
489
+ "\x00@#{name}\x00"
490
+ end
491
+
492
+ reply = reply.sub(/\(@#{Regexp.escape(name)}\)/i, result)
493
+ end
494
+
495
+ reply = reply.gsub(/\x00@([A-Za-z0-9_]+)\x00/, '(@\1)')
496
+
497
+ reply = reply.gsub(/<person>/i, "{person}<star>{/person}")
498
+ reply = reply.gsub(/<@>/i, "{@<star>}")
499
+ reply = reply.gsub(/<formal>/i, "{formal}<star>{/formal}")
500
+ reply = reply.gsub(/<sentence>/i, "{sentence}<star>{/sentence}")
501
+ reply = reply.gsub(/<uppercase>/i, "{uppercase}<star>{/uppercase}")
502
+ reply = reply.gsub(/<lowercase>/i, "{lowercase}<star>{/lowercase}")
503
+
504
+ reply = reply.gsub(/\{weight=\d+\}/i, "")
505
+ reply = reply.gsub(/<star>/i, stars[1].to_s)
506
+ reply = reply.gsub(/<botstar>/i, botstars[1].to_s)
507
+ (1...stars.length).each do |i|
508
+ reply = reply.gsub(/<star#{i}>/i, stars[i].to_s)
509
+ end
510
+ (1...botstars.length).each do |i|
511
+ reply = reply.gsub(/<botstar#{i}>/i, botstars[i].to_s)
512
+ end
513
+
514
+ history = bot_session.get(user, "__history__")
515
+ history = new_history if history == "undefined"
516
+ reply = reply.gsub(/<input>/i, history["input"] ? history["input"][0] : "undefined")
517
+ reply = reply.gsub(/<reply>/i, history["reply"] ? history["reply"][0] : "undefined")
518
+ (1..9).each do |i|
519
+ reply = reply.gsub(/<input#{i}>/i, history["input"][i - 1]) if reply.include?("<input#{i}>")
520
+ reply = reply.gsub(/<reply#{i}>/i, history["reply"][i - 1]) if reply.include?("<reply#{i}>")
521
+ end
522
+
523
+ reply = reply.gsub(/<id>/i, user)
524
+ reply = reply.gsub(/\\s/i, " ")
525
+ reply = reply.gsub(/\\n/i, "\n")
526
+ reply = reply.gsub(/\\#/i, "#")
527
+
528
+ giveup = 0
529
+ while (match = reply.match(/\{random\}(.+?)\{\/random\}/i))
530
+ if (giveup += 1) > bot_depth
531
+ warn("Infinite loop looking for random tag!")
532
+ break
533
+ end
534
+
535
+ text = match[1]
536
+ random = text.include?("|") ? text.split("|") : text.split(" ")
537
+ output = random[(rand * random.length).floor]
538
+ reply = reply.sub(/\{random\}#{Regexp.escape(text)}\{\/random\}/i, output)
539
+ end
540
+
541
+ %w[person formal sentence uppercase lowercase].each do |type|
542
+ giveup = 0
543
+ while (match = reply.match(/\{#{type}\}(.+?)\{\/#{type}\}/i))
544
+ giveup += 1
545
+ if giveup >= 50
546
+ warn("Infinite loop looking for #{type} tag!")
547
+ break
548
+ end
549
+
550
+ content = match[1]
551
+ replace = if type == "person"
552
+ substitute(content, "person")
553
+ else
554
+ Utils.string_format(type, content)
555
+ end
556
+
557
+ reply = reply.sub(/\{#{type}\}#{Regexp.escape(content)}\{\/#{type}\}/i, replace)
558
+ end
559
+ end
560
+
561
+ reply = parse_complex_tags(@master, user, reply, scope, 0)["response"]
562
+
563
+ giveup = 0
564
+ while (match = reply.match(/\{topic=(.+?)\}/i))
565
+ giveup += 1
566
+ if giveup >= 50
567
+ warn("Infinite loop looking for topic tag!")
568
+ break
569
+ end
570
+
571
+ name = match[1]
572
+ @master.set_uservar(user, "topic", name)
573
+ reply = reply.sub(/\{topic=#{Regexp.escape(name)}\}/i, "")
574
+ end
575
+
576
+ giveup = 0
577
+ while (match = reply.match(/\{@([^\}]*?)\}/))
578
+ giveup += 1
579
+ if giveup >= 50
580
+ warn("Infinite loop looking for redirect tag!")
581
+ break
582
+ end
583
+
584
+ target = format_message(Utils.strip(match[1]))
585
+ say("Inline redirection to: #{target}")
586
+
587
+ subreply = get_reply(user, target, "normal", step + 1, scope)
588
+ reply = reply.sub(/\{@#{Regexp.escape(match[1])}\}/i, subreply)
589
+ end
590
+
591
+ reply
592
+ end
593
+
594
+ def substitute(msg, type)
595
+ sort_key = type == "sub" ? "sub" : "person"
596
+ unless bot_sorted && bot_sorted[sort_key]
597
+ @master.warn("You forgot to call sortReplies()!")
598
+ return msg
599
+ end
600
+
601
+ subs = type == "sub" ? bot_sub : bot_person
602
+ maxwords = type == "sub" ? bot_submax : bot_personmax
603
+ result = ""
604
+
605
+ pattern = if !@master.unicode_punctuation.nil?
606
+ msg.gsub(@master.unicode_punctuation, "")
607
+ else
608
+ msg.gsub(/[.,!?;:]/, "")
609
+ end
610
+
611
+ giveup = 0
612
+ subgiveup = 0
613
+
614
+ while pattern.include?(" ")
615
+ giveup += 1
616
+ if giveup >= 1000
617
+ warn("Too many loops when handling substitutions!")
618
+ break
619
+ end
620
+
621
+ li = Utils.n_index_of(pattern, " ", maxwords)
622
+ subpattern = pattern[0...li]
623
+
624
+ result = subs[subpattern]
625
+ if !result.nil?
626
+ msg = msg.sub(subpattern, result)
627
+ else
628
+ while subpattern.include?(" ")
629
+ subgiveup += 1
630
+ if subgiveup >= 1000
631
+ warn("Too many loops when handling substitutions!")
632
+ break
633
+ end
634
+
635
+ li = subpattern.rindex(" ")
636
+ subpattern = subpattern[0...li]
637
+
638
+ result = subs[subpattern]
639
+ if !result.nil?
640
+ msg = msg.sub(subpattern, result)
641
+ break
642
+ end
643
+ end
644
+ end
645
+
646
+ fi = pattern.index(" ")
647
+ pattern = pattern[(fi + 1)..]
648
+ end
649
+
650
+ result = subs[pattern]
651
+ msg = msg.sub(pattern, result) if !result.nil?
652
+
653
+ msg
654
+ end
655
+
656
+ private
657
+
658
+ def bot_session
659
+ @master._session
660
+ end
661
+
662
+ def bot_sorted
663
+ @master._sorted
664
+ end
665
+
666
+ def bot_topics
667
+ @master._topics
668
+ end
669
+
670
+ def bot_thats
671
+ @master._thats
672
+ end
673
+
674
+ def bot_var
675
+ @master._var
676
+ end
677
+
678
+ def bot_array
679
+ @master._array
680
+ end
681
+
682
+ def bot_sub
683
+ @master._sub
684
+ end
685
+
686
+ def bot_person
687
+ @master._person
688
+ end
689
+
690
+ def bot_submax
691
+ @master._submax
692
+ end
693
+
694
+ def bot_personmax
695
+ @master._personmax
696
+ end
697
+
698
+ def bot_depth
699
+ @master._depth
700
+ end
701
+
702
+ def bot_includes
703
+ @master._includes
704
+ end
705
+
706
+ def bot_inherits
707
+ @master._inherits
708
+ end
709
+
710
+ def case_sensitive?
711
+ @master._case_sensitive == true
712
+ end
713
+
714
+ # Attempts to match +subject+ against +regexp+ (the compiled form of
715
+ # +pattern+). Atomic triggers are compared with plain string equality;
716
+ # non-atomic triggers are matched case-insensitively unless the bot is
717
+ # running in case-sensitive mode.
718
+ #
719
+ # Returns an array of captured stars on success (empty for an atomic
720
+ # match), or +nil+ if there was no match.
721
+ def trigger_match(subject, pattern, regexp)
722
+ if Utils.is_atomic(pattern)
723
+ subject == regexp ? [] : nil
724
+ else
725
+ m = subject.match(match_regexp(regexp))
726
+ m ? m.to_a.drop(1) : nil
727
+ end
728
+ end
729
+
730
+ def match_regexp(regexp)
731
+ case_sensitive? ? /\A#{regexp}\z/ : /\A#{regexp}\z/i
732
+ end
733
+
734
+ def get_reply(user, msg, context, step, scope)
735
+ unless bot_sorted["topics"]
736
+ warn("You forgot to call sortReplies()!")
737
+ return "ERR: Replies Not Sorted"
738
+ end
739
+
740
+ topic = @master.get_uservar(user, "topic")
741
+ topic = "random" if topic.nil? || topic == "undefined"
742
+
743
+ stars = []
744
+ thatstars = []
745
+ reply = ""
746
+
747
+ if !bot_topics[topic]
748
+ warn("User #{user} was in an empty topic named '#{topic}'")
749
+ topic = "random"
750
+ @master.set_uservar(user, "topic", topic)
751
+ end
752
+
753
+ return @master.errors["deepRecursion"] if step > bot_depth
754
+
755
+ topic = "__begin__" if context == "begin"
756
+
757
+ history = bot_session.get(user, "__history__")
758
+ if history == "undefined"
759
+ history = new_history
760
+ bot_session.set(user, { "__history__" => history })
761
+ end
762
+
763
+ unless bot_topics[topic]
764
+ return "ERR: No default topic 'random' was found!"
765
+ end
766
+
767
+ matched = nil
768
+ matched_trigger = nil
769
+ found_match = false
770
+
771
+ if step == 0
772
+ all_topics = [topic]
773
+ includes = bot_includes[topic] || {}
774
+ inherits = bot_inherits[topic] || {}
775
+ if includes.any? || inherits.any?
776
+ all_topics = Inheritance.get_topic_tree(@master, topic)
777
+ end
778
+
779
+ all_topics.each do |top|
780
+ say("Checking topic #{top} for any %Previous's")
781
+ thats_list = bot_sorted["thats"][top] || []
782
+ if !thats_list.empty?
783
+ say("There's a %Previous in this topic!")
784
+
785
+ last_reply = history["reply"] ? history["reply"][0] : "undefined"
786
+ last_reply = format_message(last_reply, true)
787
+ say("Last reply: #{last_reply}")
788
+
789
+ thats_list.each do |trig|
790
+ pattern = trig[1]["previous"]
791
+ botside = trigger_regexp(user, pattern)
792
+
793
+ say("Try to match lastReply (#{last_reply}) to #{botside}")
794
+
795
+ botstars_captured = trigger_match(last_reply, pattern, botside)
796
+ if botstars_captured
797
+ say("Bot side matched!")
798
+ thatstars = botstars_captured
799
+
800
+ user_side = trig[1]
801
+ regexp = trigger_regexp(user, user_side["trigger"])
802
+ say("Try to match \"#{msg}\" against #{user_side["trigger"]} (#{regexp})")
803
+
804
+ stars_captured = trigger_match(msg, user_side["trigger"], regexp)
805
+
806
+ if stars_captured
807
+ stars = stars_captured
808
+ matched = user_side
809
+ found_match = true
810
+ matched_trigger = user_side["trigger"]
811
+ break
812
+ end
813
+ end
814
+ end
815
+ else
816
+ say("No %Previous in this topic!")
817
+ end
818
+ break if found_match
819
+ end
820
+ end
821
+
822
+ unless found_match
823
+ say("Searching their topic for a match...")
824
+ (bot_sorted["topics"][topic] || []).each do |trig|
825
+ pattern = trig[0]
826
+ regexp = trigger_regexp(user, pattern)
827
+
828
+ say("Try to match \"#{msg}\" against #{pattern} (#{regexp})")
829
+
830
+ stars_captured = trigger_match(msg, pattern, regexp)
831
+
832
+ if stars_captured
833
+ say("Found a match!")
834
+ stars = stars_captured
835
+ matched = trig[1]
836
+ found_match = true
837
+ matched_trigger = pattern
838
+ break
839
+ end
840
+ end
841
+ end
842
+
843
+ bot_session.set(user, { "__lastmatch__" => matched_trigger })
844
+ if step == 0
845
+ bot_session.set(user, {
846
+ "__initialmatch__" => matched_trigger,
847
+ "__last_triggers__" => []
848
+ })
849
+ end
850
+
851
+ if matched
852
+ existing_triggers = bot_session.get(user, "__last_triggers__")
853
+ existing_triggers = [] unless existing_triggers.is_a?(Array)
854
+ last_triggers = existing_triggers.dup
855
+ last_triggers.push(matched)
856
+ bot_session.set(user, { "__last_triggers__" => last_triggers })
857
+
858
+ if !matched["redirect"].nil?
859
+ say("Redirecting us to #{matched["redirect"]}")
860
+ redirect = process_tags(user, msg, matched["redirect"], stars, thatstars, step, scope)
861
+ redirect = format_message(redirect)
862
+
863
+ say("Pretend user said: #{redirect}")
864
+ reply = get_reply(user, redirect, context, step + 1, scope)
865
+ else
866
+ matched["condition"].each do |row|
867
+ halves = row.split(/\s*=>\s*/)
868
+ next unless halves && halves.length == 2
869
+
870
+ condition = halves[0].match(/^(.+?)\s+(==|eq|!=|ne|<>|<|<=|>|>=)\s+(.*?)$/)
871
+ next unless condition
872
+
873
+ left = Utils.strip(condition[1])
874
+ eq = condition[2]
875
+ right = Utils.strip(condition[3])
876
+ potreply = halves[1].strip
877
+
878
+ left = process_tags(user, msg, left, stars, thatstars, step, scope)
879
+ right = process_tags(user, msg, right, stars, thatstars, step, scope)
880
+
881
+ left = "undefined" if left.empty?
882
+ right = "undefined" if right.empty?
883
+
884
+ say("Check if #{left} #{eq} #{right}")
885
+
886
+ passed = false
887
+ if %w[eq ==].include?(eq)
888
+ passed = (left == right)
889
+ elsif %w[ne != <>].include?(eq)
890
+ passed = (left != right)
891
+ else
892
+ begin
893
+ left_num = left.to_i
894
+ right_num = right.to_i
895
+ passed = case eq
896
+ when "<" then left_num < right_num
897
+ when "<=" then left_num <= right_num
898
+ when ">" then left_num > right_num
899
+ when ">=" then left_num >= right_num
900
+ else false
901
+ end
902
+ rescue StandardError
903
+ warn("Failed to evaluate numeric condition!")
904
+ end
905
+ end
906
+
907
+ if passed
908
+ reply = potreply
909
+ break
910
+ end
911
+ end
912
+
913
+ if reply.nil? || reply.empty?
914
+ bucket = []
915
+ matched["reply"].each do |rep|
916
+ weight = 1
917
+ if (wmatch = rep.match(/\{weight=(\d+?)\}/i))
918
+ weight = wmatch[1].to_i
919
+ if weight <= 0
920
+ warn("Can't have a weight <= 0!")
921
+ weight = 1
922
+ elsif weight > MAX_REPLY_WEIGHT
923
+ warn("Reply weight #{weight} exceeds maximum of #{MAX_REPLY_WEIGHT}, clamping!")
924
+ weight = MAX_REPLY_WEIGHT
925
+ end
926
+ end
927
+
928
+ weight.times { bucket.push(rep) }
929
+ end
930
+
931
+ choice = (rand * bucket.length).floor
932
+ reply = bucket[choice]
933
+ end
934
+ end
935
+ end
936
+
937
+ if !found_match
938
+ reply = @master.errors["replyNotMatched"]
939
+ elsif reply.nil? || reply.empty?
940
+ reply = @master.errors["replyNotFound"]
941
+ end
942
+
943
+ say("Reply: #{reply}")
944
+
945
+ if context == "begin"
946
+ giveup = 0
947
+ while (match = reply.match(/\{topic=(.+?)\}/i))
948
+ giveup += 1
949
+ if giveup >= 50
950
+ warn("Infinite loop looking for topic tag!")
951
+ break
952
+ end
953
+
954
+ name = match[1]
955
+ @master.set_uservar(user, "topic", name)
956
+ reply = reply.sub(/\{topic=#{Regexp.escape(name)}\}/i, "")
957
+ end
958
+
959
+ giveup = 0
960
+ while (match = reply.match(/<set (.+?)=(.+?)>/i))
961
+ giveup += 1
962
+ if giveup >= 50
963
+ warn("Infinite loop looking for set tag!")
964
+ break
965
+ end
966
+
967
+ name = match[1]
968
+ value = match[2]
969
+ @master.set_uservar(user, name, value)
970
+ reply = reply.sub(/<set #{Regexp.escape(name)}=#{Regexp.escape(value)}>/i, "")
971
+ end
972
+ else
973
+ reply = process_tags(user, msg, reply, stars, thatstars, step, scope)
974
+ end
975
+
976
+ reply
977
+ end
978
+
979
+ def new_history
980
+ {
981
+ "input" => Array.new(10, "undefined"),
982
+ "reply" => Array.new(10, "undefined")
983
+ }
984
+ end
985
+ end
986
+ end