-
Notifications
You must be signed in to change notification settings - Fork 0
/
newbot.py
executable file
·1388 lines (1119 loc) · 38.4 KB
/
newbot.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
# MVpybot, a Python IRC bot.
# Copyright (C) 2009-2017 Matt Ventura
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Not sure if this note is still relevant since I'm not the one who noticed it:
# A note:
# Please do not try to run the bot in IDLE. Your system will slow down.
# The bot will run much better just from the commandline version of Python.
# --Darren VanBuren
# Required builtin python modules
import sys
import socket
import string
import os
import time
import random
import inspect
import sharedstate
import traceback
from imp import reload
import re
# To be fixed later
import classes
from classes import *
import builtinfuncs
def reload_all():
reload(builtinfuncs)
reload(classes)
# Because sha is depreciated, we use the import ____ as ____ trickery
# as well as try...except magic.
try:
from hashlib import sha1 as sha
except ImportError:
from sha import sha as sha
from subprocess import Popen, PIPE
# Import user settings
# Options is the one the user should change most of the time
# Config is just some strings and stuff
import options
import config
# Allow easy importing from the modules folder
sys.path.append("modules")
class Bot(object):
def BotMain(self, botConn):
try:
self.BotInit()
self.RunMainLoop()
except BotStopEvent as e:
return e.retval
except:
raise
def __init__(self, botConn):
self.conn = botConn
self.builtinFuncMap = {
'test': self.testFunc,
'userinfo': self.userinfoFunc, 'auth': self.authFunc, 'auths': self.authFunc, 'authenticate': self.authFunc,
'level': self.levelFunc, 'deauth': self.deauthFunc, 'register': self.registerUserFunc,
'pass': self.passFunc, 'passwd': self.passwdFunc, 'authdump': self.authDump, 'errtest': self.errTest,
'modules': self.modFunc, 'help': self.helpFunc, 'err': self.errFunc, 'errors': self.errFunc,
'reloadopts': self.reloadOpts, 'reloadcfg': self.reloadConfig, 'perm': self.userMgmtFunc, 'user': self.userMgmtFunc
}
def BotInit(self):
# Announce that we are logging
self.logdata(time.strftime('---- [ Session starting at %y-%m-%d %H:%M:%S ] ----'))
self.showdbg('Initializing...')
self.showdbg('Loading modules...')
# Initialize authlist
self.authlist = []
self.chanMap = {}
# This is where we load plugins
# The general plugin system is this:
# Functions are things called when a user tries to run a command,
# but no built-in function is found for it. These register a function
# with the name that the user would type for the command.
# These are passed a cmdMsg object.
# Helps are like commands, but are called when you use the help command,
# and no built-in help is available. These are passed a helpCMd object.
# Listeners handle certain events (or lack thereof). Listeners need to register
# for the type of event they want (like 'join', 'privmsg', or 'part'. They are
# passed a lineEvent object. You can also register for 'any' to listen
# on any event, or 'periodic' for when the bot receives no data from the server
# in a certain period of time.
# See some of the included modules for examples. o
# Modules can declare a variable of 'enabled'. Set this to False to disable it.
# If it is omitted, True is assumed.
# These variables:
# funcregistry is a dictionary of functions, by command name.
# listenerregistry is a dictionary containing a list for each event type
# helpregistry is like funcregistry
# Each entry in these is a [modules, function] pair
# library_list then contains a list of all modules imported
# libracy_dict is a dictionary of { name : module } pairst
self.funcregistry = {}
self.listenerregistry = {}
self.helpregistry = {}
self.library_list = []
self.library_dict = {}
for file in os.listdir(os.path.abspath("modules")):
pname, fileext = os.path.splitext(file)
if fileext == '.py':
try:
module = __import__(pname)
except:
self.showErr('Error importing plugin ' + pname)
self.reportErr(sys.exc_info())
self.library_list.append(module)
self.library_dict[pname] = module
if hasattr(module, 'register') and getattr(module, 'enabled', 1):
regs = self
try:
getattr(module, 'register')(regs)
except:
self.showErr('Error registering plugin ' + pname)
self.reportErr(sys.exc_info())
# Load the simple auth file if using implicit/simple auth
# This file needs to be formatted as such:
# username1 user
# username2 admin
# It must be a space in-between, and you must use the friendly-name
# user levels, rather than numbers.
if not(self.conn.userAuth):
try:
with open('ausers') as f:
auLines = f.readlines()
for line in auLines:
line = line.rstrip()
lineParts = line.split(' ')
self.authlist.append(asUser(lineParts[0], int(lineParts[1]), lineParts[2:]))
except IOError:
self.ErrorAndStop('Error loading implicit auth list.', 255)
except:
self.ErrorAndStop('Unknown error while loading implicit auth list.', 255)
conn = self.conn
# Legacy stuff
self.host = self.conn.host
self.port = self.conn.port
# Expose our logger function to the connection object
# so that it can properly log/output data.
self.conn.setLogger(self.logdata)
# Legacy variable names
self.cspass = self.conn.csp
# Initialize these to almost-blank strings to avoid some problems
self.line = ' '
self.tosend = ' '
# If the connection is already connected (in the case of a reload),
# do nothing. Otherwise, connect and initialize.
if self.conn.fullyDone:
pass
else:
self.conn.initialize()
for channel in self.conn.initChans:
self.JoinChannel(channel)
# For if we reload, this tries to reload the old auth list
def loadauthlist():
return sharedstate.authlist
# We only do this if we're using explicit authentication
if self.conn.userAuth:
try:
self.authlist = loadauthlist()
except:
# If there's no auth list to load, load a blank one
self.authlist = []
self.conn.s.settimeout(5) # This sets the max time to wait for data
self.nick = options.NICK
def RunMainLoop(self):
while True:
retval = self.MainLoop()
if retval is not None:
return retval
def MainLoop(self):
# main loop
# Note that this will also affect how often periodics are run
linebuffer = None
try:
linebuffer = self.conn.recv()
if not(linebuffer):
# Check if there was a socket error. If there was, tell the wrapper that.
# TODO: make the bot handle reconnects internally
self.logdata(time.strftime('---- [ Session closed at %y-%m-%d %H:%M:%S ] ---- (Reason: lost connection)'))
self.ErrorAndStop('Lost connection', 255)
# No new data
except socket.error:
pass
except KeyboardInterrupt:
self.ErrorAndStop('Keyboard Interrupt', 0)
except:
pass
# If we got no data from the server, run periodics
if not(linebuffer):
self.runPeriodics()
# What to do if we did get data
else:
# If the server dumped a bunch of lines at once, split them
# TODO: check if we actually still need this
# TODO: implement some type of queue in the server object
# so that it doesn't need to be done here.
lines = linebuffer.splitlines()
for line in lines:
try:
# Turn our line into a lineEvent object
e = lineEvent(self, line)
except:
self.showdbg('Failed basic line parsing! Line: ' + line)
self.reportErr(sys.exc_info())
continue
# If the data we received was a PRIVMSG, do the usual privmsg parsing
if e.etype == 'privmsg':
# Parse returns a dictionary containing some stuff, including an action to take if applicable
lstat = self.parse(e)
try:
# If parse()'s return contains an action, carry out that action
if 'action' in lstat:
if lstat['action'] == "restart":
self.logdata(time.strftime('---- [ Session closed at %y-%m-%d %H:%M:%S ] ---- (Reason: restart requested)'))
raise BotStopEvent('Restart requested', 1)
if lstat['action'] == "die":
self.logdata(time.strftime('---- [ Session closed at %y-%m-%d %H:%M:%S ] ---- (Reason: stop requested)'))
raise BotStopEvent('Die requested', 0)
if lstat['action'] == "reload":
if self.conn.userAuth:
sharedstate.authlist = self.authlist
self.logdata(time.strftime('---- [ Session closed at %y-%m-%d %H:%M:%S ] ---- (Reason: reload requested)'))
raise BotStopEvent('Reload requested', 2)
# If there was any error, do our usual error reporting.
except BotStopEvent as e:
raise
except:
self.reportErr(sys.exc_info())
# Fail gracefuly if the server gives us an error.
if e.etype == 'error':
self.logdata(time.strftime('---- [ Session closed at %y-%m-%d %H:%M:%S ] ---- (Reason: received error from server)'))
self.ErrorAndStop('Received error from server', 255)
# If a user leaves the server, remove them from the auth list
elif (e.etype == 'quit'):
for i in self.authlist:
if i.nick == e.nick:
self.showdbg('Removing nick %s from authlist due to quit' % (e.nick))
self.authlist.remove(i)
# If a user changes their nick, update the auth list accordingly
elif (e.etype == 'nick'):
for i in self.authlist:
if i.nick == e.nick:
self.showdbg('Upddating nick %s in authlist to %s' % (e.nick, e.newNick))
i.nick = e.newNick
self.processNick(e)
elif (e.etype == 'join'):
self.processJoin(e)
elif (e.etype == 'part'):
self.processPart(e)
self.runListeners(e)
def runListeners(self, e):
# Run listeners of the apporpriate type
if e.etype:
if e.etype in self.listenerregistry:
for function in self.listenerregistry[e.etype]:
l = function[0]
target = function[1]
try:
target(e)
except:
self.reportErr(sys.exc_info())
# Also run listeners who hook onto 'any'
if 'any' in self.listenerregistry:
for function in self.listenerregistry['any']:
l = function[0]
target = function[1]
try:
target(e)
except:
self.reportErr(sys.exc_info())
modFunc = builtinfuncs.modFunc
userMgmtFunc = builtinfuncs.userMgmtFunc
# This big function is called when we want to parse an incoming PRIVMSG
# e: the event, which we've already created
def parse(self, e):
# Legacy
sender = e.nick
channel = e.channel
msg = e.message
conn = e.conn
# Figure out if it is a channel message or a private message
isprivate = e.isPrivate
if isprivate:
channel = sender
run = ''
out = ''
# Quick fix
if not msg:
msg = '(null)'
# There are three ways to call a command:
# #1: Use the command prefix, set in options
if (msg[0] == options.cmdprefix):
cmd = msg[1:].split(' ')
run = cmd[0]
cmdstring = ' '.join(cmd)
msgparts = msg.split(' ')
# #2: Say the bot's name, followed by a colon and space
if ((msgparts[0].lower() == options.NICK.lower() + ':') and msgparts):
cmd = msgparts[1:]
run = cmd[0].rstrip()
cmdstring = ' '.join(cmd)
# #3: Directly send the bot a PM
if isprivate:
cmd = msgparts[0:]
run = cmd[0].rstrip()
cmdstring = ' '.join(cmd)
# Do this stuff if it is determined that we should try
# to run a command
if (run):
# We construct this object to pass to functions that correspond to actual chat functions
msgObj = cmdMsg(self, channel, sender, cmd, run, isprivate)
funcs = self.builtinFuncMap
try:
# If the command is in our built-ins, run it
if run in funcs:
out = funcs[run](msgObj)
# Commands for stopping/starting/reloading/etc: This is important so read this.
# It's not incredibly intuitive.
# reload: the wrapper one level above this bot (mvpybot.py) just does a reload()
# on the bot module, then restarts it. Saves authenticated users first. Does not
# use a new connection.
# restart: restarts the bot. Saves auth list. Doesn't actually reload the bot
# so I'm not sure when you would actually want to use this. Uses same connection.
# die: both the bot and the wrapper one level above it stop. If you ran mvpybot.py
# directly, it will stop. If you're running start.py, it will automatically be
# restarted.
# Yes, this means that unlike most programs, 'reload' actually does more than 'restart'.
# These reside here because it would get messy to put them elsewhere.
if (run == 'restart'):
if (self.getlevel(sender) >= self.getPrivReq('power', 20)):
self.showdbg('Restart requested')
self.conn.privmsg(channel, 'Restarting...')
return {'action': "restart"}
else:
out = config.privrejectadmin
elif (run == 'die'):
if (self.getlevel(sender) >= self.getPrivReq('power', 20)):
self.showdbg('Stop requested')
self.conn.privmsg(channel, 'Stopping...')
return {'action': "die"}
else:
out = config.privrejectadmin
elif (run == 'reload'):
if (self.getlevel(sender) >= self.getPrivReq('power', 20)):
self.showdbg('Reload requested')
self.conn.privmsg(channel, 'Reloading...')
return {'action': "reload"}
else:
out = config.privrejectadmin
# Report errors that occur when running a built-in function
except:
self.reportErr(sys.exc_info())
out = config.intErrorMsg
# Functions can return True if they wish to indicate that they were successful
# but do not want to actually send anything to the server, or they have sent
# things to the server through conn.* methods.
nodata = False
try:
if out is True:
nodata = True
except:
pass
if nodata:
return {}
if (out):
# out might either be a formatted PRIVMSG or just some text
# that should be sent to the correct place
# Actually, it should never be formatted anymore.
if (out[0:7] != 'PRIVMSG'):
conn.privmsg(channel, out)
else:
conn.send(out + '\n')
# Try to run a module function, since we'll only get to this point
# if it wasn't handled by an appropriate builtin.
else:
# Check if the name we want exists in funcregistry
if run in self.funcregistry:
target = self.funcregistry[run]
l = target[0]
# Pass all this stuff in via our msgObj object
try:
out = target[1](msgObj)
if out is None:
self.showErr('Command returned None. This is most likely a bug in that command.')
return {}
# Something can return True to silenty proceed
if out is True:
return {}
if (out.split(' ')[0] != 'PRIVMSG'):
out = 'PRIVMSG %s :%s\n' % (channel, out)
except:
# If an error occurs, record it and tell the user something happened.
self.reportErr(sys.exc_info())
out = 'PRIVMSG %s :%s' % (channel, config.modErrorMsg)
else:
found = 0
# Send out data to the server, assuming there's something to send.
try:
if (out):
if (out[-1] != '\n'):
out += '\n'
self.conn.send(out)
# If the command was not found, tell the user that.
# Note that this has to be enabled in config, since
# it can end up doing annoying/undesired things sometimes.
elif config.cnf_enable:
out = 'PRIVMSG ' + channel + ' :Command not found'
conn.send(out + '\n')
# Report any error that may have happened when processing the data.
except:
self.reportErr(sys.exc_info())
# This function only returns stuff if we need to signal the main loop to exit.
return {}
# Class definitions
# This is a function that can be used to call external programs
# Be sure that the program does not hang, or else the bot will hang
# To make it un-hangable, look at the math plugin for an example
# of how to modify it.
# And of course, MAKE SURE MODULES THAT USE THIS ARE VERY SECURE!
# There's nothing stopping modules from just calling Popen or other
# things that would allow external calls anyway, so there's no global
# setting to enable/disable this.
def syscmd(self, command):
self.showdbg('SYSCMD called. Running "' + ' '.join(command) + '".')
result = Popen(command, stdout = PIPE).communicate()[0]
return result
# Tries to guess whether 'a' or 'an' should be used with a word
@staticmethod
def getArticle(word):
if not word:
return ''
elif word.lower() == 'user':
return 'a'
elif word[0] in ('a', 'e', 'i', 'o', 'u'):
return 'an'
else:
return 'a'
# Tries to find a privilege level in config.py
# Failing that, it will return the default argument.
# This can be used to allow addons to specify a default privilege
# level while still allowing it to be overridden in config.py.
def getPrivReq(self, priv, default = 0):
if priv in config.reqprivlevels:
return config.reqprivlevels[priv]
else:
return default
# Check if someone has a privilege
def hasPriv(self, nick, priv, default = 3):
# Try to find the user's current auth info
try:
auth = self.getAuth(nick)
except UserNotFound:
# If not found, just assume that the user has a level of 0
# and return True if the privilege requested is less than
# or equal to that.
return(0 >= self.getPrivReq(priv, default))
# If the user has specifically been denied the privilege in question,
# return False
if priv in auth.deny:
return False
# Otherwise, if they have specifically been granted it, return True.
elif priv in auth.grant:
return True
# If the user's level is above the level required to implicitly grant the
# privilege, return True
elif self.getlevel(nick) >= self.getPrivReq(priv, default):
return True
# Otherwise, return False
else:
return False
# Builtin command functions
# Test function
def testFunc(self, msg):
return('%s: test' % msg.nick)
# Lookup user info for a particular username
# Requires acctInfo priv, defaults to 0.
def userinfoFunc(self, msg):
if (len(msg.cmd) != 2):
return('Incorrect usage. Syntax: user <username>')
elif not(hasPriv(msg.nick, 'acctInfo', 0)):
return(config.privrejectgeneric)
else:
username = msg.cmd[1]
try:
authEntry = userLookup(username)
except UserNotFound:
return('That user is not in the user list')
fUser = authEntry.authName
fLevel = authEntry.level
fLevelStr = levelToStr(fLevel)
article = getArticle(fLevelStr)
return('%s is level %s (%s %s).' % (fUser, str(fLevel), article, fLevelStr))
def userLookup(self, authName):
"""
Look up a user's info from files only.
"""
if conn.userAuth:
authFile = 'users'
else:
authFile = 'ausers'
with open(authFile, 'r') as f:
for fline in f.readlines():
fline = fline.rstrip()
lineParts = fline.split(' ')
if lineParts[0] == authName:
fUser = lineParts[0]
# fPass = lineParts[1]
if conn.userAuth:
fLevel = int(lineParts[2])
fPerms = lineParts[3:]
else:
fLevel = int(lineParts[1])
fPerms = lineParts[2:]
return(uEntry(fUser, fLevel, fPerms))
raise(UserNotFound(authName))
def formatPerms(self, grant, deny, spacer = ' '):
"""
Format a permissions string
Format is (+|-)<perm><spacer>
For example, +acctInfo -acctMgmt
This is used both to format it for the auth files (spacer = ' ')
and for user-friendly output for account management commands (spacer = ', ')
"""
perms = ['+' + perm for perm in grant] + ['-' + perm for perm in deny]
outStr = spacer.join(perms)
return(outStr)
def authFunc(self, msg):
"""User-facing command to authenticate themselves"""
# If using implicit auth, there is no auth command.
if not(self.conn.userAuth):
return(config.simpleAuthNotice)
# Syntax error
elif len(msg.cmd) != 3:
return('Incorrect syntax. Usage: auth <username> <pass>')
# If someone tries to use this command out in the open.
elif not(msg.isPrivate):
return('''Use this in a /msg, don't use it in a channel. Now go change your passsword.''')
# If someone is already authenticated, remind them.
elif self.getlevel(msg.nick) != 0:
return('You are already authenticated. If you would like to change accounts, you can deauth and try again.')
# If none of the above apply, it's time to check their credentials.
else:
iName = msg.cmd[1].rstrip()
iPass = msg.cmd[2].rstrip()
with open('users', 'r') as f:
# found means we found the username in the auth file.
# correct means the password actually matched.
correct = False
found = False
self.showdbg('%s is attempting to authenticate.' % iName)
for fLine in f.readlines():
fLine = fLine.rstrip()
lineParts = fLine.split(' ')
# If name matches, check this entry and mark found as True.
if lineParts[0] == iName:
found = True
fPass = lineParts[1]
fLevel = int(lineParts[2])
perms = lineParts[3:]
# The password in the file might be plaintext (e.g. if it was manually
# edited into the file by an administrator) or it may be hashed (every other
# scenario).
if iPass == fPass:
correct = True
elif ('HASH:' + sha(iPass.encode()).hexdigest()) == fPass:
correct = True
# Correct username
if found:
# Correct username + password
if correct:
# If they have explicit permissions specified in the file, add those
# into our aUser object.
if perms:
self.authlist.append(aUser(msg.nick, iName, fLevel, perms))
else:
self.authlist.append(aUser(msg.nick, iName, fLevel))
self.showdbg('%s is authenticated' % iName)
# 'auths' is the command to silently succeed, i.e. we don't report
# the success back to the user.
if msg.run == 'auths':
return(True)
# Otherwise, tell them they were successful
else:
return('Password accepted, you are now authenticated')
# Correct username, but wrong password
else:
self.showdbg('%s tried to authenticate, but their password was rejected' % iName)
return('Incorrect password')
# Wrong username
else:
self.showdbg('%s tried to authenticate, but their username was not found' % iName)
return('Incorrect username')
def levelFunc(self, msg):
"""
User-facing command to get account info. Requires acctInfo privilege
or level 3 if it is not configured.
"""
if not(hasPriv(msg.nick, 'acctInfo', 3)):
return(config.privrejectgeneric)
# Checking oneself
if len(msg.cmd) == 1:
lNick = msg.nick
# Checking someone else
else:
lNick = msg.cmd[1]
try:
lAuth = getAuth(lNick)
except UserNotFound:
return('User not found.')
strLevel = levelToStr(lAuth.level)
# Always print level info
outStr = '%s is level %s (%s %s). ' % (lNick, str(lAuth.level), getArticle(strLevel), strLevel)
# Additionally, print granted and/or denied permissions if they exist.
if lAuth.grant:
outStr += 'Granted permissions: '
for i in lAuth.grant:
outStr += i + ', '
outStr = outStr[:-2] + '. '
if lAuth.deny:
outStr += 'Denied permissions: '
for i in lAuth.deny:
outStr += i + ', '
outStr = outStr[:-2] + '. '
return(outStr)
# Log out
def deauthFunc(self, msg):
if not(self.conn.userAuth):
return(config.simpleAuthNotice)
else:
found = False
iName = msg.nick
if self.getlevel(iName) != 0:
for i in self.authlist:
alName = i.nick
if alName == iName:
self.authlist.remove(i)
found = True
if found:
return('Deauthenticated')
else:
return('An error has occured')
else:
return('You are not authenticated')
# Register a new user
def registerUserFunc(self, msg):
if not(conn.userAuth):
return(config.simpleAuthNotice)
else:
if (len(msg.cmd) != 3):
return('Incorrect syntax. Usage: register <username> <password>')
else:
try:
addUser(msg.cmd[1], msg.cmd[2])
except UserAlreadyExistsError:
return('Sorry, that username is already taken')
return('Account created. You can now authenticate.')
def addUser(self, name, password = None, level = config.newUserLevel, grant = set(), deny = set()):
"""
Internal function to add a new user.
"""
if conn.userAuth:
if password is not None:
raise(NeedPasswordError())
authFile = 'users'
else:
if password is not None:
raise(PassNotNeededError())
authFile = 'ausers'
with open(authFile, 'r') as f:
valid = True
for fLine in f:
fLineSplit = fLine.split(' ')
if name == fLineSplit[0]:
valid = False
raise(UserAlreadyExistsError(name))
if valid:
with open(authFile, 'a') as f:
if conn.userAuth:
fileOut = '%s HASH:%s %s %s\n' % (name, sha(password.encode()).hexdigest(), str(config.newUserLevel), formatPerms(grant, deny))
else:
fileOut = '%s %s %s\n' % (name, str(config.newUserLevel), formatPerms(grant, deny))
f.write(fileOut)
def chgUserPass(self, user, newPass):
"""Internal function for changing a user's password"""
if not(conn.userAuth):
raise(Exception('Simple auth is enabled, so there are no passwords to change'))
else:
self.showdbg('Attempting to change password for %s' % user)
if conn.userAuth:
authFile = 'users'
else:
authFile = 'ausers'
with open(authFile, 'r') as f:
outData = ''
found = False
for fLine in f:
lineSplit = fLine.split(' ')
if user == lineSplit[0]:
outData += '%s HASH:%s %s' % (lineSplit[0], sha(newPass.encode()).hexdigest(), ' '.join(lineSplit[2:]))
self.showdbg('Found entry, modifying...')
found = True
else:
outData += fLine
if found:
with open(authFile, 'w') as f:
f.write(outData)
f.truncate()
self.showdbg('Changed password for %s' % user)
return
else:
self.showdbg('Could not find user %s' % user)
raise(UserNotFound(user))
def chgUserLvl(self, user, newLevel):
"""Change a user's privilege level"""
self.showdbg('Attempting to change level for %s' % user)
if conn.userAuth:
authFile = 'users'
else:
authFile = 'ausers'
with open(authFile, 'r') as f:
outData = ''
found = False
for fLine in f:
lineSplit = fLine.split(' ')
if user == lineSplit[0]:
if conn.userAuth:
outData += '%s %s %s' % (' '.join(lineSplit[0:2]), str(newLevel), ' '.join(lineSplit[3:]))
else:
outData += '%s %s %s' % (lineSplit[0], str(newLevel), ' '.join(lineSplit[2:]))
self.showdbg('Found entry, modifying...')
found = True
else:
outData += fLine
if found:
with open(authFile, 'w') as f:
f.write(outData)
f.truncate()
self.showdbg('Changed level for %s' % user)
return
else:
self.showdbg('Could not find user %s' % user)
raise(UserNotFound(user))
def chgUserPrivs(self, user, grant, deny):
"""
Change a user's privileges.
This completely replaces the user's privileges, so you need to specify
ALL of the desired privileges to be granted and denied.
"""
self.showdbg('Attempting to change privs for %s' % user)
if conn.userAuth:
authFile = 'users'
else:
authFile = 'ausers'
with open(authFile, 'r') as f:
outData = ''
found = False
for fLine in f:
lineSplit = fLine.split(' ')
if user == lineSplit[0]:
newPrivs = formatPerms(grant, deny)
if conn.userAuth:
outData += '%s %s\n' % (' '.join(lineSplit[0:3]), newPrivs)
else:
outData += '%s %s\n' % (' '.join(lineSplit[0:2]), newPrivs)
self.showdbg('Found entry, modifying...')
found = True
else:
outData += fLine
if found:
with open(authFile, 'w') as f:
f.write(outData)
f.truncate()
self.showdbg('Changed privs for %s' % user)
return
else:
self.showdbg('Could not find user %s' % user)
raise(UserNotFound(user))
def passFunc(self, msg):
"""User-facing function to change one's own password"""
if not(conn.userAuth):
return(config.simpleAuthNotice)
else:
if (len(msg.cmd) != 2):
return('Incorrect syntax. Usage: pass <password>')
else:
authEntry = getAuth(msg.nick)
if authEntry:
authName = authEntry.authName
result = chgUserPass(authName, msg.cmd[1])
if result:
return('Successfully changed password')
else:
return('An error has occurred')