app.py 6.4 KB

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