/
irc-subst.py
1422 lines (1085 loc) · 49.5 KB
/
irc-subst.py
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python3
# this listens to the outgoing irc lines (ones the client would send to irc) for
# keys that look like [[key]] and substitutes a value looked up in a pg database.
# Right now, this is specific to hexchat.
__module_name__ = "irc_subst"
# whether to print the config file when first loading the script
printConfigP = True
import pathlib
import re, shlex
from sqlalchemy import create_engine, MetaData, Table
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy import select, func
# import arrow # for timestamps
# from dateutil import tz
import hexchat
import sys, os
import subprocess
from subprocess import PIPE
from configparser import ConfigParser
__module_name__ = "Jim's IRC substituter"
__module_version__ = "1.0.0"
__module_description__ = "IRC substituter by Jim"
# index of item in list, or -1 if ValueError
def dex(item, lst):
result = -1
try:
result = lst.index(item)
finally:
return result
# script and config file dir
# nedbat's hack to replace __file__
def foo():
pass
pathname = pathlib.Path(foo.__code__.co_filename).parent.__str__()
sys.path.append(pathname) # so that modules that are siblings of the script can be found
from utils import commandtarget
from objects import nextObjectID
# return a string detailing a list (its items togeter with each index)
def detailList(l):
# hexchat.prnt("testing detailList:")
# test0 = []
# test1 = ["one"]
# test2 = ["one", "two"]
# hexchat.prnt(f"empty: {test0}")
# hexchat.prnt(f"1 item: {test1}")
# hexchat.prnt(f"2 items: {test2}")
reslst = []
for i, item in enumerate(l):
reslst.append(f"[{i}]: {item}")
if len(reslst) != 0:
resStr = " ".join(reslst)
else: # reslst is empty
resStr = "[]"
return resStr
# splits hostmask (string of form nick!email@site) into its parts
# returns a dict with keys nick, emailname, site
def split_hostmask(hostmask):
(nick, email) = hostmask.split(sep="!")
(emailname,site) = email.split(sep="@")
result = {}
result["nick"] = nick
result["emailname"] = emailname
result["site"] = site
return result
print( "\0034",__module_name__, __module_version__,"has been loaded\003" )
class KeywordList(object):
def __init__(self, properties):
self.string = ""
self.properties = properties
def __repr__(self):
reslist = []
for key in self.properties:
reslist.append(key + "=" + self.properties[key])
return " ".join(reslist)
def attachProp(self, prop, value):
self.properties[prop] = value
class irc_subst(commandtarget.CommandTarget):
# reload config file
#
# vars that get set as a result of this call:
# - self.cmdPrefix (the char to signal 'this is a command to the script')
# - self.dbSpecs (the dict with the database settings, to be give to psycopg2.open()
# - self.dbOK (boolean telling whether database is reachable and openable)
# - self.printConfigP (which is true if reload calls should print the config file)
def doReload(self, scriptPath):
parser = ConfigParser()
confFilePathName = scriptPath + '/' + 'irc-subst.cfg'
conffiles = None
conffile_read_p = False
conffiles = parser.read(confFilePathName)
if dex(confFilePathName, conffiles) == -1:
print("FATAL: config file '" + confFilePathName + "' cannot be found")
exit(0)
# pull stuff from general section of config file
if dex('general', parser.sections()) != -1:
if dex('command-prefix', parser.options('general')) != -1:
self.cmdPrefix = parser.get('general', 'command-prefix')
else:
# no command-prefix in general sect
self.cmdPrefix = '.' # default
if dex('print-config', parser.options('general')) != -1:
self.printConfigP = parser.get('general', 'print-config')
if self.printConfigP.startswith("t"):
self.printConfigP = True
elif self.printConfigP.startswith("f"):
self.printConfigP = False
else:
self.printConfigP = True # default
else:
# no print-config in general sect
self.printConfigP = True # default
else:
# no general sect
self.cmdPrefix = '.' # default
self.printConfigP = True # default
# if there's no db section in the config, db is bad
if dex("db", parser.sections()) == -1:
self.dbOK = False
else:
self.dbOK = True
self.dbSpecs = None
self.sqlalchemy_conn_str = None
if self.dbOK:
self.dbSpecs = {}
for option in parser.options('db'):
self.dbSpecs[option] = parser.get('db', option)
# build the sqlalchemy connect string
k = self.dbSpecs.keys()
# sample conn str: postgresql://scott:tiger@localhost/test?application_name=myapp
s = "postgresql://"
if 'user' in k:
s += self.dbSpecs['user']
if 'password' in k:
s += ':' + self.dbSpecs['password']
if 'host' in k:
s += '@' + self.dbSpecs['host']
else:
s += '@localhost'
if 'port' in k:
s += ':' + self.dbSpecs['port']
s += '/' + self.dbSpecs['dbname']
# put app name in connect string, if it appears in the config
if 'appname' in k:
s += f"?application_name={self.dbSpecs['appname']}"
self.sqlalchemy_conn_str = s
self.sqla_eng = create_engine(
self.sqlalchemy_conn_str,
client_encoding='utf8'
)
self.sqla_meta = MetaData(bind=self.sqla_eng)
self.sqla_meta.reflect()
self.sqla_factoids_table = Table\
(\
"factoids",
self.sqla_meta,
autoload=True,
autoload_with=self.sqla_eng
)
self.sqla_failed_logins_table = Table\
(\
"failed_logins_sasl",
self.sqla_meta,
autoload=True,
autoload_with=self.sqla_eng
)
# print the config file (if desired)
if self.printConfigP:
self.debugPrint("config file: ")
for sect in parser.sections():
self.debugPrint("section %s:" % sect)
for opt in parser.options(sect):
val = parser.get(sect, opt)
self.debugPrint(" %s = %s" % (opt, val))
if self.dbOK:
self.debugPrint("sqlalchemy_conn_str is " + self.sqlalchemy_conn_str)
def __init__(self, scriptPath):
# the debug tab name, which will show up in the client
self.debugtab_nick = "DebugTab" # TODO: put this in config file
self.makeDebugTab()
self.scriptPath = scriptPath
# sqlalchemy
self.sqla_eng = None
self.sqla_meta = None
self.sqla_factoids_table = None
self.doReload(self.scriptPath)
#self.sent = False
# a list of words, which if present specify a section to print debugging about.
# at first, this will be each hook
self.debugSects = []
# the list of all such sections
self.allDebugSects = []
self.allDebugSects = ["privmsgbasic", "privmsgsql", "notice", "noticetests", "join", "part", "partreas"]
self.command_dict = {}
self.command_dict["lskeys"] = self.list_keys
self.command_dict["remove"] = self.doRemove
self.command_dict["addmacro"] = self.doAddMacro
self.command_dict["rmmacro"] = self.doRMMacro
self.command_dict["showmacro"] = self.doShowMacro
self.command_dict["info"] = self.doInfo
self.command_dict["debughi"] = self.doDebugHi
self.command_dict["ancdirs"] = self.doAncestorDirs
self.command_dict["debugsects"] = self.doDebugSects
self.command_dict["lsdebugsects"] = self.doLSDebugSects
self.command_dict["lscmds"] = self.doLsCmds
#self.command_dict["cvttime"] = self.doCvtTime
self.cmdReload = "reload"
self.factoid_key_re = re.compile("^\[\[[a-zA-Z-_]+\]\]$")
self.macroname_key_re = re.compile("^[a-zA-Z0-9-_]+$")
self.macro_re = re.compile("^\(([a-zA-Z0-9-_ ]*)\)(.*)$")
self.channel_re = re.compile("^[#&~].*$")
# initialize superclass
super(irc_subst, self).__init__()
def debugSectsContains(self, sectName):
result = dex(sectName, self.debugSects) != -1
return result
def makeDebugTab(self):
# add the tab for debugging
hexchat.command(f"query {self.debugtab_nick}")
# put the channel list entry for it in the object so I can get at it
self.debug_tab = [c for c in hexchat.get_list('channels') if c.channel == self.debugtab_nick][0]
def debugPrint(self, printThis):
reprPrintThis = repr(printThis)
#self.debug_tab.context.prnt(printThis) # old debugPrintS
# try debugPrinting on curr. context, to see if I like it
hexchat.prnt(printThis) # trying this way
def doRemove(self, cmdString, argList, kwargs):
result = 0 # no error
if dex("rm", self.debugSects) != -1:
debugRm = True
else:
debugRm = False
channel = None
nick = None
reason = None
if len(argList) == 0:
self.debugPrint("remove usage:")
self.debugPrint("remove <nick>")
self.debugPrint("remove <nick> \"reason\" # must quote reason in 2-arg form")
self.debugPrint("remove <channel> <nick> <reason> # need not quote reason in 3-arg form")
else: # not zero args
if len(argList) >= 3:
reason = " ".join(argList[2:])
nick = argList[1]
channel = argList[0]
else:
if len(argList) == 2:
reason = argList[1]
nick = argList[0]
channel = hexchat.get_info("channel")
if len(argList) == 1:
nick = argList[0]
reason = nick
channel = hexchat.get_info("channel")
removeCommand = f"remove {channel} {nick}"
if reason is not None:
removeCommand += " :{reason}"
if debugRm:
self.debugPrint("debugRm: {removeCommand}")
else:
hexchat.command(removeCommand)
return result
# make list of dirs, going back to its ancestor
def doAncestorDirs(self, cmdString, argList, kwargs):
if len(argList) != 1:
self.debugPrint("takes one arg, the pathname")
else:
pathName = argList[0]
pathList = list(pathlib.Path(pathName).parents)
pathList = list(reversed(pathList))
pathList.append(pathlib.Path(pathName))
outStr = ""
for path in pathList:
outStr += str(path) + " "
self.debugPrint(outStr)
def doShowMacro(self, cmdString, argList, kwargs):
result = 0
if len(argList) == 0:
# print usage
self.debugPrint("showmacro usage:")
self.debugPrint("showmacro <key>")
elif len(argList) != 1:
# wrong nbr args
self.debugPrint("showmacro: wrong number of arguments")
else:
# correct number of args
if self.dbOK:
bad = False
key = argList[0]
if not self.macroname_key_re.match(key):
self.debugPrint("macro show: the key -- %s -- doesn't look like 'a-zA-A0-9-_'" % (key))
bad = True
if not bad:
lookupTable = self.lookupKeyList([key])
if lookupTable:
bad = False
else:
bad = True
self.debugPrint("showmacro: no such macro '%s'" % key)
if not bad:
self.debugPrint("showmacro: key %s has value \"%s\"" % (key, lookupTable[key]))
else:
self.debugPrint("showmacro: no db")
return result
def doAddMacro(self, cmdString, argList, kwargs):
result = 0 # success/command is found
if self.dbOK:
bad = True
key = ""
value = ""
if len(argList) == 0:
self.debugPrint("addmacro usage:")
self.debugPrint("addmacro <key> <value>")
elif len(argList) < 2:
self.debugPrint("macro add: too few args")
elif len(argList) > 2:
self.debugPrint("macro add: too many args")
else:
# correct number of args
bad = False
key = argList[0]
value = argList[1]
if not bad:
if not self.macroname_key_re.match(key):
self.debugPrint("macro add: the key -- %s -- doesn't look like 'a-zA-A0-9-_'" % (key))
bad = True
if not bad:
lookupTable = self.lookupKeyList([key])
if lookupTable:
# key is already in db
self.debugPrint("key %s is already in db" % (key))
bad = True
if not bad:
# do query and insert here
with self.sqla_eng.begin() as conn:
conn.execute\
(\
self.sqla_factoids_table.insert(),
{'key': key, 'value': value}
)
self.debugPrint("macro add: key \"%s\", value \"%s\"" % (key, value))
else:
self.debugPrint("no db")
return result
def doRMMacro(self, cmdString, argList, kwargs):
result = 0 # success/command is found
if self.dbOK:
bad = True
key = ""
value = ""
if len(argList) == 0:
self.debugPrint("rmmacro usage:")
self.debugPrint("rmmacro <key>")
elif len(argList) < 1:
self.debugPrint("macro remove: too few args")
elif len(argList) > 1:
self.debugPrint("macro remove: too many args")
else:
# correct number of args
bad = False
key = argList[0]
if not bad:
if not self.macroname_key_re.match(key):
self.debugPrint("macro remove: the key -- %s -- doesn't look like 'a-zA-A0-9-_'" % (key))
bad = True
if not bad:
lookupTable = self.lookupKeyList([key])
if not lookupTable:
# key is not in db
self.debugPrint("macro remove: key %s is not in db" % (key))
bad = True
if not bad:
# do delete query here
self.debugPrint("macro remove: key %s" % (key))
with self.sqla_eng.begin() as conn:
conn.execute\
(
self.sqla_factoids_table\
.delete()\
.where\
(
self.sqla_factoids_table.c.key
==
key
)
)
else:
self.debugPrint("no db")
return result
# def doCvtTime(self, cmdString, argList, kwargs):
# result = 0
#
# if len(argList) < 1 or len(argList) > 1:
# result = -1
#
# print("cvttime usage:")
# print("cvttime <timeString>")
# print("displays time in the local timezone")
#
# if len(argList) < 1:
# print("cvttime: too few args")
# elif len(argList) > 1:
# print("cvttime: too many args")
# else:
# # correct number of args
# timeString = argList[0]
# timeObj = arrow.get(timeString)
#
# print(timeObj.to('local').format('YYYY-MM-DD HH:mm:ss ZZ'))
#
# return result
def doInfo(self, cmdString, argList, kwargs):
result = 0
top_context = hexchat.find_context()
channel_list = hexchat.get_list('channels')
front_tab = [c for c in hexchat.get_list('channels') if c.context == top_context][0]
type = front_tab.type
if type == 1:
# server tab
print("server tab, server is", front_tab.server)
elif type == 2:
# channel tab
print("channel tab, channel is %s, modes are %s" % (front_tab.channel, front_tab.chanmodes))
users = top_context.get_list("users")
elif type == 3:
# dialog/query tab
print("query tab, nick is", front_tab.channel)
elif type == 4:
# notices tab
print("notices tab")
elif type == 5:
# SNotices tab
print("SNotices tab")
return result
def addDebugSect(self, addedSect):
if not self.debugSectsContains(addedSect):
self.debugSects.append(addedSect)
self.debugPrint(f"debugsects add: {addedSect}")
else:
self.debugPrint(f"debugsects add: {addedSect} already present")
def rmDebugSect(self, removedSect):
if self.debugSectsContains(removedSect):
self.debugSects.remove(removedSect)
self.debugPrint(f"debugsects rm: {removedSect}")
else:
self.debugPrint(f"debugsects rm: {removedSect} not present")
def doDebugSects(self, cmdString, argList, kwargs):
result = 0
if len(argList) == 0:
# no args, so -list- current debug sections
self.debugPrint(f"debug sections: {self.debugSects}")
elif len(argList) == 2:
if argList[0] == "add":
self.addDebugSect(argList[1])
elif argList[0] == "rm":
self.rmDebugSect(argList[1])
else:
self.debugPrint("debugsects: unrecognized subcommand '%s'" % (argList[0]))
else:
self.debugPrint("debug sections: wrong number of args")
return result
def doDebugHi(self, cmdString, argList, kwargs):
self.debugPrint("hi")
def doLSDebugSects(self, cmdString, argList, kwargs):
self.debugPrint("possible debug sections: " + repr(self.allDebugSects))
return 0
def doLsCmds(self, cmdString, argList, kwargs):
cmdList = sorted(self.command_dict)
self.debugPrint(repr(cmdList))
return 0
# override from commandtarget
#
# doCommandStr: try to find the command in command_dict
# if found, run it, returning 0 if no errors
# if not found, return 1
def doCommandStr(self, cmdString, *args, **kwargs):
result = None
debugCmd = self.debugSectsContains("cmd")
if debugCmd:
self.debugPrint(f"in doCommandStr")
argList = args[0]
if debugCmd:
print(f"command is {cmdString}")
print(f"args are {repr(argList)}")
# (extract from args whatever might be needed
# # for running the command)
if cmdString == self.cmdReload:
self.debugPrint("reloading config file...")
self.doReload(self.scriptPath)
elif cmdString in self.command_dict:
# next line calls -function- stored in command_dict
if debugCmd:
print(f"command found, run it")
result = self.command_dict[cmdString](cmdString, argList, kwargs)
else:
if debugCmd:
print(f"cmd is not reload, and is not found, pass buck to superclass")
# pass buck to superclass
result = super(irc_subst, self).doCommandStr(cmdString, args, kwargs)
# return success/fail exit status
return result
# accepts list of keys (strings of the form "[[somekey]]"),
# optionally followed by an existing dict, which will be used
# to store additional key/value pairs.
#
# returns a dictionary (possibly the one passed in) with those
# keys as keys, and values that come from the db
def lookupKeyList(self, key_list, running_dict=None):
# now query the db
if running_dict is None:
lookup = dict()
else:
lookup = running_dict
if len(key_list) == 0:
pass # through to return stmt, returning empty dict
elif self.dbOK:
factoids = self.sqla_factoids_table
# "select * from factoids where key in (key_list)"
sel_stmt = select([factoids]).\
where\
(\
factoids.c.key.in_(key_list)
)
with self.sqla_eng.begin() as conn:
result = conn.execute(sel_stmt)
# go through results, forming a lookup table
for row in result:
lookup[row[factoids.c.key]] = row[factoids.c.value]
else:
# populate lookup table with (no db) for each key
for key in key_list:
lookup[key] = "(no db)"
return lookup
# for debugging, print a list, one line per item
def debugPrintListAsStack(self, theList):
if len(theList) == 0:
self.debugPrint("stack empty")
else:
for index, item in enumerate(theList):
self.debugPrint(f"{index}: {item}")
# takes
# the string to be sent (which could be altered inside the func)
# returns a list,
# first item is True if the string is altered, False otherwise
# second item is the string
#
# extracts any strings it finds that match [[something]]
# looks up those keys
# substitutes the values for the keys
#
# also parses the [[ ... ]] stuff
# -note- there's no current reason to allow a [[ ... ]]
# to span lines
def outLine(self, inString):
debug_outline = self.debugSectsContains("outline")
debug_noOut = self.debugSectsContains("mute")
macro_stack = []
lookup = {}
modified = False
# split string using [[ and ]] as delims
linelistparen = re.split(r'(\[\[|\]\])', inString)
if debug_outline:
self.debugPrint("paren list: " + repr(linelistparen))
self.debugPrint("macro stack:")
self.debugPrintListAsStack(macro_stack)
# will become output string
resultList = []
lookup = dict()
outStrParen = ""
while len(linelistparen) != 0:
currSymbol = linelistparen.pop(0)
if currSymbol == '[[':
# start of macro call
macro_stack.append(resultList)
resultList = ["[["]
if debug_outline:
self.debugPrint("\nstart of macro")
self.debugPrint("currSymbol: %s" % (currSymbol))
self.debugPrint("resultList: %s" % (resultList))
self.debugPrint("macro stack:")
self.debugPrintListAsStack(macro_stack)
elif currSymbol == ']]':
# end of macro call
# if nothing is on macro_stack, this is an error
if len(macro_stack) == 0:
hexchat.prnt("Syntax error: ]] without [[\n")
linelistparen = []
else:
# parameter of macro call (incl. name of macro)
resultList.append(']]')
if debug_outline:
self.debugPrint("resultList just after ]] seen:")
self.debugPrint(repr(resultList))
# invoke the macro, to produce a string, then replace
# resultList with [thatString] and set the var
# modified to True, to tell hexchat not to eat it
if debug_outline:
self.debugPrint("macro call is %s\n" % (repr(resultList)))
resultList.pop(0) # [[
resultList.pop(-1) # ]]
# look name up
macro_call_name = resultList.pop(0) # name
lookup = self.lookupKeyList([macro_call_name], lookup)
# now, resultList has just has the parameters, so compare
# length of actual params to length of formal params
if macro_call_name in lookup:
macro = lookup[macro_call_name]
# resultList should now have just the params of the macro call: their number
# should match the number of formal params in the macro definition (well, first cut.)
matchObj = re.match(self.macro_re, macro)
mac_params = matchObj.group(1)
mac_body = matchObj.group(2).lstrip() # and remove leading spaces
# params in (params list) are space-separated
mac_params_array = mac_params.split()
# this simple comparison will change when I add features to macros
if len(mac_params_array) == len(resultList):
# params in the body should be of the form %name%, so change formal params to that
renamed_params = ["%" + x + "%" for x in mac_params_array]
param_lookup = dict(zip(renamed_params, resultList))
param_pattern = r"(%[a-zA-Z0-9_-]+%)"
body_param_list = re.split(param_pattern, mac_body)
# apply the macro, and substitute the params
out_list = []
for body_part in body_param_list:
if body_part in param_lookup:
out_list.append(param_lookup[body_part])
else:
out_list.append(body_part)
result_str = "".join(out_list)
resultList = [result_str]
modified = True
else: # wrong nbr of params
self.debugPrint("wrong number of macro parameters\n")
else: # macro not found in lookup table
# turn the text of the call into a string
resList = resultList[:]
resList.insert(0, macro_call_name)
resultList = [ f"[[{' '.join(resList)}]]"]
modified = True
if debug_outline:
self.debugPrint(f"converted macro call: {repr(resultList)}")
tempList = resultList
resultList = macro_stack.pop(-1)
resultList.extend(tempList)
if debug_outline:
self.debugPrint("\nend of macro")
self.debugPrint("currSymbol: %s" % (currSymbol))
self.debugPrint("resultList: %s" % (resultList))
self.debugPrint("macro stack:")
self.debugPrintListAsStack(macro_stack)
else:
# parameter of macro call or not in a macro call
# separate macro parameters
if len(macro_stack) != 0:
# inside any number of macro calls
paramsList = shlex.split(currSymbol)
if debug_outline:
self.debugPrint("\nparameter of macro")
resultList.extend(paramsList)
else:
# outside all macro calls
resultList.append(currSymbol)
if debug_outline:
self.debugPrint("\nnot inside a macro call")
if debug_outline:
self.debugPrint("currSymbol: %s" % (currSymbol))
self.debugPrint("resultList: %s" % (resultList))
self.debugPrint("macro stack:")
self.debugPrintListAsStack(macro_stack)
# while loop has exited, so linelistparen is empty.
#
# so, either the macro_stack is also empty (means we're done)
# or it's not, meaning there are missing ']]'s
if len(macro_stack) != 0:
hexchat.prnt("Syntax error: [[ without ]]\n")
outStrParen = ""
else:
outStrParen = "".join(resultList)
if debug_outline:
self.debugPrint("Exit outLine")
if debug_noOut:
result = [False, None] # debugging: MUTE output
else:
result = [modified, outStrParen]
return result
# prints to the irc client the list of keys available in the db
def list_keys(self, cmdString, argList, kwargs):
if self.dbOK:
factoids = self.sqla_factoids_table
sel = select([factoids.c.key, factoids.c.value]).order_by(factoids.c.key)
with self.sqla_eng.begin() as conn:
result = conn.execute(sel)
macro_string = ""
for row in result:
test_str = row[factoids.c.key]
macro_def = row[factoids.c.value]
macro_match_obj = self.macro_re.match(macro_def)
if macro_match_obj is not None:
macro_params = macro_match_obj.group(1) # macro params
params_list = macro_params.split()
params_list.insert(0, test_str) # put macroname as first param
if self.macroname_key_re.match(test_str):
macro_string += "[[" + " ".join(params_list) + "]]" + "\n"
else:
self.debugPrint \
(
"the macro value ('%s') at key '%s' doesn't look like a macro"
%
(macro_def, test_str)
)
# in Python 3, no strings support the buffer interface, because they don't contain bytes.
# Before, I was using print. print only writes strings. I shouldn't use print to try and
# write to a file opened in binary mode (and a pipe is opened in binary mode). I should use
# the write() method of to_col, which itself is a pipe.
column = subprocess.Popen(["/usr/bin/column"], stdin=PIPE, stdout=PIPE)
comm_stdout, comm_sterr = column.communicate(macro_string.encode())
lineList = comm_stdout.splitlines()
for line in lineList:
self.debugPrint(line.decode())
result = 0 # success
else:
print("no db")
result = 1
return result
# accepts an irc line as a list of words, which is the arguments to a command
# consolidates the quoted arguments into a single list item
# returns the list of quoted and not-quoted args
def quotizeWordList(self, lst):
li = iter(lst)
result = []
for w in li:
result.append(w)
return result
# refactored from inputHook(), this is called if the
# input line is found to be backslashed.
def process_backslashed_line(self, word_eol):
backslashed_line = word_eol[0]
hexchat.command(f"say {backslashed_line[1:]}")
outLineResult = self.outLine("say " + backslashed_line[1:])
# implement noout in debugsects by testing for None
if outLineResult[1] is None:
# means we're testing:
# noout is in debugSects, so mute user output
# and don't do anything else
pass
result = hexchat.EAT_ALL
return result
# refactored from inputHook(), this is called if the input
# is found to be a command; process that command
def process_command(self, word):
debugCmd = self.debugSectsContains("cmd")
debug_input = self.debugSectsContains("input")
if debug_input: self.debugPrint("first word starts with cmdPrefix")
result = hexchat.EAT_ALL
cmd = word[0][1:]
args = word[1:]
if debug_input or debugCmd:
self.debugPrint(f"cmd is {cmd}")
self.debugPrint(f"args are {args}")
cmdResult = self.doCommandStr(cmd, args, None)
if cmdResult == 1:
result = hexchat.EAT_NONE
print(f"command '{cmd}' not found")
else:
# eat the command the user typed if found;
# if not, let it get sent like any other input
result = hexchat.EAT_ALL
return result
def process_normal_line(self, line):
result = hexchat.EAT_NONE
outLineResult = self.outLine(f"say {line}")