index.py3 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. #!/usr/bin/env python3
  2. import os, cgi, cgitb
  3. import time, re, crypt
  4. import html, mistune
  5. markdown = mistune.markdown
  6. cgitb.enable()
  7. # To generate a mod password, use tripcode.py3 to generate a tripkey.
  8. # What comes out is the result of a code that generates something like
  9. # a public key. Type #password in the name field to have your post
  10. # render as the mod_un using a special color. Save your !tripcode in the
  11. # mod_pw field to do so, without an exclamation mark in front. Your tripkey
  12. # should be 10 letters long, the result of an 8 character secure password
  13. # that you know others can't guess easily.
  14. #
  15. # b_url and theme currently are unused.
  16. board_config = \
  17. [["b_name", "4x13 BBS"], \
  18. ["b_url", "/bbs/"], \
  19. ["mod_un", "Admin"], \
  20. ["mod_pw", "tyF/EWEkIY"], \
  21. ["theme", "alpha"], \
  22. ["t_dir", "./threads/"], \
  23. ["t_list", "./threads/list.txt"], \
  24. ["t_limit", 100]]
  25. functions = ["main", "thread", "admin", "list", "create", "reply"]
  26. t_modes = {"0":"", \
  27. "1":"<img src='./img/lock.png' alt='Lock'>", \
  28. "2":"<img src='./img/sticky.png' alt='Sticky'>", \
  29. "3":"<img src='./img/sticky.png'><img src='./img/lock.png'>", \
  30. "4":"<img src='./img/ghost.png' alt='Nobump'>"}
  31. form = cgi.FieldStorage()
  32. def main():
  33. select_func = form.getvalue('m')
  34. bbs_header()
  35. if select_func:
  36. if select_func in functions:
  37. # print("<h1>", select_func, "</h1>")
  38. print("<a href='.'>&lt;&lt; back</a><br>")
  39. print("----"*10, "<p>")
  40. if select_func == "admin":
  41. bbs_admin()
  42. elif select_func == "main":
  43. bbs_main()
  44. elif select_func == "thread":
  45. bbs_thread()
  46. elif select_func == "create":
  47. bbs_create()
  48. elif select_func == "list":
  49. bbs_list()
  50. elif select_func == "reply":
  51. do_reply()
  52. else:
  53. select_func = None
  54. if not select_func:
  55. bbs_main()
  56. bbs_foot()
  57. def bbs_header():
  58. print("Content-type: text/html\n")
  59. print("<title>{0}</title>".format(board_config[0][1]))
  60. print("<link rel='stylesheet' href='0ch.css' title='0ch'>")
  61. print("<link rel='alternate stylesheet' href='4x13.css' title='4x13'>")
  62. print("<meta name=viewport content='width=850px;initial-scale:device-width'>")
  63. print("<script language='javascript' src='style.js'></script>")
  64. print("""<script language="javascript">
  65. function addText(elId,text) {
  66. text = ">>" + text + " \\r";
  67. document.getElementById(elId).value += text;
  68. }
  69. </script>""")
  70. def bbs_admin():
  71. a_pass = ''
  72. if form.getvalue('p'):
  73. a_pass = cgi.escape(form.getvalue('p'))
  74. if a_pass != board_config[3][1]:
  75. print("<form method='post' action='.'>")
  76. print("<b>Please enter your password.</b>")
  77. print("<input type='hidden' name='m' value='admin'>")
  78. print("<input type='text' name='p'>")
  79. print("<input type='submit' value='Login'>")
  80. print("</form>")
  81. else:
  82. print("<h2>Config</h2>")
  83. for confg in board_config:
  84. print(confg[0]+":", confg[1], "<br>")
  85. print("<h2>Last 40 posts</h2>")
  86. with open(board_config[5][1]+"ips.txt") as ip_l:
  87. ip_l = ip_l.read().splitlines()
  88. ip_t = len(ip_l)
  89. for n, ip in enumerate(ip_l[-1:-41:-1]):
  90. print("#"+str(ip_t - n), "<br>", cgi.escape(ip), "<p>")
  91. def bbs_main():
  92. print("<div class='front'>")
  93. print("""[<a href="javascript:setActiveStyleSheet('4x13');">4x13</a>]
  94. [<a href="javascript:setActiveStyleSheet('0ch');">0ch</a>]""")
  95. print("<h2>{0}</h2>".format(board_config[0][1]))
  96. bbs_list(prev='1')
  97. print("<p><hr>")
  98. do_prev()
  99. print("<hr>")
  100. bbs_create()
  101. print("</div>")
  102. def bbs_thread(t_id='', prev=0):
  103. if not t_id and form.getvalue('t'):
  104. t_id = cgi.escape(form.getvalue('t'))
  105. if t_id:
  106. if t_id.isdigit():
  107. t_fn = board_config[5][1] + t_id + ".txt"
  108. else:
  109. bbs_list()
  110. return
  111. else:
  112. bbs_list()
  113. return
  114. if os.path.isfile(t_fn):
  115. with open(t_fn, "r") as the_thread:
  116. the_thread = the_thread.readlines()
  117. r_cnt = str(len(the_thread) - 1)
  118. t_m = ''
  119. if prev == 0:
  120. if "[<" in the_thread[0]:
  121. t_m = the_thread[0].split("[<")[1]
  122. the_thread[0] = the_thread[0].split("[<")[0]
  123. print("<h3>", the_thread[0] + "[" + r_cnt + "]", "</h3>")
  124. print("<div class='thread'>")
  125. p_n = 0
  126. replies=[]
  127. for reply in the_thread[1:]:
  128. p_n += 1
  129. reply = reply.split(' >< ')
  130. if len(reply) > 4:
  131. reply[3] = " >< ".join(reply[3:])
  132. if reply[2]:
  133. reply.pop(2)
  134. reply[0] = "<span class='sage'>" \
  135. + reply[0] + "</span>"
  136. else:
  137. reply.pop(2) #30c
  138. reply[0] = "<span class='bump'>" \
  139. + reply[0] + "</span>"
  140. if prev == 0:
  141. if re.compile(r'&gt;&gt;[\d]').search(reply[2]):
  142. reply[2] = re.sub(r'&gt;&gt;([\d]+)', \
  143. r'<a href="#\1">&gt;&gt;\1</a>', reply[2])
  144. reply[2] = do_format(reply[2])
  145. print("</p><a name='{0}' href='#reply'".format(p_n))
  146. print("onclick='addText(\"{1}\", \"{0}\")'".format(p_n, t_id))
  147. print("'>#{0}</a> //".format(p_n))
  148. print("Name: {0} :\n Date: {1} \n<p>{2}".format(*reply))
  149. else:
  150. if re.compile(r'&gt;&gt;[\d]').search(reply[2]):
  151. reply[2] = re.sub(r'&gt;&gt;([\d]+)', \
  152. r'<a href="?m=thread;t={0}#\1">&gt;&gt;\1</a>'.format(t_id), reply[2])
  153. reply[2] = do_format(reply[2])
  154. if len(reply[2].split('<br>')) > 8:
  155. reply[2] = '<br>'.join(reply[2].split('<br>')[:9])[:850]
  156. if "<pre" and not "</pre>" in reply[2]:
  157. reply[2] += "</pre>"
  158. if "<code" and not "</code>" in reply[2]:
  159. reply[2] += "</code>"
  160. reply[2] += "</p><div class='rmr'>Post shortened. " \
  161. + "<a href='?m=thread;t={0}'>[".format(t_id) \
  162. + "View full thread]</a></div>"
  163. elif len(reply[2]) > 850:
  164. reply[2] = reply[2][:850]
  165. if "<pre" and not "</pre>" in reply[2]:
  166. reply[2] += "</pre>"
  167. if "<code" and not "</code>" in reply[2]:
  168. reply[2] += "</code>"
  169. reply[2] += "</p><div class='rmr'>Post shortened. " +\
  170. "<a href='?m=thread;t={0}'>".format(t_id) \
  171. + "[View full thread]</a></div>"
  172. elif int(r_cnt) > 4 and p_n == int(r_cnt):
  173. reply[2] = reply[2] + "</p><div class='rmr'>" \
  174. +"<a href='?m=thread;t={0}'".format(t_id) \
  175. +">[Read all posts]</a></div>"
  176. replies.append(reply)
  177. if prev == 0:
  178. print("</div>")
  179. if t_m.strip() in ["1", "3"]:
  180. print(t_modes[t_m.strip()])
  181. print("This thread is locked. No more comments can be added.")
  182. elif int(r_cnt) < board_config[7][1]:
  183. bbs_reply(t_fn, t_id)
  184. else:
  185. print("<hr>No more replies; thread limit reached!")
  186. return replies
  187. else:
  188. bbs_list()
  189. return
  190. def bbs_create():
  191. thread_attrs = {'title':'', 'name':'', 'content':''}
  192. for key in thread_attrs.keys():
  193. if form.getvalue(key):
  194. thread_attrs[key] = form.getvalue(key)
  195. if thread_attrs['title'] and thread_attrs['content']:
  196. thread_attrs['title'] = cgi.escape(thread_attrs['title'])[:30].strip()
  197. if thread_attrs['name']:
  198. thread_attrs['name'] = \
  199. cgi.escape(thread_attrs['name'])[:18].strip()
  200. if '#' in thread_attrs['name']:
  201. namentrip = thread_attrs['name'].split('#')[:2]
  202. namentrip[1] = tripcode(namentrip[1])
  203. thread_attrs['name'] = '</span> <span class="trip">'.join(namentrip)
  204. thread_attrs['content'] = cgi.escape(thread_attrs['content']).strip().replace('\r\n', "<br>")[:2000]
  205. thread_attrs['dt'] = str(time.time())[:10]
  206. if not thread_attrs['name']:
  207. thread_attrs['name'] = 'Anonymous'
  208. local_dt = time.localtime(int(thread_attrs['dt']))
  209. date_str = "%Y-%m-%d [%a] %H:%M"
  210. thread_attrs['ldt'] = time.strftime(date_str, local_dt)
  211. t_fn = board_config[5][1] + thread_attrs['dt'] + ".txt"
  212. with open(t_fn, "x") as new_thread:
  213. new_thread.write(thread_attrs['title'] + "\n" \
  214. + thread_attrs['name'] + " >< " \
  215. + thread_attrs['ldt'] + " >< >< " \
  216. + thread_attrs['content'] + "\n" )
  217. print("Thread <i>{0}</i> posted successfully!".format(thread_attrs['title']))
  218. with open(board_config[5][1] + "ips.txt", "a") as log:
  219. ip = os.environ["REMOTE_ADDR"]
  220. # IP | location | filename | ldt | comment
  221. log_data = " | ".join([ip, t_fn, thread_attrs['ldt'], \
  222. thread_attrs['title'], thread_attrs['name'], thread_attrs['content']])
  223. log.write(log_data)
  224. print("Redirecting you in 5 seconds...")
  225. print("<meta http-equiv='refresh' content='5;.'")
  226. with open(board_config[6][1], "r") as t_list:
  227. t_list = t_list.read().splitlines()
  228. new_t = " >< ".join([thread_attrs['dt'], \
  229. thread_attrs['ldt'], thread_attrs['title'], \
  230. "1", "0"])
  231. for n, t in enumerate(t_list):
  232. t = t.split(" >< ")
  233. if len(t) == 5 and t[4] not in ["2", "3"] or len(t) == 4:
  234. t_list.insert(n, new_t)
  235. break
  236. with open(board_config[6][1], "w") as upd_list:
  237. upd_list.write('\n'.join(t_list))
  238. else:
  239. if not thread_attrs['title']:
  240. if thread_attrs['content']:
  241. print("You need to enter a title to post a new thread.<br>")
  242. elif not thread_attrs['content']:
  243. print("You need to write a message to post a new thread.<br>")
  244. with open("create.html") as c_thread:
  245. print(c_thread.read())
  246. def bbs_list(prev='0'):
  247. if prev == '0':
  248. s_ts = None
  249. else:
  250. s_ts = 7
  251. with open(board_config[6][1]) as t_list:
  252. t_list = t_list.read().splitlines()
  253. t_cnt = len(t_list)
  254. cnt = 1
  255. print("<table>")
  256. print("<th>{0} <th>Title <th>Posts <th>Last post".format(t_cnt))
  257. for t in t_list[:s_ts]:
  258. t = t.split(" >< ")
  259. print("<tr><td><a href='?m=thread;t={0}'>{1}.".format(t[0], cnt))
  260. if t[4] in t_modes.keys():
  261. t[2] = t_modes[t[4]] + t[2]
  262. if prev == '1':
  263. print("</a><td><a href='#" \
  264. + "{0}'>{2}</a>&nbsp; <td>{3} <td>{1} &nbsp;".format(*t))
  265. else:
  266. print("</a><td><a href='?m=thread;t=" \
  267. + "{0}'>{2}</a>&nbsp; <td>{3} <td>{1} &nbsp;".format(*t))
  268. cnt += 1
  269. if prev != "0":
  270. print("<tr><td>")
  271. if (t_cnt - s_ts) > 0:
  272. print("<td colspan='2'><a href='?m=list'>")
  273. print("View all threads</a> ({0} hidden)".format(t_cnt - s_ts))
  274. print("<td>")
  275. else:
  276. print("<td colspan='3'>")
  277. print("<a href='#create'>Create new thread</a>")
  278. print("</table>")
  279. def bbs_reply(t_fn='', t_id=''):
  280. with open("reply.html") as r_thread:
  281. print(r_thread.read().format(t_fn, t_id))
  282. def bbs_foot():
  283. with open("foot.html") as b_foot:
  284. print(b_foot.read())
  285. def do_reply():
  286. reply_attrs = {'name':'', 'bump':'', 'comment':'', 't':''}
  287. for key in reply_attrs.keys():
  288. if form.getvalue(key):
  289. reply_attrs[key] = form.getvalue(key)
  290. if reply_attrs['t'] and reply_attrs['comment']:
  291. reply_attrs['comment'] = cgi.escape(reply_attrs['comment']).strip().replace('\r\n', "<br>").replace("<br><br><br><br>", "<br>")[:5000]
  292. # reply_attrs['comment']
  293. if reply_attrs['name']:
  294. reply_attrs['name'] = \
  295. cgi.escape(reply_attrs['name'][:18]).strip()
  296. if '#' in reply_attrs['name']:
  297. namentrip = reply_attrs['name'].split('#')[:2]
  298. namentrip[1] = tripcode(namentrip[1])
  299. if board_config[3][1] in namentrip[1]:
  300. namentrip[1] = board_config[2][1]
  301. reply_attrs['name'] = '</span> <span class="admin">'.join(namentrip)
  302. else:
  303. reply_attrs['name'] = '</span> <span class="trip">'.join(namentrip)
  304. else:
  305. reply_attrs['name'] = "Anonymous"
  306. if not reply_attrs['bump']:
  307. reply_attrs['bump'] = "1"
  308. if reply_attrs['bump'] != "1":
  309. reply_attrs['bump'] = ''
  310. local_dt = time.localtime()
  311. date_str = "%Y-%m-%d [%a] %H:%M"
  312. reply_attrs['ldt'] = time.strftime(date_str, local_dt)
  313. reply_string = reply_attrs['name'] + " >< " \
  314. + reply_attrs['ldt'] + " >< " \
  315. + reply_attrs['bump'] + " >< " \
  316. + reply_attrs['comment'] + "\n"
  317. fale = 0
  318. with open(reply_attrs['t'], "r") as the_thread:
  319. ter = the_thread.read().splitlines()
  320. if "[<" in ter[0]:
  321. if ter[0].split("[< ")[1] in ["1", "3"]:
  322. fale = 3
  323. if (len(ter) - 1) >= board_config[7][1]:
  324. fale = 1
  325. else:
  326. ter = ter[-1].split(' >< ')
  327. if ter[-1] == reply_string.split(' >< ')[-1][:-1]:
  328. fale = 2
  329. with open(reply_attrs['t'], "a+") as the_thread:
  330. if fale == 0:
  331. the_thread.write(reply_string)
  332. elif fale == 1:
  333. print("Sorry, thread limit reached!")
  334. elif fale == 2:
  335. print("Sorry, you already posted that.")
  336. elif fale == 3:
  337. print("Sorry, the thread is locked.")
  338. with open(board_config[5][1] + "ips.txt", "a") as log:
  339. if fale == 0:
  340. ip = os.environ["REMOTE_ADDR"]
  341. log_data = " | ".join([ip, reply_attrs['t'], reply_string])
  342. log.write(log_data)
  343. print("comment successfully posted<p>")
  344. print("Redirecting you in 5 seconds...")
  345. print("<meta http-equiv='refresh' content='5;.'")
  346. with open(board_config[6][1]) as t_list:
  347. reply_attrs['t'] = ''.join([i for i in reply_attrs['t'] if i.isdigit()])
  348. t_line = [reply_attrs['t'], reply_attrs['ldt'], reply_attrs['bump']]
  349. t_list = t_list.read().splitlines()
  350. nt_list = []
  351. new_t = []
  352. sage = 0
  353. for n, t in enumerate(t_list):
  354. t = t.split(' >< ')
  355. nt_list.append(t)
  356. if t[0] == t_line[0]:
  357. if len(t) is 4 and t[4] in ["1", "3"]:
  358. print("you should not be posting in a locked thread")
  359. fale = 1
  360. break
  361. elif t_line[2] == "1" or t[4] in ("2", "4"):
  362. sage = 1
  363. t_line.pop(2)
  364. t_line.insert(2, t[2])
  365. t_line.insert(3, str(int(t[3])+1))
  366. t_line.insert(4, t[4])
  367. new_t = [' >< '.join(t), ' >< '.join(t_line)]
  368. print("<hr>")
  369. posted = 0
  370. for n, t in enumerate(nt_list):
  371. if fale == 1:
  372. print("failure!")
  373. break
  374. elif sage == 0:
  375. if posted == 0:
  376. if t[4] not in ["2", "3"]:
  377. nt_list.insert(n, new_t[1].split(" >< "))
  378. posted = 1
  379. else:
  380. if t == new_t[0].split(" >< "):
  381. nt_list.remove(t)
  382. break
  383. else:
  384. if t[0] == new_t[0].split(" >< ")[0]:
  385. nt_list[n] = new_t[1].split(" >< ")
  386. break
  387. for n, l in enumerate(nt_list):
  388. nt_list[n] = " >< ".join(l)
  389. if fale == 0:
  390. with open(board_config[6][1], "w") as new_tl:
  391. new_tl.write('\n'.join(nt_list))
  392. else:
  393. if not reply_attrs['comment']:
  394. print("You need to write something to post a comment.")
  395. def do_prev(bbt=[]):
  396. if not bbt:
  397. with open(board_config[6][1]) as t_list:
  398. t_list = t_list.read().splitlines()[:7]
  399. for n, t in enumerate(t_list):
  400. t = t.split(" >< ")
  401. bbs = bbs_thread(t[0], 1)
  402. print("<div class='thread'><a name={0}>".format(t[0]))
  403. print("<h3><a>" + str(n+1)+".</a>")
  404. do_prev([bbs, t[0]])
  405. if bbt:
  406. pstcnt = 0
  407. bbn = len(bbt[0])
  408. if bbn > 4:
  409. bbn = len(bbt[0]) - 2
  410. else:
  411. bbn = 1
  412. with open(board_config[5][1] + str(bbt[1]) + ".txt") as t:
  413. t_t = t.readline()[:-1]
  414. t_r = len(t.read().splitlines())
  415. t_m = ''
  416. if "[<" in t_t:
  417. print(t_modes[t_t.split("[< ")[1]])
  418. t_m = t_modes[t_t.split("[< ")[1]]
  419. if t_m in t_modes.keys():
  420. t_t = t_t.split(" [< ")[0] + t_m
  421. else:
  422. t_t = t_t.split(" [< ")[0]
  423. print("<a href='?m=thread;t={0}'>{1} [{2}]".format(bbt[1], t_t, len(bbt[0])))
  424. print("</a></h3>")
  425. for replies in bbt[0]:
  426. pstcnt += 1
  427. if pstcnt == 1 or pstcnt >= bbn:
  428. print("</p>#{0} //".format(pstcnt))
  429. print("Name: {0} \n: Date: {1} \n<p>{2}".format(*replies))
  430. if pstcnt == 1 and len(bbt[0]) > 4:
  431. print("<hr width='420px' align='left'>")
  432. elif pstcnt == len(bbt[0]):
  433. if "lock" in t_m:
  434. print("<br><div class='reply'>")
  435. print("<br><div class='closed'>", t_m)
  436. print("Thread locked. No more comments allowed")
  437. print("</div><br></div></div>")
  438. elif t_r < board_config[7][1]:
  439. print("<hr width='420px' align='left'>")
  440. bbs_reply(board_config[5][1] + bbt[1]+".txt")
  441. else:
  442. print("<hr><i>Thread limit reached.</i><hr><p>")
  443. print("</div>")
  444. def do_format(urp=''):
  445. urp = re.sub(r'\[yt\]http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?‌​[\w\?‌​=]*)?\[/yt\]', r'<iframe width="560" height="315" src="https://www.youtube.com/embed/\1" frameborder="0" allowfullscreen></iframe>', urp)
  446. urp = re.sub(r'\[aa\](.*?)\[/aa\]', r'<pre class="aa"><b>Ascii Art:</b><hr>\1</pre><p>', urp)
  447. urp = re.sub(r'\[spoiler\](.*?)\[/spoiler\]', r'<span class="spoiler">\1</span>', urp)
  448. urp = re.sub(r'\[code\](.*?)\[/code\]', r'<pre><b>Code:</b><hr><code>\1</code></pre><p>', urp)
  449. urp = urp.split('<br>')
  450. for num, line in enumerate(urp):
  451. if line[:4] == "&gt;":
  452. urp[num] = "<span class='quote'>"+line+"</span>"
  453. urp = '<br>'.join(urp)
  454. urp = urp.replace('&amp;', '&').encode('ascii', 'xmlcharrefreplace').decode()
  455. return urp
  456. def tripcode(pw):
  457. pw = pw[:8]
  458. salt = (pw + "H..")[1:3]
  459. trip = crypt.crypt(pw, salt)
  460. return (" !" + trip[-10:])
  461. main()