messages.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. from flask import Flask, render_template, make_response
  2. from flask_paginate import Pagination, get_page_parameter, get_page_args
  3. from flask_caching import Cache
  4. from flask import request
  5. import pendulum
  6. import subprocess
  7. import base64
  8. import os
  9. import textwrap
  10. import sys
  11. import re
  12. import json
  13. def rot47(s):
  14. x = ''
  15. for c in s:
  16. j = ord(c)
  17. if j >= 33 and j <= 126:
  18. x += chr(33+ ((j+14) % 94))
  19. else:
  20. x += c
  21. return x
  22. base_path = "/messagebase"
  23. app = Flask(__name__, static_url_path=base_path + "/static")
  24. @app.template_filter("datefmt")
  25. def format_datetime(value):
  26. # dt = pendulum.from_timestamp(value, tz=pendulum.tz.local_timezone())
  27. dt = pendulum.from_timestamp(value)
  28. return dt.to_datetime_string()
  29. # Check Configuring Flask-Caching section for more details
  30. # cache = Cache(app, config={"CACHE_TYPE": "filesystem", "CACHE_DIR": "cache"})
  31. cache = Cache(
  32. app, config={"CACHE_TYPE": "redis", "CACHE_REDIS_HOST": "redis"}
  33. )
  34. # cache = Cache(app, config={"CACHE_TYPE": "redis", "CACHE_REDIS_HOST": "olympus"})
  35. # import jammin
  36. import sqlite3
  37. dbconnect = sqlite3.connect("db/message.sqlite3")
  38. dbc = dbconnect.cursor()
  39. bases = {
  40. "FSXNET-General": "fsx_gen",
  41. "FSXNET-Ads": "fsx_ads",
  42. "FSXNET-BBS": "fsx_bbs",
  43. "FSXNET-BOT": "fsx_bot",
  44. # "FSXNET-Encryption": "msgs/fsx_cry",
  45. "FSXNET-Ham Radio": "fsx_ham",
  46. # "FSXNET-Magicka": "msgs/fsx_mag",
  47. "FSXNET-Magicka": "fsx_mag",
  48. "FSXNET-Mystic": "fsx_mys",
  49. "FSXNET-Enigma": "fsx_eng",
  50. "FSXNET-Data": "fsx_dat",
  51. # "HappyNet-General": "msgs/hpy_gen",
  52. }
  53. def bbs_get_messages(area):
  54. global dbc
  55. messages = []
  56. for row in dbc.execute(
  57. # "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp from message WHERE area_tag=?",
  58. "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp from message WHERE area_tag=? ORDER BY message_id;",
  59. (area,),
  60. ):
  61. stamp = pendulum.parse(row[4]).timestamp()
  62. messages.append(
  63. {
  64. "MsgNum": row[0],
  65. "number": row[0],
  66. "to": row[1],
  67. "from": row[2],
  68. "subject": row[3],
  69. "written": stamp,
  70. # // written
  71. # // received
  72. # // processed
  73. }
  74. )
  75. return messages
  76. MATCH1 = re.compile(">>> BEGIN(.*)>>> END", re.DOTALL)
  77. def bbs_message(area, msgno):
  78. global dbc
  79. messages = []
  80. dbc.execute(
  81. "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp, message from message WHERE message_id=?",
  82. (msgno,),
  83. )
  84. row = dbc.fetchone()
  85. stamp = pendulum.parse(row[4]).timestamp()
  86. data = {
  87. "MsgNum": row[0],
  88. "number": row[0],
  89. "to": row[1],
  90. "from": row[2],
  91. "subject": row[3],
  92. "written": stamp,
  93. "received": stamp,
  94. "processed": stamp,
  95. "text": row[5], # .decode("cp437"),
  96. "bytes": row[5].encode("cp437")
  97. # // written
  98. # // received
  99. # // processed
  100. }
  101. if (area == 'fsx_dat') and ('>>> BEGIN' in row[5]):
  102. body = row[5] + "\n"
  103. result = MATCH1.search(body)
  104. if result:
  105. data['rot47'] = rot47(result.group(1)).lstrip("\n").replace("\n", "<br />")
  106. return data
  107. # bases = {"FSX_BOT": "fsx_bot"}
  108. # @cache.memoize(timeout=5 * 60, key_prefix="messages")
  109. @cache.memoize(timeout=5 * 60)
  110. def get_messages(base):
  111. messages = bbs_get_messages(base)
  112. messages.reverse()
  113. return messages
  114. @cache.memoize(timeout=60)
  115. def get_message(base, msgno):
  116. # message = jammin.read_message(base, msgno)
  117. message = bbs_message(base, msgno)
  118. return message
  119. @app.errorhandler(404)
  120. def not_found(e):
  121. return render_template("404.html")
  122. @app.route(base_path + "/list")
  123. def list_bases():
  124. return render_template(
  125. "list.html", bases=bases, base_path=base_path, title="Message Areas"
  126. )
  127. # return 'Here would be a listing of message bases'
  128. @app.route(base_path + "/clear")
  129. def clear_cache():
  130. cache.clear()
  131. return "Cache Cleared. Back to hitting refresh!"
  132. @app.route(base_path + "/messages/<area>")
  133. def display_messages(area):
  134. if area not in bases:
  135. return render_template(
  136. "missing-area.html", base_path=base_path, title="Missing Area"
  137. )
  138. # messages = jammin.get_messages(bases[area])
  139. messages = get_messages(bases[area])
  140. # messages.reverse() # cached.reverse()
  141. page = request.args.get(get_page_parameter(), type=int, default=1)
  142. # get_page_arg defaults to page 1, per_page of 10
  143. PER_PAGE = 50
  144. total = len(messages)
  145. pagination = Pagination(
  146. page=page,
  147. total=total,
  148. css_framework="foundation",
  149. record_name="messages",
  150. per_page=PER_PAGE,
  151. )
  152. page, per_page, offset = get_page_args()
  153. start = (page - 1) * PER_PAGE
  154. end = start + PER_PAGE
  155. # messages = messages[(page-1) * PER_PAGE:offset+PER_PAGE]
  156. messages = messages[start:end]
  157. return render_template(
  158. "messages.html",
  159. messages=messages,
  160. area=area,
  161. pagination=pagination,
  162. base_path=base_path,
  163. title="Messages for " + bases[area],
  164. )
  165. @cache.memoize(timeout=60)
  166. def ansi_to_png(raw_ansi_bytes, idx):
  167. pid = os.getppid()
  168. ansifile = "{0}-{1}.ans".format(idx, pid)
  169. pngfile = "{0}-{1}.png".format(idx, pid)
  170. with open(ansifile, "wb") as fp:
  171. fp.write(raw_ansi_bytes)
  172. subprocess.run(["./ansilove", "-d", "-o", pngfile, ansifile])
  173. with open(pngfile, "rb") as fp:
  174. png = fp.read()
  175. os.unlink(ansifile)
  176. os.unlink(pngfile)
  177. return png
  178. def ansi_to_png64(raw_ansi_bytes, idx):
  179. png = ansi_to_png(raw_ansi_bytes, idx)
  180. return base64.b64encode(png).decode("utf-8")
  181. @app.route(base_path + "/image/<area>/<int:msgno>.png")
  182. def display_ansi(area, msgno):
  183. if area not in bases:
  184. return "RATS", 404
  185. message = get_message(bases[area], msgno)
  186. if not message:
  187. return "RATS", 404
  188. if not "text" in message:
  189. return "RATS", 404
  190. # png = ansi_to_png(message["bytes"].replace(b"\r", b"\n"), msgno)
  191. png = ansi_to_png(message["bytes"], msgno)
  192. # png = ansi_to_png(message["bytes"].replace("\r", "\n"), msgno)
  193. response = make_response(png)
  194. response.headers.set("Content-Type", "image/png")
  195. return response
  196. # <img alt="My Image" src="data:image/png;base64,
  197. @app.route(base_path + "/read/<area>/<int:msgno>")
  198. def display_message(area, msgno):
  199. if area not in bases:
  200. return render_template(
  201. "missing-area.html", base_path=base_path, title="Missing Area"
  202. )
  203. # message = jammin.read_message(bases[area], msgno)
  204. message = get_message(bases[area], msgno)
  205. if not message:
  206. return render_template(
  207. "missing-message.html",
  208. base_path=base_path,
  209. area=area,
  210. title="Missing Message",
  211. )
  212. messages = get_messages(bases[area])
  213. # prevmsg and nextmsg are completely different now.
  214. prevmsg = None
  215. nextmsg = None
  216. total = len(messages)
  217. for idx, msg in enumerate(messages):
  218. if msg["MsgNum"] == msgno:
  219. # Ok, found what we're looking for
  220. if idx > 0:
  221. prevmsg = messages[idx - 1]["MsgNum"]
  222. if idx + 1 < total:
  223. nextmsg = messages[idx + 1]["MsgNum"]
  224. # prevmsg = None
  225. # nextmsg = None
  226. # if msgno > 1:
  227. # prevmsg = msgno - 1
  228. # if msgno < total:
  229. # nextmsg = msgno + 1
  230. if "text" in message:
  231. if "\x1b" in message["text"]:
  232. # Ok, the message contains ANSI CODES -- Convert
  233. message["png"] = True
  234. # message["png"] = ansi_to_png64(
  235. # message["bytes"].replace(b"\r", b"\n"), msgno
  236. # )
  237. else:
  238. text = message["text"].replace("\r", "\n")
  239. # Ok, latest changes aren't doing word-wrap for us, so do it here.
  240. text = "\n".join(
  241. [
  242. textwrap.fill(txt, width=78, replace_whitespace=False)
  243. for txt in text.splitlines()
  244. ]
  245. )
  246. message["text"] = text
  247. # message["text"].replace("\r", "\n") # <br >\n")
  248. return render_template(
  249. "message.html",
  250. message=message,
  251. area=area,
  252. msgnumber=msgno,
  253. prevmsg=prevmsg,
  254. nextmsg=nextmsg,
  255. base_path=base_path,
  256. title="Message {0}".format(msgno),
  257. )
  258. # LAST CALLERS PROCESSING
  259. @cache.memoize(timeout=60)
  260. def last_bbs_callers():
  261. dbsystem = sqlite3.connect("db/system.sqlite3")
  262. dbsys = dbsystem.cursor()
  263. dbuser = sqlite3.connect("db/user.sqlite3")
  264. dbusr = dbuser.cursor()
  265. # step 1: get list of last 25 callers
  266. users = []
  267. lookup = set()
  268. for row in dbsys.execute('select id,timestamp,log_value from system_event_log where log_name="user_login_history" order by id desc limit 25;'):
  269. # Ok!
  270. # row[0], row[1], row[2]
  271. jdata = json.loads(row[2])
  272. caller = { 'logid': row[0], 'timestamp': row[1], 'userId': jdata['userId'] }
  273. lookup.add(jdata['userId'])
  274. users.append(caller)
  275. # Ok, we have a list of userIds to look up.
  276. # just look all 10 of them up. :P
  277. for row in dbusr.execute('select U.id,U.user_name, (SELECT prop_value FROM user_property AS UP WHERE U.id=user_id AND prop_name="location") as location from user as U;'):
  278. (userid, username, location) = row
  279. if userid in lookup:
  280. # Ok, we have something!
  281. for u in users:
  282. if u['userId'] == userid:
  283. u['username'] = username
  284. u['location'] = location
  285. return users;
  286. @app.route("/lastcallers")
  287. def display_lastcallers():
  288. users=last_bbs_callers()
  289. return render_template(
  290. "lastcallers.html",
  291. users=users
  292. )