Ripple's bancho server https://ripple.moe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1420 lines
45KB

  1. import json
  2. import random
  3. import re
  4. import threading
  5. import requests
  6. import time
  7. from common import generalUtils
  8. from common.constants import mods
  9. from common.log import logUtils as log
  10. from common.ripple import userUtils
  11. from constants import exceptions, slotStatuses, matchModModes, matchTeams, matchTeamTypes, matchScoringTypes
  12. from common.constants import gameModes
  13. from common.constants import privileges
  14. from constants import serverPackets
  15. from helpers import systemHelper
  16. from objects import fokabot
  17. from objects import glob
  18. from helpers import chatHelper as chat
  19. from common.web import cheesegull
  20. def bloodcatMessage(beatmapID):
  21. beatmap = glob.db.fetch("SELECT song_name, beatmapset_id FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [beatmapID])
  22. if beatmap is None:
  23. return "Sorry, I'm not able to provide a download link for this map :("
  24. return "Download [https://bloodcat.com/osu/s/{} {}] from Bloodcat".format(
  25. beatmap["beatmapset_id"],
  26. beatmap["song_name"],
  27. )
  28. """
  29. Commands callbacks
  30. Must have fro, chan and messages as arguments
  31. :param fro: username of who triggered the command
  32. :param chan: channel"(or username, if PM) where the message was sent
  33. :param message: list containing arguments passed from the message
  34. [0] = first argument
  35. [1] = second argument
  36. . . .
  37. return the message or **False** if there's no response by the bot
  38. TODO: Change False to None, because False doesn't make any sense
  39. """
  40. def instantRestart(fro, chan, message):
  41. glob.streams.broadcast("main", serverPackets.notification("We are restarting Bancho. Be right back!"))
  42. systemHelper.scheduleShutdown(0, True, delay=5)
  43. return False
  44. def faq(fro, chan, message):
  45. # TODO: Unhardcode this
  46. messages = {
  47. "rules": "Please make sure to check (Ripple's rules)[https://ripple.moe/doc/rules].",
  48. "swearing": "Please don't abuse swearing",
  49. "spam": "Please don't spam",
  50. "offend": "Please don't offend other players",
  51. "github": "(Ripple's Github page!)[https://github.com/osuripple/ripple]",
  52. "discord": "(Join Ripple's Discord!)[https://discord.gg/0rJcZruIsA6rXuIx]",
  53. "blog": "You can find the latest Ripple news on the (blog)[https://blog.ripple.moe]!",
  54. "changelog": "Check the (changelog)[https://ripple.moe/changelog] !",
  55. "status": "Check the server status (here!)[https://status.ripple.moe]",
  56. "english": "Please keep this channel in english.",
  57. "topic": "Can you please drop the topic and talk about something else?",
  58. "lines": "Please try to keep your sentences on a single line to avoid getting silenced."
  59. }
  60. key = message[0].lower()
  61. if key not in messages:
  62. return False
  63. return messages[key]
  64. def roll(fro, chan, message):
  65. maxPoints = 100
  66. if len(message) >= 1:
  67. if message[0].isdigit() and int(message[0]) > 0:
  68. maxPoints = int(message[0])
  69. points = random.randrange(0,maxPoints)
  70. return "{} rolls {} points!".format(fro, str(points))
  71. #def ask(fro, chan, message):
  72. # return random.choice(["yes", "no", "maybe"])
  73. def alert(fro, chan, message):
  74. msg = ' '.join(message[:]).strip()
  75. if not msg:
  76. return False
  77. glob.streams.broadcast("main", serverPackets.notification(msg))
  78. return False
  79. def alertUser(fro, chan, message):
  80. target = message[0].lower()
  81. targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  82. if targetToken is not None:
  83. msg = ' '.join(message[1:]).strip()
  84. if not msg:
  85. return False
  86. targetToken.enqueue(serverPackets.notification(msg))
  87. return False
  88. else:
  89. return "User offline."
  90. def moderated(fro, chan, message):
  91. try:
  92. # Make sure we are in a channel and not PM
  93. if not chan.startswith("#"):
  94. raise exceptions.moderatedPMException
  95. # Get on/off
  96. enable = True
  97. if len(message) >= 1:
  98. if message[0] == "off":
  99. enable = False
  100. # Turn on/off moderated mode
  101. glob.channels.channels[chan].moderated = enable
  102. return "This channel is {} in moderated mode!".format("now" if enable else "no longer")
  103. except exceptions.moderatedPMException:
  104. return "You are trying to put a private chat in moderated mode. Are you serious?!? You're fired."
  105. def kickAll(fro, chan, message):
  106. # Kick everyone but mods/admins
  107. toKick = []
  108. with glob.tokens:
  109. for key, value in glob.tokens.tokens.items():
  110. if not value.admin:
  111. toKick.append(key)
  112. # Loop though users to kick (we can't change dictionary size while iterating)
  113. for i in toKick:
  114. if i in glob.tokens.tokens:
  115. glob.tokens.tokens[i].kick()
  116. return "Whoops! Rip everyone."
  117. def kick(fro, chan, message):
  118. # Get parameters
  119. target = message[0].lower()
  120. if target == "fokabot":
  121. return "Nope."
  122. # Get target token and make sure is connected
  123. tokens = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True, _all=True)
  124. if len(tokens) == 0:
  125. return "{} is not online".format(target)
  126. # Kick users
  127. for i in tokens:
  128. i.kick()
  129. # Bot response
  130. return "{} has been kicked from the server.".format(target)
  131. def fokabotReconnect(fro, chan, message):
  132. # Check if fokabot is already connected
  133. if glob.tokens.getTokenFromUserID(999) is not None:
  134. return "Fokabot is already connected to Bancho"
  135. # Fokabot is not connected, connect it
  136. fokabot.connect()
  137. return False
  138. def silence(fro, chan, message):
  139. message = [x.lower() for x in message]
  140. target = message[0]
  141. amount = message[1]
  142. unit = message[2]
  143. reason = ' '.join(message[3:]).strip()
  144. if not reason:
  145. return "Please provide a valid reason."
  146. if not amount.isdigit():
  147. return "The amount must be a number."
  148. # Get target user ID
  149. targetUserID = userUtils.getIDSafe(target)
  150. userID = userUtils.getID(fro)
  151. # Make sure the user exists
  152. if not targetUserID:
  153. return "{}: user not found".format(target)
  154. # Calculate silence seconds
  155. if unit == 's':
  156. silenceTime = int(amount)
  157. elif unit == 'm':
  158. silenceTime = int(amount) * 60
  159. elif unit == 'h':
  160. silenceTime = int(amount) * 3600
  161. elif unit == 'd':
  162. silenceTime = int(amount) * 86400
  163. else:
  164. return "Invalid time unit (s/m/h/d)."
  165. # Max silence time is 7 days
  166. if silenceTime > 604800:
  167. return "Invalid silence time. Max silence time is 7 days."
  168. # Send silence packet to target if he's connected
  169. targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  170. if targetToken is not None:
  171. # user online, silence both in db and with packet
  172. targetToken.silence(silenceTime, reason, userID)
  173. else:
  174. # User offline, silence user only in db
  175. userUtils.silence(targetUserID, silenceTime, reason, userID)
  176. # Log message
  177. msg = "{} has been silenced for the following reason: {}".format(target, reason)
  178. return msg
  179. def removeSilence(fro, chan, message):
  180. # Get parameters
  181. for i in message:
  182. i = i.lower()
  183. target = message[0]
  184. # Make sure the user exists
  185. targetUserID = userUtils.getIDSafe(target)
  186. userID = userUtils.getID(fro)
  187. if not targetUserID:
  188. return "{}: user not found".format(target)
  189. # Send new silence end packet to user if he's online
  190. targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  191. if targetToken is not None:
  192. # User online, remove silence both in db and with packet
  193. targetToken.silence(0, "", userID)
  194. else:
  195. # user offline, remove islene ofnlt from db
  196. userUtils.silence(targetUserID, 0, "", userID)
  197. return "{}'s silence reset".format(target)
  198. def ban(fro, chan, message):
  199. # Get parameters
  200. for i in message:
  201. i = i.lower()
  202. target = message[0]
  203. # Make sure the user exists
  204. targetUserID = userUtils.getIDSafe(target)
  205. userID = userUtils.getID(fro)
  206. if not targetUserID:
  207. return "{}: user not found".format(target)
  208. # Set allowed to 0
  209. userUtils.ban(targetUserID)
  210. # Send ban packet to the user if he's online
  211. targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  212. if targetToken is not None:
  213. targetToken.enqueue(serverPackets.loginBanned())
  214. log.rap(userID, "has banned {}".format(target), True)
  215. return "RIP {}. You will not be missed.".format(target)
  216. def unban(fro, chan, message):
  217. # Get parameters
  218. for i in message:
  219. i = i.lower()
  220. target = message[0]
  221. # Make sure the user exists
  222. targetUserID = userUtils.getIDSafe(target)
  223. userID = userUtils.getID(fro)
  224. if not targetUserID:
  225. return "{}: user not found".format(target)
  226. # Set allowed to 1
  227. userUtils.unban(targetUserID)
  228. log.rap(userID, "has unbanned {}".format(target), True)
  229. return "Welcome back {}!".format(target)
  230. def restrict(fro, chan, message):
  231. # Get parameters
  232. for i in message:
  233. i = i.lower()
  234. target = message[0]
  235. # Make sure the user exists
  236. targetUserID = userUtils.getIDSafe(target)
  237. userID = userUtils.getID(fro)
  238. if not targetUserID:
  239. return "{}: user not found".format(target)
  240. # Put this user in restricted mode
  241. userUtils.restrict(targetUserID)
  242. # Send restricted mode packet to this user if he's online
  243. targetToken = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  244. if targetToken is not None:
  245. targetToken.setRestricted()
  246. log.rap(userID, "has put {} in restricted mode".format(target), True)
  247. return "Bye bye {}. See you later, maybe.".format(target)
  248. def unrestrict(fro, chan, message):
  249. # Get parameters
  250. for i in message:
  251. i = i.lower()
  252. target = message[0]
  253. # Make sure the user exists
  254. targetUserID = userUtils.getIDSafe(target)
  255. userID = userUtils.getID(fro)
  256. if not targetUserID:
  257. return "{}: user not found".format(target)
  258. # Set allowed to 1
  259. userUtils.unrestrict(targetUserID)
  260. log.rap(userID, "has removed restricted mode from {}".format(target), True)
  261. return "Welcome back {}!".format(target)
  262. def restartShutdown(restart):
  263. """Restart (if restart = True) or shutdown (if restart = False) pep.py safely"""
  264. msg = "We are performing some maintenance. Bancho will {} in 5 seconds. Thank you for your patience.".format("restart" if restart else "shutdown")
  265. systemHelper.scheduleShutdown(5, restart, msg)
  266. return msg
  267. def systemRestart(fro, chan, message):
  268. return restartShutdown(True)
  269. def systemShutdown(fro, chan, message):
  270. return restartShutdown(False)
  271. def systemReload(fro, chan, message):
  272. glob.banchoConf.reload()
  273. return "Bancho settings reloaded!"
  274. def systemMaintenance(fro, chan, message):
  275. # Turn on/off bancho maintenance
  276. maintenance = True
  277. # Get on/off
  278. if len(message) >= 2:
  279. if message[1] == "off":
  280. maintenance = False
  281. # Set new maintenance value in bancho_settings table
  282. glob.banchoConf.setMaintenance(maintenance)
  283. if maintenance:
  284. # We have turned on maintenance mode
  285. # Users that will be disconnected
  286. who = []
  287. # Disconnect everyone but mod/admins
  288. with glob.tokens:
  289. for _, value in glob.tokens.tokens.items():
  290. if not value.admin:
  291. who.append(value.userID)
  292. glob.streams.broadcast("main", serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later."))
  293. glob.tokens.multipleEnqueue(serverPackets.loginError(), who)
  294. msg = "The server is now in maintenance mode!"
  295. else:
  296. # We have turned off maintenance mode
  297. # Send message if we have turned off maintenance mode
  298. msg = "The server is no longer in maintenance mode!"
  299. # Chat output
  300. return msg
  301. def systemStatus(fro, chan, message):
  302. # Print some server info
  303. data = systemHelper.getSystemInfo()
  304. # Final message
  305. letsVersion = glob.redis.get("lets:version")
  306. if letsVersion is None:
  307. letsVersion = "\_(xd)_/"
  308. else:
  309. letsVersion = letsVersion.decode("utf-8")
  310. msg = "pep.py bancho server v{}\n".format(glob.VERSION)
  311. msg += "LETS scores server v{}\n".format(letsVersion)
  312. msg += "made by the Ripple team\n"
  313. msg += "\n"
  314. msg += "=== BANCHO STATS ===\n"
  315. msg += "Connected users: {}\n".format(data["connectedUsers"])
  316. msg += "Multiplayer matches: {}\n".format(data["matches"])
  317. msg += "Uptime: {}\n".format(data["uptime"])
  318. msg += "\n"
  319. msg += "=== SYSTEM STATS ===\n"
  320. msg += "CPU: {}%\n".format(data["cpuUsage"])
  321. msg += "RAM: {}GB/{}GB\n".format(data["usedMemory"], data["totalMemory"])
  322. if data["unix"]:
  323. msg += "Load average: {}/{}/{}\n".format(data["loadAverage"][0], data["loadAverage"][1], data["loadAverage"][2])
  324. return msg
  325. def getPPMessage(userID, just_data = False):
  326. try:
  327. # Get user token
  328. token = glob.tokens.getTokenFromUserID(userID)
  329. if token is None:
  330. return False
  331. currentMap = token.tillerino[0]
  332. currentMods = token.tillerino[1]
  333. currentAcc = token.tillerino[2]
  334. # Send request to LETS api
  335. url = "{}/v1/pp?b={}&m={}".format(glob.conf.config["server"]["letsapiurl"].rstrip("/"), currentMap, currentMods)
  336. resp = requests.get(url, timeout=10)
  337. try:
  338. assert resp is not None
  339. data = json.loads(resp.text)
  340. except (json.JSONDecodeError, AssertionError):
  341. raise exceptions.apiException()
  342. # Make sure status is in response data
  343. if "status" not in data:
  344. raise exceptions.apiException()
  345. # Make sure status is 200
  346. if data["status"] != 200:
  347. if "message" in data:
  348. return "Error in LETS API call ({}).".format(data["message"])
  349. else:
  350. raise exceptions.apiException()
  351. if just_data:
  352. return data
  353. # Return response in chat
  354. # Song name and mods
  355. msg = "{song}{plus}{mods} ".format(song=data["song_name"], plus="+" if currentMods > 0 else "", mods=generalUtils.readableMods(currentMods))
  356. # PP values
  357. if currentAcc == -1:
  358. msg += "95%: {pp95}pp | 98%: {pp98}pp | 99% {pp99}pp | 100%: {pp100}pp".format(pp100=data["pp"][0], pp99=data["pp"][1], pp98=data["pp"][2], pp95=data["pp"][3])
  359. else:
  360. msg += "{acc:.2f}%: {pp}pp".format(acc=token.tillerino[2], pp=data["pp"][0])
  361. originalAR = data["ar"]
  362. # calc new AR if HR/EZ is on
  363. if (currentMods & mods.EASY) > 0:
  364. data["ar"] = max(0, data["ar"] / 2)
  365. if (currentMods & mods.HARDROCK) > 0:
  366. data["ar"] = min(10, data["ar"] * 1.4)
  367. arstr = " ({})".format(originalAR) if originalAR != data["ar"] else ""
  368. # Beatmap info
  369. msg += " | {bpm} BPM | AR {ar}{arstr} | {stars:.2f} stars".format(bpm=data["bpm"], stars=data["stars"], ar=data["ar"], arstr=arstr)
  370. # Return final message
  371. return msg
  372. except requests.exceptions.RequestException:
  373. # RequestException
  374. return "API Timeout. Please try again in a few seconds."
  375. except exceptions.apiException:
  376. # API error
  377. return "Unknown error in LETS API call."
  378. #except:
  379. # Unknown exception
  380. # TODO: print exception
  381. # return False
  382. def tillerinoNp(fro, chan, message):
  383. try:
  384. # Bloodcat trigger for #spect_
  385. if chan.startswith("#spect_"):
  386. spectatorHostUserID = getSpectatorHostUserIDFromChannel(chan)
  387. spectatorHostToken = glob.tokens.getTokenFromUserID(spectatorHostUserID, ignoreIRC=True)
  388. if spectatorHostToken is None:
  389. return False
  390. return bloodcatMessage(spectatorHostToken.beatmapID)
  391. # Run the command in PM only
  392. if chan.startswith("#"):
  393. return False
  394. playWatch = message[1] == "playing" or message[1] == "watching"
  395. # Get URL from message
  396. if message[1] == "listening":
  397. beatmapURL = str(message[3][1:])
  398. elif playWatch:
  399. beatmapURL = str(message[2][1:])
  400. else:
  401. return False
  402. modsEnum = 0
  403. mapping = {
  404. "-Easy": mods.EASY,
  405. "-NoFail": mods.NOFAIL,
  406. "+Hidden": mods.HIDDEN,
  407. "+HardRock": mods.HARDROCK,
  408. "+Nightcore": mods.NIGHTCORE,
  409. "+DoubleTime": mods.DOUBLETIME,
  410. "-HalfTime": mods.HALFTIME,
  411. "+Flashlight": mods.FLASHLIGHT,
  412. "-SpunOut": mods.SPUNOUT
  413. }
  414. if playWatch:
  415. for part in message:
  416. part = part.replace("\x01", "")
  417. if part in mapping.keys():
  418. modsEnum += mapping[part]
  419. # Get beatmap id from URL
  420. beatmapID = fokabot.npRegex.search(beatmapURL).groups(0)[0]
  421. # Update latest tillerino song for current token
  422. token = glob.tokens.getTokenFromUsername(fro)
  423. if token is not None:
  424. token.tillerino = [int(beatmapID), modsEnum, -1.0]
  425. userID = token.userID
  426. # Return tillerino message
  427. return getPPMessage(userID)
  428. except:
  429. return False
  430. def tillerinoMods(fro, chan, message):
  431. try:
  432. # Run the command in PM only
  433. if chan.startswith("#"):
  434. return False
  435. # Get token and user ID
  436. token = glob.tokens.getTokenFromUsername(fro)
  437. if token is None:
  438. return False
  439. userID = token.userID
  440. # Make sure the user has triggered the bot with /np command
  441. if token.tillerino[0] == 0:
  442. return "Please give me a beatmap first with /np command."
  443. # Check passed mods and convert to enum
  444. modsList = [message[0][i:i+2].upper() for i in range(0, len(message[0]), 2)]
  445. modsEnum = 0
  446. for i in modsList:
  447. if i not in ["NO", "NF", "EZ", "HD", "HR", "DT", "HT", "NC", "FL", "SO"]:
  448. return "Invalid mods. Allowed mods: NO, NF, EZ, HD, HR, DT, HT, NC, FL, SO. Do not use spaces for multiple mods."
  449. if i == "NO":
  450. modsEnum = 0
  451. break
  452. elif i == "NF":
  453. modsEnum += mods.NOFAIL
  454. elif i == "EZ":
  455. modsEnum += mods.EASY
  456. elif i == "HD":
  457. modsEnum += mods.HIDDEN
  458. elif i == "HR":
  459. modsEnum += mods.HARDROCK
  460. elif i == "DT":
  461. modsEnum += mods.DOUBLETIME
  462. elif i == "HT":
  463. modsEnum += mods.HALFTIME
  464. elif i == "NC":
  465. modsEnum += mods.NIGHTCORE
  466. elif i == "FL":
  467. modsEnum += mods.FLASHLIGHT
  468. elif i == "SO":
  469. modsEnum += mods.SPUNOUT
  470. # Set mods
  471. token.tillerino[1] = modsEnum
  472. # Return tillerino message for that beatmap with mods
  473. return getPPMessage(userID)
  474. except:
  475. return False
  476. def tillerinoAcc(fro, chan, message):
  477. try:
  478. # Run the command in PM only
  479. if chan.startswith("#"):
  480. return False
  481. # Get token and user ID
  482. token = glob.tokens.getTokenFromUsername(fro)
  483. if token is None:
  484. return False
  485. userID = token.userID
  486. # Make sure the user has triggered the bot with /np command
  487. if token.tillerino[0] == 0:
  488. return "Please give me a beatmap first with /np command."
  489. # Convert acc to float
  490. acc = float(message[0])
  491. # Set new tillerino list acc value
  492. token.tillerino[2] = acc
  493. # Return tillerino message for that beatmap with mods
  494. return getPPMessage(userID)
  495. except ValueError:
  496. return "Invalid acc value"
  497. except:
  498. return False
  499. def tillerinoLast(fro, chan, message):
  500. try:
  501. # Run the command in PM only
  502. if chan.startswith("#"):
  503. return False
  504. data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*,
  505. beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc
  506. FROM scores
  507. LEFT JOIN beatmaps ON beatmaps.beatmap_md5=scores.beatmap_md5
  508. LEFT JOIN users ON users.id = scores.userid
  509. WHERE users.username = %s
  510. ORDER BY scores.time DESC
  511. LIMIT 1""", [fro])
  512. if data is None:
  513. return False
  514. diffString = "difficulty_{}".format(gameModes.getGameModeForDB(data["play_mode"]))
  515. rank = generalUtils.getRank(data["play_mode"], data["mods"], data["accuracy"],
  516. data["300_count"], data["100_count"], data["50_count"], data["misses_count"])
  517. ifPlayer = "{0} | ".format(fro) if chan != "FokaBot" else ""
  518. ifFc = " (FC)" if data["max_combo"] == data["fc"] else " {0}x/{1}x".format(data["max_combo"], data["fc"])
  519. beatmapLink = "[http://osu.ppy.sh/b/{1} {0}]".format(data["sn"], data["bid"])
  520. hasPP = data["play_mode"] != gameModes.CTB
  521. msg = ifPlayer
  522. msg += beatmapLink
  523. if data["play_mode"] != gameModes.STD:
  524. msg += " <{0}>".format(gameModes.getGameModeForPrinting(data["play_mode"]))
  525. if data["mods"]:
  526. msg += ' +' + generalUtils.readableMods(data["mods"])
  527. if not hasPP:
  528. msg += " | {0:,}".format(data["score"])
  529. msg += ifFc
  530. msg += " | {0:.2f}%, {1}".format(data["accuracy"], rank.upper())
  531. msg += " {{ {0} / {1} / {2} / {3} }}".format(data["300_count"], data["100_count"], data["50_count"], data["misses_count"])
  532. msg += " | {0:.2f} stars".format(data[diffString])
  533. return msg
  534. msg += " ({0:.2f}%, {1})".format(data["accuracy"], rank.upper())
  535. msg += ifFc
  536. msg += " | {0:.2f}pp".format(data["pp"])
  537. stars = data[diffString]
  538. if data["mods"]:
  539. token = glob.tokens.getTokenFromUsername(fro)
  540. if token is None:
  541. return False
  542. userID = token.userID
  543. token.tillerino[0] = data["bid"]
  544. token.tillerino[1] = data["mods"]
  545. token.tillerino[2] = data["accuracy"]
  546. oppaiData = getPPMessage(userID, just_data=True)
  547. if "stars" in oppaiData:
  548. stars = oppaiData["stars"]
  549. msg += " | {0:.2f} stars".format(stars)
  550. return msg
  551. except Exception as a:
  552. log.error(a)
  553. return False
  554. def mm00(fro, chan, message):
  555. random.seed()
  556. return random.choice(["meme", "MA MAURO ESISTE?"])
  557. def pp(fro, chan, message):
  558. if chan.startswith("#"):
  559. return False
  560. gameMode = None
  561. if len(message) >= 1:
  562. gm = {
  563. "standard": 0,
  564. "std": 0,
  565. "taiko": 1,
  566. "ctb": 2,
  567. "mania": 3
  568. }
  569. if message[0].lower() not in gm:
  570. return "What's that game mode? I've never heard of it :/"
  571. else:
  572. gameMode = gm[message[0].lower()]
  573. token = glob.tokens.getTokenFromUsername(fro)
  574. if token is None:
  575. return False
  576. if gameMode is None:
  577. gameMode = token.gameMode
  578. if gameMode == gameModes.TAIKO or gameMode == gameModes.CTB:
  579. return "PP for your current game mode is not supported yet."
  580. pp = userUtils.getPP(token.userID, gameMode)
  581. return "You have {:,} pp".format(pp)
  582. def updateBeatmap(fro, chan, message):
  583. try:
  584. # Run the command in PM only
  585. if chan.startswith("#"):
  586. return False
  587. # Get token and user ID
  588. token = glob.tokens.getTokenFromUsername(fro)
  589. if token is None:
  590. return False
  591. # Make sure the user has triggered the bot with /np command
  592. if token.tillerino[0] == 0:
  593. return "Please give me a beatmap first with /np command."
  594. # Send the request to cheesegull
  595. ok, message = cheesegull.updateBeatmap(token.tillerino[0])
  596. if ok:
  597. return "An update request for that beatmap has been queued. Check back in a few minutes and the beatmap should be updated!"
  598. else:
  599. return "Error in beatmap mirror API request: {}".format(message)
  600. except:
  601. return False
  602. def report(fro, chan, message):
  603. msg = ""
  604. try:
  605. # TODO: Rate limit
  606. # Regex on message
  607. reportRegex = re.compile("^(.+) \((.+)\)\:(?: )?(.+)?$")
  608. result = reportRegex.search(" ".join(message))
  609. # Make sure the message matches the regex
  610. if result is None:
  611. raise exceptions.invalidArgumentsException()
  612. # Get username, report reason and report info
  613. target, reason, additionalInfo = result.groups()
  614. target = chat.fixUsernameForBancho(target)
  615. # Make sure the target is not foka
  616. if target.lower() == "fokabot":
  617. raise exceptions.invalidUserException()
  618. # Make sure the user exists
  619. targetID = userUtils.getID(target)
  620. if targetID == 0:
  621. raise exceptions.userNotFoundException()
  622. # Make sure that the user has specified additional info if report reason is 'Other'
  623. if reason.lower() == "other" and additionalInfo is None:
  624. raise exceptions.missingReportInfoException()
  625. # Get the token if possible
  626. chatlog = ""
  627. token = glob.tokens.getTokenFromUsername(userUtils.safeUsername(target), safe=True)
  628. if token is not None:
  629. chatlog = token.getMessagesBufferString()
  630. # Everything is fine, submit report
  631. glob.db.execute("INSERT INTO reports (id, from_uid, to_uid, reason, chatlog, time) VALUES (NULL, %s, %s, %s, %s, %s)", [userUtils.getID(fro), targetID, "{reason} - ingame {info}".format(reason=reason, info="({})".format(additionalInfo) if additionalInfo is not None else ""), chatlog, int(time.time())])
  632. msg = "You've reported {target} for {reason}{info}. A Community Manager will check your report as soon as possible. Every !report message you may see in chat wasn't sent to anyone, so nobody in chat, but admins, know about your report. Thank you for reporting!".format(target=target, reason=reason, info="" if additionalInfo is None else " (" + additionalInfo + ")")
  633. adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo)
  634. # Log report in #admin and on discord
  635. chat.sendMessage("FokaBot", "#admin", adminMsg)
  636. log.warning(adminMsg, discord="cm")
  637. except exceptions.invalidUserException:
  638. msg = "Hello, FokaBot here! You can't report me. I won't forget what you've tried to do. Watch out."
  639. except exceptions.invalidArgumentsException:
  640. msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'."
  641. except exceptions.userNotFoundException:
  642. msg = "The user you've tried to report doesn't exist."
  643. except exceptions.missingReportInfoException:
  644. msg = "Please specify the reason of your report."
  645. except:
  646. raise
  647. finally:
  648. if msg != "":
  649. token = glob.tokens.getTokenFromUsername(fro)
  650. if token is not None:
  651. if token.irc:
  652. chat.sendMessage("FokaBot", fro, msg)
  653. else:
  654. token.enqueue(serverPackets.notification(msg))
  655. return False
  656. def getMatchIDFromChannel(chan):
  657. if not chan.lower().startswith("#multi_"):
  658. raise exceptions.wrongChannelException()
  659. parts = chan.lower().split("_")
  660. if len(parts) < 2 or not parts[1].isdigit():
  661. raise exceptions.wrongChannelException()
  662. matchID = int(parts[1])
  663. if matchID not in glob.matches.matches:
  664. raise exceptions.matchNotFoundException()
  665. return matchID
  666. def getSpectatorHostUserIDFromChannel(chan):
  667. if not chan.lower().startswith("#spect_"):
  668. raise exceptions.wrongChannelException()
  669. parts = chan.lower().split("_")
  670. if len(parts) < 2 or not parts[1].isdigit():
  671. raise exceptions.wrongChannelException()
  672. userID = int(parts[1])
  673. return userID
  674. def multiplayer(fro, chan, message):
  675. def mpMake():
  676. if len(message) < 2:
  677. raise exceptions.invalidArgumentsException("Wrong syntax: !mp make <name>")
  678. matchName = " ".join(message[1:]).strip()
  679. if not matchName:
  680. raise exceptions.invalidArgumentsException("Match name must not be empty!")
  681. matchID = glob.matches.createMatch(matchName, generalUtils.stringMd5(generalUtils.randomString(32)), 0, "Tournament", "", 0, -1, isTourney=True)
  682. glob.matches.matches[matchID].sendUpdates()
  683. return "Tourney match #{} created!".format(matchID)
  684. def mpJoin():
  685. if len(message) < 2 or not message[1].isdigit():
  686. raise exceptions.invalidArgumentsException("Wrong syntax: !mp join <id>")
  687. matchID = int(message[1])
  688. userToken = glob.tokens.getTokenFromUsername(fro, ignoreIRC=True)
  689. if userToken is None:
  690. raise exceptions.invalidArgumentsException(
  691. "No game clients found for {}, can't join the match. "
  692. "If you're a referee and you want to join the chat "
  693. "channel from IRC, use /join #multi_{} instead.".format(fro, matchID)
  694. )
  695. userToken.joinMatch(matchID)
  696. return "Attempting to join match #{}!".format(matchID)
  697. def mpClose():
  698. matchID = getMatchIDFromChannel(chan)
  699. glob.matches.disposeMatch(matchID)
  700. return "Multiplayer match #{} disposed successfully".format(matchID)
  701. def mpLock():
  702. matchID = getMatchIDFromChannel(chan)
  703. glob.matches.matches[matchID].isLocked = True
  704. return "This match has been locked"
  705. def mpUnlock():
  706. matchID = getMatchIDFromChannel(chan)
  707. glob.matches.matches[matchID].isLocked = False
  708. return "This match has been unlocked"
  709. def mpSize():
  710. if len(message) < 2 or not message[1].isdigit() or int(message[1]) < 2 or int(message[1]) > 16:
  711. raise exceptions.invalidArgumentsException("Wrong syntax: !mp size <slots(2-16)>")
  712. matchSize = int(message[1])
  713. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  714. _match.forceSize(matchSize)
  715. return "Match size changed to {}".format(matchSize)
  716. def mpMove():
  717. if len(message) < 3 or not message[2].isdigit() or int(message[2]) < 0 or int(message[2]) > 16:
  718. raise exceptions.invalidArgumentsException("Wrong syntax: !mp move <username> <slot>")
  719. username = message[1]
  720. newSlotID = int(message[2])
  721. userID = userUtils.getIDSafe(username)
  722. if userID is None:
  723. raise exceptions.userNotFoundException("No such user")
  724. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  725. success = _match.userChangeSlot(userID, newSlotID)
  726. if success:
  727. result = "Player {} moved to slot {}".format(username, newSlotID)
  728. else:
  729. result = "You can't use that slot: it's either already occupied by someone else or locked"
  730. return result
  731. def mpHost():
  732. if len(message) < 2:
  733. raise exceptions.invalidArgumentsException("Wrong syntax: !mp host <username>")
  734. username = message[1].strip()
  735. if not username:
  736. raise exceptions.invalidArgumentsException("Please provide a username")
  737. userID = userUtils.getIDSafe(username)
  738. if userID is None:
  739. raise exceptions.userNotFoundException("No such user")
  740. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  741. success = _match.setHost(userID)
  742. return "{} is now the host".format(username) if success else "Couldn't give host to {}".format(username)
  743. def mpClearHost():
  744. matchID = getMatchIDFromChannel(chan)
  745. glob.matches.matches[matchID].removeHost()
  746. return "Host has been removed from this match"
  747. def mpStart():
  748. def _start():
  749. matchID = getMatchIDFromChannel(chan)
  750. success = glob.matches.matches[matchID].start()
  751. if not success:
  752. chat.sendMessage("FokaBot", chan, "Couldn't start match. Make sure there are enough players and "
  753. "teams are valid. The match has been unlocked.")
  754. else:
  755. chat.sendMessage("FokaBot", chan, "Have fun!")
  756. def _decreaseTimer(t):
  757. if t <= 0:
  758. _start()
  759. else:
  760. if t % 10 == 0 or t <= 5:
  761. chat.sendMessage("FokaBot", chan, "Match starts in {} seconds.".format(t))
  762. threading.Timer(1.00, _decreaseTimer, [t - 1]).start()
  763. if len(message) < 2 or not message[1].isdigit():
  764. startTime = 0
  765. else:
  766. startTime = int(message[1])
  767. force = False if len(message) < 3 else message[2].lower() == "force"
  768. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  769. # Force everyone to ready
  770. someoneNotReady = False
  771. for i, slot in enumerate(_match.slots):
  772. if slot.status != slotStatuses.READY and slot.user is not None:
  773. someoneNotReady = True
  774. if force:
  775. _match.toggleSlotReady(i)
  776. if someoneNotReady and not force:
  777. return "Some users aren't ready yet. Use '!mp start force' if you want to start the match, " \
  778. "even with non-ready players."
  779. if startTime == 0:
  780. _start()
  781. return "Starting match"
  782. else:
  783. _match.isStarting = True
  784. threading.Timer(1.00, _decreaseTimer, [startTime - 1]).start()
  785. return "Match starts in {} seconds. The match has been locked. " \
  786. "Please don't leave the match during the countdown " \
  787. "or you might receive a penalty.".format(startTime)
  788. def mpInvite():
  789. if len(message) < 2:
  790. raise exceptions.invalidArgumentsException("Wrong syntax: !mp invite <username>")
  791. username = message[1].strip()
  792. if not username:
  793. raise exceptions.invalidArgumentsException("Please provide a username")
  794. userID = userUtils.getIDSafe(username)
  795. if userID is None:
  796. raise exceptions.userNotFoundException("No such user")
  797. token = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True)
  798. if token is None:
  799. raise exceptions.invalidUserException("That user is not connected to bancho right now.")
  800. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  801. _match.invite(999, userID)
  802. token.enqueue(serverPackets.notification("Please accept the invite you've just received from FokaBot to "
  803. "enter your tourney match."))
  804. return "An invite to this match has been sent to {}".format(username)
  805. def mpMap():
  806. if len(message) < 2 or not message[1].isdigit() or (len(message) == 3 and not message[2].isdigit()):
  807. raise exceptions.invalidArgumentsException("Wrong syntax: !mp map <beatmapid> [<gamemode>]")
  808. beatmapID = int(message[1])
  809. gameMode = int(message[2]) if len(message) == 3 else 0
  810. if gameMode < 0 or gameMode > 3:
  811. raise exceptions.invalidArgumentsException("Gamemode must be 0, 1, 2 or 3")
  812. beatmapData = glob.db.fetch("SELECT * FROM beatmaps WHERE beatmap_id = %s LIMIT 1", [beatmapID])
  813. if beatmapData is None:
  814. raise exceptions.invalidArgumentsException("The beatmap you've selected couldn't be found in the database."
  815. "If the beatmap id is valid, please load the scoreboard first in "
  816. "order to cache it, then try again.")
  817. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  818. _match.beatmapID = beatmapID
  819. _match.beatmapName = beatmapData["song_name"]
  820. _match.beatmapMD5 = beatmapData["beatmap_md5"]
  821. _match.gameMode = gameMode
  822. _match.resetReady()
  823. _match.sendUpdates()
  824. return "Match map has been updated"
  825. def mpSet():
  826. if len(message) < 2 or not message[1].isdigit() or \
  827. (len(message) >= 3 and not message[2].isdigit()) or \
  828. (len(message) >= 4 and not message[3].isdigit()):
  829. raise exceptions.invalidArgumentsException("Wrong syntax: !mp set <teammode> [<scoremode>] [<size>]")
  830. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  831. matchTeamType = int(message[1])
  832. matchScoringType = int(message[2]) if len(message) >= 3 else _match.matchScoringType
  833. if not 0 <= matchTeamType <= 3:
  834. raise exceptions.invalidArgumentsException("Match team type must be between 0 and 3")
  835. if not 0 <= matchScoringType <= 3:
  836. raise exceptions.invalidArgumentsException("Match scoring type must be between 0 and 3")
  837. oldMatchTeamType = _match.matchTeamType
  838. _match.matchTeamType = matchTeamType
  839. _match.matchScoringType = matchScoringType
  840. if len(message) >= 4:
  841. _match.forceSize(int(message[3]))
  842. if _match.matchTeamType != oldMatchTeamType:
  843. _match.initializeTeams()
  844. if _match.matchTeamType == matchTeamTypes.TAG_COOP or _match.matchTeamType == matchTeamTypes.TAG_TEAM_VS:
  845. _match.matchModMode = matchModModes.NORMAL
  846. _match.sendUpdates()
  847. return "Match settings have been updated!"
  848. def mpAbort():
  849. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  850. _match.abort()
  851. return "Match aborted!"
  852. def mpKick():
  853. if len(message) < 2:
  854. raise exceptions.invalidArgumentsException("Wrong syntax: !mp kick <username>")
  855. username = message[1].strip()
  856. if not username:
  857. raise exceptions.invalidArgumentsException("Please provide a username")
  858. userID = userUtils.getIDSafe(username)
  859. if userID is None:
  860. raise exceptions.userNotFoundException("No such user")
  861. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  862. slotID = _match.getUserSlotID(userID)
  863. if slotID is None:
  864. raise exceptions.userNotFoundException("The specified user is not in this match")
  865. for i in range(0, 2):
  866. _match.toggleSlotLocked(slotID)
  867. return "{} has been kicked from the match.".format(username)
  868. def mpPassword():
  869. password = "" if len(message) < 2 or not message[1].strip() else message[1]
  870. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  871. _match.changePassword(password)
  872. return "Match password has been changed!"
  873. def mpRandomPassword():
  874. password = generalUtils.stringMd5(generalUtils.randomString(32))
  875. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  876. _match.changePassword(password)
  877. return "Match password has been changed to a random one"
  878. def mpMods():
  879. if len(message) < 2:
  880. raise exceptions.invalidArgumentsException("Wrong syntax: !mp <mod1> [<mod2>] ...")
  881. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  882. newMods = 0
  883. freeMod = False
  884. for _mod in message[1:]:
  885. if _mod.lower().strip() == "hd":
  886. newMods |= mods.HIDDEN
  887. elif _mod.lower().strip() == "hr":
  888. newMods |= mods.HARDROCK
  889. elif _mod.lower().strip() == "dt":
  890. newMods |= mods.DOUBLETIME
  891. elif _mod.lower().strip() == "fl":
  892. newMods |= mods.FLASHLIGHT
  893. elif _mod.lower().strip() == "fi":
  894. newMods |= mods.FADEIN
  895. elif _mod.lower().strip() == "ez":
  896. newMods |= mods.EASY
  897. if _mod.lower().strip() == "none":
  898. newMods = 0
  899. if _mod.lower().strip() == "freemod":
  900. freeMod = True
  901. _match.matchModMode = matchModModes.FREE_MOD if freeMod else matchModModes.NORMAL
  902. _match.resetReady()
  903. if _match.matchModMode == matchModModes.FREE_MOD:
  904. _match.resetMods()
  905. _match.changeMods(newMods)
  906. return "Match mods have been updated!"
  907. def mpTeam():
  908. if len(message) < 3:
  909. raise exceptions.invalidArgumentsException("Wrong syntax: !mp team <username> <colour>")
  910. username = message[1].strip()
  911. if not username:
  912. raise exceptions.invalidArgumentsException("Please provide a username")
  913. colour = message[2].lower().strip()
  914. if colour not in ["red", "blue"]:
  915. raise exceptions.invalidArgumentsException("Team colour must be red or blue")
  916. userID = userUtils.getIDSafe(username)
  917. if userID is None:
  918. raise exceptions.userNotFoundException("No such user")
  919. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  920. _match.changeTeam(userID, matchTeams.BLUE if colour == "blue" else matchTeams.RED)
  921. return "{} is now in {} team".format(username, colour)
  922. def mpSettings():
  923. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  924. single = False if len(message) < 2 else message[1].strip().lower() == "single"
  925. msg = "PLAYERS IN THIS MATCH "
  926. if not single:
  927. msg += "(use !mp settings single for a single-line version):"
  928. msg += "\n"
  929. else:
  930. msg += ": "
  931. empty = True
  932. for slot in _match.slots:
  933. if slot.user is None:
  934. continue
  935. readableStatuses = {
  936. slotStatuses.READY: "ready",
  937. slotStatuses.NOT_READY: "not ready",
  938. slotStatuses.NO_MAP: "no map",
  939. slotStatuses.PLAYING: "playing",
  940. }
  941. if slot.status not in readableStatuses:
  942. readableStatus = "???"
  943. else:
  944. readableStatus = readableStatuses[slot.status]
  945. empty = False
  946. msg += "* [{team}] <{status}> ~ {username}{mods}{nl}".format(
  947. team="red" if slot.team == matchTeams.RED else "blue" if slot.team == matchTeams.BLUE else "!! no team !!",
  948. status=readableStatus,
  949. username=glob.tokens.tokens[slot.user].username,
  950. mods=" (+ {})".format(generalUtils.readableMods(slot.mods)) if slot.mods > 0 else "",
  951. nl=" | " if single else "\n"
  952. )
  953. if empty:
  954. msg += "Nobody.\n"
  955. msg = msg.rstrip(" | " if single else "\n")
  956. return msg
  957. def mpScoreV():
  958. if len(message) < 2 or message[1] not in ("1", "2"):
  959. raise exceptions.invalidArgumentsException("Wrong syntax: !mp scorev <1|2>")
  960. _match = glob.matches.matches[getMatchIDFromChannel(chan)]
  961. _match.matchScoringType = matchScoringTypes.SCORE_V2 if message[1] == "2" else matchScoringTypes.SCORE
  962. _match.sendUpdates()
  963. return "Match scoring type set to scorev{}".format(message[1])
  964. def mpHelp():
  965. return "Supported subcommands: !mp <{}>".format("|".join(k for k in subcommands.keys()))
  966. try:
  967. subcommands = {
  968. "make": mpMake,
  969. "close": mpClose,
  970. "join": mpJoin,
  971. "lock": mpLock,
  972. "unlock": mpUnlock,
  973. "size": mpSize,
  974. "move": mpMove,
  975. "host": mpHost,
  976. "clearhost": mpClearHost,
  977. "start": mpStart,
  978. "invite": mpInvite,
  979. "map": mpMap,
  980. "set": mpSet,
  981. "abort": mpAbort,
  982. "kick": mpKick,
  983. "password": mpPassword,
  984. "randompassword": mpRandomPassword,
  985. "mods": mpMods,
  986. "team": mpTeam,
  987. "settings": mpSettings,
  988. "scorev": mpScoreV,
  989. "help": mpHelp
  990. }
  991. requestedSubcommand = message[0].lower().strip()
  992. if requestedSubcommand not in subcommands:
  993. raise exceptions.invalidArgumentsException("Invalid subcommand")
  994. return subcommands[requestedSubcommand]()
  995. except (exceptions.invalidArgumentsException, exceptions.userNotFoundException, exceptions.invalidUserException) as e:
  996. return str(e)
  997. except exceptions.wrongChannelException:
  998. return "This command only works in multiplayer chat channels"
  999. except exceptions.matchNotFoundException:
  1000. return "Match not found"
  1001. except:
  1002. raise
  1003. def switchServer(fro, chan, message):
  1004. # Get target user ID
  1005. target = message[0]
  1006. newServer = message[1].strip()
  1007. if not newServer:
  1008. return "Invalid server IP"
  1009. targetUserID = userUtils.getIDSafe(target)
  1010. userID = userUtils.getID(fro)
  1011. # Make sure the user exists
  1012. if not targetUserID:
  1013. return "{}: user not found".format(target)
  1014. # Connect the user to the end server
  1015. userToken = glob.tokens.getTokenFromUserID(userID, ignoreIRC=True, _all=False)
  1016. userToken.enqueue(serverPackets.switchServer(newServer))
  1017. # Disconnect the user from the origin server
  1018. # userToken.kick()
  1019. return "{} has been connected to {}".format(target, newServer)
  1020. def reloadConfig(fro, chan, message):
  1021. if chan.startswith("#"):
  1022. return
  1023. try:
  1024. if not glob.conf.reload():
  1025. return "Invalid configuration file structure. The new configuration file was not reloaded."
  1026. except Exception as e:
  1027. return "Unhandled exception while reloading the configuration file: {}".format(str(e))
  1028. return "Configuration file reloaded successfully"
  1029. def delta(fro, chan, message):
  1030. if chan.startswith("#"):
  1031. return
  1032. if not glob.conf.config["server"]["deltaurl"].strip():
  1033. return "Delta is disabled."
  1034. userToken = glob.tokens.getTokenFromUserID(userUtils.getID(fro), ignoreIRC=True, _all=False)
  1035. if userToken is None:
  1036. return "You must be connected from a game client to switch to delta"
  1037. if not generalUtils.stringToBool(glob.conf.config["server"]["publicdelta"]) and not userToken.admin:
  1038. return "You can't use delta yet. Try again later."
  1039. userToken.enqueue(serverPackets.switchServer(glob.conf.config["server"]["deltaurl"]))
  1040. return "Connecting to delta..."
  1041. def rtx(fro, chan, message):
  1042. target = message[0]
  1043. message = " ".join(message[1:]).strip()
  1044. if not message:
  1045. return "Invalid message"
  1046. targetUserID = userUtils.getIDSafe(target)
  1047. if not targetUserID:
  1048. return "{}: user not found".format(target)
  1049. userToken = glob.tokens.getTokenFromUserID(targetUserID, ignoreIRC=True, _all=False)
  1050. userToken.enqueue(serverPackets.rtx(message))
  1051. return ":ok_hand:"
  1052. def bloodcat(fro, chan, message):
  1053. try:
  1054. matchID = getMatchIDFromChannel(chan)
  1055. except exceptions.wrongChannelException:
  1056. matchID = None
  1057. try:
  1058. spectatorHostUserID = getSpectatorHostUserIDFromChannel(chan)
  1059. except exceptions.wrongChannelException:
  1060. spectatorHostUserID = None
  1061. if matchID is not None:
  1062. if matchID not in glob.matches.matches:
  1063. return "This match doesn't seem to exist... Or does it...?"
  1064. beatmapID = glob.matches.matches[matchID].beatmapID
  1065. else:
  1066. spectatorHostToken = glob.tokens.getTokenFromUserID(spectatorHostUserID, ignoreIRC=True)
  1067. if spectatorHostToken is None:
  1068. return "The spectator host is offline."
  1069. beatmapID = spectatorHostToken.beatmapID
  1070. return bloodcatMessage(beatmapID)
  1071. """
  1072. Commands list
  1073. trigger: message that triggers the command
  1074. callback: function to call when the command is triggered. Optional.
  1075. response: text to return when the command is triggered. Optional.
  1076. syntax: command syntax. Arguments must be separated by spaces (eg: <arg1> <arg2>)
  1077. privileges: privileges needed to execute the command. Optional.
  1078. """
  1079. commands = [
  1080. {
  1081. "trigger": "!roll",
  1082. "callback": roll
  1083. }, {
  1084. "trigger": "!faq",
  1085. "syntax": "<name>",
  1086. "callback": faq
  1087. }, {
  1088. "trigger": "!report",
  1089. "callback": report
  1090. }, {
  1091. "trigger": "!help",
  1092. "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list"
  1093. }, #{
  1094. #"trigger": "!ask",
  1095. #"syntax": "<question>",
  1096. #"callback": ask
  1097. #}, {
  1098. {
  1099. "trigger": "!mm00",
  1100. "callback": mm00
  1101. }, {
  1102. "trigger": "!alert",
  1103. "syntax": "<message>",
  1104. "privileges": privileges.ADMIN_SEND_ALERTS,
  1105. "callback": alert
  1106. }, {
  1107. "trigger": "!alertuser",
  1108. "syntax": "<username> <message>",
  1109. "privileges": privileges.ADMIN_SEND_ALERTS,
  1110. "callback": alertUser,
  1111. }, {
  1112. "trigger": "!moderated",
  1113. "privileges": privileges.ADMIN_CHAT_MOD,
  1114. "callback": moderated
  1115. }, {
  1116. "trigger": "!kickall",
  1117. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1118. "callback": kickAll
  1119. }, {
  1120. "trigger": "!kick",
  1121. "syntax": "<target>",
  1122. "privileges": privileges.ADMIN_KICK_USERS,
  1123. "callback": kick
  1124. }, {
  1125. "trigger": "!fokabot reconnect",
  1126. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1127. "callback": fokabotReconnect
  1128. }, {
  1129. "trigger": "!silence",
  1130. "syntax": "<target> <amount> <unit(s/m/h/d)> <reason>",
  1131. "privileges": privileges.ADMIN_SILENCE_USERS,
  1132. "callback": silence
  1133. }, {
  1134. "trigger": "!removesilence",
  1135. "syntax": "<target>",
  1136. "privileges": privileges.ADMIN_SILENCE_USERS,
  1137. "callback": removeSilence
  1138. }, {
  1139. "trigger": "!system restart",
  1140. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1141. "callback": systemRestart
  1142. }, {
  1143. "trigger": "!system shutdown",
  1144. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1145. "callback": systemShutdown
  1146. }, {
  1147. "trigger": "!system reload",
  1148. "privileges": privileges.ADMIN_MANAGE_SETTINGS,
  1149. "callback": systemReload
  1150. }, {
  1151. "trigger": "!system maintenance",
  1152. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1153. "callback": systemMaintenance
  1154. }, {
  1155. "trigger": "!system status",
  1156. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1157. "callback": systemStatus
  1158. }, {
  1159. "trigger": "!ban",
  1160. "syntax": "<target>",
  1161. "privileges": privileges.ADMIN_BAN_USERS,
  1162. "callback": ban
  1163. }, {
  1164. "trigger": "!unban",
  1165. "syntax": "<target>",
  1166. "privileges": privileges.ADMIN_BAN_USERS,
  1167. "callback": unban
  1168. }, {
  1169. "trigger": "!restrict",
  1170. "syntax": "<target>",
  1171. "privileges": privileges.ADMIN_BAN_USERS,
  1172. "callback": restrict
  1173. }, {
  1174. "trigger": "!unrestrict",
  1175. "syntax": "<target>",
  1176. "privileges": privileges.ADMIN_BAN_USERS,
  1177. "callback": unrestrict
  1178. }, {
  1179. "trigger": "\x01ACTION is listening to",
  1180. "callback": tillerinoNp
  1181. }, {
  1182. "trigger": "\x01ACTION is playing",
  1183. "callback": tillerinoNp
  1184. }, {
  1185. "trigger": "\x01ACTION is watching",
  1186. "callback": tillerinoNp
  1187. }, {
  1188. "trigger": "!with",
  1189. "callback": tillerinoMods,
  1190. "syntax": "<mods>"
  1191. }, {
  1192. "trigger": "!last",
  1193. "callback": tillerinoLast
  1194. }, {
  1195. "trigger": "!ir",
  1196. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1197. "callback": instantRestart
  1198. }, {
  1199. "trigger": "!pp",
  1200. "callback": pp
  1201. }, {
  1202. "trigger": "!update",
  1203. "callback": updateBeatmap
  1204. }, {
  1205. "trigger": "!mp",
  1206. "privileges": privileges.USER_TOURNAMENT_STAFF,
  1207. "syntax": "<subcommand>",
  1208. "callback": multiplayer
  1209. }, {
  1210. "trigger": "!switchserver",
  1211. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1212. "syntax": "<username> <server_address>",
  1213. "callback": switchServer
  1214. }, {
  1215. "trigger": "!rtx",
  1216. "privileges": privileges.ADMIN_MANAGE_USERS,
  1217. "syntax": "<username> <message>",
  1218. "callback": rtx
  1219. }, {
  1220. "trigger": "!bloodcat",
  1221. "callback": bloodcat
  1222. }, {
  1223. "trigger": "!delta",
  1224. "callback": delta
  1225. }, {
  1226. "trigger": "!reloadconfig",
  1227. "privileges": privileges.ADMIN_MANAGE_SERVERS,
  1228. "callback": reloadConfig
  1229. }
  1230. #
  1231. # "trigger": "!acc",
  1232. # "callback": tillerinoAcc,
  1233. # "syntax": "<accuarcy>"
  1234. #}
  1235. ]
  1236. # Commands list default values
  1237. for cmd in commands:
  1238. cmd.setdefault("syntax", "")
  1239. cmd.setdefault("privileges", None)
  1240. cmd.setdefault("callback", None)
  1241. cmd.setdefault("response", "u w0t m8?")