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.

hqtoxbot.nim 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import toxcore
  2. import std/asyncdispatch, std/asyncfile, std/base64, std/json, std/httpclient,
  3. std/os, std/strutils, std/uri
  4. {.passL: "-lcrypto".}
  5. const
  6. readmeText = readFile "README.md"
  7. spaceApiUrl = "http://spaceapi.hq.c3d2.de:3000/spaceapi.json"
  8. shalterLockUrl = "http://172.22.99.204/door/lock"
  9. shalterUnlockUrl = "http://172.22.99.204/door/unlock"
  10. # These are IP addresses so the door can be unlocked without DNS
  11. saveFileName = "hqtoxbot.save"
  12. iconFilename = "avatar.png"
  13. adminIds = [
  14. toAddress "DF0AC9107E0A30E7201C6832B017AC836FBD1EDAC390EE99B68625D73C3FD929FB47F1872CA4"
  15. # Emery
  16. ]
  17. proc bootstrap(bot: Tox) =
  18. const servers = [
  19. ("tox.neuland.technology",
  20. "15E9C309CFCB79FDDF0EBA057DABB49FE15F3803B1BFF06536AE2E5BA5E4690E".toPublicKey
  21. )
  22. ]
  23. for host, key in servers.items:
  24. bot.bootstrap(host, key)
  25. proc addAdmin(bot: Tox; id: Address) =
  26. try:
  27. discard bot.addFriend(
  28. id, "You have been granted administrative rights to " & bot.name)
  29. except ToxError:
  30. discard
  31. proc sendAvatar(bot: Tox; friend: Friend) =
  32. discard bot.send(
  33. friend, TOX_FILE_KIND_AVATAR.uint32,
  34. iconFilename.getFileSize, iconFilename)
  35. proc sendAvatarChunk(bot: Tox; friend: Friend; file: FileTransfer; pos: uint64;
  36. size: int) {.async.} =
  37. let iconFile = openAsync(iconFilename, fmRead)
  38. iconFile.setFilePos(pos.int64);
  39. let chunk = await iconFile.read(size)
  40. close iconFile
  41. bot.sendChunk(friend, file, pos, chunk)
  42. proc updateAvatar(bot: Tox) =
  43. assert(iconFilename != "")
  44. for friend in bot.friends:
  45. if bot.connectionStatus(friend) != TOX_CONNECTION_NONE:
  46. bot.sendAvatar(friend)
  47. proc updateStatus(bot: Tox) {.async.} =
  48. try:
  49. let
  50. http = newAsyncHttpClient()
  51. rsp = await http.get(spaceApiUrl)
  52. body = await rsp.body
  53. space = parseJson body
  54. status = space["status"].getStr
  55. open = space["state"]["open"].getBool
  56. if bot.statusMessage != status:
  57. bot.statusMessage = status
  58. if open:
  59. bot.status = TOX_USER_STATUS_NONE
  60. let iconUrl = space["icon"]["open"].getStr.parseUri
  61. await http.downloadFile($iconUrl, iconFilename)
  62. else:
  63. bot.status = TOX_USER_STATUS_AWAY
  64. let iconUrl = space["icon"]["closed"].getStr.parseUri
  65. await http.downloadFile($iconUrl, iconFilename)
  66. bot.updateAvatar()
  67. close http
  68. except:
  69. echo getCurrentExceptionMsg()
  70. bot.statusMessage = "status update failed"
  71. type Command = enum
  72. doorpass,
  73. help,
  74. invite,
  75. lock,
  76. readme,
  77. revoke,
  78. unlock
  79. proc setup(bot: Tox) =
  80. addTimer(20*1000, oneshot = false) do (fd: AsyncFD) -> bool:
  81. asyncCheck updateStatus(bot)
  82. let schalterClient = newAsyncHttpClient()
  83. schalterClient.headers = newHttpHeaders()
  84. let conference = bot.newConference()
  85. `title=`(bot, conference, "C3D2")
  86. bot.onFriendConnectionStatus do (f: Friend; status: Connection):
  87. if status != TOX_CONNECTION_NONE:
  88. bot.invite(f, conference)
  89. bot.sendAvatar(f)
  90. bot.onFriendMessage do (f: Friend; msg: string; kind: MessageType):
  91. proc reply(msg: string) =
  92. discard bot.send(f, msg)
  93. try:
  94. var words = msg.split(' ')
  95. if words.len < 1:
  96. words.add "help"
  97. if words.len < 2:
  98. words.add ""
  99. case parseEnum[Command](words[0].normalize)
  100. of help:
  101. var cmd = help
  102. if words[1] != "":
  103. try: cmd = parseEnum[Command](words[1].normalize)
  104. except:
  105. reply("$1 is not a help topic" % words[1])
  106. case cmd:
  107. of doorpass:
  108. reply("""Set the authorization string for opening the door lock. """ &
  109. """The authorization string will peristent until the bot is restarted.""")
  110. of help:
  111. var resp = """Return help message for the following commands:"""
  112. for e in Command:
  113. resp.add "\n\t"
  114. resp.add $e
  115. reply(resp)
  116. of invite:
  117. reply """Invite a new user to the bot."""
  118. of lock:
  119. reply("""Lock the HQ door. """ &
  120. """If the door is already open his command is not effective.""")
  121. of readme:
  122. reply """Return bot README"""
  123. of revoke:
  124. reply """Remove yourself from the bot roster."""
  125. of unlock:
  126. reply """Unlock the HQ door."""
  127. of doorpass:
  128. schalterClient.headers["Authorization"] =
  129. "Basic " & base64.encode(words[1], newline = "")
  130. of invite:
  131. for id in words[1..words.high]:
  132. try:
  133. discard bot.addFriend(id.toAddress,
  134. "You have been invited to the $1 by $2 ($3)" % [bot.name,
  135. bot.name(f), $bot.publicKey(f)])
  136. except:
  137. reply(getCurrentExceptionMsg())
  138. of lock:
  139. bot.typing(f, true)
  140. let fut = schalterClient.post(shalterLockUrl)
  141. fut.addCallback do ():
  142. bot.typing(f, false)
  143. if fut.failed:
  144. reply("lock failed")
  145. else:
  146. reply(fut.read.status)
  147. of readme:
  148. reply readmeText
  149. of revoke:
  150. reply """Tchuss"""
  151. discard bot.delete(f)
  152. of unlock:
  153. bot.typing(f, true)
  154. let fut = schalterClient.post(shalterUnlockUrl)
  155. fut.addCallback do ():
  156. bot.typing(f, false)
  157. if fut.failed:
  158. reply("unlock failed")
  159. else:
  160. reply(fut.read.status)
  161. except:
  162. reply(getCurrentExceptionMsg())
  163. bot.onFileChunkRequest do (friend: Friend; file: FileTransfer; pos: uint64; size: int):
  164. if size != 0:
  165. asyncCheck bot.sendAvatarChunk(friend, file, pos, size)
  166. proc newBot(name: string): Tox =
  167. result = newTox do (opts: Options):
  168. opts.localDiscoveryEnabled = true
  169. opts.ipv6Enabled = true
  170. if existsFile saveFileName:
  171. opts.saveDataType = TOX_SAVEDATA_TYPE_TOX_SAVE
  172. opts.savedata = readFile(saveFileName)
  173. result.name = name
  174. result.setup()
  175. for id in adminIds: result.addAdmin(id)
  176. result.bootstrap()
  177. echo result.name, " is at ", result.address
  178. proc main() =
  179. let bot = newBot("HQ Bot")
  180. writeFile(saveFileName, bot.saveData)
  181. echo "DHT port and key: ", bot.udpPort, " ", bot.dhtId
  182. while true:
  183. iterate bot
  184. poll bot.iterationInterval
  185. main()