Maki is a Discord bot that does things. Written in Python 3 and relies on Discord.py API implementation.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

414 lignes
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. }