messages.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. # Should this be in the actual calls?
  38. # (So I don't keep a connection open all the time?)
  39. dbconnect = sqlite3.connect("db/message.sqlite3")
  40. dbc = dbconnect.cursor()
  41. bases = {
  42. "FSXNET-General": "fsx_gen",
  43. "FSXNET-Ads": "fsx_ads",
  44. "FSXNET-BBS": "fsx_bbs",
  45. "FSXNET-BOT": "fsx_bot",
  46. "FSXNET-Encryption": "fsx_cry",
  47. "FSXNET-Network": "fsx_net",
  48. "FSXNET-Ham Radio": "fsx_ham",
  49. "FSXNET-Talisman - Magicka": "fsx_mag",
  50. "FSXNET-Mystic": "fsx_mys",
  51. "FSXNET-Enigma": "fsx_eng",
  52. "FSXNET-Gaming": "fsx_gaming",
  53. "FSXNET-Space & Astronomy": "fsx_space",
  54. "FSXNET-Sports": "fsx_sports",
  55. "FSXNET-Retro Computing": "fsx_retro",
  56. "FSXNET-Transport": "fsx_transport",
  57. "FSXNET-Video": "fsx_video",
  58. "FSXNET-Music": "fsx_music",
  59. "FSXNET-Do It Yourself": "fsx_diy",
  60. "FSXNET-Food": "fsx_food",
  61. "FSXNET-Gardening": "fsx_gardening",
  62. "FSXNET-Arts": "fsx_arts",
  63. "FSXNET-Data": "fsx_dat",
  64. # "HappyNet-General": "msgs/hpy_gen",
  65. }
  66. def bbs_get_messages(area):
  67. global dbc
  68. messages = []
  69. for row in dbc.execute(
  70. # "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp from message WHERE area_tag=?",
  71. "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp from message WHERE area_tag=? ORDER BY message_id;",
  72. (area,),
  73. ):
  74. stamp = pendulum.parse(row[4]).timestamp()
  75. messages.append(
  76. {
  77. "MsgNum": row[0],
  78. "number": row[0],
  79. "to": row[1],
  80. "from": row[2],
  81. "subject": row[3],
  82. "written": stamp,
  83. # // written
  84. # // received
  85. # // processed
  86. }
  87. )
  88. return messages
  89. MATCH1 = re.compile(">>> BEGIN(.*)>>> END", re.DOTALL)
  90. def bbs_message(area, msgno):
  91. global dbc
  92. messages = []
  93. dbc.execute(
  94. "SELECT message_id, to_user_name, from_user_name, subject, modified_timestamp, message from message WHERE message_id=?",
  95. (msgno,),
  96. )
  97. row = dbc.fetchone()
  98. if (not row):
  99. return
  100. stamp = pendulum.parse(row[4]).timestamp()
  101. data = {
  102. "MsgNum": row[0],
  103. "number": row[0],
  104. "to": row[1],
  105. "from": row[2],
  106. "subject": row[3],
  107. "written": stamp,
  108. "received": stamp,
  109. "processed": stamp,
  110. "text": row[5], # .decode("cp437"),
  111. "bytes": row[5].encode("cp437")
  112. # // written
  113. # // received
  114. # // processed
  115. }
  116. if (area == 'fsx_dat') and ('>>> BEGIN' in row[5]):
  117. body = row[5] + "\n"
  118. result = MATCH1.search(body)
  119. if result:
  120. data['rot47'] = rot47(result.group(1)).lstrip("\n").replace("\n", "<br />")
  121. return data
  122. # bases = {"FSX_BOT": "fsx_bot"}
  123. # @cache.memoize(timeout=5 * 60, key_prefix="messages")
  124. @cache.memoize(timeout=5 * 60)
  125. def get_messages(base):
  126. messages = bbs_get_messages(base)
  127. messages.reverse()
  128. return messages
  129. @cache.memoize(timeout=60)
  130. def get_message(base, msgno):
  131. # message = jammin.read_message(base, msgno)
  132. message = bbs_message(base, msgno)
  133. return message
  134. @app.errorhandler(404)
  135. def not_found(e):
  136. return render_template("404.html")
  137. @app.route(base_path + "/list")
  138. def list_bases():
  139. return render_template(
  140. "list.html", bases=bases, base_path=base_path, title="Message Areas"
  141. )
  142. # return 'Here would be a listing of message bases'
  143. @app.route(base_path + "/clear")
  144. def clear_cache():
  145. cache.clear()
  146. return "Cache Cleared. Back to hitting refresh!"
  147. @app.route(base_path + "/messages/<area>")
  148. def display_messages(area):
  149. if area not in bases:
  150. return render_template(
  151. "missing-area.html", base_path=base_path, title="Missing Area"
  152. )
  153. # messages = jammin.get_messages(bases[area])
  154. messages = get_messages(bases[area])
  155. # messages.reverse() # cached.reverse()
  156. page = request.args.get(get_page_parameter(), type=int, default=1)
  157. # get_page_arg defaults to page 1, per_page of 10
  158. PER_PAGE = 50
  159. total = len(messages)
  160. pagination = Pagination(
  161. page=page,
  162. total=total,
  163. css_framework="foundation",
  164. record_name="messages",
  165. per_page=PER_PAGE,
  166. )
  167. page, per_page, offset = get_page_args()
  168. start = (page - 1) * PER_PAGE
  169. end = start + PER_PAGE
  170. # messages = messages[(page-1) * PER_PAGE:offset+PER_PAGE]
  171. messages = messages[start:end]
  172. return render_template(
  173. "messages.html",
  174. messages=messages,
  175. area=area,
  176. pagination=pagination,
  177. base_path=base_path,
  178. title="Messages for " + bases[area],
  179. )
  180. @cache.memoize(timeout=60)
  181. def ansi_to_png(raw_ansi_bytes, idx):
  182. pid = os.getppid()
  183. ansifile = "{0}-{1}.ans".format(idx, pid)
  184. pngfile = "{0}-{1}.png".format(idx, pid)
  185. with open(ansifile, "wb") as fp:
  186. fp.write(raw_ansi_bytes)
  187. subprocess.run(["./ansilove", "-d", "-o", pngfile, ansifile])
  188. with open(pngfile, "rb") as fp:
  189. png = fp.read()
  190. os.unlink(ansifile)
  191. os.unlink(pngfile)
  192. return png
  193. def ansi_to_png64(raw_ansi_bytes, idx):
  194. png = ansi_to_png(raw_ansi_bytes, idx)
  195. return base64.b64encode(png).decode("utf-8")
  196. @app.route(base_path + "/image/<area>/<int:msgno>.png")
  197. def display_ansi(area, msgno):
  198. if area not in bases:
  199. return "RATS", 404
  200. message = get_message(bases[area], msgno)
  201. if not message:
  202. return "RATS", 404
  203. if not "text" in message:
  204. return "RATS", 404
  205. # png = ansi_to_png(message["bytes"].replace(b"\r", b"\n"), msgno)
  206. png = ansi_to_png(message["bytes"], msgno)
  207. # png = ansi_to_png(message["bytes"].replace("\r", "\n"), msgno)
  208. response = make_response(png)
  209. response.headers.set("Content-Type", "image/png")
  210. return response
  211. # <img alt="My Image" src="data:image/png;base64,
  212. @app.route(base_path + "/read/<area>/<int:msgno>")
  213. def display_message(area, msgno):
  214. if area not in bases:
  215. return render_template(
  216. "missing-area.html", base_path=base_path, title="Missing Area"
  217. )
  218. # message = jammin.read_message(bases[area], msgno)
  219. message = get_message(bases[area], msgno)
  220. if not message:
  221. return render_template(
  222. "missing-message.html",
  223. base_path=base_path,
  224. area=area,
  225. title="Missing Message",
  226. )
  227. messages = get_messages(bases[area])
  228. # prevmsg and nextmsg are completely different now.
  229. prevmsg = None
  230. nextmsg = None
  231. total = len(messages)
  232. for idx, msg in enumerate(messages):
  233. if msg["MsgNum"] == msgno:
  234. # Ok, found what we're looking for
  235. if idx > 0:
  236. prevmsg = messages[idx - 1]["MsgNum"]
  237. if idx + 1 < total:
  238. nextmsg = messages[idx + 1]["MsgNum"]
  239. # prevmsg = None
  240. # nextmsg = None
  241. # if msgno > 1:
  242. # prevmsg = msgno - 1
  243. # if msgno < total:
  244. # nextmsg = msgno + 1
  245. if "text" in message:
  246. if "\x1b" in message["text"]:
  247. # Ok, the message contains ANSI CODES -- Convert
  248. message["png"] = True
  249. # message["png"] = ansi_to_png64(
  250. # message["bytes"].replace(b"\r", b"\n"), msgno
  251. # )
  252. else:
  253. text = message["text"].replace("\r", "\n")
  254. # Ok, latest changes aren't doing word-wrap for us, so do it here.
  255. text = "\n".join(
  256. [
  257. textwrap.fill(txt, width=78, replace_whitespace=False)
  258. for txt in text.splitlines()
  259. ]
  260. )
  261. message["text"] = text
  262. # message["text"].replace("\r", "\n") # <br >\n")
  263. return render_template(
  264. "message.html",
  265. message=message,
  266. area=area,
  267. msgnumber=msgno,
  268. prevmsg=prevmsg,
  269. nextmsg=nextmsg,
  270. base_path=base_path,
  271. title="Message {0}".format(msgno),
  272. )
  273. # LAST CALLERS PROCESSING
  274. def time_duration(time_delta):
  275. if (time_delta.in_seconds() < 60):
  276. return "{0} seconds ago".format(time_delta.in_seconds())
  277. if (time_delta.in_minutes() < 60):
  278. return "{0} minutes ago".format(time_delta.in_minutes())
  279. if (time_delta.in_hours() < 24):
  280. return "{0} hours ago".format(time_delta.in_hours())
  281. return "{0} days ago".format(time_delta.in_days())
  282. # in_months, in_years ...
  283. @cache.memoize(timeout=60)
  284. def last_bbs_callers():
  285. dbsystem = sqlite3.connect("db/system.sqlite3")
  286. dbsys = dbsystem.cursor()
  287. dbuser = sqlite3.connect("db/user.sqlite3")
  288. dbusr = dbuser.cursor()
  289. # step 1: get list of last 25 callers
  290. users = []
  291. lookup = set()
  292. now = pendulum.now()
  293. 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;'):
  294. # Ok!
  295. # row[0], row[1], row[2]
  296. jdata = json.loads(row[2])
  297. called_when = pendulum.parse(row[1])
  298. long_ago = time_duration(now - called_when)
  299. caller = { 'logid': row[0], 'timestamp': row[1], 'userId': jdata['userId'], 'ago': long_ago }
  300. lookup.add(jdata['userId'])
  301. users.append(caller)
  302. # Ok, we have a list of userIds to look up.
  303. # just look all 10 of them up. :P
  304. 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;'):
  305. (userid, username, location) = row
  306. if userid in lookup:
  307. # Ok, we have something!
  308. for u in users:
  309. if u['userId'] == userid:
  310. u['username'] = username
  311. u['location'] = location
  312. return users;
  313. @app.route("/lastcallers")
  314. def display_lastcallers():
  315. users=last_bbs_callers()
  316. return render_template(
  317. "lastcallers.html",
  318. users=users,
  319. title="Last Callers"
  320. )