Maki is a Discord bot that does things. Written in Python 3 and relies on Discord.py API implementation.
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.

414 lines
11KB

  1. # Maki
  2. # ----
  3. # Discord bot by MrDetonia
  4. #
  5. # Copyright 2018 Zac Herd
  6. # Licensed under BSD 3-clause License, see LICENSE.md for more info
  7. # IMPORTS
  8. import asyncio
  9. import os
  10. import sys
  11. import re
  12. import requests
  13. import random
  14. import subprocess
  15. import itertools
  16. import ftfy
  17. # LOCAL IMPORTS
  18. from common import *
  19. from helpers import *
  20. from secret import lfmkey, steamkey
  21. import markov
  22. # COMMAND IMPLEMENTATIONS
  23. @asyncio.coroutine
  24. def cmd_help(client, msg):
  25. yield from discord_send(client, msg, helptext)
  26. @asyncio.coroutine
  27. def cmd_info(client, msg):
  28. pyver = "{}.{}.{}".format(sys.version_info[0], sys.version_info[1],
  29. sys.version_info[2])
  30. appinfo = yield from client.application_info()
  31. response = "I am **{}**, a Discord bot by **{}** | `{}` | Python `{}` | discord.py `{}`".format(
  32. appinfo.name, appinfo.owner.name, version, pyver, discord.__version__)
  33. yield from discord_send(client, msg, response)
  34. @asyncio.coroutine
  35. def cmd_upskirt(client, msg):
  36. response = "No, don\'t look at my pantsu, baka! <https://github.com/MrDetonia/maki>"
  37. yield from discord_send(client, msg, response)
  38. whoistring = "**{}#{}**: `{}`\n**Account Created:** `{}`"
  39. @asyncio.coroutine
  40. def cmd_whoami(client, msg):
  41. response = whoistring.format(msg.author.name,
  42. msg.author.discriminator, msg.author.id,
  43. strfromdt(msg.author.created_at))
  44. yield from discord_send(client, msg, response)
  45. @asyncio.coroutine
  46. def cmd_whois(client, msg):
  47. tmp = msg.content[7:]
  48. user = msg.server.get_member_named(tmp)
  49. if user == None:
  50. reponse = "I can't find `{}`".format(tmp)
  51. else:
  52. response = whoistring.format(user.name, user.discriminator, user.id,
  53. strfromdt(user.created_at))
  54. yield from discord_send(client, msg, response)
  55. @asyncio.coroutine
  56. def cmd_seen(client, msg):
  57. tmp = msg.content[6:]
  58. user = msg.server.get_member_named(tmp)
  59. if user == None:
  60. reponse = "I can't find `{}`".format(tmp)
  61. elif user.name == "Maki":
  62. reponse = "I'm right here!"
  63. else:
  64. target = msg.server.id + user.id
  65. if target in history and history[target][0] == msg.server.id:
  66. response = "**{}** was last seen saying the following at {}:\n{}".format(
  67. user.name, strfromdt(dtfromts(history[target][1])),
  68. history[target][2])
  69. else:
  70. response = "I haven't seen **{}** speak yet!".format(tmp)
  71. yield from discord_send(client, msg, response)
  72. @asyncio.coroutine
  73. def cmd_say(client, msg):
  74. response = msg.content[5:]
  75. yield from client.delete_message(msg)
  76. yield from discord_send(client, msg, response)
  77. @asyncio.coroutine
  78. def cmd_sayy(client, msg):
  79. response = " ".join(msg.content[6:])
  80. yield from client.delete_message(msg)
  81. yield from discord_send(client, msg, response)
  82. @asyncio.coroutine
  83. def cmd_markov(client, msg):
  84. yield from discord_typing(client, msg)
  85. tmp = msg.content[8:]
  86. target = ""
  87. if tmp == "Maki":
  88. response = "My markovs always say the same thing"
  89. else:
  90. if tmp == "":
  91. target = "{}-{}".format(msg.server.id, msg.author.id)
  92. else:
  93. try:
  94. target = "{}-{}".format(msg.server.id,
  95. msg.server.get_member_named(tmp).id)
  96. except AttributeError:
  97. reponse = "I can't find `{}`".format(tmp)
  98. if target != "":
  99. mfile = "./persist/markovs/" + target
  100. if os.path.isfile(mfile):
  101. mc = markov.Markov(open(mfile))
  102. response = mc.generate_text(random.randint(20, 40))
  103. else:
  104. response = "I haven't seen `{}` speak yet.".format(tmp)
  105. yield from discord_send(client, msg, response)
  106. @asyncio.coroutine
  107. def cmd_roll(client, msg):
  108. tmp = msg.content[6:]
  109. pattern = re.compile("^(\d+)d(\d+)([+-]\d+)?(.*)?$")
  110. pattern2 = re.compile("^d(\d+)([+-]\d+)?(.*)?$")
  111. # extract numbers
  112. nums = [int(s) for s in re.findall(r"\d+", tmp)]
  113. if pattern.match(tmp):
  114. numdice = nums[0]
  115. diceval = nums[1]
  116. elif pattern2.match(tmp):
  117. numdice = 1
  118. diceval = nums[0]
  119. else:
  120. response = "Expected format: `[<num>]d<value>[{+-}<modifier>]`"
  121. yield from discord_send(client, msg, response)
  122. # extract modifier, if any
  123. modifier = 0
  124. modpattern = re.compile("^(\d+)?d(\d+)[+-]\d+(.*)?$")
  125. if modpattern.match(tmp):
  126. modifier = nums[len(nums) - 1]
  127. # negate modifier, if necessary
  128. modpattern = re.compile("^(\d+)?d(\d+)\-\d+(.*)?$")
  129. if modpattern.match(tmp):
  130. modifier = -modifier
  131. # limit ranges
  132. numdice = clamp(numdice, 1, 100)
  133. diceval = clamp(diceval, 1, 1000)
  134. # roll and sum dice
  135. rolls = []
  136. for i in range(numdice):
  137. rolls.append(random.randint(1, diceval))
  138. rollsum = sum(rolls) + modifier
  139. # generate response text
  140. response = "**{} rolled:** {}d{}".format(msg.author.display_name, numdice,
  141. diceval)
  142. if modifier > 0:
  143. response += "+{}".format(modifier)
  144. if modifier < 0:
  145. response += "{}".format(modifier)
  146. response += "\n**Rolls:** `{}`".format(rolls)
  147. response += "\n**Result:** `{}`".format(rollsum)
  148. if rollsum - modifier == numdice * diceval:
  149. response += " *(Natural - confirmed `{}`)*".format(
  150. random.randint(1, 20))
  151. elif rollsum - modifier == numdice:
  152. response += " *(Crit fail - confirmed `{}`)*".format(
  153. random.randint(1, 20))
  154. yield from discord_send(client, msg, response)
  155. @asyncio.coroutine
  156. def cmd_spell(client, msg):
  157. searchterm = msg.content[7:]
  158. # perform search on user input
  159. results = []
  160. for result in itertools.islice(search(spellslist, searchterm), 3):
  161. results.append(result)
  162. # default response is an error
  163. response = "Couldn't find any matching spells!"
  164. # otherwise, grab spell data and generate response text
  165. if results:
  166. result = requests.get(results[0][1]).json()
  167. response = "**Spell:** " + result['name']
  168. if result['concentration'] is "yes":
  169. response += " *(C)*"
  170. response += " " + str(result['components'])
  171. response += "\n\n**Level:** " + str(result['level'])
  172. response += "\n\n**Description:**"
  173. for s in result['desc']:
  174. response += '\n' + s
  175. if 'higher_level' in result:
  176. response += "\n\n**Higher Level:**"
  177. for s in result['higher_level']:
  178. response += '\n' + s
  179. response += "\n\n**Range:** " + result['range']
  180. response += "\n\n**Casting Time:** " + result['casting_time']
  181. response += "\n\n**Duration:** " + result['duration']
  182. # repair encoding errors from API
  183. response = ftfy.fix_text(response)
  184. # append next search matches, if any
  185. matches = []
  186. for k,_ in results[1:]:
  187. matches.append(k)
  188. if matches:
  189. response += "\n\n*Possible Matches: " + str(matches) + "*"
  190. yield from discord_send(client, msg, response)
  191. @asyncio.coroutine
  192. def cmd_qr(client, msg):
  193. tmp = msg.content[4:]
  194. yield from discord_typing(client, msg)
  195. # generate qr code
  196. qr = subprocess.Popen(
  197. "qrencode -t png -o -".split(),
  198. stdin=subprocess.PIPE,
  199. stdout=subprocess.PIPE)
  200. qr.stdin.write(tmp.encode("utf-8"))
  201. qr.stdin.close()
  202. out = subprocess.check_output(
  203. "curl -F upload=@- https://w1r3.net".split(), stdin=qr.stdout)
  204. response = out.decode("utf-8").strip()
  205. yield from discord_send(client, msg, response)
  206. @asyncio.coroutine
  207. def cmd_np(client, msg):
  208. tmp = msg.content[4:]
  209. if tmp == "":
  210. response = lastfm_np(msg.author.name)
  211. else:
  212. response = lastfm_np(tmp)
  213. print("CALLING SEND")
  214. yield from discord_send(client, msg, response)
  215. @asyncio.coroutine
  216. def cmd_steam(client, msg):
  217. tmp = msg.content[7:]
  218. if tmp == "":
  219. response = steamdata(msg.author.name)
  220. else:
  221. response = steamdata(tmp)
  222. yield from discord_send(client, msg, response)
  223. # HELPER FUNCTIONS
  224. # gets now playing information from last.fm
  225. def lastfm_np(username):
  226. # sanitise username
  227. cleanusername = re.sub(r'[^a-zA-Z0-9_-]', '', username, 0)
  228. # fetch JSON from last.fm
  229. payload = {
  230. 'format': 'json',
  231. 'method': 'user.getRecentTracks',
  232. 'user': cleanusername,
  233. 'limit': '1',
  234. 'api_key': lfmkey
  235. }
  236. r = requests.get("http://ws.audioscrobbler.com/2.0/", params=payload)
  237. # read json data
  238. np = r.json()
  239. # check we got a valid response
  240. if 'error' in np:
  241. return "I couldn't get last.fm data for `{}`".format(username)
  242. # get fields
  243. try:
  244. username = np['recenttracks']['@attr']['user']
  245. track = np['recenttracks']['track'][0]
  246. album = track['album']['#text']
  247. artist = track['artist']['#text']
  248. song = track['name']
  249. nowplaying = '@attr' in track
  250. except IndexError:
  251. return "It looks like `{}` hasn't played anything recently.".format(
  252. username)
  253. # grammar
  254. if album != "":
  255. albumtext = "` from the album `{}`".format(album)
  256. else:
  257. albumtext = "`"
  258. if nowplaying == True:
  259. nowplaying = " is listening"
  260. else:
  261. nowplaying = " last listened"
  262. # construct string
  263. return "{}{} to `{}` by `{}{}".format(username, nowplaying, song, artist,
  264. albumtext)
  265. # gets general steam user info from a vanityurl name
  266. def steamdata(vanityname):
  267. # sanitise username
  268. cleanvanityname = re.sub(r'[^a-zA-Z0-9_-]', '', vanityname, 0)
  269. resolveurl = 'http://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key='
  270. dataurl = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key='
  271. # fetch json from steam
  272. try:
  273. idresponse = requests.get(resolveurl + steamkey + '&vanityurl=' +
  274. vanityname).json()['response']
  275. except:
  276. return "I can't connect to Steam"
  277. # check if user was found and extract steamid
  278. if idresponse['success'] is not 1:
  279. return "I couldn't find `{}`".format(vanityname)
  280. else:
  281. steamid = idresponse['steamid']
  282. # fetch steam user info
  283. try:
  284. dataresponse = requests.get(dataurl + steamkey + '&steamids=' +
  285. steamid).json()['response']['players'][0]
  286. except:
  287. return "Can't find info on `{}`".format(vanityname)
  288. personastates = [
  289. 'Offline', 'Online', 'Busy', 'Away', 'Snoozed', 'Looking to trade',
  290. 'Looking to play'
  291. ]
  292. if 'personaname' in dataresponse: namestr = dataresponse['personaname']
  293. else: namestr = ''
  294. if 'personastate' in dataresponse:
  295. statestr = '`' + personastates[dataresponse['personastate']] + '`'
  296. else:
  297. statestr = ''
  298. if 'gameextrainfo' in dataresponse:
  299. gamestr = ' playing `' + dataresponse['gameextrainfo'] + '`'
  300. else:
  301. gamestr = ''
  302. responsetext = [(namestr + ' is ' + statestr + gamestr).replace(' ', ' ')]
  303. return '\n'.join(responsetext)
  304. # COMMAND HANDLING
  305. prefix = "."
  306. commands = {
  307. "help": cmd_help,
  308. "info": cmd_info,
  309. "upskirt": cmd_upskirt,
  310. "whoami": cmd_whoami,
  311. "whois": cmd_whois,
  312. "seen": cmd_seen,
  313. "say": cmd_say,
  314. "sayy": cmd_sayy,
  315. "markov": cmd_markov,
  316. "roll": cmd_roll,
  317. "spell": cmd_spell,
  318. "qr": cmd_qr,
  319. "np": cmd_np,
  320. "steam": cmd_steam,
  321. }