app.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env python3
  2. from telnetlib import Telnet
  3. from collections import defaultdict
  4. from datetime import timedelta
  5. import toml
  6. from flask import Flask, jsonify, render_template_string
  7. IDLE_TIMEOUT = timedelta(minutes=5)
  8. app = Flask(__name__)
  9. def parse_ts_response(response):
  10. # entries separated by |'s
  11. entries = response.split("|")
  12. # entries contain key=value pairs separated by spaces
  13. pairs = [[pr.split("=", 1) for pr in ent.split()] for ent in entries]
  14. # rearrange these into maps for convenience
  15. return [{k: v for k, v in pr} for pr in pairs]
  16. def get_users():
  17. with open("secret.toml") as infile:
  18. cfg = toml.load(infile)
  19. login = ("login %s %s\n" % (cfg["user"], cfg["pass"])).encode("utf-8")
  20. with Telnet(cfg["host"], cfg["port"], 5) as tn:
  21. print("connection")
  22. print(tn.read_until(b"\n").decode("utf-8"))
  23. print(tn.read_until(b"\n").decode("utf-8"))
  24. print("----")
  25. tn.write(login)
  26. print("after login")
  27. print(tn.read_until(b"\n").decode("utf-8"))
  28. print("----")
  29. tn.write(b"use 1 -virtual\n")
  30. print("after use")
  31. print(tn.read_until(b"\n").decode("utf-8"))
  32. print("----")
  33. tn.write(b"channellist\n")
  34. response = tn.read_until(b"\n").decode("utf-8")
  35. print("after channellist")
  36. print(response)
  37. print(tn.read_until(b"\n").decode("utf-8"))
  38. print("----")
  39. channel_maps = parse_ts_response(response)
  40. # rearrange the maps into one large channel lookup map
  41. channels = {info["cid"]: info["channel_name"].replace(r"\s", " ") for info in channel_maps}
  42. tn.write(b"clientlist\n")
  43. response = tn.read_until(b"\n").decode("utf-8")
  44. print("after clientlist")
  45. print(response)
  46. print(tn.read_until(b"\n").decode("utf-8"))
  47. print("----")
  48. entry_maps = parse_ts_response(response)
  49. # combine the maps into one large map, ignoring serveradmin query user
  50. client_info = {info["client_nickname"]: info for info in entry_maps if "serveradmin" not in info["client_nickname"]}
  51. for k, v in client_info.items():
  52. tn.write(f"clientinfo clid={v['clid']}\n".encode("utf-8"))
  53. response = tn.read_until(b"\n").decode("utf-8")
  54. print(f"after clientinfo for {k}")
  55. print(response)
  56. print(tn.read_until(b"\n").decode("utf-8"))
  57. print("----")
  58. # info is key=value pairs separated by spaces
  59. pairs = [ent.split("=", 1) for ent in response.split() if "=" in ent]
  60. # rearrange into a map and put in the client_info
  61. v["client_info"] = {k: v for k, v in pairs}
  62. tn.write(b"quit\n")
  63. print("after quit")
  64. print(tn.read_until(b"\n").decode("utf-8"))
  65. users = []
  66. channel_users = defaultdict(list)
  67. for name, info in client_info.items():
  68. user_text = name
  69. audio_status = []
  70. if info["client_info"].get("client_input_muted", "0") == "1":
  71. audio_status.append("Mic muted")
  72. if info["client_info"].get("client_output_muted", "0") == "1":
  73. audio_status.append("Sound muted")
  74. if len(audio_status) > 0:
  75. user_text += f" ({', '.join(audio_status)})"
  76. idle = timedelta(milliseconds=int(info["client_info"].get("client_idle_time", "0")))
  77. if idle >= IDLE_TIMEOUT:
  78. # strip out the sub-second resolution
  79. idle -= timedelta(microseconds=idle.microseconds)
  80. user_text += f" (Idle for {idle})"
  81. users.append(user_text)
  82. channel_users[channels[info["cid"]]].append(user_text)
  83. return sorted(users), {k: sorted(v) for k, v in channel_users.items()}
  84. @app.route("/")
  85. def get_status():
  86. return jsonify({"users": get_users()[0]})
  87. @app.route("/page")
  88. def get_status_page():
  89. # JS adapted from
  90. # https://stackoverflow.com/questions/6152522/how-can-i-make-a-paragraph-bounce-around-in-a-div
  91. return render_template_string("""
  92. <!doctype html>
  93. <title>Teamspeak Server Status</title>
  94. <style>
  95. body {
  96. margin: 0px 0px 0px 0px;
  97. padding: 0px 0px 0px 0px;
  98. font-family: verdana, arial, helvetica, sans-serif;
  99. color: #ccc;
  100. background-color: #333;
  101. }
  102. h1 {
  103. font-size: 24px;
  104. line-height: 44px;
  105. font-weight: bold;
  106. margin-top: 0;
  107. margin-bottom: 0;
  108. }
  109. h2 {
  110. font-size: 18px;
  111. line-height: 20px;
  112. margin-top: 0;
  113. margin-left: 15px;
  114. margin-bottom: -10px;
  115. }
  116. #bounceBox {
  117. position: absolute;
  118. top: 0;
  119. right: 0;
  120. bottom: 0;
  121. left: 0;
  122. }
  123. #bouncer {
  124. position: absolute;
  125. }
  126. </style>
  127. <script>
  128. let vx = 3;
  129. let vy = 2;
  130. const buffer = 5;
  131. const hitLR = (el, bounding) => {
  132. if (el.offsetLeft <= buffer && vx < 0) {
  133. vx = -1 * vx;
  134. }
  135. if ((el.offsetLeft + el.offsetWidth) >= (bounding.offsetWidth - buffer)) {
  136. vx = -1 * vx;
  137. }
  138. if (el.offsetTop <= buffer && vy < 0) {
  139. vy = -1 * vy;
  140. }
  141. if ((el.offsetTop + el.offsetHeight) >= (bounding.offsetHeight - buffer)) {
  142. vy = -1 * vy;
  143. }
  144. }
  145. const mover = (el, bounding) => {
  146. hitLR(el, bounding);
  147. el.style.left = el.offsetLeft + vx + 'px';
  148. el.style.top = el.offsetTop + vy + 'px';
  149. setTimeout(function() {
  150. mover(el, bounding);
  151. }, 30);
  152. }
  153. setTimeout(() => mover(
  154. document.getElementById("bouncer"),
  155. document.getElementById("bounceBox")
  156. ), 30);
  157. </script>
  158. <body>
  159. <div id="bounceBox">
  160. <div id="bouncer">
  161. <h1>TeamSpeak Server Status</h1>
  162. {% if users|length == 0 %}
  163. No one in teamspeak!
  164. {% else %}
  165. {% for channel, people in users.items() %}
  166. <h2>{{ channel }}</h2>
  167. <ul>
  168. {% for user in people %}
  169. <li>{{ user }}</li>
  170. {% endfor %}
  171. </ul>
  172. {% endfor %}
  173. {% endif %}
  174. </div>
  175. </div>
  176. </body>
  177. """, users=get_users()[1])
  178. if __name__ == "__main__":
  179. app.run("0.0.0.0", 5000, debug=True, threaded=True)