├── .gitattributes ├── .gitignore ├── LICENSE ├── antispam.py ├── api.py ├── api_commons.py ├── app.py ├── aql.py ├── avatar_generation.py ├── blur.py ├── cachy.py ├── chat.py ├── colors.py ├── commons.py ├── commons_static.py ├── database_dump_repeat.bat ├── database_dump_upload.bat ├── database_restore.bat ├── database_up_python_main_example.bat ├── deploy.sh ├── flask_response_gzip.py ├── html_stuff.py ├── i18n.py ├── i18n_api.py ├── imgproc.py ├── leet.py ├── leet_challenge.py ├── liangjiahe.txt ├── list_of_package_to_install.txt ├── load_balancer.py ├── logger_config.py ├── main.py ├── make_archive_backup_release.py ├── make_archive_delete_older.py ├── medals.py ├── monkeypatch.py ├── parse ├── parse.py └── standarize.py ├── pgp_stuff.py ├── pmf.py ├── polls.py ├── pymain.bat ├── pymain_debug.bat ├── questions.py ├── quotes.py ├── quotes.txt ├── quotes_process.py ├── readme.md ├── recently.py ├── render.py ├── runcode.py ├── sb1024 ├── junda.py └── sb1024.py ├── sb1024_encryption.py ├── search.py ├── session.py ├── start_load_balancer.bat ├── takeoff ├── takeoff.py └── takeoff_search.py ├── template_globals.py ├── templates ├── 404.html.jinja ├── base.html.jinja ├── chat.html.jinja ├── chat_message.html.jinja ├── choice_stats.html.jinja ├── comment_section.html.jinja ├── conversations.html.jinja ├── css │ ├── labnol_youtube.css │ ├── normalize.css │ └── styles.css ├── editor.html.jinja ├── entities.html.jinja ├── exam.html.jinja ├── favorites.html.jinja ├── highlight │ ├── LICENSE │ ├── highlight.pack.js │ └── styles │ │ └── atom-one-light.css ├── iframe.html.jinja ├── images │ ├── 2049bbslogo_clipped_small_pressed.png │ ├── avatar-max-img-hc.png │ ├── avatar-max-img.png │ ├── exc.png │ ├── favicon.png │ ├── favicon_new_pressed.png │ ├── jiangle.jpg │ ├── liren_logo_large_pressed.png │ ├── logo.png │ ├── mohu_favicon.ico │ ├── mohu_logo.svg │ ├── pc_favicon.ico │ ├── pincong_bkgnd.svg │ ├── pincong_bkgnd_mod.svg │ ├── pincong_logo.svg │ ├── play.png │ ├── pooh.jpg │ └── youtube_small.png ├── js │ ├── ace │ │ ├── ace.js │ │ ├── mode-javascript.js │ │ ├── mode-python.js │ │ ├── theme-monokai.js │ │ └── theme-xcode.js │ ├── md5.js │ ├── relaxed-json.js │ └── util.js ├── leet.html.jinja ├── leet_challenge.html.jinja ├── liangjiahe.html.jinja ├── links.html.jinja ├── login.html.jinja ├── macros.html.jinja ├── messages.html.jinja ├── notifications.html.jinja ├── oplog.html.jinja ├── poll_one.html.jinja ├── postlist.html.jinja ├── postlist_userposts.html.jinja ├── qs.html.jinja ├── qs_polls.html.jinja ├── quotes.html.jinja ├── register.html.jinja ├── robots.txt ├── sandbox.html.jinja ├── sb1024.html.jinja ├── search.html.jinja ├── search_guizhou.html.jinja ├── searchpm.html.jinja ├── threadlist.html.jinja ├── threadlist_right.html.jinja ├── trans.html.jinja ├── ts_expl.html.jinja ├── user404.html.jinja ├── userlist.html.jinja ├── usermedals.html.jinja └── userpage.html.jinja ├── trust_score.py ├── vendor.yml └── views.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | 2049bbs.github.io 3 | secrets* 4 | schedrun* 5 | sand.py 6 | sand.js 7 | secret.bin 8 | jinja_test/ 9 | dump* 10 | *?deploy.sh 11 | j*b/ 12 | ga.png 13 | *.tar* 14 | release_token* 15 | backup 16 | *.psd 17 | test_view* 18 | *.cover 19 | trace/ 20 | trace.txt 21 | prof/* 22 | hash.py 23 | default_ques* 24 | *creds* 25 | arangodb*.bat 26 | temp/ 27 | templates/images/*material* 28 | prof/ 29 | tbd.py 30 | recovery/ 31 | 32 | templates/highlight/*.* 33 | !templates/highlight/*.js 34 | !templates/highlight/LICENSE 35 | 36 | templates/highlight/styles/*.* 37 | !templates/highlight/styles/atom-one-light.css 38 | 39 | takeoff/root_path.py 40 | takeoff/testfile.py 41 | 42 | takeoff/yyets_parse.py 43 | takeoff/yyparse2.py 44 | 45 | Thumbs.* 46 | ads/ 47 | 48 | database_up_python_main.bat 49 | leet_challenges/ 50 | 51 | spamgoods.* 52 | spamdb.* 53 | 54 | blacklisted.txt 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tsinghua PhD (github.com/thphd) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api_commons.py: -------------------------------------------------------------------------------- 1 | def content_length_check(content, allow_short=False): 2 | maxlen = 40000 3 | if len(content)>maxlen: 4 | raise Exception('content too long {}/{}'.format(len(content), maxlen)) 5 | if (len(content)<2 and allow_short==False) or len(content)==0: 6 | raise Exception('content too short') 7 | 8 | def title_length_check(title): 9 | if len(title)>140: 10 | raise Exception('title too long') 11 | if len(title)<2: 12 | raise Exception('title too short') 13 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # from flask_cors import CORS 2 | 3 | import flask 4 | from flask import Flask, g, abort # session 5 | 6 | # FUCK YOU, FLASK 7 | # https://github.com/pallets/flask/issues/2989#issuecomment-695412677 8 | class FlaskPatched(Flask): 9 | def select_jinja_autoescape(self, filename): 10 | """Returns ``True`` if autoescaping should be active for the given 11 | template name. If no template name is given, returns `True`. 12 | 13 | .. versionadded:: 0.5 14 | """ 15 | if filename is None: 16 | return True 17 | return filename.endswith(('.html', '.htm', '.xml', '.xhtml', 'html.jinja')) 18 | 19 | from flask import render_template, request, send_from_directory, make_response 20 | # from flask_gzip import Gzip 21 | 22 | from werkzeug.middleware.proxy_fix import ProxyFix 23 | 24 | app = FlaskPatched(__name__, static_url_path='') 25 | 26 | app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 27 | app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) 28 | 29 | # CORS(app) 30 | from flask_response_gzip import gzipify 31 | gzipify(app) 32 | -------------------------------------------------------------------------------- /aql.py: -------------------------------------------------------------------------------- 1 | from colors import colored_print_generator as cpg, prettify as pfy 2 | from colors import * 3 | import requests as r 4 | import time 5 | 6 | import os 7 | 8 | def get_environ(k): 9 | k = k.upper() 10 | if k in os.environ: 11 | return os.environ[k] 12 | else: 13 | return None 14 | 15 | # rs = r.Session() 16 | basic_auth = r.auth.HTTPBasicAuth('root','') 17 | 18 | def extract(key, d, default): 19 | if key in d: 20 | a = d[key] 21 | del d[key] 22 | return a 23 | return default 24 | 25 | import time, random 26 | ts = time.sleep 27 | rr = random.random 28 | 29 | # interface with arangodb. 30 | class AQLController: 31 | def request(self, method, endp, raise_error=True, **kw): 32 | waittime = 0.3 33 | while 1: 34 | resp = self.session.request( 35 | method, 36 | self.dburl + endp, 37 | auth = basic_auth, 38 | json = kw, 39 | timeout = self.timeout, 40 | proxies = {}, 41 | ).json() 42 | 43 | if ('error' not in resp) or resp['error'] == False: 44 | # server returned success 45 | return resp 46 | else: 47 | em = resp['errorMessage'] 48 | if 'write-write' in em and 'conflict' in em: 49 | wrr = waittime*rr() 50 | print_err('WWC: write-write conflict detected, retry...') 51 | print_up(f'wait for {wrr:.2f}s') 52 | ts(wrr) 53 | waittime*=2 54 | continue 55 | 56 | else: 57 | if not raise_error: 58 | print_err(str(resp)) 59 | return False 60 | else: 61 | raise Exception(str(resp)) 62 | 63 | def __init__(self, dburl, dbname, collections=[], timeout=12): 64 | self.dburl = dburl or (get_environ('dbaddr') or 'http://127.0.0.1:8529') 65 | self.dbname = dbname 66 | self.collections = collections 67 | self.prepared = False 68 | 69 | self.session = r.Session() 70 | a = r.adapters.HTTPAdapter(pool_connections=30, pool_maxsize=15,) 71 | self.session.mount('http://', a) 72 | 73 | self.timeout = timeout 74 | 75 | def prepare(self): 76 | if not self.prepared: 77 | # create database if nonexistent 78 | self.request('post','/_api/database', name=self.dbname, raise_error=False) 79 | 80 | self.prepared = True 81 | # create collections if nonexistent 82 | for c in self.collections: 83 | self.create_collection(c) 84 | 85 | 86 | def create_collection(self, name): 87 | self.prepare() 88 | return self.request('POST', '/_db/'+self.dbname+'/_api/collection', 89 | name=name, waitForSync=True, raise_error=False) 90 | 91 | def clear_collection(self, name, filter=''): 92 | self.prepare() 93 | return self.aql('for i in {} {} remove i in {}'.format( 94 | name, filter, name)) 95 | 96 | def create_index(self, collection, **kw): 97 | self.prepare() 98 | return self.request('post', '/_db/'+self.dbname+'/_api/index?collection='+collection, raise_error=False, **kw) 99 | 100 | def aql(self, query, **kw): 101 | 102 | if isinstance(query, QueryString): 103 | kw.update(query.kw) 104 | return self.aql(query.s, **kw) 105 | 106 | silent = extract('silent', kw, False) 107 | raise_error = extract('raise_error', kw, True) 108 | 109 | self.prepare() 110 | 111 | if not silent: print_up('AQL >>',query,kw) 112 | 113 | t0 = time.time() 114 | 115 | resp = self.request( 116 | 'POST', '/_db/'+self.dbname+'/_api/cursor', 117 | query = query, 118 | batchSize = 1000, 119 | raise_error = raise_error, 120 | bindVars = kw, 121 | ) 122 | if raise_error==False and resp==False: 123 | return [] 124 | 125 | res = resp['result'] 126 | 127 | t = time.time()-t0 128 | if t>0.15: 129 | print_info('== AQL took {:d}ms =='.format(int(t*1000))) 130 | 131 | if not silent: print_down('AQL <<', str(res)) 132 | return res 133 | 134 | def change_view_properties(self, viewname, **kw): 135 | print_info(f'attempting to change view properties on {self.dbname}/{viewname}') 136 | resp = self.request('PATCH', 137 | '/_db/'+self.dbname+'/_api/view/'+viewname 138 | +'/properties#ArangoSearch', 139 | **kw, 140 | ) 141 | 142 | def from_filter(self, _from, _filter, **kw): 143 | self.prepare() 144 | return self.aql('for i in {} filter {} return i'.format(_from, _filter), **kw) 145 | 146 | def wait_for_online(self): 147 | i = 1 148 | while 1: 149 | try: 150 | one = self.aql('return 1')[0] 151 | except Exception as e: 152 | print_err(e) 153 | print_up('fail #' + str(i) + '\n') 154 | i+=1 155 | time.sleep(1) 156 | else: 157 | if one==1: 158 | return 159 | 160 | class QueryString: 161 | def __init__(self, s='', **kw): 162 | self.s = s 163 | self.kw = kw.copy() 164 | 165 | def append(self, s='', **kw): 166 | self.s += '\n' + s 167 | self.kw.update(kw) 168 | 169 | def prepend(self, s='', **kw): 170 | self.s = s+'\n'+self.s 171 | self.kw.update(kw) 172 | 173 | def __add__(self, b): 174 | k = self.kw.copy() 175 | k.update(b.kw) 176 | result = QueryString(self.s+'\n'+b.s, **k) 177 | return result 178 | 179 | def fix_view_loss(addr): 180 | aqlc = AQLController(addr, 'db2047', timeout=60) 181 | aqlc.change_view_properties( 182 | 'sv', 183 | links = { 184 | 'threads':{ 185 | 'analyzers':['text_zh'], 186 | 'fields':{ 187 | 'title':{}, 188 | 'content':{}, 189 | }, 190 | }, 191 | 'posts':{ 192 | 'analyzers':['text_zh'], 193 | 'fields':{ 194 | 'content':{}, 195 | }, 196 | }, 197 | 'users':{ 198 | 'analyzers':['text_zh'], 199 | 'fields':{ 200 | 'name':{}, 201 | 'brief':{}, 202 | }, 203 | }, 204 | } 205 | ) 206 | 207 | if __name__ == '__main__': 208 | 209 | # aqlc = AQLController('http://127.0.0.1:8529', 'test',[ 210 | # 'queue' 211 | # ]) 212 | # aql = aqlc.aql 213 | # 214 | # aql('insert {a:1} into queue') 215 | # a = aql('for u in queue return u') 216 | # 217 | # aql('for u in queue filter u.a==1 remove u in queue') 218 | # a = aql('for u in queue return u') 219 | # 220 | # print(a) 221 | 222 | pass 223 | -------------------------------------------------------------------------------- /avatar_generation.py: -------------------------------------------------------------------------------- 1 | from functools import * 2 | from PIL import Image, ImageDraw, ImageChops 3 | import Identicon 4 | BACKGROUND_COLOR = (244,)*3 5 | Identicon._crop_coner_round = lambda a,b:a # don't cut corners, please 6 | def _set_pixels(flatten_grid): 7 | # len(list) should be a squared of integer value 8 | # Caculate pixels 9 | pixels = [] 10 | unit = 100/7 11 | for i, val in enumerate(flatten_grid): 12 | x = i%5 * unit + unit 13 | y = i//5 * unit + unit 14 | 15 | top_left = (round(x), round(y)) 16 | bottom_right = (round(x + unit), round(y + unit)) 17 | 18 | pixels.append([top_left, bottom_right]) 19 | 20 | return pixels 21 | Identicon._set_pixels = _set_pixels 22 | 23 | identicon_bkgnd = Image.open('./templates/images/avatar-max-img-hc.png').convert('RGB') 24 | white = Image.new('RGB', (100,100), BACKGROUND_COLOR) 25 | 26 | def _draw_identicon(color, grid_list, pixels): 27 | identicon_im = white.copy() 28 | # identicon_im = identicon_bkgnd.copy() 29 | draw = ImageDraw.Draw(identicon_im) 30 | for grid, pixel in zip(grid_list, pixels): 31 | if grid != 0: # for not zero 32 | draw.rectangle(pixel, fill=color) 33 | 34 | identicon_im = ImageChops.blend(identicon_im, white, 0.7) 35 | out = ImageChops.multiply(identicon_im, identicon_bkgnd) 36 | return out 37 | Identicon._draw_identicon = _draw_identicon 38 | 39 | @lru_cache(maxsize=4096) 40 | def render_identicon(string): 41 | identicon = Identicon.render(string) 42 | return identicon 43 | -------------------------------------------------------------------------------- /blur.py: -------------------------------------------------------------------------------- 1 | # let's randomize things a bit 2 | 3 | import subprocess as sp 4 | 5 | def get_ts_of(n): 6 | compro = sp.run( 7 | ("git show @{" 8 | +str(n) 9 | +"} --pretty='%cI' --no-patch --no-notes").split(' ') 10 | , 11 | capture_output=True, 12 | ) 13 | b = compro.stdout 14 | b = b.decode('ascii').strip().replace("'",'') 15 | return dfs(b) 16 | 17 | from commons import * 18 | 19 | t0 = get_ts_of(0) 20 | t1 = get_ts_of(1) 21 | 22 | print('last commit is at', t0) 23 | print('last commit before is at', t1) 24 | 25 | td = t0-t1 26 | 27 | assert td > dttd(seconds=0) 28 | totals = td.total_seconds() 29 | 30 | print('delta is', totals) 31 | 32 | import random 33 | 34 | while 1: 35 | sec = random.randint(-totals, -(totals//2)) 36 | 37 | print('add', sec, 'seconds') 38 | nt = t0 + dttd(seconds=sec) 39 | 40 | print('proposed time is', nt) 41 | 42 | if nt>t1: 43 | print('pass') 44 | break 45 | else: 46 | print('proposed time smaller than commit before, try again...') 47 | continue 48 | 49 | # nt is proposed time 50 | import os 51 | os.environ['GIT_COMMITTER_DATE'] = str(nt) 52 | 53 | command = f'''git commit --amend --no-edit --date "{str(nt).replace(' ','T')}"''' 54 | 55 | print(command) 56 | 57 | sp.run(command.split(' ')) 58 | -------------------------------------------------------------------------------- /cachy.py: -------------------------------------------------------------------------------- 1 | import cachetools, cachetools.func, time, threading, traceback 2 | from flaskthreads import AppContextThread 3 | from flaskthreads.thread_helpers import has_app_context, _app_ctx_stack, APP_CONTEXT_ERROR 4 | from flask import g 5 | 6 | import concurrent.futures 7 | from concurrent.futures.thread import _threads_queues 8 | 9 | import functools 10 | 11 | def get_context(): return _app_ctx_stack.top if has_app_context() else None 12 | 13 | class TPEMod(concurrent.futures.ThreadPoolExecutor): 14 | def submit(self, fn, *a, **kw): 15 | context = get_context() 16 | def fnwrapper(*aa, **akw): 17 | if context: 18 | with context: 19 | return fn(*aa, **akw) 20 | else: 21 | return fn(*aa, **akw) 22 | 23 | res = super().submit(fnwrapper, *a, **kw) 24 | _threads_queues.clear() # hack to stop joining from preventing ctrl-c 25 | return res 26 | 27 | tpe = TPEMod(max_workers=256) 28 | 29 | class AppContextThreadMod(threading.Thread): 30 | """Implements Thread with flask AppContext.""" 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | self.app_ctx = get_context() 35 | 36 | def run(self): 37 | if self.app_ctx: 38 | with self.app_ctx: 39 | super().run() 40 | else: 41 | super().run() 42 | 43 | Thread = AppContextThreadMod 44 | 45 | # hashkey = cachetools.keys.hashkey 46 | tm = time.monotonic 47 | 48 | empty = 0 49 | idle = 1 50 | dispatching = 2 51 | 52 | import time, random 53 | ts = time.sleep 54 | rr = random.random 55 | 56 | def tsr():ts(rr()*.1) 57 | 58 | # buffer that refreshes in the bkgnd 59 | class StaleBuffer: 60 | 61 | # f returns what we want to serve 62 | def __init__(self, f, ttr=5, ttl=10): # time to refresh / time to live 63 | self.a = None 64 | 65 | self.ts = tm() 66 | self.l = threading.Lock() 67 | self.state = empty 68 | 69 | self.f = f 70 | 71 | self.ttr = ttr 72 | self.ttl = ttl 73 | assert ttl>ttr 74 | 75 | def refresh_threaded(self): 76 | # tsr() 77 | try: 78 | r = self.f() 79 | except Exception as e: 80 | traceback.print_exc() 81 | with self.l: 82 | self.state = idle 83 | else: 84 | with self.l: 85 | self.state = idle 86 | self.a = r 87 | self.ts = tm() 88 | 89 | def dispatch_refresh(self): 90 | tpe.submit(self.refresh_threaded) 91 | 92 | # t = Thread(target=self.refresh_threaded, daemon=True) 93 | # t.start() 94 | 95 | def get(self): 96 | # ttl = self.ttl 97 | # ttr = self.ttr 98 | # f = self.f 99 | 100 | # last = self.ts 101 | # now = tm() 102 | # past = now - last 103 | past = tm() - self.ts 104 | state = self.state 105 | 106 | 107 | # we couldn't afford expensive locking everytime, so 108 | if state==idle and past < self.ttr: 109 | return self.a 110 | elif state==dispatching: 111 | return self.a 112 | 113 | else: 114 | with self.l: 115 | # cache is empty 116 | if state == empty: 117 | self.a = self.f() 118 | self.ts = tm() 119 | self.state = idle 120 | 121 | # cache is not empty, no dispatch on the way 122 | elif state == idle: 123 | # is cache fresh? 124 | if past > self.ttl: 125 | # too old. 126 | self.a = self.f() 127 | self.ts = tm() 128 | 129 | elif past > self.ttr: 130 | # kinda old 131 | self.state = dispatching 132 | self.dispatch_refresh() 133 | 134 | # # cache is fresh 135 | # else: 136 | # pass 137 | 138 | # elif self.state == 'dispatching': 139 | # pass 140 | # else: 141 | # pass 142 | 143 | return self.a 144 | 145 | 146 | tmg = tm() 147 | def update_tmg(): 148 | global tmg 149 | while 1: 150 | tmg = tm() 151 | time.sleep(0.2) 152 | tpe.submit(update_tmg) 153 | 154 | def StaleBufferFunctional(f, ttr=10, ttl=1800): 155 | global tmg 156 | 157 | a = None 158 | tspttr = 0 159 | tspttl = 0 160 | 161 | l = threading.Lock() 162 | state = empty 163 | 164 | def update_t(): 165 | nonlocal tspttl,tspttr 166 | tspttr = tmg+ttr 167 | tspttl = tmg+ttl 168 | 169 | def refresh_threaded(): 170 | nonlocal a,state 171 | # tsr() 172 | try: 173 | res = f() 174 | except Exception as e: 175 | traceback.print_exc() 176 | with l: 177 | state = idle 178 | else: 179 | with l: 180 | state = idle 181 | a = res 182 | update_t() 183 | 184 | def dispatch_refresh(): 185 | tpe.submit(refresh_threaded) 186 | 187 | def get(): 188 | nonlocal a,state,tspttl,tspttr 189 | 190 | # past = tm() - ts 191 | 192 | # we couldn't afford expensive locking everytime, so 193 | if state==idle and tmg < tspttr: 194 | # return a 195 | pass 196 | 197 | elif state==dispatching: 198 | # return a 199 | pass 200 | else: 201 | with l: 202 | # cache is empty 203 | if state == empty: 204 | a = f() 205 | update_t() 206 | state = idle 207 | 208 | # cache is not empty, no dispatch on the way 209 | elif state == idle: 210 | 211 | # is cache fresh? 212 | if tmg > tspttl: 213 | # too old. 214 | a = f() 215 | update_t() 216 | 217 | elif tmg > tspttr: 218 | # kinda old 219 | state = dispatching 220 | dispatch_refresh() 221 | 222 | # # cache is fresh 223 | # else: 224 | # pass 225 | 226 | # elif self.state == 'dispatching': 227 | # pass 228 | # else: 229 | # pass 230 | 231 | return a 232 | return get 233 | 234 | 235 | if 1 and __name__ == '__main__': 236 | from commons_static import timethis 237 | 238 | def by33():return random.random()+random.random()*111 239 | sb = StaleBuffer(by33, 15, 1000) 240 | 241 | sbf = StaleBufferFunctional(by33) 242 | 243 | timethis('$by33()') 244 | timethis('$sb.get()') 245 | timethis('$sbf()') 246 | 247 | 248 | if 0 and __name__ == '__main__': 249 | def kg(): 250 | j = 1 251 | def k(): 252 | nonlocal j 253 | j+=1 254 | time.sleep(1) 255 | return j 256 | return k 257 | 258 | sb = StaleBuffer(kg(), ttr=1, ttl=6) 259 | sbf = StaleBufferFunctional(kg(), ttr=1, ttl=6) 260 | for i in range(10): 261 | print('old',sb.get(), sb.state) 262 | print('new',sbf()) 263 | time.sleep(0.3) 264 | 265 | print('stalebuf test end') 266 | 267 | def stale_cache_old(ttr=3, ttl=6, maxsize=128): 268 | def stale_cache_wrapper(f): 269 | 270 | @cachetools.func.lru_cache(maxsize=maxsize) 271 | def get_stale_buffer(*a, **kw): 272 | def sbw(): 273 | return f(*a, **kw) 274 | 275 | sb = StaleBuffer(sbw, ttr=ttr, ttl=ttl) 276 | return sb 277 | 278 | def stale_cache_inner(*a, **kw): 279 | sb = get_stale_buffer(*a, **kw) 280 | return sb.get() 281 | 282 | return stale_cache_inner 283 | return stale_cache_wrapper 284 | 285 | def stale_cache(ttr=3, ttl=6, maxsize=128): 286 | def stale_cache_wrapped(f): 287 | 288 | @functools.lru_cache(maxsize=maxsize) 289 | def get_stale_buffer(*a, **kw): 290 | return StaleBufferFunctional( 291 | lambda:f(*a, **kw), 292 | ttr=ttr, 293 | ttl=ttl, 294 | ) 295 | 296 | def stale_cache_inner(*a, **kw): 297 | return get_stale_buffer(*a, **kw)() 298 | 299 | return stale_cache_inner 300 | 301 | return stale_cache_wrapped 302 | 303 | if 1 and __name__ == '__main__': 304 | from commons_static import timethis 305 | 306 | print('00000'*5) 307 | 308 | @stale_cache_old() 309 | def by33():return random.random()+random.random()*111 310 | 311 | @stale_cache() 312 | def by34():return random.random()+random.random()*111 313 | 314 | timethis('$by33()') 315 | timethis('$by34()') 316 | 317 | if 0 and __name__ == '__main__': 318 | 319 | def return3(): 320 | return 31234019374194 321 | 322 | future = tpe.submit(return3) 323 | print(future.result()) 324 | 325 | 326 | j = 1 327 | k = 1 328 | 329 | @stale_cache(ttr=1.5) 330 | def a(i): 331 | global j 332 | j+=1 333 | time.sleep(.5) 334 | return i*j 335 | @stale_cache2(ttr=1.5) 336 | def a2(i): 337 | global j 338 | j+=1 339 | time.sleep(.5) 340 | return i*j 341 | 342 | @stale_cache(ttr=3) 343 | def b(n): 344 | global k 345 | k+=1 346 | time.sleep(.7) 347 | return k*n 348 | @stale_cache2(ttr=3) 349 | def b2(n): 350 | global k 351 | k+=1 352 | time.sleep(.7) 353 | return k*n 354 | 355 | for i in range(20): 356 | print('old',a(3.5), b(6)) 357 | print('new',a2(3.5), b2(6)) 358 | time.sleep(0.4) 359 | -------------------------------------------------------------------------------- /chat.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from api import * 3 | 4 | aqlc.create_collection('chat_messages') 5 | aqlc.create_collection('chat_channels') 6 | aqlc.create_collection('chat_memberships') 7 | 8 | 9 | ''' 10 | chat messages 11 | 12 | - uid 13 | - cid 14 | - content 15 | - t_c 16 | 17 | chat_channels 18 | 19 | - uid (creator) 20 | - t_c 21 | - title 22 | 23 | chat_memberships 24 | 25 | - uid 26 | - cid 27 | - t_c 28 | 29 | 30 | ''' 31 | class Chat: 32 | def get_new_channel_id(self): 33 | return obtain_new_id('chat_channel') 34 | 35 | def get_channel(self, cid): 36 | return aql('for i in chat_channels filter i.cid==@cid return i', 37 | cid=cid, silent=True)[0] 38 | 39 | def new_membership(self, cid, uid): 40 | exist = aql('''for i in chat_memberships 41 | filter i.cid==@cid and i.uid==@uid 42 | return i''', cid=cid, uid=uid, silent=True) 43 | 44 | if exist: 45 | raise Exception('you are already in that channel') 46 | 47 | return aql('insert @k into chat_memberships', k=dict( 48 | uid=uid, 49 | cid=cid, 50 | t_c = time_iso_now(), 51 | silent=True, 52 | )) 53 | 54 | def create_channel_uid(self, uid, title): 55 | existing = aql('for i in chat_channels filter i.uid==@uid and i.title==@title return i', uid=uid, title=title) 56 | 57 | if existing: 58 | raise Exception('channel with the same title and owner already exists') 59 | 60 | cid = self.get_new_channel_id() 61 | 62 | newc = aql('insert @k into chat_channels return NEW', 63 | k=dict( 64 | uid=uid, 65 | title=title, 66 | cid=cid, 67 | t_c = time_iso_now(), 68 | ))[0] 69 | return newc 70 | 71 | def post_message(self, cid, uid, content): 72 | banned_check() 73 | 74 | content = content.strip() 75 | content_length_check(content) 76 | # check if channel exists 77 | channel = self.get_channel(cid) 78 | if not channel: raise Exception('channel id not found') 79 | 80 | # check if user in channel 81 | if uid>0 and channel['cid']!=1: 82 | if not aql(''' 83 | for i in chat_memberships 84 | filter i.uid==@uid and i.cid==@cid 85 | return i''', uid=uid, cid=cid): 86 | 87 | raise Exception('you are not member of that channel') 88 | 89 | lastm = self.get_last_message(uid) 90 | 91 | cdt = 15 92 | earlier = time_iso_now(-cdt) 93 | 94 | if lastm: 95 | if lastm['content']==content: 96 | raise Exception('repeatedly sending same message') 97 | 98 | if lastm['t_c']>earlier: 99 | raise Exception(f'两次发送间隔应大于{cdt}秒,请稍微等一下') 100 | 101 | new_msg = dict( 102 | cid=cid, uid=uid, content=content, t_c=time_iso_now() 103 | ) 104 | spam_detected = spam_kill(content) 105 | if spam_detected: 106 | new_msg['spam']=True 107 | 108 | aql('insert @k into chat_messages', k=new_msg) 109 | 110 | return {'error':False} 111 | 112 | 113 | def get_last_message(self, uid): 114 | lastm = aql(''' 115 | for i in chat_messages filter i.uid==@uid sort i.t_c desc 116 | limit 1 return i 117 | ''', uid=uid) 118 | return None if not lastm else lastm[0] 119 | 120 | ############ 121 | 122 | def create_channel(self, title): 123 | must_be_logged_in() 124 | uid = g.selfuid 125 | title_length_check(title) 126 | newc = self.create_channel_uid(uid, title) 127 | cid = newc['cid'] 128 | return {'error':False,'channel':newc, 'cid':cid} 129 | 130 | def list_channels(self): 131 | uid = g.selfuid 132 | res = aql('for i in chat_channels return i') 133 | return {'channels':res} 134 | 135 | def join_channel(self, cid): 136 | must_be_logged_in() 137 | channel = self.get_channel(cid) 138 | if not channel: 139 | raise Exception('channel cid not found') 140 | 141 | uid = g.selfuid 142 | cuid = channel['uid'] 143 | 144 | if uid!=cuid and channel['title']!="公海": 145 | # you are not owner of said channel 146 | followings = aql(''' 147 | for i in followings 148 | filter i.follow==true 149 | and i.to_uid==@uid and i.uid==@cuid 150 | return i 151 | ''', uid=uid, cuid=cuid) 152 | if not followings: 153 | raise Exception('cant join channels of someone who didnt follow you') 154 | 155 | self.new_membership(cid, uid) 156 | 157 | return {'error':False} 158 | 159 | def post(self, cid, content): 160 | must_be_logged_in() 161 | uid = g.selfuid 162 | return self.post_message(cid, uid, content) 163 | 164 | def get_messages_after(self, cid, ts): 165 | ma = aqls(''' 166 | for i in chat_messages 167 | filter i.cid==@cid and i.t_c > @ts 168 | sort i.t_c asc 169 | limit 25 170 | return i 171 | ''', ts=ts, cid=cid) 172 | 173 | for m in ma: m['content'] = self.render_message(m) 174 | return {'messages': ma} 175 | 176 | def get_messages_before(self, cid, ts): 177 | ma = aqls(''' 178 | for i in chat_messages 179 | filter i.cid==@cid and i.t_c < @ts 180 | sort i.t_c desc 181 | limit 25 182 | return i 183 | ''', ts=ts, cid=cid) 184 | 185 | for m in ma: m['content'] = self.render_message(m) 186 | return {'messages': ma} 187 | 188 | def render_message(self, m): 189 | rendered = render_template_g('chat_message.html.jinja', 190 | message = m, 191 | ) 192 | return rendered.strip() 193 | 194 | def test(self): 195 | return {'test':'success'} 196 | 197 | chat = Chat() 198 | 199 | IndexCreator.create_indices('chat_messages', [['cid','t_c'],['uid','t_c']]) 200 | IndexCreator.create_indices('chat_channels', [['cid','t_c'],['uid','t_c']]) 201 | IndexCreator.create_indices('chat_memberships', 202 | [['cid','uid','t_c'],['uid','t_c']]) 203 | 204 | @register('chat') 205 | def _(): 206 | j = g.j 207 | 208 | fname = j['f'] 209 | args = j['a'] if 'a' in j else [] 210 | kwargs = j['kw'] if 'kw' in j else {} 211 | 212 | f = getattr(chat, fname) 213 | res = f(*args, **kwargs) 214 | 215 | return res 216 | 217 | @app.route('/deer') 218 | @app.route('/liao') 219 | @app.route('/chat') 220 | def chatpage(): 221 | # m = chat.get_messages_before(1, '2047')['messages'] 222 | m = [] 223 | return render_template_g('chat.html.jinja', 224 | page_title = '聊天室', 225 | hide_title = True, 226 | messages = m, 227 | ) 228 | -------------------------------------------------------------------------------- /colors.py: -------------------------------------------------------------------------------- 1 | from termcolor import colored, cprint 2 | import colorama 3 | 4 | colorama.init() 5 | 6 | import logger_config 7 | import logging 8 | logger = logging.getLogger('2047') 9 | 10 | import threading 11 | 12 | # _pq = [] 13 | # _pqc = threading.Condition() 14 | 15 | def qprint(*a,**kw): 16 | to_print = ' '.join((str(i) for i in a)) 17 | logger.info(to_print) 18 | 19 | # with _pqc: 20 | # _pq.append((a, kw)) 21 | # _pqc.notify() 22 | 23 | def async_printer(): 24 | while 1: 25 | with _pqc: 26 | while len(_pq)==0: 27 | _pqc.wait() 28 | a,kw = _pq.pop(0) 29 | # print(*a, **kw) 30 | to_print = ' '.join((str(i) for i in a)) 31 | logger.info(to_print) 32 | 33 | # def dispatch(f): 34 | # t = threading.Thread(target=f, daemon=True) 35 | # t.start() 36 | 37 | # dispatch(async_printer) 38 | 39 | def restrict_gbk(text): 40 | # escape unsupported unicode in gbk 41 | # (to prevent emojis from crashing CMD 42 | return text.encode(encoding='gbk', errors='replace').decode(encoding='gbk') 43 | 44 | def colored_print_generator(*a,**kw): 45 | def colored_print(*items,**incase): 46 | text = ' '.join((str(i) for i in items)) 47 | text = restrict_gbk(text) 48 | 49 | qprint(colored(text, *a,**kw),**incase) 50 | return colored_print 51 | 52 | def colored_format_generator(*a,**kw): 53 | def colored_format(s): 54 | text = restrict_gbk(s) 55 | 56 | return colored(text, *a,**kw) 57 | return colored_format 58 | 59 | import pprint 60 | def prettify(json): 61 | return pprint.pformat(json, indent=4, width=80, depth=None, compact=True) 62 | 63 | cpg = colored_print_generator 64 | cfg = colored_format_generator 65 | 66 | print_info = cpg('green',) 67 | print_debug = cpg('yellow') 68 | print_up = cpg('yellow', attrs=['bold']) 69 | print_down = cpg('cyan', attrs=['bold']) 70 | print_err = cpg('red', attrs=['bold']) 71 | 72 | colored_info = cfg('green',) 73 | colored_debug = cfg('yellow') 74 | colored_up = cfg('yellow', attrs=['bold']) 75 | colored_down = cfg('cyan', attrs=['bold']) 76 | colored_err = cfg('red', attrs=['bold']) 77 | 78 | def log_info(*a): 79 | text = ' '.join(map(lambda i:str(i), a)) 80 | logger.warning(colored_info(text)) 81 | def log_up(*a): 82 | text = ' '.join(map(lambda i:str(i), a)) 83 | logger.warning(colored_up(text)) 84 | def log_down(*a): 85 | text = ' '.join(map(lambda i:str(i), a)) 86 | logger.warning(colored_down(text)) 87 | def log_err(*a): 88 | text = ' '.join(map(lambda i:str(i), a)) 89 | logger.warning(colored_err(text)) 90 | 91 | if __name__ == '__main__': 92 | cpg = colored_print_generator 93 | printredcyan = cpg('red', 'on_cyan') 94 | 95 | printredcyan('red', 'on_cyan') 96 | print(prettify({'asd':'gerf','a':{'v':'b'}})) 97 | -------------------------------------------------------------------------------- /commons_static.py: -------------------------------------------------------------------------------- 1 | import os, hashlib, binascii as ba 2 | import base64, re 3 | import time, math 4 | from colors import * 5 | # from functools import lru_cache 6 | from numba import jit 7 | 8 | from cachetools.func import * 9 | from cachy import * 10 | 11 | def iif(a,b,c):return b if a else c 12 | 13 | import json 14 | def obj2json(obj): 15 | return json.dumps(obj, ensure_ascii=False, sort_keys=True, indent=2) 16 | 17 | @stale_cache(ttr=1, ttl=30) 18 | def readfile(fn, mode='rb', *a, **kw): 19 | if 'b' not in mode: 20 | with open(fn, mode, encoding='utf8', *a, **kw) as f: 21 | return f.read() 22 | else: 23 | with open(fn, mode, *a, **kw) as f: 24 | return f.read() 25 | 26 | def writefile(fn, data, mode='wb', encoding='utf8', *a, **kw): 27 | if 'b' not in mode: 28 | with open(fn,mode, encoding=encoding, *a,**kw) as f: 29 | f.write(data) 30 | else: 31 | with open(fn,mode,*a,**kw) as f: 32 | f.write(data) 33 | 34 | def removefile(fn): 35 | try: 36 | os.remove(fn) 37 | except Exception as e: 38 | print_err(e) 39 | print_err('failed to remove', fn) 40 | else: 41 | return 42 | 43 | import threading 44 | 45 | def dispatch(f): 46 | return tpe.submit(f) 47 | # t = AppContextThreadMod(target=f, daemon=True) 48 | # # t = threading.Thread(target=f, daemon=True) 49 | # t.start() 50 | 51 | def dispatch_with_retries(f): 52 | n = 0 53 | def wrapper(): 54 | nonlocal n 55 | while 1: 56 | try: 57 | f() 58 | except Exception as e: 59 | print_err(e) 60 | n+=1 61 | time.sleep(0.5) 62 | print_up(f'{f.__name__}() retry #{n}') 63 | else: 64 | print_down(f'{f.__name__}() success on attempt #{n}') 65 | break 66 | return tpe.submit(wrapper) 67 | 68 | def init_directory(d): 69 | try: 70 | os.mkdir(d) 71 | except FileExistsError as e: 72 | print_err('directory {} already exists.'.format(d), e) 73 | else: 74 | print_info('directory {} created.'.format(d)) 75 | 76 | def key(d, k): 77 | if k in d: 78 | return d[k] 79 | else: 80 | return None 81 | 82 | def intify(s, name=''): 83 | try: 84 | return int(s) 85 | except: 86 | if s: 87 | # print_err('intifys',s,name) 88 | pass 89 | return 0 90 | 91 | def floatify(s): 92 | try: 93 | return float(s) 94 | except: 95 | if s: 96 | pass 97 | return 0. 98 | 99 | def get_environ(k): 100 | k = k.upper() 101 | if k in os.environ: 102 | return os.environ[k] 103 | else: 104 | return None 105 | 106 | def clip(a,b): 107 | def _clip(c): 108 | return min(b,max(a, c)) 109 | return _clip 110 | 111 | clip01 = clip(0,1) 112 | 113 | import zlib 114 | 115 | def calculate_checksum(bin): return zlib.adler32(bin).to_bytes(4,'big') 116 | 117 | def calculate_checksum_base64(bin): 118 | csum = calculate_checksum(bin) 119 | chksum_encoded = base64.b64encode(csum).decode('ascii') 120 | return chksum_encoded 121 | 122 | def calculate_checksum_base64_replaced(bin): 123 | return calculate_checksum_base64(bin).replace('+','-').replace('/','_') 124 | 125 | def calculate_etag(bin): 126 | return calculate_checksum_base64_replaced(bin) 127 | 128 | 129 | # pw hashing 130 | 131 | def bytes2hexstr(b): 132 | return ba.b2a_hex(b).decode('ascii') 133 | 134 | def hexstr2bytes(h): 135 | return ba.a2b_hex(h.encode('ascii')) 136 | 137 | # https://nitratine.net/blog/post/how-to-hash-passwords-in-python/ 138 | def get_salt(): 139 | return os.urandom(32) 140 | 141 | def get_random_hex_string(b=8): 142 | return base64.b16encode(os.urandom(b)).decode('ascii') 143 | 144 | def hash_pw(salt, string): 145 | return hashlib.pbkdf2_hmac( 146 | 'sha256', 147 | string.encode('ascii'), 148 | salt, 149 | 100000, 150 | ) 151 | 152 | # input string, output hash and salt 153 | def hash_w_salt(string): 154 | salt = get_salt() 155 | hash = hash_pw(salt, string) 156 | return bytes2hexstr(hash), bytes2hexstr(salt) 157 | 158 | # input hash,salt,string, output comparison result 159 | def check_hash_salt_pw(hashstr, saltstr, string): 160 | chash = hash_pw(hexstr2bytes(saltstr), string) 161 | return chash == hexstr2bytes(hashstr) 162 | 163 | 164 | def timethis(stmt): 165 | import re, timeit 166 | print('timing', stmt) 167 | broken = re.findall(f'\$([a-zA-Z][0-9a-zA-Z_\-]*)', stmt) 168 | stmt = stmt.replace('$','') 169 | setup = f"from __main__ import {','.join(broken)}" 170 | 171 | exec(setup) # preheat 172 | exec(stmt) 173 | 174 | timeit.Timer(stmt, 175 | setup=setup 176 | ).autorange( 177 | lambda a,b:print(f'{a} in {b:.4f}, avg: {b/a*1000_000:.4f}us')) 178 | 179 | # if __name__ == '__main__': 180 | # k = time.time() 181 | # def hello(): 182 | # if time.time() - k < 2: 183 | # raise Exception('nah') 184 | # 185 | # dispatch_with_retries(hello) 186 | # time.sleep(4) 187 | 188 | if __name__ == '__main__': 189 | toenc = b"r12uf-398gy309ghh123r1"*100000 190 | timethis('calculate_checksum_base64_replaced(toenc)') 191 | 192 | 193 | 194 | # everything time related 195 | 196 | import datetime 197 | 198 | dtdt = datetime.datetime 199 | dtt = datetime.time 200 | dtd = datetime.date 201 | dtn = dtdt.now 202 | dttz = datetime.timezone 203 | dttd = datetime.timedelta 204 | 205 | # default time parsing 206 | def dtdt_from_stamp(stamp): 207 | return dtdt.fromisoformat(stamp) 208 | 209 | dfs = dtdt_from_stamp 210 | 211 | def dfshk(stamp): 212 | return dfs(stamp).replace(tzinfo=working_timezone) 213 | 214 | # proper time formatting 215 | # input: string iso timestamp 216 | # output: string formatted time 217 | 218 | def format_time(dtdt,s): 219 | return dtdt.strftime(s) 220 | 221 | # default time formatting 222 | def format_time_iso(dtdt): 223 | return dtdt.isoformat(timespec='seconds')[:19] 224 | fti = format_time_iso 225 | 226 | format_time_datetime = lambda s: format_time(dfs(s), '%Y-%m-%d %H:%M') 227 | format_time_datetime_second = lambda s: format_time(dfs(s), '%Y-%m-%d %H:%M:%S') 228 | format_time_dateonly = lambda s: format_time(dfs(s), '%Y-%m-%d') 229 | format_time_timeonly = lambda s: format_time(dfs(s), '%H:%M') 230 | 231 | def days_since(ts): 232 | then = dfshk(ts) 233 | now = dtn(working_timezone) 234 | dt = now - then 235 | return dt.days 236 | 237 | def days_between(ts0, ts1): 238 | return abs(days_since(ts0) - days_since(ts1)) 239 | 240 | def seconds_since(ts): 241 | then = dfshk(ts) 242 | now = dtn(working_timezone) 243 | dt = now - then 244 | return dt.total_seconds() 245 | 246 | def cap(x, mi, ma): 247 | return min(max(x, mi),ma) 248 | 249 | working_timezone = dttz(dttd(hours=+8)) # Hong Kong 250 | gmt_timezone = dttz(dttd(hours=0)) # GMT 251 | 252 | def time_iso_now(dt=0): # dt in seconds 253 | return format_time_iso(dtn(working_timezone) + dttd(seconds=dt)) 254 | -------------------------------------------------------------------------------- /database_dump_repeat.bat: -------------------------------------------------------------------------------- 1 | rem dump repeatedly 2 | 3 | timeout /t 10 4 | 5 | :loop 6 | 7 | arangodump --output-directory dump --server.database db2047 --server.password "" --overwrite true --log.color false 8 | 9 | # assume you have the right github token stored in release_token.txt 10 | python make_archive_backup_release.py 11 | python make_archive_delete_older.py 12 | 13 | timeout /t 600 14 | 15 | goto loop -------------------------------------------------------------------------------- /database_dump_upload.bat: -------------------------------------------------------------------------------- 1 | arangodump --output-directory dump --server.database db2047 --server.password "" --overwrite true --log.color false 2 | 3 | rem # arangodump --output-directory dump_pmf --server.database dbpmf --server.password "" --overwrite true --log.color false 4 | 5 | set https_proxy=localhost:1080 6 | # assume you have the right github token stored in release_token.txt 7 | python make_archive_backup_release.py --upload 8 | 9 | timeout 10 10 | -------------------------------------------------------------------------------- /database_restore.bat: -------------------------------------------------------------------------------- 1 | arangorestore --create-database true --server.database db2047 --server.password "" --log.color false --input-directory dump 2 | arangorestore --create-database true --server.database dbpmf --server.password "" --log.color false --input-directory dump_pmf 3 | -------------------------------------------------------------------------------- /database_up_python_main_example.bat: -------------------------------------------------------------------------------- 1 | 2 | 3 | start arangodb_loop.bat 4 | start arangodb_killer.bat 5 | 6 | start database_dump_repeat.bat 7 | 8 | set port=5000 9 | start pymain.bat 10 | 11 | set port=5001 12 | start pymain.bat 13 | 14 | start start_load_balancer.bat 15 | 16 | start echo "empty window" -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # this script should be run on an instance of ubuntu 1604/1804 2 | # assume you already cloned the repo 3 | # assume you start this script via ./2047/deploy.sh 4 | 5 | # install [latest stable version of] arangodb 6 | 7 | wget https://download.arangodb.com/nightly/3.7/Linux/arangodb3_3.7.3~~nightly-1_amd64.deb 8 | 9 | dpkg -i arangodb3_3.7.1-1_amd64.deb 10 | 11 | systemctl unmask arangodb3 12 | systemctl start arangodb3 13 | 14 | # install screen 15 | 16 | apt update 17 | apt --assume-yes install screen 18 | 19 | # install python 20 | 21 | apt update 22 | add-apt-repository --yes ppa:deadsnakes/ppa 23 | apt update 24 | apt --assume-yes install python3.7 25 | apt --assume-yes install python3-pip 26 | 27 | # code 28 | git clone https://github.com/thphd/2047 29 | python3.7 -m pip install --upgrade pip 30 | python3.7 -m pip install termcolor cachetools flask-threads 31 | python3.7 -m pip install colorama 32 | # python3.7 -m pip install markdown2 33 | python3.7 -m pip install mistletoe beautifulsoup4 python-snappy cryptography 34 | python3.7 -m pip install pillow 35 | python3.7 -m pip install Flask 36 | python3.7 -m pip install flask_cors 37 | # python3.7 -m pip install Flask_gzip 38 | python3.7 -m pip install githubrelease 39 | python3.7 -m pip install qrcode 40 | python3.7 -m pip install forbiddenfruit 41 | 42 | git clone https://github.com/flavono123/identicon 43 | python3.7 -m pip install -e ./identicon 44 | 45 | # upload the dumped database backup files to ./2047/dump 46 | # cd 2047 47 | # run ./database_restore.bat to load data from the backup 48 | 49 | # python3.7 main.py 50 | 51 | # access from browser @ localhost:5000 52 | -------------------------------------------------------------------------------- /flask_response_gzip.py: -------------------------------------------------------------------------------- 1 | import gzip, brotli 2 | from io import BytesIO 3 | 4 | from functools import lru_cache 5 | 6 | from flask import request 7 | 8 | from commons import * 9 | 10 | @lru_cache(maxsize=512) 11 | def zipthis(data, level): 12 | gzip_buffer = BytesIO() 13 | gzip_file = gzip.GzipFile(mode='wb', compresslevel=level, fileobj=gzip_buffer) 14 | gzip_file.write(data) 15 | gzip_file.close() 16 | return gzip_buffer.getvalue() 17 | 18 | @lru_cache(maxsize=512) 19 | def brothis(data, level=4): 20 | return brotli.compress(data, 21 | mode = 0, 22 | quality = level, 23 | lgwin = 22, 24 | lgblock = 0, 25 | ) 26 | 27 | # code borrowed from pypi package Flask-gzip 28 | def gzipify(app): 29 | compress_level = 6 30 | minimum_size = 500 31 | 32 | def ar(response): 33 | ael = {i.strip() for i in request.headers.get('Accept-Encoding', '').lower().split(',')} 34 | 35 | import time 36 | t = time.time() 37 | 38 | rd = response.get_data() 39 | 40 | rh = response.headers 41 | ctype = rh['Content-Type'] 42 | 43 | rsc = response.status_code 44 | 45 | def compare_size_and_log(alg, compressed): 46 | elapsed = int((time.time() - t)*1000) 47 | print_down(f'{alg} ({len(compressed)/len(rd)*100:.1f}%) '+ 48 | f'{elapsed}ms {len(compressed)}/{len(rd)}') 49 | 50 | def compress_etag_response(format, data): 51 | if format=='br': 52 | compressed = brothis(data, 8) 53 | elif format=='gzip': 54 | compressed = zipthis(data, 6) 55 | 56 | compare_size_and_log(format, compressed) 57 | 58 | response.set_data(compressed) 59 | 60 | rh['Content-Encoding'] = format 61 | rh['Content-Length'] = len(compressed) 62 | etag304(response) 63 | 64 | return response 65 | 66 | if (rsc < 200 or rsc >= 300 67 | or response.direct_passthrough 68 | or len(rd) < minimum_size 69 | or ('br' not in ael and 'gzip' not in ael) 70 | or 'Content-Encoding' in rh 71 | or ('image' in ctype and 'svg' not in ctype) 72 | ): 73 | pass 74 | 75 | elif 'br' in ael and ('application/json'in ctype or 'text/html' in ctype): 76 | response = compress_etag_response('br', rd) 77 | 78 | elif 'gzip' in ael: 79 | response = compress_etag_response('gzip', rd) 80 | 81 | return response 82 | 83 | app.after_request(ar) 84 | -------------------------------------------------------------------------------- /i18n_api.py: -------------------------------------------------------------------------------- 1 | from api import * 2 | from commons import * 3 | from app import app 4 | 5 | aqlc.create_collection('translations') 6 | 7 | @register('set_locale') 8 | def _(): 9 | j = g.j 10 | locale = es('locale') 11 | if locale not in dict_of_languages or locale=='expl': 12 | raise Exception('this locale is not yet supported here.') 13 | return {'error':False,'set_locale':locale} 14 | 15 | @register('get_allowed_languages') 16 | def _(): 17 | return {'allowed_languages':dict_of_languages} 18 | 19 | @register('update_translation') 20 | def _(): 21 | j = g.j 22 | must_be_logged_in() 23 | banned_check() 24 | if current_user_doesnt_have_enough_likes(): 25 | raise Exception('you don\'t have enough likes to submit translation') 26 | 27 | original = es('original') 28 | o2=j['original'] 29 | lang = es('lang') 30 | string = es('string') 31 | 32 | uid = g.current_user['uid'] 33 | 34 | if lang not in trans.allowed_languages: 35 | raise Exception('this language is not supported yet.') 36 | 37 | # if original not in trans.d: 38 | # print(original) 39 | # print(o2) 40 | # raise Exception('original text not in trans.d') 41 | 42 | t_c = time_iso_now() 43 | 44 | new_trans = dict( 45 | original=original, 46 | lang=lang, 47 | string=string, 48 | uid=uid, 49 | t_c=t_c, 50 | t_u=t_c, 51 | approved=False, 52 | ) 53 | 54 | aql(''' 55 | let d = @nt 56 | upsert {uid:d.uid, lang:d.lang, original:d.original} 57 | insert d update {string:d.string, t_u:d.t_c} into translations 58 | ''', nt=new_trans) 59 | 60 | return {} 61 | 62 | @register('approve_translation') 63 | def _(): 64 | must_be_logged_in() 65 | must_be_admin() 66 | 67 | trans_id = es('id') 68 | delete = eb('delete') 69 | 70 | aql(''' 71 | for t in translations 72 | filter t._key==@key 73 | update t with {approved:@approved} in translations 74 | ''', key=trans_id, approved=not delete) 75 | 76 | return {} 77 | 78 | def textualize(s): 79 | resp = make_response(s, 200) 80 | resp.headers['Content-type'] = 'text/plain; charset=utf-8' 81 | return resp 82 | 83 | def get_all_translations(): 84 | all_translations = aql(''' 85 | for t in translations 86 | let user = (for i in users filter i.uid==t.uid return i)[0] 87 | collect original = t.original into groups = merge(t, {user}) 88 | return {original, groups} 89 | ''', silent=True) 90 | 91 | return all_translations 92 | 93 | # for d in all_translations: 94 | # original = d['original'] 95 | 96 | @stale_cache(ttr=10, ttl=1200) # database to code form 97 | def at2d2(): 98 | at = get_all_translations() 99 | d2 = {} 100 | for d in at: 101 | orig = d['original'] 102 | lots = d['groups'] 103 | d2[orig] = d2o = {} 104 | 105 | for t in lots: # t -> translation object {original, string, lang} 106 | t_lang = t['lang'] 107 | if t['approved']: 108 | if t_lang not in d2o: 109 | d2o[t_lang] = t 110 | else: 111 | if d2o[t_lang]['user']['pagerank'] < t['user']['pagerank']: 112 | d2o[t_lang] = t 113 | 114 | for t_lang in d2o: 115 | d2o[t_lang] = d2o[t_lang]['string'] 116 | 117 | return d2 118 | 119 | @stale_cache(ttr=3, ttl=120) # code to database form 120 | def d2at(): 121 | if not trans.scanned: 122 | trans.scan_dir_and_extract() 123 | 124 | d = trans.d 125 | l = [] 126 | for original,langs in d.items(): 127 | od = dict(original=original, groups=[]) 128 | groups = od['groups'] 129 | l.append(od) 130 | 131 | # commented out because jinja2 is inspect-unfriendly 132 | 133 | # ts2o = trans.s2[original] 134 | # fn,ln = ts2o['filename'], ts2o['lineno'] 135 | 136 | for lang, string in langs.items(): 137 | nd = dict(lang=lang, string=string) 138 | if original in trans.s2: 139 | nd['filename'] = trans.s2[original]['filename'] 140 | nd['lineno'] = trans.s2[original]['lineno'] 141 | nd['original'] = original 142 | # dict(lang=lang, string=string, filename=fn, lineno=ln) 143 | groups.append(nd) 144 | 145 | return l 146 | 147 | trans.get_d2 = at2d2 148 | 149 | @app.route('/translations') 150 | def translations_page(): 151 | at = get_all_translations() # from database 152 | at2 = d2at() # from code 153 | 154 | ld = {} 155 | for d in at2+at: 156 | original = d['original'] 157 | if original not in ld: 158 | ld[original] = [] 159 | ld[original]+=d['groups'] 160 | 161 | l = [(k,v) for k,v in ld.items()] 162 | l.sort(key=lambda t:t[0]) 163 | 164 | return render_template_g('trans.html.jinja', translations=l, page_title='翻译') 165 | 166 | @app.route('/translations/list_allowed_languages') 167 | def list_allowed_languages(): 168 | tal = trans.allowed_languages 169 | s = '\n'.join((str(k) +' '+ repr(v) for k,v in tal.items())) 170 | return textualize(s) 171 | 172 | @app.route('/translations/list_translations') 173 | def list_translations(): 174 | d = at2d2() 175 | s = '\n'.join((str(k) +' '+ repr(v) for k,v in d.items())) 176 | return textualize(s) 177 | -------------------------------------------------------------------------------- /imgproc.py: -------------------------------------------------------------------------------- 1 | import PIL 2 | from PIL import Image 3 | import io 4 | 5 | def load_img_from_bin(b): 6 | im = Image.open(io.BytesIO(b)) 7 | return im.convert(mode='RGBA') 8 | 9 | def resize_to(im, target=72): 10 | w,h = ow,oh = im.size 11 | longer = max(w,h) 12 | shorter = min(w,h) 13 | 14 | # crop the thing to square 15 | l = int((longer-shorter)/2+.5) 16 | if longer==w: 17 | nim = im.crop((l,0,w-l,h)) 18 | else: 19 | nim = im.crop((0,l,w,h-l)) 20 | 21 | # resize to a smaller square 22 | w,h = nim.size 23 | ratio = target/w 24 | nw,nh = round(w*ratio), round(w*ratio) 25 | 26 | if nw == ow and nh==oh: 27 | return im 28 | return nim.resize((nw,nh), resample=Image.BICUBIC, reducing_gap=3.5) 29 | 30 | def avatar_pipeline(b): 31 | # user upload binary file 32 | # convert it into avatar format 33 | # output binary 34 | 35 | im = load_img_from_bin(b) 36 | im = resize_to(im, 96).quantize( 37 | colors=64, 38 | method=Image.FASTOCTREE, # only applicapable 39 | kmeans=1, # larger faster 40 | dither=Image.FLOYDSTEINBERG, 41 | ) 42 | print(im.size) 43 | result_file = io.BytesIO() 44 | im.save(result_file,'PNG',optimize=True) 45 | return result_file.getvalue() 46 | 47 | if __name__ == '__main__': 48 | import sys 49 | if len(sys.argv)>1: 50 | fn = sys.argv[1] 51 | 52 | im = load_img_from_bin(open(fn,'rb').read()) 53 | 54 | im = im.quantize( 55 | colors=128, 56 | method=Image.FASTOCTREE, # only applicapable 57 | kmeans=1, # larger faster 58 | dither=Image.FLOYDSTEINBERG, 59 | ) 60 | 61 | im.save(fn,'PNG',optimize=True) 62 | print('saved to', fn) 63 | else: 64 | print('no args') 65 | 66 | # b = open('templates/images/logo.png','rb').read() 67 | # # b = b[:100] 68 | # im = load_img_from_bin(b) 69 | # 70 | # print(im) 71 | # # resize_to(im, 84).quantize( 72 | # # colors=32, 73 | # # method=Image.FASTOCTREE, # only applicapable 74 | # # kmeans=1, # larger faster 75 | # # dither=Image.FLOYDSTEINBERG, 76 | # # ).save('test.png') 77 | -------------------------------------------------------------------------------- /leet.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from api import * 3 | 4 | from runcode import run_python_code 5 | from leet_challenge import LeetChallenge, LeetChallenges 6 | 7 | lcs = None 8 | 9 | def delayed_init(): 10 | global lcs 11 | lcs = LeetChallenges(os.path.dirname(os.path.abspath(__file__))+'/leet_challenges') 12 | 13 | dispatch(delayed_init) 14 | 15 | def get_everyone_submission_stat(): 16 | stats = aql(''' 17 | for i in challenge_submissions 18 | sort i.t_c desc 19 | collect lcn = i.lcn AGGREGATE uids = UNIQUE(i.uid) 20 | return {lcn, uids} 21 | ''') 22 | d = {k['lcn']:k['uids'] for k in stats} 23 | return d 24 | 25 | def get_my_most_recent_sub_on(lcn): 26 | r = aql('for i in challenge_submissions \ 27 | filter i.uid==@uid and i.lcn==@lcn sort i.t_c desc limit 1 return i', 28 | silent=True, lcn = lcn, uid = g.selfuid) 29 | return r[0] if r else None 30 | 31 | def get_my_sub_on_every(): 32 | r = aql(''' 33 | for i in challenge_submissions 34 | filter i.uid==@uid 35 | collect g = i.lcn 36 | return g 37 | ''', silent=True, uid=g.selfuid) 38 | 39 | d = {i:True for i in r} 40 | return d 41 | 42 | @app.route('/leet/') 43 | def leet_lcn(lcn): 44 | 45 | if lcn not in lcs.d: 46 | raise Exception('challenge not found') 47 | 48 | lc = lcs.d[lcn] 49 | lc.update() 50 | 51 | r = get_my_most_recent_sub_on(lcn) 52 | 53 | nlinesofcode = len(lc.user_code_reference.split('\n')) 54 | 55 | return render_template_g('leet_challenge.html.jinja', 56 | page_title=f'Challenge {lcn}', 57 | page_title_title=f'Challenge {lcn} {lc.name}', 58 | lcs = lcs, 59 | lcn = lcn, 60 | lc = lc, 61 | most_recent = r, 62 | loc = nlinesofcode, 63 | ) 64 | 65 | 66 | @app.route('/leet') 67 | def leet(): 68 | 69 | my_subs = get_my_sub_on_every() 70 | other_subs = get_everyone_submission_stat() 71 | 72 | return render_template_g('leet.html.jinja', 73 | page_title=f'Coding Challenges', 74 | lcs = lcs, 75 | my_subs = my_subs, 76 | other_subs = other_subs, 77 | ) 78 | 79 | aqlc.create_collection('challenge_submissions') 80 | 81 | @register('runcode') 82 | def _(): 83 | must_be_logged_in() 84 | input_text = es('input') 85 | code = es('code') 86 | 87 | if len(code)>1000: 88 | raise Exception('代码太长') 89 | if len(input_text)>100: 90 | raise Exception('输入太长') 91 | 92 | tlr = key(g.current_user, 't_last_runcode') 93 | if tlr and tlr>time_iso_now(-5): 94 | raise Exception('两次运行代码间隔应大于5秒') 95 | 96 | else: 97 | aql('for i in users filter i.uid==@uid update i with {t_last_runcode:@t} in users', uid=g.selfuid, t=time_iso_now(), silent=True) 98 | 99 | lcn = es('challenge_name') 100 | is_submission = eb('is_submission') 101 | 102 | print_info(input_text) 103 | 104 | if lcn not in lcs.d: 105 | raise Exception('challenge not found') 106 | 107 | lc = lcs.d[lcn] 108 | lc.update() 109 | 110 | def pack(a,b): 111 | return {'error':a, 'result':b} 112 | 113 | try: 114 | if not is_submission: 115 | result_text = lc.eval_test(code, input_text) 116 | else: 117 | result_text = lc.eval_submit(code) 118 | except Exception as e: 119 | return pack(True, flask.escape(str(e))) 120 | else: 121 | if is_submission: 122 | aql(''' 123 | insert @k in challenge_submissions 124 | ''', k=dict( 125 | uid = g.selfuid, 126 | t_c = time_iso_now(), 127 | lcn = lcn, 128 | code = code, 129 | )) 130 | 131 | return pack(False, flask.escape(str(result_text))) 132 | -------------------------------------------------------------------------------- /list_of_package_to_install.txt: -------------------------------------------------------------------------------- 1 | # workhorse 2 | 3 | flask 4 | requests 5 | cachetools 6 | 7 | # util 8 | 9 | flask_cors 10 | qrcode 11 | pillow 12 | githubrelease 13 | git clone https://github.com/flavono123/identicon 14 | 15 | # logging 16 | 17 | termcolor 18 | colorama 19 | 20 | # html/markdown 21 | 22 | mistletoe 23 | beautifulsoup4 24 | 25 | # monkeypatching 26 | 27 | forbiddenfruit (requires VS build tools on Windows 28 | 29 | pytio 30 | brotli 31 | numba -------------------------------------------------------------------------------- /load_balancer.py: -------------------------------------------------------------------------------- 1 | import asyncio, socket 2 | import functools 3 | import random, time 4 | 5 | afi = socket.AF_INET 6 | rr = random.random 7 | 8 | import logging 9 | from colors import * 10 | from commons_static import * 11 | from aiorun import run 12 | 13 | async def ever(): 14 | while 1: 15 | yield 1 16 | 17 | afut = asyncio.Future 18 | 19 | good_upstreams = {} 20 | bad_upstreams = {} 21 | 22 | class UpStream: 23 | def __init__(self, h, p): 24 | self.h = h 25 | self.p = p 26 | 27 | self.s = f'{h}:{p}' 28 | self.lasthit = time.monotonic() 29 | 30 | self.accumulator = 0 31 | 32 | self.bad() 33 | 34 | def accumulate(self, t): 35 | self.accumulator = (self.accumulator + t) * 0.95 36 | 37 | def good(self): 38 | self.available = True 39 | self.lasthit = time.monotonic() + self.accumulator*.1 40 | 41 | ss = self.s 42 | good_upstreams[ss] = self 43 | if ss in bad_upstreams: 44 | del bad_upstreams[ss] 45 | 46 | def bad(self): 47 | self.available = False 48 | self.lasthit = time.monotonic() + self.accumulator*.1 49 | 50 | ss = self.s 51 | bad_upstreams[ss] = self 52 | if ss in good_upstreams: 53 | del good_upstreams[ss] 54 | 55 | async def update_availability(self): 56 | while 1: 57 | if self.available == False: 58 | 59 | k = await self.open() 60 | if k is None: 61 | print_err(f'failed to reach upstream {self.s}') 62 | continue 63 | 64 | print_info(f'upstream ok {self.s}') 65 | ur, uw = k 66 | await closew(uw) 67 | 68 | else: 69 | await asleep(0.2*rr()) 70 | 71 | async def open(self): 72 | try: 73 | conn = await make_conn(self.h, self.p) 74 | 75 | except ConnectionRefusedError as e: 76 | print_err('CRE', e) 77 | self.bad() 78 | return None 79 | 80 | else: 81 | self.good() 82 | return conn 83 | 84 | upstreams = [ 85 | UpStream('127.0.0.1', 5000), 86 | UpStream('127.0.0.1', 5001), 87 | # UpStream('127.0.0.1', 5002), 88 | # UpStream('127.0.0.1', 5003), 89 | ] 90 | 91 | def make_conn(h,p): 92 | return asyncio.open_connection( 93 | h, p, family=afi, 94 | ) 95 | 96 | async def update_statuses(): 97 | await asyncio.gather(*(i.update_availability() for i in upstreams)) 98 | 99 | def get_one_upstream(): 100 | 101 | lgu = list(good_upstreams.values()) 102 | lgu.sort(key=lambda u:u.lasthit) 103 | lu = len(lgu) 104 | 105 | if lu: 106 | return lgu[0] 107 | 108 | lbu = list(bad_upstreams.values()) 109 | lbu.sort(key=lambda u:u.lasthit) 110 | lu = len(lbu) 111 | 112 | if lu: 113 | return lbu[0] 114 | 115 | return None 116 | 117 | q = [] 118 | 119 | async def put_conn_in_q(): 120 | while 1: 121 | if len(q) >= 40: 122 | await asleep(0.05) 123 | continue 124 | 125 | upstream = get_one_upstream() 126 | 127 | if upstream is None: 128 | await asleep(0) 129 | continue 130 | 131 | conn = await upstream.open() 132 | 133 | if conn is None: 134 | continue 135 | 136 | q.append((upstream, conn)) 137 | # print_info(f'-> q:{len(q)}') 138 | 139 | def get_conn_from_q(): 140 | # print_down(f'<- q:{len(q)}') 141 | while len(q): 142 | first = q.pop(0) 143 | upst, conn = first 144 | r,w = conn 145 | if r.at_eof() or w.is_closing(): 146 | print_err(f'<- q:{len(q)} (drop closed conn)') 147 | continue 148 | else: 149 | return first 150 | 151 | return None 152 | 153 | asleep = asyncio.sleep 154 | async def stream(r, w): 155 | rateof = r.at_eof 156 | rread = r.read 157 | wdrain = w.drain 158 | wwrite = w.write 159 | 160 | nbytes = 0 161 | firstline = None 162 | 163 | while not rateof(): 164 | data = await rread(800000) 165 | # qprint(f'{note} {len(data)} bytes') 166 | if data: 167 | 168 | wwrite(data) 169 | await wdrain() 170 | nbytes+=len(data) 171 | 172 | if firstline is None: 173 | idx = data.find(b'\r\n') 174 | if idx<100: 175 | firstline = data[:idx].decode('utf-8') 176 | else: 177 | firstline = '(toolong)' 178 | # qprint(f'{firstline}') 179 | 180 | await closew(w) 181 | # qprint(f'{note} {nbytes:8d}') 182 | return nbytes, firstline 183 | 184 | conn_counter = 0 185 | 186 | async def closew(dw): 187 | dw.write_eof() 188 | dw.close() 189 | await dw.wait_closed() 190 | 191 | async def tcp_lb_server(nretries=10): 192 | async def client_conn_cb(dr, dw): 193 | global conn_counter 194 | conn_counter+=1 195 | 196 | try: 197 | ts = time.monotonic() 198 | 199 | addr = dw.get_extra_info('peername') 200 | ads = f'{addr[0]}:{addr[1]}' 201 | 202 | retry_counter = 0 203 | while True: 204 | retry_counter+=1 205 | if retry_counter > nretries: 206 | print_err(f'#{conn_counter} exceeded max retries') 207 | 208 | await closew(dw) 209 | return False 210 | 211 | elif retry_counter>1: 212 | print_up(f'#{conn_counter} attempt #{retry_counter}') 213 | await asleep(0.01*1.5**retry_counter*rr()) 214 | 215 | k = get_conn_from_q() 216 | 217 | if k is None: 218 | print_err(f'#{conn_counter} attempt #{retry_counter} no upstream available') 219 | continue 220 | 221 | upstream, conn = k 222 | ur, uw = conn 223 | 224 | break 225 | 226 | up = stream(dr, uw) 227 | down = stream(ur, dw) 228 | 229 | cc = colored_info(f'#{str(conn_counter)}') 230 | cc2 = (f'#{str(conn_counter)}') 231 | qprint(cc, 232 | f'{time_iso_now()} {ads} --> {upstream.h}:{upstream.p}', 233 | colored_up(f'{upstream.accumulator:.3f}')) 234 | 235 | 236 | (upb, ufl), (dnb, dfl) = await asyncio.gather(up, down) 237 | 238 | elap = time.monotonic() - ts 239 | 240 | qprint(cc2, 241 | colored_up(f'{upb:7d} sent'), 242 | colored_info(ufl), 243 | colored_down(f'{dnb:7d} rcvd'), 244 | colored_err(f'{int(elap*1000):5d} ms'), 245 | colored_info(dfl)) 246 | 247 | upstream.accumulate(elap) 248 | 249 | except Exception as e: 250 | print_err('ccc',e) 251 | 252 | server = await asyncio.start_server( 253 | client_conn_cb, 254 | host=lhost, 255 | port=lport, 256 | ) 257 | 258 | async with server: 259 | await server.serve_forever() 260 | 261 | lhost, lport = '0.0.0.0', 5100 262 | 263 | async def main(): 264 | await asyncio.gather( 265 | update_statuses(), 266 | put_conn_in_q(), 267 | tcp_lb_server(), 268 | ) 269 | 270 | run(main(), stop_on_unhandled_errors=True) 271 | -------------------------------------------------------------------------------- /logger_config.py: -------------------------------------------------------------------------------- 1 | from logging.config import dictConfig 2 | 3 | dictConfig({ 4 | 'version': 1, 5 | 'formatters': {'default': { 6 | 'format': '%(levelname)s/%(module)s %(message)s', 7 | }}, 8 | 'handlers': {'wsgi': { 9 | 'class': 'logging.StreamHandler', 10 | 'stream': 'ext://flask.logging.wsgi_errors_stream', 11 | 'formatter': 'default' 12 | }}, 13 | 'root': { 14 | 'level': 'INFO', 15 | 'handlers': ['wsgi'] 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /make_archive_backup_release.py: -------------------------------------------------------------------------------- 1 | import shutil, os, tarfile, argparse as ap 2 | from commons import * 3 | 4 | parser = ap.ArgumentParser() 5 | parser.add_argument('-u','--upload', action='store_true') 6 | args = parser.parse_args() 7 | 8 | 9 | dir = os.path.abspath('./dump/') 10 | 11 | init_directory('./backup') 12 | 13 | basename = 'backup/2047backup_'+time_iso_now().replace(':','').replace('-','')+'.tar' 14 | basename_p = basename.replace('backup_', 'backup_publish_') 15 | 16 | print('working on', dir) 17 | 18 | # https://stackoverflow.com/a/38883728 19 | tar = tarfile.open(basename,"w") 20 | tar.add('dump') 21 | tar.close() 22 | print('complete backup written to', basename) 23 | 24 | if not args.upload: 25 | exit() 26 | 27 | yay = ''' 28 | admins aliases avatars blacklist categories chat_channels chat_memberships 29 | counters entities favorites followings poll_votes polls posts 30 | tags threads translations users view_counters votes 31 | ''' 32 | 33 | yay = re.findall(r'[a-z\_]+', yay) 34 | print(yay) 35 | yay = set(yay) 36 | 37 | # exit() 38 | 39 | nope = ('conversations histories messages logs passwords invitations exams answersheets operations notifications questions challenge_submissions punchcards' 40 | .split(' ')) 41 | def accept(fn): 42 | if fn=='dump': 43 | return True 44 | 45 | # for obvious reasons 46 | sfn = '_'.join(fn.split('_')[:-1])[5:] 47 | # print(fn,sfn) 48 | # print(sfn) 49 | 50 | if sfn not in yay: 51 | print('x', fn) 52 | return False 53 | else: 54 | print('√', fn) 55 | return True 56 | # for k in nope: 57 | # if k in fn: 58 | # print('skip', fn) 59 | # return False 60 | # print('accept', fn) 61 | # return True 62 | 63 | tar = tarfile.open(basename_p,"w") 64 | tar.add('dump', filter=lambda x: x if accept(x.name) else None) 65 | tar.close() 66 | print('partial backup written to', basename_p) 67 | 68 | 69 | tok = open('release_token.txt','r').read().strip() 70 | os.environ['GITHUB_TOKEN'] = tok 71 | 72 | print('github token is', tok) 73 | 74 | from github_release import * 75 | 76 | while 1: 77 | while 1: 78 | try: 79 | gh_release_delete('thphd/2047','0.0.1') 80 | except Exception as e: 81 | print(e) 82 | else: 83 | break 84 | 85 | 86 | try: 87 | gh_release_create('thphd/2047', '0.0.1', 88 | publish=True, 89 | # name="database backup", 90 | asset_pattern=basename_p, 91 | prerelease=True, 92 | ) 93 | 94 | os.remove(basename_p) 95 | 96 | except Exception as e: 97 | print(e) 98 | import time 99 | time.sleep(1) 100 | else: 101 | break 102 | -------------------------------------------------------------------------------- /make_archive_delete_older.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | 3 | import os, re 4 | 5 | 6 | bkup = './backup' 7 | l = os.listdir(bkup) 8 | 9 | import monkeypatch 10 | 11 | l=l.filter(lambda s:s.endswith('.tar')).map(lambda s:(s, s.split('_')[-1][:-4])) 12 | 13 | now = dtn() 14 | print(now, now.year, now.month) 15 | 16 | def pd(s): 17 | return datetime.datetime.strptime(s, '%Y%m%dT%H%M%S') 18 | 19 | 20 | dategroups = {} 21 | 22 | for fn, ts in l: 23 | 24 | t = pd(ts) 25 | def endg(date): 26 | if date not in dategroups: 27 | dategroups[date] = [] 28 | dategroups[date].append((fn, ts)) 29 | 30 | if now - t > dttd(days=0.5): 31 | # not recent 32 | endg(ts[:11]) # hour 33 | 34 | if now - t > dttd(days=2): 35 | # not recent 36 | endg(ts[:8]) # day 37 | 38 | if now - t > dttd(days=90): 39 | # not this month 40 | endg(ts[:6]) # month 41 | 42 | for k in dategroups: 43 | print('group', k) 44 | 45 | l = dategroups[k] 46 | 47 | l = sorted(l,key=lambda k:k[1]) 48 | # print(l) 49 | 50 | for j in l[1:-1]: # no head no tail 51 | ffn = bkup+'/'+j[0] 52 | print('about to delete', ffn) 53 | os.remove(ffn) 54 | -------------------------------------------------------------------------------- /medals.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from api import * 3 | 4 | @stale_cache(ttr=15, ttl=1800) 5 | def get_medals(): 6 | medals = QueryString(''' 7 | let doc = document('counters/medals') 8 | let doc_medals = doc.medals 9 | 10 | let pgpmedalists = (for i in users 11 | filter i.pgp_login==true 12 | sort i.t_c asc 13 | return i) 14 | 15 | let high_ts = (for i in users filter i.trust_score>(1600/1000000) 16 | sort i.trust_score desc 17 | return i) 18 | 19 | let custom_medals_merged = [ 20 | {name:'非对称奖章', brief:"成功使用PGP签名登录2047的用户", listusers:pgpmedalists}, 21 | {name:'SAT满分', brief:"信用分大于1600的用户", listusers:high_ts}, 22 | ] 23 | 24 | let doc_medals_merged = ( 25 | for medal in doc_medals 26 | let listusers = ( 27 | for name in medal.list 28 | let u = (for i in users filter i.name==name return i)[0] 29 | sort u.t_c asc 30 | return u 31 | ) 32 | return merge(medal, {listusers}) 33 | ) 34 | let combined_medals = append(doc_medals_merged, custom_medals_merged) 35 | 36 | for medal in combined_medals 37 | return medal 38 | 39 | ''', silent=True) 40 | 41 | medals = aql(medals) 42 | 43 | for medald in medals: 44 | medald['uids'] = [] 45 | for user in medald['listusers']: 46 | medald['uids'].append(user['uid']) 47 | 48 | return medals 49 | 50 | @stale_cache(ttr=3, ttl=1800) 51 | def get_user_medals(uid): 52 | medals = get_medals() 53 | u = get_user_by_id(uid) 54 | res = [] 55 | 56 | uname = key(u, 'name') 57 | for medal in medals: 58 | if key(medal, 'list'): 59 | for name in medal['list']: 60 | if name==uname: 61 | res.append(medal['name']) 62 | elif key(medal, 'listusers'): 63 | for name in medal['listusers'].map(lambda k:k['name']): 64 | if name==uname: 65 | res.append(medal['name']) 66 | 67 | return res 68 | -------------------------------------------------------------------------------- /monkeypatch.py: -------------------------------------------------------------------------------- 1 | # monkey patching 2 | 3 | from forbiddenfruit import curse 4 | curse(list, 'map', lambda self,f:list(map(f,self))) 5 | curse(list, 'filter', lambda self,f:list(filter(f,self))) 6 | # curse(list, 'sort', lambda self,f:sorted(self, key=f)) 7 | curse(list, 'join', lambda self,s:s.join(list(self))) 8 | -------------------------------------------------------------------------------- /parse/parse.py: -------------------------------------------------------------------------------- 1 | import sys,os 2 | sys.path.append(os.path.abspath('..')) 3 | 4 | # AQL 5 | from aql import AQLController 6 | 7 | aqlc = AQLController('http://127.0.0.1:8529', 'db2047',[ 8 | 'postlets','userlets' 9 | ]) 10 | aql = aqlc.aql 11 | aqlcl = aqlc.clear_collection 12 | 13 | def listdir(dir): 14 | l = os.listdir(dir) 15 | return [dir+'/'+i for i in l] 16 | 17 | 18 | 19 | def read(fn): 20 | with open(fn, 'r', encoding='utf8') as f: 21 | return f.read() 22 | 23 | # yaml 24 | from yaml import load as nload, load_all as nloadall, dump as ndump, dump_all as ndumpall 25 | try: 26 | from yaml import CLoader as Loader, CDumper as Dumper 27 | print('has LibYAML support') 28 | except ImportError: 29 | print('no LibYAML support') 30 | from yaml import Loader, Dumper 31 | 32 | # from yaml import Loader, Dumper 33 | 34 | def load(x): return nload(x, Loader=Loader) 35 | def loadall(x): return nloadall(x, Loader=Loader) 36 | def dump(x): return ndump(x, Dumper=Dumper) 37 | def dumpall(x): return ndumpall(x, Dumper=Dumper) 38 | 39 | 40 | 41 | 42 | def datetime2str(datetime): 43 | return datetime.isoformat(timespec='seconds')[:19] 44 | 45 | def nodate(obj): 46 | def fd(k): 47 | if k in obj: 48 | obj[k] = datetime2str(obj[k]) 49 | 50 | fd('addTime') 51 | fd('date') 52 | fd('regTime') 53 | 54 | if 'comments' in obj: 55 | for i in obj['comments']: 56 | nodate(i) 57 | return obj 58 | 59 | # l0 = list(loadall(ml))[0] 60 | # nodate(l0) 61 | # print(l0) 62 | 63 | if __name__ == '__main__': 64 | 65 | # 1. load all _posts into postlets 66 | 67 | if 1: 68 | 69 | # list all files under _posts 70 | l = listdir('./2049bbs.github.io/_posts') 71 | # print(l) 72 | 73 | # test first file 74 | ml = read(l[0]) 75 | print(ml) 76 | 77 | 78 | # aql('for i in postlets remove i in postlets') 79 | aqlcl('postlets') 80 | 81 | # t = read('./2049bbs.github.io/_posts/2018-01-11-10.md') 82 | # open('debug.yaml','w', encoding='utf8').write(t) 83 | # open('shit.yaml','w',encoding='utf8').write(dumpall(['jaime\n@fasdfasfasfafadf','@$$dfasdf\nadsfasfasf'])) 84 | 85 | for fn in l: 86 | yamlt = read(fn) 87 | print(fn) 88 | # print(yamlt) # commented for faster loading 89 | 90 | ly = yamlt.split('\n---\n\n') 91 | # length = len(ly) 92 | assert(len(ly)==2) 93 | 94 | # print(ly) 95 | 96 | # ly = list(loadall(yamlt)) 97 | # ly = list(loadall(yamlt)) 98 | 99 | parsed = load(ly[0]) 100 | parsed['content'] = ly[1].strip() 101 | 102 | nodate(parsed) 103 | aql('insert @i into postlets', i=parsed, silent=True) 104 | 105 | # print() 106 | # print() 107 | 108 | # 2. load all _users into userlets 109 | 110 | if 1: 111 | l = listdir('./2049bbs.github.io/_users') 112 | 113 | ml = read(l[2]) 114 | print(ml) 115 | aql('for i in userlets remove i in userlets') 116 | 117 | for fn in l: 118 | yamlt = read(fn) 119 | print(fn) 120 | # print(yamlt) # commented for faster loading 121 | 122 | ly = yamlt.split('\n---\n\n') 123 | # length = len(ly) 124 | assert(len(ly)==2) 125 | 126 | # print(ly) 127 | 128 | # ly = list(loadall(yamlt)) 129 | # ly = list(loadall(yamlt)) 130 | 131 | parsed = load(ly[0]) 132 | parsed['brief'] = ly[1].strip() 133 | 134 | nodate(parsed) 135 | aql('insert @i into userlets', i=parsed, silent=True) 136 | 137 | # 3. categories 138 | 139 | if 1: 140 | l = listdir('./2049bbs.github.io/_category_info') 141 | 142 | ml = read(l[2]) 143 | print(ml) 144 | 145 | aqlc.create_collection('catlets') 146 | aql('for i in catlets remove i in catlets') 147 | 148 | for fn in l: 149 | yamlt = read(fn) 150 | print(fn) 151 | # print(yamlt) # commented for faster loading 152 | 153 | ly = yamlt.split('\n---\n\n') 154 | # length = len(ly) 155 | assert(len(ly)==2) 156 | 157 | # print(ly) 158 | 159 | # ly = list(loadall(yamlt)) 160 | # ly = list(loadall(yamlt)) 161 | 162 | parsed = load(ly[0]) 163 | parsed['brief'] = ly[1].strip() 164 | 165 | nodate(parsed) 166 | aql('insert @i into catlets', i=parsed, silent=True) 167 | -------------------------------------------------------------------------------- /parse/standarize.py: -------------------------------------------------------------------------------- 1 | # such that the names of variables could live in harmony... 2 | 3 | from parse import * 4 | 5 | def cl(colname): 6 | # aqlcl(colname) 7 | aqlcl(colname, 'filter i.imp==true') 8 | 9 | aqlc.create_collection('threads') 10 | cl('threads') 11 | 12 | aqlc.create_collection('posts') 13 | cl('posts') 14 | 15 | aqlc.create_collection('categories') 16 | cl('categories') 17 | 18 | aqlc.create_collection('users') 19 | cl('users') 20 | 21 | aqlc.create_collection('avatars') 22 | cl('avatars') 23 | 24 | aqlc.create_collection('counters') 25 | cl('counters') 26 | 27 | aqlc.create_collection('invitations') 28 | cl('invitations') 29 | 30 | aqlc.create_collection('passwords') 31 | cl('passwords') 32 | 33 | if 1: 34 | aql(''' 35 | for i in postlets insert { 36 | tid:i.aid, 37 | cid:i.cid, 38 | uid:i.authorID, 39 | t_c:i.addTime, 40 | t_u:i.date, 41 | tags:i.tags, 42 | title:i.title, 43 | content:i.content, 44 | imp:true, 45 | } into threads 46 | ''') 47 | 48 | 49 | aql(''' 50 | for j in postlets 51 | for i in j.comments 52 | insert { 53 | uid:i.authorID, 54 | t_c:i.addTime, 55 | content:i.content, 56 | tid:j.aid, 57 | imp:true, 58 | } into posts 59 | ''') 60 | 61 | aql(''' 62 | for i in catlets 63 | insert { 64 | cid:i.cid, 65 | name:i.name, 66 | brief:i.brief, 67 | imp:true, 68 | } into categories 69 | ''') 70 | 71 | aql(''' 72 | for i in userlets 73 | insert { 74 | uid:i.userID, 75 | name:i.userName, 76 | t_c:i.regTime, 77 | url:i.userURL, 78 | brief:i.brief, 79 | imp:true, 80 | } into users 81 | ''') 82 | 83 | aql(''' 84 | for i in userlets 85 | insert { 86 | uid:i.userID, 87 | data:i.avatar, 88 | imp:true, 89 | } into avatars 90 | ''') 91 | 92 | # create users whose posts got hidden(not included in backup). 93 | aql(''' 94 | for p in posts 95 | 96 | let u = (for u in users filter u.uid==p.uid return u)[0] 97 | 98 | filter u==null 99 | 100 | collect uid = p.uid 101 | 102 | insert {name:'(Removed)', uid:uid, 103 | imp:true, 104 | } into users 105 | ''') 106 | 107 | aql('insert @i into threads', i={ 108 | "tid": 7000, 109 | "cid": 4, 110 | "uid": 1, 111 | "t_c": "2020-07-26T11:14:00", 112 | "t_u": "2020-07-26T11:14:00", 113 | "tags": [ 114 | "图片" 115 | ], 116 | "title": "请大家保持耐心", 117 | "content": "我(不是小二,是他的一个好朋友)正在很努力地恢复2049论坛的所有功能。由于没有原始数据库(如果你们知道哪里有,请去github合适地方反馈一下),并不能直接拿2049的代码来用,才决定自己写。", 118 | 'imp':True, 119 | }) 120 | 121 | aql(''' 122 | let lt = (for t in threads sort t.tid desc limit 1 return t)[0] 123 | let lu = (for u in users sort u.uid desc limit 1 return u)[0] 124 | insert {_key:'counters', tid:lt.tid+100, uid:lu.uid+100, imp:True} into counters 125 | ''') 126 | 127 | aql(''' 128 | insert {_key:"genesis", active:true, imp:true} into invitations 129 | ''') 130 | -------------------------------------------------------------------------------- /pgp_stuff.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | import os 3 | 4 | def pgp_check(): 5 | init_directory('./temp') 6 | 7 | # gpg must exist on your system 8 | status = os.system('gpg --version') 9 | if status==0: 10 | print_up('gpg is found') 11 | else: 12 | print_err('can\'t find gpg') 13 | 14 | def verify_publickey_message(pk, msg): 15 | # obtain a temp filename 16 | fn = get_random_hex_string(10) 17 | 18 | # save the public key file and the message file 19 | pkfn = f'./temp/{fn}.pk' 20 | pkbinfn = pkfn+'.gpg' 21 | msgfn = f'./temp/{fn}.msg' 22 | 23 | writefile(pkfn, pk, mode='w', encoding='utf-8') 24 | writefile(msgfn, msg, mode='w', encoding='utf-8') 25 | 26 | def cleanup(): 27 | removefile(pkfn) 28 | removefile(msgfn) 29 | removefile(pkbinfn) 30 | 31 | # remove armor 32 | status = os.system(f'gpg --dearmor {pkfn}') 33 | if status != 0: 34 | qprint('status:', status) 35 | cleanup() 36 | raise Exception('failed to dearmor the public key (there might be something wrong with your public key)') 37 | 38 | # verify 39 | status = os.system(f'gpg --no-default-keyring --keyring {pkbinfn} --verify {msgfn}') 40 | if status != 0: 41 | qprint('status:', status) 42 | cleanup() 43 | raise Exception('failed to verify the message (your public key is okay but the signature you supplied does not match the public key, or is of a wrong format)') 44 | 45 | cleanup() 46 | return True 47 | -------------------------------------------------------------------------------- /pmf.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from search import break_terms 3 | 4 | aqlc2 = AQLController(None, 'dbpmf') 5 | aqlc2.create_collection('pms') 6 | aql = aqlc2.aql 7 | 8 | def loadall(): 9 | datapath = r'[redacted]\ill\result.txt' 10 | 11 | keys = 'id name gender race area committee sfz addr mobile mobile2 education'.split(' ') 12 | lk = len(keys) 13 | 14 | aql('for i in pms remove i in pms') 15 | 16 | dl = [] 17 | def flush(): 18 | nonlocal dl 19 | print('got', len(dl)) 20 | aql('for i in @k insert i into pms', silent=True, k=dl) 21 | dl = [] 22 | 23 | with open(datapath, 'r', encoding='utf-8') as f: 24 | for l in f: 25 | if not l: 26 | break 27 | else: 28 | l = l.strip() 29 | if not len(l): 30 | continue 31 | else: 32 | l = l.split(',') 33 | assert len(l) == lk 34 | 35 | d = {} 36 | 37 | for k,v in zip(keys, l): 38 | d[k] = v 39 | 40 | dl.append(d) 41 | # aql('''insert @k into pms''', k=d, silent=True) 42 | if len(dl) > 500: 43 | flush() 44 | 45 | flush() 46 | 47 | @ttl_cache(ttl=3600, maxsize=256) 48 | def search_term(term): 49 | s = break_terms(term) 50 | 51 | terms = {} 52 | 53 | query = 'for i in pmv search ' 54 | 55 | for idx, i in enumerate(s): 56 | query += '(' 57 | query += f' boost(phrase(i.name, @term{idx}, "text_zh"), 2.5)' 58 | query += f' or boost(ngram_match(i.name, @term{idx}, 1, "text_zh"), 1)' 59 | query += f' or ngram_match(i.addr, @term{idx}, 0.6,"text_zh")' 60 | query += f' or ngram_match(i.committee, @term{idx}, 0.6,"text_zh")' 61 | query += f' or ngram_match(i.area, @term{idx}, 0.6,"text_zh")' 62 | query += f' or phrase(i.mobile, @term{idx}, "text_zh")' 63 | query += f' or phrase(i.mobile2, @term{idx},"text_zh")' 64 | query += f' or phrase(i.sfz, @term{idx},"text_zh")' 65 | query += ')' 66 | 67 | terms['term'+str(idx)] = i 68 | 69 | if idx!=len(s)-1: 70 | query += ' and ' 71 | 72 | query += f''' 73 | let score = tfidf(i) 74 | sort score desc 75 | limit 100 76 | 77 | return merge(i,{{score}}) 78 | ''' 79 | 80 | search_result = aql(query, silent=True, **terms) 81 | 82 | return dict(pms=search_result, terms=s) 83 | 84 | if __name__ == '__main__': 85 | # loadall() 86 | pass 87 | -------------------------------------------------------------------------------- /polls.py: -------------------------------------------------------------------------------- 1 | import monkeypatch 2 | from commons import * 3 | from app import app 4 | # from views import * 5 | 6 | test_post = ''' 7 | each poll in the form of: 8 | 9 | 10 | 11 | question1 12 | ##writein, maxchoices=3 13 | $choice1 14 | $choice2 15 | $choice3 16 | $choice4 17 | 18 | 19 | ''' 20 | 21 | test_text = ''' 22 | question1 23 | ##writein, maxchoices=3 24 | $choice1 25 | $choice2 26 | $choice3 27 | $choice4 28 | ''' 29 | 30 | strip = lambda s:s.strip() 31 | 32 | def str2p(s): 33 | parts = s.split('$').map(strip) 34 | q = parts[0].split('##').map(strip) 35 | choices = parts[1:] 36 | 37 | if len(choices)<2: 38 | raise Exception('you need at least 2 choices') 39 | 40 | opts = {} 41 | 42 | writein = False 43 | maxchoices = 1 44 | # _key = None 45 | 46 | if len(q)>1: 47 | opts = q[1] 48 | opts = opts.split(',') 49 | opts = [k.split('=').map(strip) for k in opts] 50 | opts = {k[0]:(k[1] if len(k)>1 else True) for k in opts} 51 | 52 | writein = key(opts,'writein') or writein 53 | maxchoices = int(key(opts,'maxchoices') or maxchoices) 54 | # _key = key(opts, '_key') or _key 55 | 56 | maxchoices = max(1, maxchoices) 57 | 58 | q = q[0] 59 | 60 | return dict( 61 | question=q, 62 | writein=writein, 63 | maxchoices=maxchoices, 64 | choices=choices, 65 | ) 66 | 67 | def p2str(poll): 68 | s = f'''{poll['question']}\n##writein={'true' if poll['writein'] else ""}, maxchoices={poll['maxchoices']}\n''' 69 | 70 | s+= '\n'.join(['$'+i for i in poll['choices']]) 71 | return s 72 | 73 | from flask import g 74 | 75 | # create or update a poll 76 | def create_or_update_poll(poll): 77 | if '_key' not in poll: 78 | # create new 79 | poll['t_c'] = time_iso_now() 80 | poll['t_u'] = poll['t_c'] 81 | poll['uid'] = g.selfuid 82 | 83 | exist = aqls(''' 84 | for i in polls filter i.uid==@uid and i.t_c>@recent 85 | return i 86 | ''', uid=g.selfuid, recent = time_iso_now(-300) 87 | ) 88 | 89 | if exist: 90 | raise Exception('you can only create one new poll every 5 minutes') 91 | 92 | newpoll = aql(''' 93 | insert @p in polls return NEW 94 | ''', p=poll)[0] 95 | else: 96 | poll['t_u'] = time_iso_now() 97 | newpoll = aql(''' 98 | update @p in polls return NEW 99 | ''', p=poll)[0] 100 | return newpoll 101 | 102 | # def render_poll(s): 103 | # poll = parse_poll(s) 104 | 105 | def add_poll(s): 106 | poll = str2p(s) 107 | create_or_update_poll(poll) 108 | 109 | def modify_poll(qid, s): 110 | poll = str2p(s) 111 | poll['_key'] = qid 112 | create_or_update_poll(poll) 113 | 114 | def add_poll_vote(id, choice, delete=False): 115 | uid = g.selfuid 116 | 117 | poll = aql('for i in polls filter i._key==@k return i',k=id) 118 | if not poll: 119 | raise Exception(f'poll {id} does not exist') 120 | poll = poll[0] 121 | 122 | mc = poll['maxchoices'] 123 | 124 | votes = aql(''' 125 | for i in poll_votes 126 | filter i.uid==@uid and i.pollid==@k 127 | return i 128 | ''', uid=uid, k=id) 129 | 130 | if delete: 131 | for v in votes: 132 | if v['choice']==choice: # found 133 | aql('remove @v in poll_votes', v=v) 134 | return 135 | raise Exception('cant delete votes you havent cast') 136 | 137 | else: 138 | if choice in [v['choice'] for v in votes]: 139 | raise Exception(f'option {choice} chosen already') 140 | 141 | if mc>1: # multiple 142 | if len(votes) >= mc: 143 | raise Exception(f'you can make at most {mc} choices') 144 | 145 | else: # single 146 | if len(votes): 147 | vote = votes[0] 148 | aql(''' 149 | remove @v in poll_votes 150 | ''', v=vote) 151 | 152 | aql('insert @v into poll_votes', v=dict( 153 | t_c=time_iso_now(), 154 | uid=uid, 155 | choice=choice, 156 | pollid=poll['_key'], 157 | )) 158 | 159 | get_poll_q = QueryString(''' 160 | let votes = ( 161 | for j in poll_votes 162 | filter j.pollid==poll._key 163 | for u in users filter u.uid==j.uid 164 | collect choice = j.choice aggregate k=count(u), k2 = sum(u.pagerank), k3=sum(u.trust_score) 165 | return {choice, nvotes:k, nvotes_pr:k2, nvotes_ts:k3} 166 | ) 167 | 168 | let stats = ( 169 | for j in poll_votes 170 | filter j.pollid==poll._key 171 | for u in users filter u.uid==j.uid 172 | collect uid = u.uid aggregate nn=count(u) into ulist = u 173 | let u = ulist[0] 174 | //return {pr:u.pagerank,ts:u.trust_score, n} 175 | collect aggregate n=count(uid), ts = sum(u.trust_score), pr=sum(u.pagerank) 176 | return {n, ts, pr} 177 | )[0] 178 | let pr_total = stats.pr 179 | let ts_total = stats.ts 180 | let nvoters = stats.n 181 | 182 | let selfvotes = ( 183 | for j in poll_votes 184 | filter j.pollid==poll._key and j.uid==@uid 185 | return j 186 | ) 187 | 188 | return merge(poll, {votes, nvoters, selfvotes, pr_total, ts_total}) 189 | ''') 190 | 191 | def poll_postprocess(poll): 192 | choices = poll['choices'] # available choices 193 | votes = poll['votes'] # available votes 194 | nvoters = poll['nvoters'] 195 | selfvotes = poll['selfvotes'] 196 | 197 | d = {} 198 | max_votes = 0 199 | 200 | for c in choices: 201 | if c not in d: 202 | d[c] = {'text': c} 203 | 204 | # sumvotes_pr, sumvotes_ts = 0,0 205 | for v in votes: 206 | vc = v['choice'] 207 | if vc not in d: 208 | d[vc] = {'text': vc} 209 | d[vc].update(v) 210 | # sumvotes_pr+=v['nvotes_pr'] 211 | # sumvotes_ts+=v['nvotes_ts'] 212 | 213 | for v in selfvotes: 214 | vc = v['choice'] 215 | if vc in d: 216 | d[vc]['self_voted'] = True 217 | 218 | for v in votes: 219 | max_votes = max(max_votes, v['nvotes']) 220 | 221 | poll['choices_postprocessed'] = [d[k] for k in d] 222 | poll['max_votes'] = max_votes 223 | 224 | def get_poll(id, selfuid): 225 | q = QueryString(''' 226 | let poll = (for i in polls filter i._key==@k return i)[0] 227 | ''', k=id) + get_poll_q + QueryString(uid=g.selfuid) 228 | 229 | ans = aql(q, silent=True) 230 | poll_postprocess(ans[0]) 231 | return ans[0] 232 | 233 | # from flask import render_template 234 | # def render_poll(poll): 235 | # return render_template('poll_one.html.jinja', poll=poll) 236 | 237 | 238 | 239 | 240 | @app.route('/polls') 241 | def list_polls(): 242 | from views import generate_simple_pagination 243 | 244 | # must_be_admin() 245 | 246 | start, pagesize, pagenumber, eat_count = generate_simple_pagination(pagesize=10) 247 | 248 | count = aql('return length(for i in polls return i)')[0] 249 | pagination = eat_count(count) 250 | 251 | q = (QueryString(''' 252 | for i in polls sort i.t_c desc 253 | let user = (for u in users filter u.uid==i.uid return u)[0] 254 | let pollobj = (let poll = i 255 | ''') 256 | + get_poll_q + 257 | QueryString(''' 258 | )[0] 259 | '''+ f'limit {start}, {pagesize}' +''' 260 | return merge(i,{user, pollobj}) 261 | ''') 262 | ) 263 | 264 | qs = aql(q, uid=g.selfuid, silent=True) 265 | qs.map(lambda i:poll_postprocess(i['pollobj'])) 266 | 267 | return render_template_g( 268 | 'qs_polls.html.jinja', 269 | page_title='投票', 270 | # questions = qs, 271 | polls = qs, 272 | pagination = pagination, 273 | ) 274 | 275 | @app.route('/polls/') 276 | def one_poll(pollid): 277 | poll = get_poll(pollid, g.selfuid) 278 | return render_template_g( 279 | 'poll_one.html.jinja', 280 | poll = poll, 281 | ) 282 | @app.route('/polls//votes') 283 | def one_poll_votes(pollid): 284 | votes = aql(''' 285 | for i in poll_votes 286 | filter i.pollid==@pollid 287 | sort i.t_c desc 288 | let user = (for u in users filter u.uid==i.uid return u)[0] 289 | return merge(i, {user}) 290 | ''', pollid=pollid, silent=True) 291 | 292 | s = '' 293 | for v in votes: 294 | vu = v['user'] 295 | s+= f"{v['t_c']} ({vu['uid']}) {vu['name']} [rep={pagerank_format(vu)} ts={trust_score_format(vu)}] --> {v['choice']} \n" 296 | 297 | from views import doc2resp 298 | return doc2resp(s) 299 | 300 | if __name__ == '__main__': 301 | poll = str2p(test_text) 302 | print(poll) 303 | 304 | poll = p2str(poll) 305 | print(poll) 306 | 307 | poll = str2p(poll) 308 | print(poll) 309 | 310 | # render_poll(get_poll('38311091', -1)) 311 | -------------------------------------------------------------------------------- /pymain.bat: -------------------------------------------------------------------------------- 1 | :looop 2 | python main.py 3 | timeout /t 2 4 | goto looop -------------------------------------------------------------------------------- /pymain_debug.bat: -------------------------------------------------------------------------------- 1 | set debug=1 2 | set port=50000 3 | cmd /k python main.py 4 | -------------------------------------------------------------------------------- /questions.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from api import * 3 | from flask import g 4 | import random 5 | from app import app 6 | 7 | def str2q(s): 8 | s = s.split('$') 9 | 10 | if len(s)!=6: 11 | raise Exception('not splitted into 6 parts') 12 | 13 | q = [t.strip() for t in s] 14 | question = q[0] 15 | choices = q[1:1+4].copy() 16 | category = q[5] 17 | 18 | qobj = dict( 19 | question=question, 20 | choices=choices, 21 | category=category, 22 | # uid=g.current_user['uid'], 23 | # t_c=tc, 24 | ) 25 | return qobj 26 | 27 | def q2str(q): 28 | return '\n$'.join([q['question']]+q['choices']+[q['category']]) 29 | 30 | def add_question(s): 31 | qobj = str2q(s) 32 | 33 | tc = time_iso_now() 34 | qobj['uid'] = g.logged_in['uid'] 35 | qobj['t_c'] = tc 36 | 37 | aql('insert @k into questions', k=qobj) 38 | 39 | def modify_question(qid, s): 40 | q = str2q(s) 41 | q['_key'] = qid 42 | q['t_u'] = time_iso_now() 43 | aql('update @k with @k in questions', k=q) 44 | 45 | def find_question(qq): 46 | return aql('for i in questions filter i.question==@qq return i',qq=qq) 47 | 48 | # def add_questions(s): 49 | # s = s.split('##') 50 | # for q in s: 51 | # add_question(q) 52 | 53 | def choose_questions(seed, nquestions): 54 | rng = random.Random(seed) 55 | 56 | # num questions available 57 | nqa = aql('return length(for i in questions filter i.delete==null return i)', silent=True)[0] 58 | 59 | got_questions = [] 60 | gqkeys = {} 61 | while len(got_questions) dttd(seconds=time_to_submit*60): 160 | raise Exception('time limit exceeded. please try again later') 161 | 162 | # from questions import qs, qsd 163 | 164 | total = len(answers) 165 | # assert total==5 166 | score = 0 167 | for idx, choice in enumerate(answers): 168 | if choice == -1: 169 | # -1 means no choice 170 | continue 171 | 172 | question = exam['questions'][idx] 173 | chosen = question['choices'][choice] 174 | 175 | found = find_question(question['question']) 176 | 177 | if not found: 178 | raise Exception('question not found in db') 179 | print(found) 180 | if chosen == found[0]['choices'][0]: 181 | # answer is correct 182 | score+=1 183 | 184 | print_err('score:', score) 185 | if score < min_pass_score: 186 | 187 | aql('insert @k into answersheets',k=dict( 188 | # invitaiton=code, 189 | examid=eid, 190 | answers=answers, 191 | t_c = now, 192 | passed = False, 193 | salt = get_current_salt(), 194 | score = score, 195 | )) 196 | raise Exception('your score is too low. please try again') 197 | 198 | # obtain an invitation code 199 | code = generate_invitation_code(None) 200 | 201 | aql('insert @k into answersheets',k=dict( 202 | invitaiton=code, 203 | examid=eid, 204 | answers=answers, 205 | t_c = now, 206 | passed = True, 207 | salt = get_current_salt(), 208 | score = score, 209 | )) 210 | 211 | return {'url':'/register?code='+code, 'code':code} 212 | 213 | @register('add_question') 214 | def _(): 215 | must_be_admin() 216 | j = g.j 217 | qs = j['question'] 218 | add_question(qs) 219 | return {'error':False} 220 | @register('modify_question') 221 | def _(): 222 | must_be_admin() 223 | qv = g.j['question'] 224 | qid = g.j['qid'] 225 | modify_question(qid, qv) 226 | return {'error':False} 227 | 228 | @stale_cache(ttr=10, ttl=3600) 229 | def get_choice_stats(): 230 | res = aql(""" 231 | for i in answersheets 232 | sort i.t_c desc 233 | filter i.examid!=null 234 | 235 | let exam = (for e in exams filter e._key==i.examid return e)[0] 236 | 237 | for j in range(0, length(i.answers)-1) 238 | let ans = i.answers[j] 239 | let qid = exam.questions[j]._id 240 | 241 | or (for k in questions filter k.question==exam.questions[j].question 242 | return k._id)[0] 243 | 244 | filter qid 245 | 246 | let qchoice = exam.questions[j].choices[ans] 247 | 248 | // determine each choice is correct or not 249 | //let q = document(qid) 250 | //let correct = (qchoice == q.choices[0])?1:0 251 | 252 | //return {qchoice, correct, qid} 253 | 254 | collect aqid=qid aggregate tot=count(qid) into result_groups = qchoice 255 | let q = document(aqid) 256 | let cstat = ( 257 | for qchoice in result_groups collect aqc=qchoice with count into n 258 | let fraction = n/tot 259 | sort fraction desc 260 | 261 | // determine each choice is correct or not 262 | let correct = (aqc == q.choices[0])?1:0 263 | return {choice:aqc, count:n, fraction, correct} 264 | ) 265 | sort tot desc 266 | return {qid:aqid, question:q.question, uid:q.uid, choices:cstat, total:tot} 267 | """, silent=True) 268 | return res 269 | 270 | 271 | if __name__ == '__main__': 272 | print(qs) 273 | print(make_exam('asdf', 3)) 274 | # 275 | # for i in qs: 276 | # insert_question(i) 277 | -------------------------------------------------------------------------------- /quotes.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from commons import * 3 | 4 | import random 5 | 6 | @app.route('/quotes') 7 | def show_quotes(): 8 | return render_template_g('quotes.html.jinja', 9 | page_title="语录", 10 | ) 11 | 12 | @stale_cache(ttr=10, ttl=900) 13 | def get_quotes():return get_quotes_raw() 14 | 15 | @stale_cache(ttr=30, ttl=900) 16 | def get_quotes_slow():return get_quotes_raw() 17 | 18 | def get_quotes_raw(): 19 | quotes = aql(''' 20 | for i in entities 21 | 22 | filter i.type=='famous_quotes' or i.type=='famous_quotes_v2' 23 | sort i.t_c desc 24 | 25 | let user = (for u in users filter u.uid==i.uid return u)[0] 26 | return merge(i, {user}) 27 | 28 | //for j in i.doc 29 | //return {quote:j[0], quoting:j[1], user, t_u:(i.t_u or i.t_c)} 30 | 31 | ''', silent=True) 32 | 33 | q = [] 34 | for i in quotes: 35 | if i['type']=='famous_quotes': 36 | if isinstance(i['doc'], list): 37 | for j in i['doc']: 38 | if len(j)>=2: 39 | q.append(dict( 40 | quote=j[0], 41 | quoting=j[1], 42 | user=i['user'], 43 | t_u= i['t_e'] if 't_e' in i else i['t_c'], 44 | **{'_key':i['_key']}, 45 | )) 46 | 47 | elif i['type']=='famous_quotes_v2': 48 | if 'quoting' in i['doc'] and 'quotes' in i['doc']: 49 | if isinstance(i['doc']['quotes'], list): 50 | for j in i['doc']['quotes']: 51 | q.append(dict( 52 | quote=j, 53 | quoting=i['doc']['quoting'], 54 | user=i['user'], 55 | t_u= i['t_e'] if 't_e' in i else i['t_c'], 56 | **{'_key':i['_key']}, 57 | )) 58 | 59 | return q 60 | 61 | def get_quote(): 62 | quotes = get_quotes_slow() 63 | return random.choice(quotes) 64 | -------------------------------------------------------------------------------- /quotes.txt: -------------------------------------------------------------------------------- 1 | [["真理是时间之产物,而不是权威之产物。", "弗兰西斯·培根"], ["Nothing happens until something moves.", "Albert Einstein"], ["Whenever you feel like criticizing any one, just remember that all the people in this world haven’t had the advantages that you’ve had.", "The Great Gatsby"], ["比起法西斯,我宁愿做一只猪,猪是没有国家和法律可言的。", "《红猪》"], ["请记得那些对你好的人,因为他本可以不这样。", "《千与千寻》"], ["Peace has cost you your strength! Victory has defeated you.", "The Dark Knight Rises"], ["安逸令你虚弱,胜利麻痹了你的神经。", "《黑暗骑士崛起》"], ["Most men die at twenty or thirty; thereafter they are only reflections of themselves: for the rest of their lives they are aping themselves, repeating from day to day more and more mechanically and affectedly what they said and did and thought and loved when they were alive.", "'Jean-Christophe' by Romain Rolland"], ["灾难并不是死了两万人这样一件事,而是死了一个人这件事,发生了两万次。 ", "北野武"], ["Government is not the solution to our problem, government is the problem.", "Ronald Reagan"], ["If the facts scare you, the problem isn't with the facts.", ""], ["我想在一切终结的时候,能够像一个真正的诗人那样说:我们不是懦夫,我们做完了所有能做的。", "阿莱杭德娜·皮扎尼克"], ["人生还不如波德莱尔的一行诗。", "芥川龙之介"], ["The highest activity a human being can attain is learning for understanding, because to understand is to be free. 人类可以达到的最高行为是学会理解,因为理解使人自由。", "斯宾诺莎"], ["A fronte praecipitium a tergo lupi. 悬崖在面前,狼群在背后。", ""], ["Absenti nemo non nocuisse velit. 愿没有人会说不在场人的坏话。", ""], ["Abusus non tollit usum. 滥用不排除好用。", ""], ["Actus non facit reum nisi mens est rea. 非有意犯罪的行为不算犯罪行为。", ""], ["Aliquando bonus dormitat Homerus. 有时候连好人荷马也会打瞌睡。", ""], ["Happiness is a virtue, not its reward. 快乐是一种美德,而不是一种奖赏。", "斯宾诺莎"], ["Be careful what you wish for, lest it come true! 小心许愿,当心成真!", "伊索寓言"]] -------------------------------------------------------------------------------- /quotes_process.py: -------------------------------------------------------------------------------- 1 | quotes = ''' 2 | 真理是时间之产物,而不是权威之产物。##弗兰西斯·培根 3 | Nothing happens until something moves.##Albert Einstein 4 | Whenever you feel like criticizing any one, just remember that all the people in this world haven’t had the advantages that you’ve had.##The Great Gatsby 5 | 比起法西斯,我宁愿做一只猪,猪是没有国家和法律可言的。##《红猪》 6 | 请记得那些对你好的人,因为他本可以不这样。##《千与千寻》 7 | 8 | Peace has cost you your strength! Victory has defeated you.##The Dark Knight Rises 9 | 安逸令你虚弱,胜利麻痹了你的神经。##《黑暗骑士崛起》 10 | 11 | Most men die at twenty or thirty; thereafter they are only reflections of themselves: for the rest of their lives they are aping themselves, repeating from day to day more and more mechanically and affectedly what they said and did and thought and loved when they were alive.##'Jean-Christophe' by Romain Rolland 12 | 13 | 灾难并不是死了两万人这样一件事,而是死了一个人这件事,发生了两万次。 ##北野武 14 | Government is not the solution to our problem, government is the problem.##Ronald Reagan 15 | If the facts scare you, the problem isn't with the facts. 16 | 我想在一切终结的时候,能够像一个真正的诗人那样说:我们不是懦夫,我们做完了所有能做的。##阿莱杭德娜·皮扎尼克 17 | 人生还不如波德莱尔的一行诗。##芥川龙之介 18 | The highest activity a human being can attain is learning for understanding, because to understand is to be free. 人类可以达到的最高行为是学会理解,因为理解使人自由。##斯宾诺莎 19 | A fronte praecipitium a tergo lupi. 悬崖在面前,狼群在背后。 20 | 21 | Absenti nemo non nocuisse velit. 愿没有人会说不在场人的坏话。 22 | 23 | Abusus non tollit usum. 滥用不排除好用。 24 | 25 | Actus non facit reum nisi mens est rea. 非有意犯罪的行为不算犯罪行为。 26 | 27 | Aliquando bonus dormitat Homerus. 有时候连好人荷马也会打瞌睡。 28 | 29 | Happiness is a virtue, not its reward. 快乐是一种美德,而不是一种奖赏。##斯宾诺莎 30 | Be careful what you wish for, lest it come true! 小心许愿,当心成真!##伊索寓言 31 | ''' 32 | 33 | import monkeypatch 34 | 35 | quotes = quotes.split('\n').map(lambda l:l.strip()).filter(lambda l:len(l)>0) 36 | 37 | quotes = quotes.map(lambda s:s.split('##')).map(lambda l:(l[0], l[1] if len(l)==2 else '')) 38 | 39 | import random 40 | 41 | def get_quote(): 42 | return random.choice(quotes) 43 | 44 | if __name__ == '__main__': 45 | # for i in quotes: 46 | # print(get_quote()) 47 | 48 | import json 49 | a = (json.dumps(quotes, ensure_ascii=False)) 50 | print(a) 51 | open('quotes.txt','w',encoding='utf-8').write(a) 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 2047 2 | 3 | An attempt to bring 2049bbs.xyz back to life. 4 | 5 | Backstory: [Silenced in China: The Archivists](https://www.hrw.org/news/2020/07/22/silenced-china-archivists) 6 | 7 | In short, the former owner/webmaster of was detained by the Chinese Government. 8 | 9 | We decided to make a new forum titled 2047 that continues the legacy of 2049bbs. 10 | 11 | ## Steps 12 | 13 | 1. Install the dependencies. See `deploy.sh` 14 | 15 | 2. You can either restore the data from a database dump as described in `deploy.sh`, or parse the crawled backup files from https://github.com/2049bbs/2049bbs.github.io 16 | 17 | ```bash 18 | # download backup files 19 | cd parse 20 | git clone https://github.com/2049bbs/2049bbs.github.io 21 | 22 | # parse, put into DB, process 23 | python parse.py 24 | python standarize.py 25 | ``` 26 | 27 | 5. access from browser: `127.0.0.1:5000` 28 | 29 | ## License 30 | 31 | MIT 32 | -------------------------------------------------------------------------------- /recently.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | 3 | from commons import * 4 | 5 | tf = time_factor = 0.99999985 6 | 7 | spans = [] 8 | for i in range(11): 9 | p = 1.5 10 | b = -9 11 | a = 10 12 | spans.append([a*p**i+b if i else 0, a*p**(i+1)+b]) 13 | 14 | def avg_integrate(low, high): 15 | return integrate(low, high) / (high-low) 16 | 17 | def integrate(low, high, fact=tf): 18 | ''' 19 | integrate 0.9^x from 1 to 10 20 | ''' 21 | def antid(x): 22 | return fact**x / math.log(fact) 23 | 24 | return antid(high) - antid(low) 25 | 26 | if __name__ == '__main__': 27 | print(spans) 28 | print(integrate(1, 10, .9)) 29 | 30 | for low,high in spans: 31 | avgi = avg_integrate(low*86400, high*86400) 32 | if __name__=='__main__': 33 | print(f'{low:.4f}, {high:.4f}, {avgi:.4f}') 34 | 35 | exponential_falloff_spans = _efs = [] 36 | 37 | for low,high in spans: 38 | avgi = avg_integrate(low*86400, high*86400) 39 | 40 | _efs.append(( 41 | -low*86400, # later 42 | -high*86400, # earlier 43 | avgi, # factor 44 | )) 45 | 46 | def get_exponential_falloff_spans_for_now(): 47 | return [ 48 | [time_iso_now(earlier), time_iso_now(later), factor] 49 | for i, (later, earlier, factor) in enumerate(_efs) 50 | ] 51 | 52 | if __name__ == '__main__': 53 | print(str(get_exponential_falloff_spans_for_now()).replace("'", '"')) 54 | 55 | if __name__ == '__main__': 56 | timethis('$get_exponential_falloff_spans_for_now()') 57 | -------------------------------------------------------------------------------- /runcode.py: -------------------------------------------------------------------------------- 1 | from pytio import Tio, TioRequest, TioFile 2 | import subprocess, sys, inspect 3 | 4 | from cachetools.func import * 5 | from commons_static import * 6 | 7 | class CorrectedTioRequest(TioRequest): 8 | ''' 9 | the author of pytio package did not fully test his code. 10 | full of bugs. 11 | ''' 12 | 13 | def append_binary_file(self, name:str, content:bytes): 14 | self._bytes += b'F'+bytes(name, 'utf-8')+b'\x00' \ 15 | + str(len(content)).encode('utf-8') + b'\x00' \ 16 | + content + b'\x00' 17 | 18 | @lru_cache(maxsize=128) 19 | def run_python_code(code, stdin=b'', online=True): 20 | use_tio = online 21 | 22 | def sanitize(s): return s.replace(b'\r\n',b'\n') 23 | 24 | if use_tio: 25 | tio = Tio() 26 | request = CorrectedTioRequest(lang='python3', code=code) 27 | request.append_binary_file('.input.tio', stdin) 28 | 29 | print(f'sending tio request...({len(stdin)})') 30 | response = tio.send(request) 31 | print(f'got tio response') 32 | 33 | res = iif(isinstance(response._result, bytes), 34 | response._result, b'') 35 | 36 | err = iif(isinstance(response._error, bytes), 37 | response._error, b'') 38 | 39 | else: 40 | res = subprocess.run([sys.executable, 41 | '-Wignore','-I','-X', 'utf8', '-c', code], 42 | input=stdin, 43 | capture_output=True) 44 | 45 | err = res.stderr 46 | res = res.stdout 47 | 48 | return sanitize(res), sanitize(err) 49 | 50 | if __name__ == '__main__': 51 | if 1: 52 | res, err = run_python_code( 53 | 'a = int(input()); print(a,a*2)', b'33', online=False) 54 | k = res+err 55 | print(k.decode('utf-8')+'(eof)local', len(k)) 56 | 57 | res, err = run_python_code( 58 | 'a = int(input()); print(a,a*2)', b'33', online=True) 59 | k = res+err 60 | print(k.decode('utf-8')+'(eof)online', len(k)) 61 | 62 | def run_method_remotely(c, mname, args:tuple): 63 | import base64,pickle 64 | 65 | remote_code = ''' 66 | import base64,pickle,sys 67 | packed = sys.stdin.buffer.read() 68 | packed = base64.b64decode(packed) 69 | c, mname, args = pickle.loads(packed) 70 | print(c, mname, args) 71 | 72 | res = c.__getattr__(mname)(*args) 73 | res = base64.b64encode(pickle.dumps(res)) 74 | sys.stdout.buffer.write(res) 75 | print(res) 76 | ''' 77 | 78 | pickled = base64.b64encode(pickle.dumps((c, mname, args))) 79 | print('input pickled', pickled) 80 | 81 | out, err = run_python_code(remote_code, stdin=pickled, online=False) 82 | 83 | print(out.decode('utf8')) 84 | print(err.decode('utf8')) 85 | 86 | # run_method_remotely(k,'double',(3,)) 87 | -------------------------------------------------------------------------------- /sb1024_encryption.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from api import register, es 3 | from app import app 4 | 5 | from flask import request 6 | 7 | import sys 8 | sys.path.append('./sb1024') 9 | from sb1024 import sb1024_cc2_str_encrypt, sb1024_cc2_str_decrypt 10 | @register('sb1024_encrypt') 11 | def _(): 12 | plain = es('plain') 13 | key = es('key') 14 | ct = sb1024_cc2_str_encrypt(key, plain) 15 | return {'ct': ct} 16 | 17 | @register('sb1024_decrypt') 18 | def _(): 19 | ct = es('ct') 20 | key = es('key') 21 | plain = sb1024_cc2_str_decrypt(key, ct) 22 | return {'plain':plain} 23 | 24 | @app.route('/sinocrypt') 25 | def sinocrypt(): 26 | supplied_key = request.args['key'] if 'key' in request.args else '' 27 | 28 | return render_template_g( 29 | 'sb1024.html.jinja', 30 | page_title='SinoCrypt', 31 | skey=supplied_key, 32 | ) 33 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | from commons import * 2 | from app import app 3 | 4 | aqlc_pmf = AQLController(None, 'dbpmf') 5 | aql_pmf = aqlc_pmf.aql 6 | 7 | def break_terms(s): 8 | s = s.split(' ') 9 | s = [i.strip() for i in s if len(i.strip())] 10 | s = s[:4] # take first 4 terms only 11 | return s 12 | 13 | def break_terms_arangosearch(s): 14 | return aql("return tokens(@s,'text_zh')", s=s)[0] 15 | 16 | @stale_cache(maxsize=256, ttr=10, ttl=3600) 17 | def search_term(s, start=0, length=25): 18 | # 1. break str into pieces 19 | original_terms = s 20 | bt = broken_terms = break_terms_arangosearch(original_terms) 21 | bt = [i for i in bt if i not in '的 不 是 了 嗯 一 个'.split(' ')] 22 | bt2 = broken_terms2 = break_terms(original_terms) 23 | 24 | bta = broken_terms+broken_terms2 25 | 26 | search_result2 = aql(f''' 27 | for i in sv 28 | search analyzer( 29 | i.title in @bt 30 | or i.content in @bt 31 | or boost(ngram_match(i.title, @bt2, 0.1, 'text_zh'), 20) 32 | or boost(ngram_match(i.content, @bt2, 0.1, 'text_zh'), 20) 33 | , 'text_zh') 34 | 35 | let score = tfidf(i) + sqrt(i.votes) * 5 36 | sort score desc 37 | limit {start},{length} 38 | 39 | let t = i.title?null:(for k in threads filter k.tid==i.tid return k)[0] 40 | let user = (for u in users filter u.uid==i.uid return u)[0] 41 | 42 | return merge(i, {{score, t, user}}) 43 | 44 | ''', silent=True, bt2 = bt2[0], bt = bt) 45 | 46 | # print(search_result) 47 | # print(search_terms) 48 | 49 | uq = ''' 50 | for i in sv 51 | search analyzer( 52 | i.name in tokens(@st, 'text_zh') 53 | or i.brief in tokens(@st, 'text_zh') 54 | , 'text_zh') 55 | 56 | let score = tfidf(i) + (sqrt(i.nlikes)*1.0 + sqrt(i.nposts)*0.1+sqrt(i.nthreads)*1.0)*0.6 57 | sort score desc 58 | limit 6 59 | filter i.delete==null 60 | return merge(i,{score}) 61 | ''' 62 | 63 | if start==0: 64 | user_search_result = aql(uq, silent=True, st=original_terms)[:3] 65 | else: 66 | user_search_result = None 67 | 68 | return dict( 69 | users = user_search_result, 70 | results = search_result2, 71 | terms = bt, 72 | original_terms = original_terms, 73 | ) 74 | 75 | @lru_cache(maxsize=256) 76 | def search_yyets(s): 77 | return aql_pmf(''' 78 | for i in yyv 79 | search analyzer( 80 | i.title in tokens(@st, 'text_zh') 81 | or boost(ngram_match(i.title, @st, 0.05, 'text_zh'), 3) 82 | , 'text_zh') 83 | 84 | let score = tfidf(i) //+ sqrt(i.votes) * 3 85 | sort score desc 86 | 87 | limit 36 88 | 89 | let category = i.data.data.info.channel_cn or i.data.data.info.channel 90 | let area = i.data.data.info.area 91 | let no = i.no 92 | let title = i.title 93 | 94 | return merge({title, no, score, category, area}) 95 | ''', silent=True, st=s) 96 | 97 | @app.route('/search') 98 | def search(): 99 | q = ras('q').strip() 100 | if not q: 101 | return render_template_g( 102 | 'search.html.jinja', 103 | hide_title=True, 104 | page_title='搜索', 105 | has_result=False, 106 | ) 107 | else: 108 | result = search_term(q, start=0, length=20) 109 | return render_template_g( 110 | 'search.html.jinja', 111 | query=q, 112 | hide_title=True, 113 | page_title='搜索 - '+q, 114 | has_result=True, 115 | **result 116 | ) 117 | 118 | @app.route('/search_yyets') 119 | def search_yyets_page(): 120 | q = ras('q').strip() 121 | if not q: 122 | return render_template_g( 123 | 'search.html.jinja', 124 | hide_title=True, 125 | page_title='影视搜索', 126 | has_result=False, 127 | 128 | mode='yyets', 129 | ) 130 | else: 131 | yyets_results = search_yyets(q) 132 | return render_template_g( 133 | 'search.html.jinja', 134 | query=q, 135 | hide_title=True, 136 | page_title='影视搜索 - '+q, 137 | has_result=True, 138 | 139 | mode='yyets', 140 | yyets_results = yyets_results, 141 | original_terms = q, 142 | ) 143 | 144 | aqlc_pmf.create_index('yyets', type='persistent', fields=['no'], 145 | unique=False,sparse=False) 146 | 147 | @lru_cache(maxsize=128) 148 | def yyets_formatter(_id): 149 | j = aql_pmf('''for i in yyets filter i.no==@no return i''', silent=True, no=_id) 150 | 151 | if not j: 152 | raise Exception('id not found in yyets') 153 | 154 | j = j[0] 155 | 156 | lines = [] 157 | 158 | l = j['data']['data']['list'] 159 | for seas in l: 160 | season_str = seas['season_cn'] 161 | resos = seas['items'] 162 | 163 | for reso in resos: 164 | details = resos[reso] 165 | for ep in details: 166 | epn = ep['episode'] 167 | files = ep['files'] 168 | 169 | name = ep['name'] 170 | size = ep['size'] 171 | if isinstance(files, list): 172 | for file in files: 173 | addr = file['address'] 174 | meth = file['way_cn'] 175 | pw = file['passwd'] 176 | 177 | line = ' '.join( 178 | [season_str, reso, name, size, meth, addr, pw]) 179 | lines.append(line) 180 | 181 | lines = sorted(lines) 182 | lines.insert(0, '数据来自 https://t.me/mikuri520/676') 183 | lines = '\n'.join(lines) 184 | return j,lines 185 | 186 | @app.route('/yyets/') 187 | def yyets_page(_id): 188 | j,lines = yyets_formatter(_id) 189 | 190 | # r = make_response(obj2json(j), 200) 191 | # r.headers['Content-Type'] = 'application/json' 192 | 193 | r = make_text_response(lines) 194 | 195 | return r 196 | 197 | def _main(): 198 | search('自由 亚洲') 199 | 200 | if __name__ == '__main__': 201 | _main() 202 | -------------------------------------------------------------------------------- /session.py: -------------------------------------------------------------------------------- 1 | # because flask session is a total disaster 2 | 3 | from itsdangerous import Signer 4 | import os,json,base64 5 | from base64 import b64encode,b64decode 6 | from flask import request, g 7 | from colors import * 8 | 9 | from functools import lru_cache 10 | 11 | def get_secret(): 12 | fn = 'secret.bin' 13 | if os.path.exists(fn): 14 | f = open(fn, 'rb');r = f.read();f.close() 15 | else: 16 | r = os.urandom(32) 17 | f = open(fn, 'wb');f.write(r);f.close() 18 | return r 19 | 20 | secret = get_secret() 21 | signer = Signer(secret) 22 | 23 | # rough implementation of JSON Web Signature (JWS) 24 | def signj(json_object): # object -> utf8str 25 | json_str = json.dumps(json_object) 26 | json_bin = json_str.encode('utf-8') 27 | json_bin_b64 = b64encode(json_bin) 28 | json_bin_b64_signed_bin = signer.sign(json_bin_b64) 29 | json_bin_b64_signed_str = json_bin_b64_signed_bin.decode('utf-8') 30 | return json_bin_b64_signed_str 31 | 32 | def unsignj(json_bin_b64_signed_str): 33 | return unsignj_cached(json_bin_b64_signed_str).copy() 34 | 35 | @lru_cache(maxsize=4096) 36 | def unsignj_cached(json_bin_b64_signed_str): # utf8str -> object 37 | json_bin_b64_signed_bin = json_bin_b64_signed_str.encode('utf-8') 38 | json_bin_b64 = signer.unsign(json_bin_b64_signed_bin) 39 | json_bin = b64decode(json_bin_b64) 40 | json_str = json_bin.decode('utf-8') 41 | json_object = json.loads(json_str) 42 | return json_object 43 | 44 | def save_session(resp): 45 | curr_sess_string = get_current_session_str() 46 | newly_signed_sess_string = signj(g.session) 47 | 48 | if newly_signed_sess_string != curr_sess_string: 49 | 50 | print_down('save_session', g.session) 51 | 52 | resp.set_cookie( 53 | key='session', 54 | value=newly_signed_sess_string, 55 | max_age=86400*31, 56 | httponly=True, 57 | samesite='strict', 58 | ) 59 | 60 | def load_session(): 61 | css = get_current_session_str() 62 | if not css: return {} 63 | try: 64 | return unsignj(css) 65 | except Exception as e: 66 | print_err('unsign err', e) 67 | return {} 68 | 69 | def get_current_session_str(): 70 | return request.cookies.get('session', default='') 71 | 72 | 73 | def sign(s): 74 | return signer.sign(s.encode('utf-8')) 75 | 76 | def unsign(s): 77 | try: 78 | u = signer.unsign(s) 79 | u = u.decode('utf-8') 80 | except Exception as e: 81 | return '{}' 82 | else: 83 | return u 84 | 85 | 86 | if __name__ == '__main__': 87 | a = sign('helloworld') 88 | print(a) 89 | b = unsign(a) 90 | print(b) 91 | c = unsign(a+b'j') 92 | print(unsign(c)) 93 | 94 | d = signj({1:2}) 95 | e = unsignj(d) 96 | print(d) 97 | print(e) 98 | -------------------------------------------------------------------------------- /start_load_balancer.bat: -------------------------------------------------------------------------------- 1 | python load_balancer.py 2 | 3 | timeout 10 -------------------------------------------------------------------------------- /takeoff/takeoff_search.py: -------------------------------------------------------------------------------- 1 | from takeoff import * 2 | import time 3 | 4 | class Search: 5 | def __init__(self, noisy=False): 6 | self.g = [i(noisy=noisy) for i in ( 7 | Weibo,QQ, JD, SF, Pingan, CarOwner20, Telegram40, Hotel2013, 8 | Momo2015, 9 | )] 10 | 11 | def get_sources(self): 12 | return [dict( 13 | path = i.path, 14 | origsize = i.origsize, 15 | dbsize = i.dbsize, 16 | abbr = i.abbr, 17 | ) for i in self.g] 18 | 19 | def search(self, s): 20 | t0 = time.time() 21 | 22 | s = s.strip().split(' ')[0] 23 | res = [] 24 | 25 | for i in self.g: 26 | r = i.find(s) 27 | res += r 28 | 29 | res = reversed( 30 | sorted(res, key=lambda a: a['maxscore']) 31 | ) 32 | 33 | res = [k for k in res if k['maxscore'] > 0.25] 34 | 35 | t1 = time.time()-t0 36 | 37 | return res, t1 38 | 39 | if __name__ == '__main__': 40 | s = Search(noisy=True) 41 | print(s.search('13915466930')) 42 | print(s.search('3798002017')) 43 | -------------------------------------------------------------------------------- /template_globals.py: -------------------------------------------------------------------------------- 1 | # ugh 2 | 3 | tgr = template_globals_registry = {} 4 | -------------------------------------------------------------------------------- /templates/404.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 |
4 | 5 | 6 | {%if not e500%} 7 |
8 | {%if to_show%} 9 | {{to_show}} 10 | {%else%} 11 | 您要找的页面不存在 12 | {%endif%} 13 |
14 |
15 | {%if reason%} 16 | {{convert_markdown(reason)|safe}} 17 | {%else%} 18 | 点击 19 | 这里 20 | 回到首页,点击 21 | 这里 22 | 返回上一页 23 | {%endif%} 24 |
25 | 26 | {%else%} 27 |
28 | 服务器内部错误 29 |
30 |
31 | 请等待后台工程师处理 32 |
33 |
34 | 点击 35 | 这里 36 | 回到首页,点击 37 | 这里 38 | 返回上一页 39 |
40 | {%endif%} 41 | 42 | {%if err %} 43 |
44 |
{{err}}
45 |
46 | {%endif%} 47 | 48 |
49 |

50 | 如果你认为这个页面不应该出现,请通过element/matrix联系 51 | @thphd:matrix.org 52 | 或 53 | #teahall:matrix.org 54 | 向我们反馈问题。 55 |

56 |
57 | 58 |
59 | {%endblock%} 60 | -------------------------------------------------------------------------------- /templates/chat.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 |
6 | 7 |
8 | 9 |
10 | 11 | {%for m in messages%} 12 | {{macros.post_chat_message(m)}} 13 | {%endfor%} 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 | 29 | {%endblock%} 30 | -------------------------------------------------------------------------------- /templates/chat_message.html.jinja: -------------------------------------------------------------------------------- 1 | {%import 'macros.html.jinja' as macros with context%} 2 | 3 | {{macros.post_chat_message(message)}} 4 | -------------------------------------------------------------------------------- /templates/choice_stats.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 |
7 | 8 | {%for q in cstat%} 9 | 10 |

11 | {{macros.username(get_user_by_id_cached(q.uid))}} 12 | 13 | {%if q.question.startswith('!!')%} 14 | !! 15 | {%endif%} 16 |
17 | 18 | {{q.qid}} 19 | 20 | ({{q.total}}人) 21 | 22 |

23 | 24 |
    25 | {%for choice in q.choices%} 26 |
  • 27 |
    28 | {{convert_markdown(choice.choice)|safe}} 29 |
    30 | ({{choice.count}}人, {{(choice.fraction*100)|int}}%) 31 |
  • 32 | {%endfor%} 33 |
34 | 35 | {%endfor%} 36 | 37 |
38 | 39 |
40 | {%endblock%} 41 | -------------------------------------------------------------------------------- /templates/comment_section.html.jinja: -------------------------------------------------------------------------------- 1 | {# include all macros used by project #} 2 | {%import 'macros.html.jinja' as macros with context%} 3 | {{macros.comment_section(parent, comments)}} 4 | -------------------------------------------------------------------------------- /templates/conversations.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 | {%for c in conversations%} 7 | {%if c and c.last and c.last.user and c.last.to_user%} 8 | {{macros.post_conversation(c)}} 9 | {%endif%} 10 | {%endfor%} 11 | 12 |
13 |
14 |
15 | {{messaging_warning|safe}} 16 |
17 | 18 |
19 | 20 | {%endblock%} 21 | -------------------------------------------------------------------------------- /templates/css/labnol_youtube.css: -------------------------------------------------------------------------------- 1 | .youtube-player { 2 | position: relative; 3 | padding-bottom: 56.23%; 4 | /* Use 75% for 4:3 videos */ 5 | /* max-width: 500px; */ 6 | overflow: hidden; 7 | min-width: 200px; 8 | background: #000; 9 | margin-bottom: 1em; 10 | margin-top: 0.8em; 11 | /* margin: 5px; */ 12 | } 13 | 14 | .youtube-player iframe { 15 | position: absolute; 16 | /* top: 0; */ 17 | /* left: 0; */ 18 | width: 100%; 19 | height: 100%; 20 | z-index: 100; 21 | background: transparent; 22 | } 23 | 24 | .youtube-player img { 25 | bottom: 0; 26 | display: block; 27 | left: 0; 28 | margin: auto; 29 | max-width: 100%; 30 | width: 100%; 31 | position: absolute; 32 | right: 0; 33 | top: 0; 34 | border: none; 35 | height: auto; 36 | cursor: pointer; 37 | -webkit-transition: .4s all; 38 | -moz-transition: .4s all; 39 | transition: .4s all; 40 | } 41 | 42 | .youtube-player img:hover { 43 | -webkit-filter: brightness(75%); 44 | } 45 | 46 | .youtube-player .play { 47 | height: 72px; 48 | width: 72px; 49 | left: 50%; 50 | top: 50%; 51 | margin-left: -36px; 52 | margin-top: -36px; 53 | position: absolute; 54 | background: url("/images/play.png") no-repeat; 55 | cursor: pointer; 56 | } 57 | -------------------------------------------------------------------------------- /templates/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /templates/editor.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | {%if details%} 6 | {{macros.editor(target, has_title=details.has_title, title=details.title, content=details.content, mode=details.mode)}} 7 | 8 | {%else%} 9 | {{macros.editor(target, has_title=True)}} 10 | 11 | {%endif%} 12 | 13 |
14 | 15 | {%endblock%} 16 | -------------------------------------------------------------------------------- /templates/entities.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | {%if pagenumber and pagenumber==1%} 6 |
7 | {{entity_info|safe}} 8 |
9 | {%endif%} 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | {{macros.paginate(pagination, class='')}} 21 | 22 |
23 | 24 | {%for q in entities%} 25 |
26 |
27 | 类型:{{q.type}} 创建者:{{q.user.name}} 28 |
29 | ID(点击预览):{{q._key}} 30 |
31 | 32 | 33 | 34 |
35 | {%if q.uid==g.logged_in.uid or g.logged_in.uid==5108%} 36 | 应用修改 37 | 删除 38 | {%endif%} 39 | 40 | {{format_time_dateifnottoday(q.t_c)}} 41 | 42 |
43 | 44 |
45 | 46 | 47 | {%endfor%} 48 | 49 | 50 |
51 | 52 | {{macros.paginate(pagination, class='')}} 53 | 54 | 55 |
56 | 57 | 58 | {%endblock%} 59 | -------------------------------------------------------------------------------- /templates/exam.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 |
7 | 8 |

本考试开卷,请考生于20分钟内完成,未按时提交答题卡的作零分处理。

9 | 10 | 11 |
12 |
    13 | {%for q in exam.questions %} 14 | {% set qi = loop.index0 %} 15 |
  1. 16 |
    17 |
    18 | {{convert_markdown(q.question)|safe}} 19 |
    20 |
    21 | {%for choice in q.choices%} 22 | {%set ci = loop.index0%} 23 |
    24 | 25 | {# {{choice|safe}} #} 26 |
    27 | {{convert_markdown(choice)|safe}} 28 |
    29 |
    30 | {%endfor%} 31 |
    32 |
    33 | {%set user = get_user_by_id_cached(q.uid)%} 34 | ({{q.category}})by {{macros.username(user,class='opaque5')}} 35 |
    36 |
    37 | 38 |
  2. 39 | 40 | {%endfor%} 41 |
42 | 43 | 44 | 45 |
46 |
47 | {%endblock%} 48 | -------------------------------------------------------------------------------- /templates/favorites.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {{macros.paginate(pagination, class='padlr')}} 5 | 6 |
7 | {{macros.mixed_content_list(list_items)}} 8 |
9 | 10 | {{macros.paginate(pagination, class='padlr')}} 11 | 12 | {%endblock%} 13 | -------------------------------------------------------------------------------- /templates/highlight/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2006, Ivan Sagalaev. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /templates/highlight/styles/atom-one-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Atom One Light by Daniel Gamage 4 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 5 | 6 | base: #fafafa 7 | mono-1: #383a42 8 | mono-2: #686b77 9 | mono-3: #a0a1a7 10 | hue-1: #0184bb 11 | hue-2: #4078f2 12 | hue-3: #a626a4 13 | hue-4: #50a14f 14 | hue-5: #e45649 15 | hue-5-2: #c91243 16 | hue-6: #986801 17 | hue-6-2: #c18401 18 | 19 | */ 20 | 21 | .hljs { 22 | display: block; 23 | overflow-x: auto; 24 | padding: 0.5em; 25 | color: #383a42; 26 | background: #fafafa; 27 | } 28 | 29 | .hljs-comment, 30 | .hljs-quote { 31 | color: #a0a1a7; 32 | font-style: italic; 33 | } 34 | 35 | .hljs-doctag, 36 | .hljs-keyword, 37 | .hljs-formula { 38 | color: #a626a4; 39 | } 40 | 41 | .hljs-section, 42 | .hljs-name, 43 | .hljs-selector-tag, 44 | .hljs-deletion, 45 | .hljs-subst { 46 | color: #e45649; 47 | } 48 | 49 | .hljs-literal { 50 | color: #0184bb; 51 | } 52 | 53 | .hljs-string, 54 | .hljs-regexp, 55 | .hljs-addition, 56 | .hljs-attribute, 57 | .hljs-meta-string { 58 | color: #50a14f; 59 | } 60 | 61 | .hljs-built_in, 62 | .hljs-class .hljs-title { 63 | color: #c18401; 64 | } 65 | 66 | .hljs-attr, 67 | .hljs-variable, 68 | .hljs-template-variable, 69 | .hljs-type, 70 | .hljs-selector-class, 71 | .hljs-selector-attr, 72 | .hljs-selector-pseudo, 73 | .hljs-number { 74 | color: #986801; 75 | } 76 | 77 | .hljs-symbol, 78 | .hljs-bullet, 79 | .hljs-link, 80 | .hljs-meta, 81 | .hljs-selector-id, 82 | .hljs-title { 83 | color: #4078f2; 84 | } 85 | 86 | .hljs-emphasis { 87 | font-style: italic; 88 | } 89 | 90 | .hljs-strong { 91 | font-weight: bold; 92 | } 93 | 94 | .hljs-link { 95 | text-decoration: underline; 96 | } 97 | -------------------------------------------------------------------------------- /templates/iframe.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | {{url}} 6 |
7 |
8 | 9 | 11 | 12 | {%endblock%} 13 | -------------------------------------------------------------------------------- /templates/images/2049bbslogo_clipped_small_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/2049bbslogo_clipped_small_pressed.png -------------------------------------------------------------------------------- /templates/images/avatar-max-img-hc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/avatar-max-img-hc.png -------------------------------------------------------------------------------- /templates/images/avatar-max-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/avatar-max-img.png -------------------------------------------------------------------------------- /templates/images/exc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/exc.png -------------------------------------------------------------------------------- /templates/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/favicon.png -------------------------------------------------------------------------------- /templates/images/favicon_new_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/favicon_new_pressed.png -------------------------------------------------------------------------------- /templates/images/jiangle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/jiangle.jpg -------------------------------------------------------------------------------- /templates/images/liren_logo_large_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/liren_logo_large_pressed.png -------------------------------------------------------------------------------- /templates/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/logo.png -------------------------------------------------------------------------------- /templates/images/mohu_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/mohu_favicon.ico -------------------------------------------------------------------------------- /templates/images/pc_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/pc_favicon.ico -------------------------------------------------------------------------------- /templates/images/pincong_bkgnd.svg: -------------------------------------------------------------------------------- 1 | 品葱 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | pincong.rocks -------------------------------------------------------------------------------- /templates/images/pincong_bkgnd_mod.svg: -------------------------------------------------------------------------------- 1 | 品葱 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | pincong.org 23 | -------------------------------------------------------------------------------- /templates/images/pincong_logo.svg: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /templates/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/play.png -------------------------------------------------------------------------------- /templates/images/pooh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/pooh.jpg -------------------------------------------------------------------------------- /templates/images/youtube_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thphd/2047/bd6781b9502c6fdbd4745be5084977f679fa3fc5/templates/images/youtube_small.png -------------------------------------------------------------------------------- /templates/js/ace/theme-monokai.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"], function(require, exports, module) { 2 | 3 | exports.isDark = true; 4 | exports.cssClass = "ace-monokai"; 5 | exports.cssText = ".ace-monokai .ace_gutter {\ 6 | background: #2F3129;\ 7 | color: #8F908A\ 8 | }\ 9 | .ace-monokai .ace_print-margin {\ 10 | width: 1px;\ 11 | background: #555651\ 12 | }\ 13 | .ace-monokai {\ 14 | background-color: #272822;\ 15 | color: #F8F8F2\ 16 | }\ 17 | .ace-monokai .ace_cursor {\ 18 | color: #F8F8F0\ 19 | }\ 20 | .ace-monokai .ace_marker-layer .ace_selection {\ 21 | background: #49483E\ 22 | }\ 23 | .ace-monokai.ace_multiselect .ace_selection.ace_start {\ 24 | box-shadow: 0 0 3px 0px #272822;\ 25 | }\ 26 | .ace-monokai .ace_marker-layer .ace_step {\ 27 | background: rgb(102, 82, 0)\ 28 | }\ 29 | .ace-monokai .ace_marker-layer .ace_bracket {\ 30 | margin: -1px 0 0 -1px;\ 31 | border: 1px solid #49483E\ 32 | }\ 33 | .ace-monokai .ace_marker-layer .ace_active-line {\ 34 | background: #202020\ 35 | }\ 36 | .ace-monokai .ace_gutter-active-line {\ 37 | background-color: #272727\ 38 | }\ 39 | .ace-monokai .ace_marker-layer .ace_selected-word {\ 40 | border: 1px solid #49483E\ 41 | }\ 42 | .ace-monokai .ace_invisible {\ 43 | color: #52524d\ 44 | }\ 45 | .ace-monokai .ace_entity.ace_name.ace_tag,\ 46 | .ace-monokai .ace_keyword,\ 47 | .ace-monokai .ace_meta.ace_tag,\ 48 | .ace-monokai .ace_storage {\ 49 | color: #F92672\ 50 | }\ 51 | .ace-monokai .ace_punctuation,\ 52 | .ace-monokai .ace_punctuation.ace_tag {\ 53 | color: #fff\ 54 | }\ 55 | .ace-monokai .ace_constant.ace_character,\ 56 | .ace-monokai .ace_constant.ace_language,\ 57 | .ace-monokai .ace_constant.ace_numeric,\ 58 | .ace-monokai .ace_constant.ace_other {\ 59 | color: #AE81FF\ 60 | }\ 61 | .ace-monokai .ace_invalid {\ 62 | color: #F8F8F0;\ 63 | background-color: #F92672\ 64 | }\ 65 | .ace-monokai .ace_invalid.ace_deprecated {\ 66 | color: #F8F8F0;\ 67 | background-color: #AE81FF\ 68 | }\ 69 | .ace-monokai .ace_support.ace_constant,\ 70 | .ace-monokai .ace_support.ace_function {\ 71 | color: #66D9EF\ 72 | }\ 73 | .ace-monokai .ace_fold {\ 74 | background-color: #A6E22E;\ 75 | border-color: #F8F8F2\ 76 | }\ 77 | .ace-monokai .ace_storage.ace_type,\ 78 | .ace-monokai .ace_support.ace_class,\ 79 | .ace-monokai .ace_support.ace_type {\ 80 | font-style: italic;\ 81 | color: #66D9EF\ 82 | }\ 83 | .ace-monokai .ace_entity.ace_name.ace_function,\ 84 | .ace-monokai .ace_entity.ace_other,\ 85 | .ace-monokai .ace_entity.ace_other.ace_attribute-name,\ 86 | .ace-monokai .ace_variable {\ 87 | color: #A6E22E\ 88 | }\ 89 | .ace-monokai .ace_variable.ace_parameter {\ 90 | font-style: italic;\ 91 | color: #FD971F\ 92 | }\ 93 | .ace-monokai .ace_string {\ 94 | color: #E6DB74\ 95 | }\ 96 | .ace-monokai .ace_comment {\ 97 | color: #75715E\ 98 | }\ 99 | .ace-monokai .ace_indent-guide {\ 100 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y\ 101 | }"; 102 | 103 | var dom = require("../lib/dom"); 104 | dom.importCssString(exports.cssText, exports.cssClass); 105 | }); (function() { 106 | ace.require(["ace/theme/monokai"], function(m) { 107 | if (typeof module == "object" && typeof exports == "object" && module) { 108 | module.exports = m; 109 | } 110 | }); 111 | })(); 112 | -------------------------------------------------------------------------------- /templates/js/ace/theme-xcode.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/xcode",["require","exports","module","ace/lib/dom"], function(require, exports, module) { 2 | 3 | exports.isDark = false; 4 | exports.cssClass = "ace-xcode"; 5 | exports.cssText = "\ 6 | .ace-xcode .ace_gutter {\ 7 | background: #e8e8e8;\ 8 | color: #333\ 9 | }\ 10 | .ace-xcode .ace_print-margin {\ 11 | width: 1px;\ 12 | background: #e8e8e8\ 13 | }\ 14 | .ace-xcode {\ 15 | background-color: #FFFFFF;\ 16 | color: #000000\ 17 | }\ 18 | .ace-xcode .ace_cursor {\ 19 | color: #000000\ 20 | }\ 21 | .ace-xcode .ace_marker-layer .ace_selection {\ 22 | background: #B5D5FF\ 23 | }\ 24 | .ace-xcode.ace_multiselect .ace_selection.ace_start {\ 25 | box-shadow: 0 0 3px 0px #FFFFFF;\ 26 | }\ 27 | .ace-xcode .ace_marker-layer .ace_step {\ 28 | background: rgb(198, 219, 174)\ 29 | }\ 30 | .ace-xcode .ace_marker-layer .ace_bracket {\ 31 | margin: -1px 0 0 -1px;\ 32 | border: 1px solid #BFBFBF\ 33 | }\ 34 | .ace-xcode .ace_marker-layer .ace_active-line {\ 35 | background: rgba(0, 0, 0, 0.071)\ 36 | }\ 37 | .ace-xcode .ace_gutter-active-line {\ 38 | background-color: rgba(0, 0, 0, 0.071)\ 39 | }\ 40 | .ace-xcode .ace_marker-layer .ace_selected-word {\ 41 | border: 1px solid #B5D5FF\ 42 | }\ 43 | .ace-xcode .ace_constant.ace_language,\ 44 | .ace-xcode .ace_keyword,\ 45 | .ace-xcode .ace_meta,\ 46 | .ace-xcode .ace_variable.ace_language {\ 47 | color: #C800A4\ 48 | }\ 49 | .ace-xcode .ace_invisible {\ 50 | color: #BFBFBF\ 51 | }\ 52 | .ace-xcode .ace_constant.ace_character,\ 53 | .ace-xcode .ace_constant.ace_other {\ 54 | color: #275A5E\ 55 | }\ 56 | .ace-xcode .ace_constant.ace_numeric {\ 57 | color: #3A00DC\ 58 | }\ 59 | .ace-xcode .ace_entity.ace_other.ace_attribute-name,\ 60 | .ace-xcode .ace_support.ace_constant,\ 61 | .ace-xcode .ace_support.ace_function {\ 62 | color: #450084\ 63 | }\ 64 | .ace-xcode .ace_fold {\ 65 | background-color: #C800A4;\ 66 | border-color: #000000\ 67 | }\ 68 | .ace-xcode .ace_entity.ace_name.ace_tag,\ 69 | .ace-xcode .ace_support.ace_class,\ 70 | .ace-xcode .ace_support.ace_type {\ 71 | color: #790EAD\ 72 | }\ 73 | .ace-xcode .ace_storage {\ 74 | color: #C900A4\ 75 | }\ 76 | .ace-xcode .ace_string {\ 77 | color: #DF0002\ 78 | }\ 79 | .ace-xcode .ace_comment {\ 80 | color: #008E00\ 81 | }\ 82 | .ace-xcode .ace_indent-guide {\ 83 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==) right repeat-y\ 84 | }"; 85 | 86 | var dom = require("../lib/dom"); 87 | dom.importCssString(exports.cssText, exports.cssClass); 88 | }); (function() { 89 | ace.require(["ace/theme/xcode"], function(m) { 90 | if (typeof module == "object" && typeof exports == "object" && module) { 91 | module.exports = m; 92 | } 93 | }); 94 | })(); 95 | -------------------------------------------------------------------------------- /templates/leet.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | {%for lcn in lcs.l%} 6 |
7 | {%set lc = lcs.d[lcn]%} 8 | 9 |
10 | {{lcn}} {{lc.name}} 11 | {%if lcn in my_subs%} 12 | 13 | {%endif%} 14 | 15 | {%if other_subs and (lcn in other_subs)%} 16 | {%set uids = other_subs[lcn] %} 17 |
18 | {%for uid in uids%} 19 | {{macros.avatar(get_user_by_id(uid), class='follower_avatar', no_decoration=True)}} 20 | {%endfor%} 21 |
22 | {%endif%} 23 | 24 |
25 | 26 | 27 |
28 | {%endfor%} 29 |
30 | {%endblock%} 31 | -------------------------------------------------------------------------------- /templates/leet_challenge.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | 5 | 13 | 14 |
15 |
16 |

{{lc.name}}

17 | {{convert_markdown(lc.description)|safe}} 18 |
19 | 20 |
21 |
{{(most_recent and most_recent.code) or lc.user_code}}
22 |
23 | 24 | 25 | 30 | 31 |
32 | 33 |
34 | 测试输入 35 |
36 | 37 |
38 | 39 |
(result will show up here)
40 | 41 |
42 | 43 |
44 | {%endblock%} 45 | -------------------------------------------------------------------------------- /templates/liangjiahe.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | {%for k in ljhd%} 6 | 7 |
8 | {{k}}. {{ljhd[k]}} 9 |
10 | 11 | {%endfor%} 12 |
13 | 14 | 15 | {%endblock%} 16 | -------------------------------------------------------------------------------- /templates/links.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | 3 | {%block content%} 4 | 5 | {%import 'macros.html.jinja' as macros with context%} 6 | 7 |
8 |
9 | 欲向此处添加更多链接,请访问 Entities 10 |
11 | 12 |
13 | 14 | 27 |
28 | {%endblock%} 29 | -------------------------------------------------------------------------------- /templates/login.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | {{zh('忘记密码')}} 20 | {{zh('注册新账号')}} 21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | {{zh('语言 (LANG)')}} 31 | {{macros.language_selector()}} 32 |
33 | 34 |
35 | 36 |
37 |

用PGP签名登陆(说明书在这里

38 |
39 | 40 |
41 | 42 |
43 | 将命令拷贝到剪贴板 (copy to clipboard) 44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 | 64 | 65 |
66 | 67 |
68 | {{public_key_info|safe}} 69 |
70 |
71 | {{cant_login_info|safe}} 72 |
73 |
74 | 75 | {%endblock%} 76 | -------------------------------------------------------------------------------- /templates/messages.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if editor_target and g.logged_in%} 5 | {{macros.post_targeted_editor(editor_target)}} 6 | {%endif%} 7 | 8 |
9 | 10 | {%for c in messages%} 11 | {{macros.post_message(c)}} 12 | {%endfor%} 13 | 14 |
15 | {%endblock%} 16 | -------------------------------------------------------------------------------- /templates/notifications.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 | {%for n in notifications%} 7 | {{macros.post_notification(n)}} 8 | {%endfor%} 9 | 10 |
11 | 12 | {%endblock%} 13 | -------------------------------------------------------------------------------- /templates/oplog.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 | {%for l in oplog%} 7 |
8 | {{macros.avatar(l.user)}} 9 | {{macros.username(l.user)}} 10 | 11 | [{{l.op}}] 12 | [{{l.target}}] 13 | 14 | {%if l.cid -%} 15 | [->{{l.cid}}] 16 | {%- endif%} 17 | 18 | [{{format_time_datetime(l.t_c)}}] 19 | 20 | 21 | {%if l.reason%} 22 | reason: {{l.reason}} 23 | {%endif%} 24 |
25 | {%endfor%} 26 | 27 |
28 | 29 | {%endblock%} 30 | -------------------------------------------------------------------------------- /templates/poll_one.html.jinja: -------------------------------------------------------------------------------- 1 | {# include all macros used by project #} 2 | {%import 'macros.html.jinja' as macros with context%} 3 | 4 | {{macros.render_poll(poll)}} 5 | -------------------------------------------------------------------------------- /templates/postlist.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'threadlist.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if t%}{# if there is a thread object #} 5 |
6 | {{macros.post_postlist_header(t)}} 7 |
8 | {%endif%} 9 | 10 | {%if t and t.count%} 11 |
12 |
13 | {{ 14 | spf( 15 | zhen('$0 个评论','$0 comment$es0') 16 | if t.mode!='question' else 17 | zhen('$0 个回答', '$0 answer$es0') 18 | 19 | )(t.count) }} 20 |
21 |
22 | {%endif%} 23 | 24 | {{macros.paginate(pagination, class='padlr')}} 25 | 26 |
27 | 28 | {%for p in postlist%} 29 | {%if t%} 30 | {{macros.post_postlist_threadless(p, in_thread=t)}} 31 | 32 | {%else%} 33 | {{macros.post_postlist_threadless(p)}} 34 | 35 | {%endif%} 36 | {%endfor%} 37 | 38 |
39 | 40 | {{macros.paginate(pagination, class='padlr')}} 41 | 42 | 43 | {%if t and g.logged_in%} 44 | {{macros.post_postlist_editor(t)}} 45 | 46 | {%set tsc = trust_score_format(g.current_user)%} 47 | 48 | {%set dlp = dlp_ts(tsc)%} 49 | {%set dnp = daily_number_posts(g.selfuid)%} 50 | 51 | {%set dlt = dlt_ts(tsc)%} 52 | {%set dnt = daily_number_threads(g.selfuid)%} 53 | 54 | 55 | {%if (dlp-dnp)<5 or (dlt-dnt)<3%} 56 |
57 | 你的社会信用分是{{tsc}},过去48小时发表了 {{- dnp -}} 个回复(最多可以发表{{dlp}}个) 58 | {%- if dnt%},{{dnt}}个主题(最多可以发表{{dlt}}个) 59 | {%-endif%} 60 | 61 | {%- if not current_user_can_post_outside_baodao() -%} 62 |
63 | 如果你是刚注册的新用户,发帖前请先去新人报道 64 | {%- endif -%} 65 |
66 | {%endif%} 67 | 68 | {%elif t%} 69 |
70 | {{convert_markdown(zh( 71 | '''欲参与讨论,请 [登录](/login) 或 [注册](/register)。''' 72 | ))|safe}} 73 |
74 | {%endif%} 75 | 76 | {%endblock%} 77 | 78 | {%block navigator%} 79 | 129 | {%endblock%} 130 | -------------------------------------------------------------------------------- /templates/postlist_userposts.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'postlist.html.jinja'%} 2 | {%block navigator%} 3 | {%endblock%} 4 | -------------------------------------------------------------------------------- /templates/qs.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 |
6 | {{convert_markdown(''' 7 | - Answer to any question must not occur in the Google search result of that question. 8 | - Do not ask questions where greater than 1/3 of our active user base could not answer within 3 minutes. See 9 | - Please do not ask questions that requires greater than senior high-school level of education. 10 | - **Never set up traps that waste our target users\' time. Our goal is to deter fools, not making the lives of wise men more miserable.** 11 | - The first choice will be used as the correct choice. 12 | - Our users may use either Chrome, Firefox, Tor Browser or Safari from either desktop or mobile. Please make sure your questions work nicely for all of them. Please do not ask browser-specific or platform-dependent questions. 13 | - Questions starting with `!!` will not be included in the exam. If you find one of your questions prepended with `!!`, it usually means that question was deemed unsuitable for use in the exam after being reviewed by the site owner. 14 | 15 | ''')|safe}} 16 | 17 |
18 | 19 |
20 | 21 | 添加新题目 22 |
23 |
24 | 25 |
26 | 27 | {%for q in questions %} 28 |
29 |
30 | # {{q._key}} 31 | {%if q.uid%} 32 | 出题人:{{q.user.name}} 33 | {%endif%} 34 |
35 | 36 | 37 | 38 |
39 | {%if q.uid==g.logged_in.uid or g.logged_in.uid in [5108,5347,2764,3793,3393]%} 40 | 应用修改 41 | {%endif%} 42 | {{format_time_dateifnottoday(q.t_c)}} 43 | 预览 44 |
45 | 46 |
47 | {%endfor%} 48 | 49 | 50 |
51 | 52 |
53 | {%endblock%} 54 | -------------------------------------------------------------------------------- /templates/qs_polls.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 |
6 | 若在创建投票之后修改选项的名称,原选项所得票数不会计入修改后的新选项。 7 |
8 | 9 |
10 | 11 | 添加新投票 12 |
13 |
14 | 15 | 16 | {{macros.paginate(pagination, class='')}} 17 | 18 |
19 | 20 | {%for q in polls %} 21 | 22 | {%set poll=q.pollobj%} 23 | 24 |
25 |
26 | # {{q._key}} 27 | {%if q.uid%} 28 | 创建者:{{q.user.name}} 29 | {%endif%} 30 |
31 | 32 | 33 | 34 |
35 | {%if q.uid==g.logged_in.uid or g.logged_in.uid==5108%} 36 | 应用修改 37 | {%endif%} 38 | {{format_time_dateifnottoday(q.t_c)}} 39 |
40 | 41 |
42 | 代码:#poll{{poll._key}} 43 |
44 | {# 45 | {{macros.render_poll(poll)}} 46 | #} 47 |
48 | {# 49 |
50 | #} 51 | 52 | {{macros.render_poll(poll)}} 53 | 54 |
55 | 56 |
57 | 58 | 59 | {%endfor%} 60 | 61 | 62 |
63 | 64 | {{macros.paginate(pagination, class='')}} 65 | 66 | 67 |
68 | {%endblock%} 69 | -------------------------------------------------------------------------------- /templates/quotes.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 |
6 | (欲向语录上传更多内容,请访问 Entities) 7 |
8 | 9 | 10 |
11 | {% for quote in get_quotes() %} 12 |
13 | {{macros.quote_section(quote)}} 14 |
由 {{macros.username(quote.user)}} 上传于 15 | 16 | {{format_time_dateifnottoday(quote.t_u)}} 17 | 18 |
19 |
20 | 21 | {% endfor %} 22 | 23 |
24 |
25 | {%endblock%} 26 | -------------------------------------------------------------------------------- /templates/register.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 | {{zh('语言 (LANG)')}} 34 | {{macros.language_selector()}} 35 |
36 |
37 | 38 | 39 |
40 | {{invitation_info|safe}} 41 |
42 |
43 | {{register_warning|safe}} 44 |
45 |
46 | {{register_warning2|safe}} 47 |
48 | 49 |
50 | {{cant_login_info|safe}} 51 |
52 | 53 |
54 | {{password_warning|safe}} 55 |
56 | 57 | 58 |
59 | {{public_key_info|safe}} 60 |
61 |
62 | 63 | 64 | {%endblock%} 65 | -------------------------------------------------------------------------------- /templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /*/code 3 | Disallow: /qr/* 4 | Disallow: /p/* 5 | Disallow: /u/all* 6 | -------------------------------------------------------------------------------- /templates/sandbox.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 8 |
9 | 10 | {%endblock%} 11 | -------------------------------------------------------------------------------- /templates/sb1024.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 | 什么是SinoCrypt? 6 |
7 |
8 | 9 | 10 | 11 | 12 |
13 | {%for i in ['光复香港','新疆集中营','2047']%} 14 | 15 | {%endfor%} 16 | 17 |
18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 |
31 | 32 |
33 | 34 |
35 | {%endblock%} 36 | -------------------------------------------------------------------------------- /templates/search.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if not query%} 5 |
6 | jiangle 7 |
8 | {%else%} 9 |
10 | {%endif%} 11 | 12 |
13 | 14 | 15 |
16 | 17 | {%macro post_or_thread(r)%} 18 | {%if r.title%} 19 | {{macros.post_postlist_header_kept(r)}} 20 | {%else%} 21 | {{macros.post_postlist_threadless(r)}} 22 | {%endif%} 23 | {%endmacro%} 24 | 25 |
26 | {%if has_result%} 27 | 28 | {%if mode=='yyets'%} 29 |
30 | 影视搜索结果 ↗站内搜索 31 |
32 |
33 | {%for item in yyets_results%} 34 |
35 | {{item.category}} 36 | {{item.area}} 37 | {{item.title|replace('&','&')}} 38 |
39 | {%endfor%} 40 |
41 | {%endif%} 42 | 43 | {%if mode!='yyets'%} 44 | {%if users%} 45 |
46 | 用户搜索结果 47 |
48 | 49 | {%for u in users%} 50 | {{macros.post_userlist(u)}} 51 | {%endfor%} 52 | {%endif%} 53 | 54 |
55 | 关键字搜索结果 ( 56 | {%for term in terms%} 57 | {{term}} 58 | {%endfor%} 59 | ) 60 | ↗影视搜索(站内) 61 | 62 | ↗换成谷歌搜索 63 |
64 | 65 | 68 | 69 | 111 | 112 | {%for r in results%} 113 | {{post_or_thread(r)}} 114 | {%endfor%} 115 | {%endif%} 116 | 117 | {%endif%} 118 | 119 |
120 | {%endblock%} 121 | -------------------------------------------------------------------------------- /templates/search_guizhou.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if not query%} 5 |
6 |

云上贵州

7 |
8 | 9 |
10 | jiangle 11 |
12 | 13 |
14 | {{convert_markdown(''' 15 | ## 法律声明 16 | 17 | - 数据来自网络 18 | - 搜索结果不包含完整身份信息,仅可用于验证个人隐私是否被互联网公司泄露,不构成隐私侵犯。 19 | - 若您认为搜索结果包含的信息对你构成不利影响,请立即联系我们,我们的通信地址在页面下方。 20 | - [讨论区](/t/9849) 21 | 22 | 北京市五道口计算机技术有限公司 23 | 24 | ''')|safe}} 25 | 26 |

数据源

27 |
    28 | {%for i in sources or []%} 29 |
  • [ {{i.abbr}} ] {{i.path}} ({{(i.origsize//(1024*1024))}} MB into {{(i.dbsize//(1024*1024))}} MB)
  • 30 | {%endfor%} 31 |
32 | 33 |
34 | 35 | {%else%} 36 |
37 | {%endif%} 38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | {%if query%} 48 |
49 | 搜索结果 ( 50 | {{query}} 51 | ),耗时 {{'{:.3f}'.format(t1)}} 秒, 52 | 返回贵州 53 | 54 |
55 | {%endif%} 56 | 57 | {%for i in result%} 58 |
59 | {%set hit = i.hit or ''%} 60 | {%for k in i%} 61 | {%if i[k] and i[k]!='\\n' and (k not in ['hit', 'maxscore']) %} 62 |
65 | 66 | {{k}} 67 | 68 | 69 | {%if g.logged_in or k=='source'%} 70 | {{i[k] | string}} 71 | {%else%} 72 | {{redact(i[k] | string)}} 73 | {%endif%} 74 | 75 |
76 | {%endif%} 77 | {%endfor%} 78 |
79 | {%endfor%} 80 | 81 |
82 | {%endblock%} 83 | -------------------------------------------------------------------------------- /templates/searchpm.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if not query%} 5 |
6 |

维尼查

7 |
8 | 9 |
10 | {{convert_markdown(''' 11 | ## 法律声明 12 | 13 | - 数据来自 14 | - 搜索结果不包含完整的身份信息,仅可用于验证某人的共产党员身份,不构成隐私侵犯。 15 | - 若您认为搜索结果包含的信息对你构成不利影响,请立即联系我们,我们的通信地址在页面下方。 16 | - [讨论区](/t/7830)(包括对原作者xiaolan的介绍) 17 | 18 | 北京市五道口计算机技术有限公司 19 | 20 | ''')|safe}} 21 |
22 | 23 |
24 | jiangle 25 |
26 | {%else%} 27 |
28 | {%endif%} 29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 | {%macro pair(i, keyname, display)%} 39 | {%if i[keyname] and i[keyname]!='null'%} 40 |
41 | {{display}} 42 | {%if g.logged_in or keyname=='id'%} 43 | {{i[keyname]}} 44 | {%else%} 45 | {{redact(i[keyname])}} 46 | {%endif%} 47 |
48 | {%endif%} 49 | {%endmacro%} 50 | 51 | 52 | {%if terms%} 53 |
54 | 搜索结果 ( 55 | {%for term in terms%} 56 | {{term}} 57 | {%endfor%} 58 | ) 59 | 60 |
61 | {%endif%} 62 | {%for i in pms%} 63 |
64 | {{pair(i, 'id', '编号')}} 65 | {{pair(i, 'name', '姓名')}} 66 | {{pair(i, 'gender', '性别')}} 67 | {{pair(i, 'race', '民族')}} 68 | {{pair(i, 'education', '教育')}} 69 | {{pair(i, 'sfz', '身份证')}} 70 |
71 | {{pair(i, 'committee', '支部')}} 72 | {{pair(i, 'area', '籍贯')}} 73 | {{pair(i, 'addr', '地址')}} 74 | {{pair(i, 'mobile', '电话')}} 75 | {{pair(i, 'mobile2', '电话2')}} 76 | 77 |
78 | {%endfor%} 79 | 80 |
81 | {%endblock%} 82 | -------------------------------------------------------------------------------- /templates/threadlist.html.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 | {#-------------------------------------------------------------#} 4 | 5 | {%extends 'base.html.jinja'%} 6 | {%block content%} 7 | 8 |
9 | {{macros.paginate(pagination, class='padlr')}} 10 |
11 | 12 |
13 | {%for t in threadlist%} 14 | {{macros.post_threadlist(t)}} 15 | {%endfor%} 16 |
17 | 18 | {{macros.paginate(pagination, class='padlr')}} 19 | 20 | 21 | {%endblock%} 22 | 23 | {%block navigator%} 24 | 30 | 31 | {%endblock%} 32 | -------------------------------------------------------------------------------- /templates/threadlist_right.html.jinja: -------------------------------------------------------------------------------- 1 | {%import 'macros.html.jinja' as macros with context%} 2 | 3 | {%macro switchy_tabs(tabs=[], index=0)%} 4 | {%set id = get_random_hex_string(6)%} 5 | {%set tablen = tabs|length %} 6 | 7 | 14 | 15 |
16 | 17 | {%for tab in tabs%} 18 |
19 | {{tab[1]}} 20 |
21 | {%endfor%} 22 |
23 | 24 | 44 | {%endmacro%} 45 | 46 | 47 | 48 | 49 | {%if cid and category_mode%} 50 | {% set cats_two_parts = get_categories_info_twoparts( 51 | cid=cid, mode=category_mode) %} 52 | 53 |
54 |
55 | {{zhen('分类列表','Categories')}} 56 |
57 | 58 | {%if cats_two_parts%} 59 |
60 | {%for cats in cats_two_parts%} 61 | 62 | {%for c in cats%} 63 | 64 | {%if not c%} 65 |
66 | {%else%} 67 | {{macros.category_bubble(c)}} 68 | {%endif%} 69 | {%endfor%} 70 | 71 | {%if loop.index0==0%} 72 |
73 | {%endif%} 74 | 75 | {%endfor%} 76 |
77 | {%endif%} 78 | 79 |
80 | 81 | {%endif%} 82 | 83 |
84 |
85 | {{zhen('语录','Quotes')}} {{zhen('查看更多','More')}} 86 |
87 | 90 |
91 | 92 | {%set link = get_link_one()%} 93 |
94 |
95 | {{link.category}} {{zh('查看更多')}} 96 |
97 | 98 | 101 |
102 | 103 | {%if common_links %} 104 |
105 |
106 | {{zhen('常用链接','Useful Links')}} 107 |
108 |
109 | {%for l in common_links%} 110 | {{macros.link_bubble(l)}} 111 | {%endfor%} 112 |
113 | 114 |
115 | 116 | {%endif%} 117 | 118 | 119 | 120 | 121 | 122 |
123 |
124 | {{zhen('当值', 'On Duty')}} 125 |
126 | 147 |
148 | 149 |
150 |
151 | {{zhen('最佳帖文','Best Thrd/Posts')}} 152 |
153 | 161 |
162 | 163 | 164 | {# 165 |
166 |
167 | {{zhen('新人榜','Awesome Newcomers')}} 168 |
169 | 172 |
173 | 174 |
175 |
176 | {{zhen('社会信用榜','Trust Scoreboard')}} 177 | {{ 178 | zhen('了解更多','Learn More')}} 179 |
180 | 183 |
184 | #} 185 | 186 | 187 | 188 |
189 |
190 | {{zhen('高赞用户','Most Liked')}} 191 |
192 | 200 |
201 | 202 |
203 |
204 | {{zhen('守望','Keep Watch')}} {{ 205 | zhen('了解更多','Learn More')}} 206 |
207 | 208 |
209 | {{days_between('2020-04-19','2021-08-15')}} 210 |
211 |
212 | days between 2020-4-19
and 2021-8-15 213 |
214 | 215 |
216 | {{days_since('2021-08-15')}} 217 |
218 |
219 | days since 2021-8-15 220 |
221 | 222 |
223 | 224 |
225 |
226 | {{zhen('铭记','Never Forget')}} 227 |
228 |
229 | {{days_since('1989-06-04')}} 230 |
231 |
232 | days since 1989-06-04 233 |
234 |
235 | 236 |
237 |
238 | {{zhen('语言 (LANG)','Language')}} 239 | {{ 240 | zhen('帮助翻译','Help Translate')}} 241 |
242 | {{macros.language_selector()}} 243 |
244 | 245 | {%if ads%} 246 |
247 |
248 | {{zh('广告')}} 关于广告 249 |
250 | 251 |
252 | {%for ad in ads%} 253 |
254 | {{ad.alt}} 259 |
260 | {%endfor%} 261 |
262 | 263 |
264 | {%endif%} 265 | -------------------------------------------------------------------------------- /templates/trans.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 |
5 |
6 | {{convert_markdown(''' 7 | ## 须知 8 | 9 | - 开始翻译前,建议先去主页,将语言设置调整为你即将翻译的语言。 10 | - 请仅为你常用的语言提供翻译。不同地区的语言习惯不同,存在诸如打印机、列印机、印表机的区别。 11 | - 译文长度应和原文长度相近,超长会影响页面排版。 12 | - $符号是占位符,如果不清楚是什么意思,请不要提交翻译。 13 | - 请等待管理员审批你的翻译。站方对译文有多方面严格要求,如果未通过审批,请不要气馁。 14 | - 如果你打算提交的en-us翻译,已经被原en翻译包含,或者zh-cn已经被zh包含,请不要重复提交。 15 | - 如需增加语言,请确保你经常使用该语言,然后与站长联络。 16 | 17 | ''')|safe}} 18 |
19 | 20 | {%for original,tns in translations%} 21 |
22 | {{original}} 23 | 创建/更新翻译 26 |
27 | 28 | 71 | 72 | {%endfor%} 73 | 74 |
75 | 76 | {%endblock%} 77 | -------------------------------------------------------------------------------- /templates/ts_expl.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | 5 | {%if limits%} 6 |
7 |
8 | 9 |

下表根据当前计算规则自动生成

10 | 11 |

关于社会信用分的更多信息在这里

12 | 13 |

全站信用分统计在这里

14 | 15 |

如果不想因为社会信用分太低而被限制发言,最简单的方式是去水区新人报道,让其他信用分高的用户给你点赞,点赞之后你的信用分就会增加

16 | 17 |

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {%for ts, lp, lt, is_me in limits %} 27 | 28 | {%if is_me%} 29 | 30 | {%else%} 31 | 32 | 33 | {%endif%} 34 | 35 | 36 | 37 | 38 | {%endfor%} 39 | 40 | 41 |
社会信用分每48h评论数每48h主题数
{{ts}}{{lp}}{{lt}}
42 | 43 | 44 |

45 | 46 |
47 |
48 | {%endif %} 49 | 50 | {%if spam_words%} 51 |
52 |
53 |

为节省篇幅,本表只列出{{spam_words_length}}个条目中的{{spam_words|length}}个。

54 | 55 | 56 |

57 |

58 | gram 59 | log(p) 60 |
61 | 62 |

63 | 64 | {%for word, logp in spam_words%} 65 |
67 | {{word}} 68 | {{'%.3f'|format(logp)}} 69 |
70 | {%endfor%} 71 | 72 |

令人感到遗憾的是,本表顶部是台湾,底部是中国。

73 | 74 |
75 |
76 | {%endif %} 77 | 78 | 79 | {%endblock%} 80 | -------------------------------------------------------------------------------- /templates/user404.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 |
4 |
5 |

找不到用户: {{name}}

6 | 20 |
21 |
22 | {%endblock%} 23 | -------------------------------------------------------------------------------- /templates/userlist.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if t%}{# if there is a thread object #} 5 |
6 | {{macros.post_postlist_header(t)}} 7 |
8 | {%endif%} 9 | 10 | {{macros.paginate(pagination, class='padlr') if pagination}} 11 | 12 |
13 | 14 | {%for u in userlist%} 15 | {{macros.post_userlist(u)}} 16 | {%endfor%} 17 | 18 |
19 | {{macros.paginate(pagination, class='padlr') if pagination}} 20 | 21 | {%endblock%} 22 | -------------------------------------------------------------------------------- /templates/usermedals.html.jinja: -------------------------------------------------------------------------------- 1 | {%extends 'base.html.jinja'%} 2 | {%block content%} 3 | 4 | {%if t%}{# if there is a thread object #} 5 |
6 | {{macros.mixed_content(t)}} 7 |
8 | {%endif%} 9 | 10 | {%for medal in medals%} 11 |
12 |
13 | 14 |
15 |

{{medal.name}}

16 |

{{medal.brief}}

17 |
18 | 19 | 20 |
21 | {%for user in medal.listusers%} 22 | {{macros.avatar(user, class='follower_avatar', no_decoration=True)}} 23 | {%endfor%} 24 |
25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 | {%endfor%} 34 | {%endblock%} 35 | -------------------------------------------------------------------------------- /trust_score.py: -------------------------------------------------------------------------------- 1 | from flask import * 2 | from api import * 3 | from app import app 4 | 5 | from antispam import st 6 | 7 | limits = [] 8 | 9 | for i in [0,25,50,75,100,150,200,250,300,400,500,600,800,1000, 10 | 1200,1500,2000,3000,4000,6000,10000,20000]: 11 | # print(i, dlp_ts(i),dlt_ts(i)) 12 | 13 | limits.append((i, dlp_ts(i), dlt_ts(i), False)) 14 | 15 | @app.route('/trust_score_explained') 16 | def ts_explain(): 17 | 18 | mylimits = limits.copy() 19 | 20 | if g.selfuid>0: 21 | myts = trust_score_format(g.current_user) 22 | lp = dlp_ts(myts) 23 | lt = dlt_ts(myts) 24 | 25 | j = [] 26 | flag = 0 27 | for tup in mylimits: 28 | if myts<=tup[0] and flag==0: 29 | flag = 1 30 | j.append((myts, lp, lt, True)) 31 | j.append(tup) 32 | 33 | if flag==0: 34 | flag = 1 35 | j.append((myts, lp, lt, True)) 36 | 37 | mylimits = j 38 | 39 | return render_template_g('ts_expl.html.jinja', 40 | page_title = '社会信用分与发言限制对照', 41 | limits = mylimits, 42 | ) 43 | 44 | @lru_cache() 45 | def get_spamwords(): 46 | wl = len(st.spamgoods) 47 | words = [(k,logp) for idx,(k,logp) in enumerate(st.spamgoods.items()) 48 | if idx**2.114514*8964 % 1 < ((abs(logp)-2)/5)**1.72323 49 | ] 50 | words.sort(key=lambda a:-a[1]) 51 | return wl, words 52 | 53 | @app.route('/anti_spam_explained') 54 | def as_explain(): 55 | wl, words = get_spamwords() 56 | return render_template_g('ts_expl.html.jinja', 57 | page_title = 'Spam Classifier Dictionary', 58 | # limits = mylimits, 59 | spam_words = words, 60 | spam_words_length = wl, 61 | ) 62 | -------------------------------------------------------------------------------- /vendor.yml: -------------------------------------------------------------------------------- 1 | templates/css/normalize.css linguist-vendored 2 | templates/highlight/** linguist-vendored 3 | templates/js/ace/** linguist-vendored 4 | templates/js/md5.js linguist-vendored 5 | templates/js/relaxed-json.js linguist-vendored 6 | --------------------------------------------------------------------------------