yacscript 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +24 -0
- app/app.py +698 -0
- app/db.py +25 -0
- app/definitions.py +71 -0
- app/static/admin.js +110 -0
- app/static/file.mp3 +0 -0
- app/static/icon.png +0 -0
- app/static/join.mp3 +0 -0
- app/static/leave.mp3 +0 -0
- app/static/msg.mp3 +0 -0
- app/static/room.js +364 -0
- app/static/send.mp3 +0 -0
- app/static/style.css +64 -0
- app/static/tile.gif +0 -0
- app/static/tile.jpg +0 -0
- app/static/welcome.mp3 +0 -0
- app/templates/index.jinja +47 -0
- app/templates/room.jinja +107 -0
- yacscript-0.3.0.dist-info/METADATA +250 -0
- yacscript-0.3.0.dist-info/RECORD +23 -0
- yacscript-0.3.0.dist-info/WHEEL +4 -0
- yacscript-0.3.0.dist-info/entry_points.txt +3 -0
- yacscript-0.3.0.dist-info/licenses/COPYING +13 -0
app/app.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import random
|
|
3
|
+
import tomllib
|
|
4
|
+
import uuid
|
|
5
|
+
from base64 import b64encode
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from os import path, mkdir
|
|
9
|
+
from string import ascii_lowercase, ascii_uppercase
|
|
10
|
+
from time import time
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
|
|
13
|
+
import bbcode
|
|
14
|
+
from app.db import get_db
|
|
15
|
+
from app.definitions import CONFIG_DEFAULT
|
|
16
|
+
from captcha.image import ImageCaptcha
|
|
17
|
+
from flask import (Flask, Response, redirect, render_template,
|
|
18
|
+
request, send_file, jsonify)
|
|
19
|
+
from flask_socketio import (SocketIO, emit, join_room,
|
|
20
|
+
leave_room, disconnect)
|
|
21
|
+
from webargs import fields, validate
|
|
22
|
+
from webargs.flaskparser import use_args
|
|
23
|
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
24
|
+
from werkzeug.utils import secure_filename
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- GLOBAL DEFINITIONS ---
|
|
28
|
+
|
|
29
|
+
app = Flask(__name__)
|
|
30
|
+
socketio = SocketIO(app, cors_allowed_origins='*')
|
|
31
|
+
|
|
32
|
+
# Caches
|
|
33
|
+
candidates: OrderedDict = OrderedDict()
|
|
34
|
+
online = dict()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --- HELPER FUNCTIONS ---
|
|
38
|
+
|
|
39
|
+
def initialization(config=None):
|
|
40
|
+
app.config.update(CONFIG_DEFAULT)
|
|
41
|
+
if config is not None:
|
|
42
|
+
if path.isfile(path.abspath(config)):
|
|
43
|
+
with open(path.abspath(path.abspath(config)), 'rb') as file:
|
|
44
|
+
try:
|
|
45
|
+
conf = tomllib.load(file)
|
|
46
|
+
except tomllib.TOMLDecodeError as e:
|
|
47
|
+
return e
|
|
48
|
+
# read built-in values
|
|
49
|
+
top = conf.get('flask', None)
|
|
50
|
+
if top:
|
|
51
|
+
app.config.update(conf['flask'])
|
|
52
|
+
conf.pop('flask')
|
|
53
|
+
app.config.update(conf)
|
|
54
|
+
if app.config['app']['proxy_fix']:
|
|
55
|
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)
|
|
56
|
+
if not path.isdir(app.config['res']['path']):
|
|
57
|
+
mkdir(app.config['res']['path'])
|
|
58
|
+
|
|
59
|
+
# Making global variables
|
|
60
|
+
global logger
|
|
61
|
+
global conn
|
|
62
|
+
global SIZE_MAX_BYTE
|
|
63
|
+
global challenge_set
|
|
64
|
+
global image_captcha
|
|
65
|
+
app.logger.setLevel(app.config['app']['log_level'])
|
|
66
|
+
logger = app.logger
|
|
67
|
+
conn = get_db(app.config['db']['path'], logger)
|
|
68
|
+
SIZE_MAX_BYTE = app.config['res']['size_max'] * 1000000
|
|
69
|
+
options = app.config['captcha']['options']
|
|
70
|
+
challenge_set = ['', ascii_lowercase][options['lowercase']] + \
|
|
71
|
+
['', ascii_uppercase][options['uppercase']] + \
|
|
72
|
+
['', '0123456789'][options['numbers']]
|
|
73
|
+
image_captcha = ImageCaptcha()
|
|
74
|
+
return 'ok'
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_channel_member(channel_id: int):
|
|
78
|
+
resp = []
|
|
79
|
+
for k, v in online.items():
|
|
80
|
+
cid = v['channel']
|
|
81
|
+
if cid == channel_id:
|
|
82
|
+
resp.append(k)
|
|
83
|
+
return resp
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def push_candidate(identifier, challenge, time):
|
|
87
|
+
if len(candidates) >= app.config['captcha']['max_cache']:
|
|
88
|
+
candidates.popitem(last=False)
|
|
89
|
+
candidates[identifier] = (challenge, time)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def verify_candidate(identifier, challenge, time):
|
|
93
|
+
try:
|
|
94
|
+
candidate = candidates.pop(identifier)
|
|
95
|
+
except:
|
|
96
|
+
return False
|
|
97
|
+
if (time - candidate[1]) > app.config['captcha']['expire']:
|
|
98
|
+
return False
|
|
99
|
+
if challenge == candidate[0]:
|
|
100
|
+
return True
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clean_user(name):
|
|
105
|
+
item = online.pop(name)
|
|
106
|
+
if item['channel'] != 0:
|
|
107
|
+
emit('leaving', {'target': name},
|
|
108
|
+
to=item['channel'], namespace=item['namespace'])
|
|
109
|
+
leave_room(
|
|
110
|
+
item['channel'],
|
|
111
|
+
item['sid'],
|
|
112
|
+
item['namespace']
|
|
113
|
+
)
|
|
114
|
+
disconnect(
|
|
115
|
+
item['sid'],
|
|
116
|
+
item['namespace']
|
|
117
|
+
)
|
|
118
|
+
logger.info(f'User {name} logged out.')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def time_milisecond():
|
|
122
|
+
return round(time() * 1000)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def garbage_collect():
|
|
126
|
+
for k, v in list(online.items()):
|
|
127
|
+
if (time_milisecond() - v['last_heartbeat']) > app.config['app']['timeout']:
|
|
128
|
+
clean_user(k)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.errorhandler(422)
|
|
132
|
+
@app.errorhandler(400)
|
|
133
|
+
def handle_error(err):
|
|
134
|
+
headers = err.data.get('headers', None)
|
|
135
|
+
messages = err.data.get('messages', ['Invalid request.'])
|
|
136
|
+
if headers:
|
|
137
|
+
return jsonify({'errors': messages}), err.code, headers
|
|
138
|
+
else:
|
|
139
|
+
return jsonify({'errors': messages}), err.code
|
|
140
|
+
|
|
141
|
+
# --- WRAPPERS ---
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def login_required(f):
|
|
145
|
+
@wraps(f)
|
|
146
|
+
def decorated(*args, **kwargs):
|
|
147
|
+
auth = request.headers.get('Authorization', default=None)
|
|
148
|
+
if not auth:
|
|
149
|
+
return Response(status=401)
|
|
150
|
+
try:
|
|
151
|
+
_, nick, token = str(auth).split(' ')
|
|
152
|
+
except:
|
|
153
|
+
return Response(status=401)
|
|
154
|
+
if nick not in online.keys():
|
|
155
|
+
return Response(status=401)
|
|
156
|
+
if not online[nick]['token'] == token:
|
|
157
|
+
return Response(status=401)
|
|
158
|
+
return f(*args, **kwargs)
|
|
159
|
+
return decorated
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def admin_login_required(f):
|
|
163
|
+
@wraps(f)
|
|
164
|
+
def decorated(*args, **kwargs):
|
|
165
|
+
auth = request.headers.get('Authorization', default=None)
|
|
166
|
+
if not auth:
|
|
167
|
+
return Response(status=401)
|
|
168
|
+
try:
|
|
169
|
+
_, nick, token = str(auth).split(' ')
|
|
170
|
+
except:
|
|
171
|
+
return Response(status=401)
|
|
172
|
+
if nick not in online.keys():
|
|
173
|
+
return Response(status=401)
|
|
174
|
+
if not online[nick]['token'] == token:
|
|
175
|
+
return Response(status=401)
|
|
176
|
+
if not online[nick]['is_admin']:
|
|
177
|
+
return Response(status=401)
|
|
178
|
+
return f(*args, **kwargs)
|
|
179
|
+
return decorated
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --- VIEW ROUTES ---
|
|
183
|
+
# ROUTES HERE ARE SPECIFIALLY FOR RENDERING VIEWS
|
|
184
|
+
|
|
185
|
+
@app.route('/')
|
|
186
|
+
def landing_page():
|
|
187
|
+
# get possible failure callback
|
|
188
|
+
failed = request.args.get('failed', default=None)
|
|
189
|
+
if failed:
|
|
190
|
+
failed = failed.replace('+', ' ')
|
|
191
|
+
# making captcha
|
|
192
|
+
identifier = str(uuid.uuid4())
|
|
193
|
+
challenge = ''.join(random.choices(
|
|
194
|
+
challenge_set, k=app.config['captcha']['length']))
|
|
195
|
+
push_candidate(identifier, challenge, time())
|
|
196
|
+
data = image_captcha.generate(challenge).read()
|
|
197
|
+
captcha_data = b64encode(data).decode()
|
|
198
|
+
return render_template(
|
|
199
|
+
'index.jinja',
|
|
200
|
+
title=app.config['custom']['title'],
|
|
201
|
+
captcha=captcha_data,
|
|
202
|
+
identifier=identifier,
|
|
203
|
+
failed=failed
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
LOGIN_FORM = {
|
|
208
|
+
'nick': fields.Str(required=True, validate=[validate.Length(min=3, max=16), validate.ContainsOnly(list(ascii_lowercase + ascii_uppercase + '0123456789_'))]),
|
|
209
|
+
'phrase': fields.Str(),
|
|
210
|
+
'captcha': fields.Str(required=True),
|
|
211
|
+
'identifier': fields.Str(required=True)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.route('/auth', methods=['POST'])
|
|
216
|
+
@use_args(LOGIN_FORM, location='form')
|
|
217
|
+
def auth(form):
|
|
218
|
+
if form['phrase'] != app.config['app']['admin_phrase']:
|
|
219
|
+
if form['phrase'] != app.config['app']['user_phrase']:
|
|
220
|
+
return redirect('/?failed=Wrong+passphrase')
|
|
221
|
+
|
|
222
|
+
if verify_candidate(form['identifier'], form['captcha'], time()):
|
|
223
|
+
garbage_collect()
|
|
224
|
+
if form['nick'] in online.keys():
|
|
225
|
+
return redirect('/?failed=User+exists')
|
|
226
|
+
token = str(uuid.uuid4())
|
|
227
|
+
is_admin = (form['phrase'] == app.config['app']['admin_phrase'])
|
|
228
|
+
online[form['nick']] = dict(
|
|
229
|
+
token=token,
|
|
230
|
+
is_admin=is_admin,
|
|
231
|
+
last_heartbeat=time_milisecond(),
|
|
232
|
+
files_pending=dict(),
|
|
233
|
+
channel=0,
|
|
234
|
+
sid=None,
|
|
235
|
+
namespace=None,
|
|
236
|
+
is_uploading=False,
|
|
237
|
+
is_alive=False
|
|
238
|
+
)
|
|
239
|
+
logger.info(f'User {form['nick']} logged in.')
|
|
240
|
+
return redirect(f'/room?nickname={form['nick']}&token={token}')
|
|
241
|
+
return redirect('/?failed=CAPTCHA+failed')
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@app.route('/room-preview', methods=['POST', 'GET'])
|
|
245
|
+
def room_preview():
|
|
246
|
+
if not app.config['DEBUG']:
|
|
247
|
+
return Response(status=404)
|
|
248
|
+
name = 'test_' + ''.join(random.choices(challenge_set, k=5))
|
|
249
|
+
online[name] = {
|
|
250
|
+
'token': 'test',
|
|
251
|
+
'is_admin': True,
|
|
252
|
+
'last_heartbeat': time_milisecond(),
|
|
253
|
+
'files_pending': dict(),
|
|
254
|
+
'channel': 0,
|
|
255
|
+
'sid': None,
|
|
256
|
+
'namespace': None,
|
|
257
|
+
'is_uploading': False,
|
|
258
|
+
'is_alive': False,
|
|
259
|
+
}
|
|
260
|
+
return render_template(
|
|
261
|
+
'room.jinja',
|
|
262
|
+
title=app.config['custom']['title'],
|
|
263
|
+
token='test',
|
|
264
|
+
nick=name,
|
|
265
|
+
emotes=app.config['custom']['emoticons'],
|
|
266
|
+
motd=app.config['custom']['motd'],
|
|
267
|
+
is_admin=True
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
ROOM_FORM = {
|
|
272
|
+
'nickname': fields.Str(validate=lambda x: (x in online.keys()), required=True),
|
|
273
|
+
'token': fields.Str(required=True),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@app.route('/room')
|
|
278
|
+
@use_args(ROOM_FORM, location='query')
|
|
279
|
+
def room_view(auth):
|
|
280
|
+
nick = auth['nickname']
|
|
281
|
+
token = auth['token']
|
|
282
|
+
try:
|
|
283
|
+
if online[nick]['token'] != token:
|
|
284
|
+
return redirect('/?failed=Invalid+authentication')
|
|
285
|
+
except:
|
|
286
|
+
return redirect('/?failed=Invalid+authentication')
|
|
287
|
+
if online[nick]['last_heartbeat']:
|
|
288
|
+
if (time_milisecond() - online[nick]['last_heartbeat']) > app.config['app']['timeout']:
|
|
289
|
+
clean_user(nick)
|
|
290
|
+
return redirect('/?failed=Authentication+expired')
|
|
291
|
+
return render_template(
|
|
292
|
+
'room.jinja',
|
|
293
|
+
title=app.config['custom']['title'],
|
|
294
|
+
emotes=app.config['custom']['emoticons'],
|
|
295
|
+
nick=nick,
|
|
296
|
+
token=token,
|
|
297
|
+
motd=app.config['custom']['motd'],
|
|
298
|
+
is_admin=online[nick]['is_admin'],
|
|
299
|
+
timeout=app.config['app']['timeout'],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# --- SECRET API ---
|
|
303
|
+
# ROUTES HERE NEED TO BE CALLED BY LOGGED IN CLIENT SCRIPT
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@app.route('/channels')
|
|
307
|
+
@login_required
|
|
308
|
+
def get_channels():
|
|
309
|
+
auth = request.headers.get('Authorization', default=None)
|
|
310
|
+
nick = str(auth).split(' ')[1]
|
|
311
|
+
res = conn.execute(
|
|
312
|
+
'SELECT * FROM CHANNEL WHERE IS_DELETED=0 AND (ADMIN_ONLY=0 OR ?=1)', (online[nick]['is_admin'],))
|
|
313
|
+
channels = []
|
|
314
|
+
for row in res.fetchall():
|
|
315
|
+
channels.append({
|
|
316
|
+
'id': row[0],
|
|
317
|
+
'name': row[1],
|
|
318
|
+
'is_admin': bool(row[3])
|
|
319
|
+
})
|
|
320
|
+
return channels
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@app.route('/index_upload', methods=['POST'])
|
|
324
|
+
@login_required
|
|
325
|
+
def upload_index():
|
|
326
|
+
auth = request.headers.get('Authorization', default=None)
|
|
327
|
+
uploader = str(auth).split(' ')[1]
|
|
328
|
+
if online[uploader]['is_uploading']:
|
|
329
|
+
return Response(status=429)
|
|
330
|
+
# lock
|
|
331
|
+
online[uploader]['is_uploading'] = True
|
|
332
|
+
|
|
333
|
+
file = request.files.get("file", default=None)
|
|
334
|
+
if file is None:
|
|
335
|
+
online[uploader]['is_uploading'] = False
|
|
336
|
+
return Response(status=400)
|
|
337
|
+
|
|
338
|
+
name = file.filename
|
|
339
|
+
if name is None:
|
|
340
|
+
online[uploader]['is_uploading'] = False
|
|
341
|
+
return Response(status=400)
|
|
342
|
+
|
|
343
|
+
name = secure_filename(name)
|
|
344
|
+
mime = file.mimetype
|
|
345
|
+
binary = file.read()
|
|
346
|
+
if len(binary) > SIZE_MAX_BYTE:
|
|
347
|
+
online[uploader]['is_uploading'] = False
|
|
348
|
+
return Response(status=400)
|
|
349
|
+
id = str(uuid.uuid4())
|
|
350
|
+
online[uploader]['files_pending'].update({
|
|
351
|
+
id: (binary, name, mime)
|
|
352
|
+
})
|
|
353
|
+
online[uploader]['is_uploading'] = False
|
|
354
|
+
return {'uuid': id, 'file_name': name}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.route('/submit_upload')
|
|
358
|
+
@login_required
|
|
359
|
+
def upload_submit():
|
|
360
|
+
auth = request.headers.get('Authorization', default=None)
|
|
361
|
+
user = str(auth).split(' ')[1]
|
|
362
|
+
|
|
363
|
+
recall = request.args.get('recall', default=None)
|
|
364
|
+
submit = request.args.get('submit', default=None)
|
|
365
|
+
|
|
366
|
+
if recall:
|
|
367
|
+
if recall not in online[user]['files_pending'].keys():
|
|
368
|
+
return Response(status=400)
|
|
369
|
+
online[user]['files_pending'].pop(recall)
|
|
370
|
+
return Response(status=200)
|
|
371
|
+
|
|
372
|
+
if submit:
|
|
373
|
+
if submit not in online[user]['files_pending'].keys():
|
|
374
|
+
return Response(status=400)
|
|
375
|
+
binary, name, mime = online[user]['files_pending'].pop(submit)
|
|
376
|
+
|
|
377
|
+
# Write to disk
|
|
378
|
+
extension = path.splitext(name)[1]
|
|
379
|
+
file_path = path.abspath(
|
|
380
|
+
path.join(app.config['res']['path'], f'{submit}{extension}'))
|
|
381
|
+
with open(file_path, 'xb') as f:
|
|
382
|
+
f.write(binary)
|
|
383
|
+
|
|
384
|
+
# Write to DB
|
|
385
|
+
conn.execute(
|
|
386
|
+
'INSERT INTO RESOURCE (UUID, FILE_NAME, MIME_TYPE) VALUES (?, ?, ?);',
|
|
387
|
+
(submit, name, mime)
|
|
388
|
+
)
|
|
389
|
+
return Response(status=200)
|
|
390
|
+
return Response(status=400)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@app.route('/messages/<int:channel_id>')
|
|
394
|
+
@login_required
|
|
395
|
+
def get_messages(channel_id):
|
|
396
|
+
if channel_id == 0:
|
|
397
|
+
return Response(status=404)
|
|
398
|
+
count = 30
|
|
399
|
+
if 'count' in request.args.keys():
|
|
400
|
+
count = int(request.args['count'])
|
|
401
|
+
offset = 0
|
|
402
|
+
if 'offset' in request.args.keys():
|
|
403
|
+
offset = int(request.args['offset'])
|
|
404
|
+
|
|
405
|
+
# check your fucking privilege
|
|
406
|
+
auth = request.headers.get('Authorization', default=None)
|
|
407
|
+
nick = str(auth).split(' ')[1]
|
|
408
|
+
cur = conn.execute(
|
|
409
|
+
'SELECT ADMIN_ONLY FROM CHANNEL WHERE ID=?;', (channel_id,))
|
|
410
|
+
row = cur.fetchone()
|
|
411
|
+
if row is None:
|
|
412
|
+
return Response(status=403)
|
|
413
|
+
priv = row[0]
|
|
414
|
+
if priv == 1 and online[nick]['is_admin'] == 0:
|
|
415
|
+
return Response(status=403)
|
|
416
|
+
|
|
417
|
+
# get latest message first
|
|
418
|
+
res = conn.execute(
|
|
419
|
+
'SELECT * FROM CHAT WHERE CHANNEL_ID=? AND IS_DELETED=0 ORDER BY ID DESC LIMIT ? OFFSET ?;', (channel_id, count, offset))
|
|
420
|
+
res = res.fetchall()
|
|
421
|
+
# Process BBCODE
|
|
422
|
+
resp = []
|
|
423
|
+
for row in res:
|
|
424
|
+
cur = conn.execute(
|
|
425
|
+
'SELECT RESOURCE_ID FROM ATTACHMENT WHERE CHAT_ID=?;', (row[0],))
|
|
426
|
+
cur = cur.fetchall()
|
|
427
|
+
attachments = [x[0] for x in cur]
|
|
428
|
+
msg = {
|
|
429
|
+
'id': row[0],
|
|
430
|
+
'author': row[4],
|
|
431
|
+
'datetime': row[2],
|
|
432
|
+
'body': row[1],
|
|
433
|
+
'attachments': attachments
|
|
434
|
+
}
|
|
435
|
+
resp.append(msg)
|
|
436
|
+
return resp
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@app.route('/fellows')
|
|
440
|
+
@login_required
|
|
441
|
+
def get_fellows():
|
|
442
|
+
auth = request.headers.get('Authorization', default=None)
|
|
443
|
+
if auth == None:
|
|
444
|
+
# auth is guaranteed to be valid after decoration
|
|
445
|
+
# so uh this is merely a trick to disable static checker
|
|
446
|
+
return Response(status=400)
|
|
447
|
+
user = auth.split(' ')[1]
|
|
448
|
+
channel = online[user]['channel']
|
|
449
|
+
if channel == 0:
|
|
450
|
+
return Response(status=404)
|
|
451
|
+
return {'fellows': get_channel_member(channel)}
|
|
452
|
+
|
|
453
|
+
# --- PUBLIC APIS ---
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@app.route('/resource_meta/<resource_id>')
|
|
457
|
+
def get_resource_meta(resource_id):
|
|
458
|
+
row = conn.execute(
|
|
459
|
+
'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
|
|
460
|
+
row = row.fetchone()
|
|
461
|
+
if not row:
|
|
462
|
+
return Response(status=404)
|
|
463
|
+
return {
|
|
464
|
+
'filename': row[0],
|
|
465
|
+
'mime': row[1]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.route('/resource/<resource_id>')
|
|
470
|
+
def get_resource(resource_id):
|
|
471
|
+
row = conn.execute(
|
|
472
|
+
'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
|
|
473
|
+
row = row.fetchone()
|
|
474
|
+
if not row:
|
|
475
|
+
return Response(status=404)
|
|
476
|
+
extension = path.splitext(row[0])[1]
|
|
477
|
+
file_path = path.abspath(path.join(
|
|
478
|
+
app.config['res']['path'],
|
|
479
|
+
f'{resource_id}.{extension}'
|
|
480
|
+
))
|
|
481
|
+
if path.exists(file_path):
|
|
482
|
+
return send_file(file_path, mimetype=row[1], download_name=row[0])
|
|
483
|
+
file_path = path.abspath(path.join(
|
|
484
|
+
app.config['res']['path'],
|
|
485
|
+
f'{resource_id}{extension}'
|
|
486
|
+
))
|
|
487
|
+
if path.exists(file_path):
|
|
488
|
+
return send_file(file_path, mimetype=row[1], download_name=row[0])
|
|
489
|
+
try:
|
|
490
|
+
conn.execute(
|
|
491
|
+
'UPDATE RESOURCE SET IS_EXPIRED=1 WHERE UUID=?;',
|
|
492
|
+
(resource_id, )
|
|
493
|
+
)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.error(
|
|
496
|
+
f'Error deleting resource {resource_id} with exception {e}')
|
|
497
|
+
return Response(status=404)
|
|
498
|
+
|
|
499
|
+
# --- ADMIN APIS ---
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@app.route('/channel', methods=['POST', 'PUT', 'DELETE'])
|
|
503
|
+
@admin_login_required
|
|
504
|
+
def channel_control():
|
|
505
|
+
if request.method == 'POST':
|
|
506
|
+
try:
|
|
507
|
+
name = request.get_json()['name']
|
|
508
|
+
except:
|
|
509
|
+
return Response(status=415)
|
|
510
|
+
conn.execute(
|
|
511
|
+
'INSERT INTO CHANNEL (NAME, ADMIN_ONLY) VALUES (?, 0)', (name, ))
|
|
512
|
+
return Response(status=200)
|
|
513
|
+
elif request.method == 'PUT':
|
|
514
|
+
if 'sw_priv' in request.args.keys():
|
|
515
|
+
id = int(request.args['sw_priv'])
|
|
516
|
+
conn.execute(
|
|
517
|
+
'UPDATE CHANNEL SET ADMIN_ONLY = 1 - ADMIN_ONLY WHERE ID=?;',
|
|
518
|
+
(id,)
|
|
519
|
+
)
|
|
520
|
+
return Response(status=200)
|
|
521
|
+
elif 'name' in request.args.keys() and 'id' in request.args.keys():
|
|
522
|
+
id = int(request.args['id'])
|
|
523
|
+
name = request.args['name']
|
|
524
|
+
conn.execute(
|
|
525
|
+
'UPDATE CHANNEL SET NAME=? WHERE ID=?;',
|
|
526
|
+
(name, id)
|
|
527
|
+
)
|
|
528
|
+
return Response(status=200)
|
|
529
|
+
elif request.method == 'DELETE':
|
|
530
|
+
if 'id' in request.args.keys():
|
|
531
|
+
id = request.args['id']
|
|
532
|
+
conn.execute(
|
|
533
|
+
'UPDATE CHANNEL SET IS_DELETED=1 WHERE ID=?;',
|
|
534
|
+
(id, )
|
|
535
|
+
)
|
|
536
|
+
return Response(status=200)
|
|
537
|
+
return Response(status=404)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@app.route('/online/<name>', methods=['DELETE'])
|
|
541
|
+
@admin_login_required
|
|
542
|
+
def kick_user(name):
|
|
543
|
+
if name in online.keys():
|
|
544
|
+
clean_user(name)
|
|
545
|
+
return Response(status=200)
|
|
546
|
+
return Response(status=400)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@app.route('/resource/<res_id>', methods=['DELETE'])
|
|
550
|
+
@admin_login_required
|
|
551
|
+
def delete_resource(res_id):
|
|
552
|
+
try:
|
|
553
|
+
conn.execute(
|
|
554
|
+
'UPDATE RESOURCE SET IS_EXPIRED=1 WHERE UUID=?;',
|
|
555
|
+
(res_id, )
|
|
556
|
+
)
|
|
557
|
+
except Exception as e:
|
|
558
|
+
logger.error(f'Error deleting resource {res_id} with exception {e}')
|
|
559
|
+
return Response(status=400)
|
|
560
|
+
return Response(status=200)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@app.route('/message/<int:id>', methods=['DELETE'])
|
|
564
|
+
@admin_login_required
|
|
565
|
+
def delete_msg(id: int):
|
|
566
|
+
try:
|
|
567
|
+
conn.execute(
|
|
568
|
+
'UPDATE CHAT SET IS_DELETED=1 WHERE ID=?;',
|
|
569
|
+
(id, )
|
|
570
|
+
)
|
|
571
|
+
except Exception as e:
|
|
572
|
+
logger.error(f'Error deleting msg {id} with exception {e}')
|
|
573
|
+
return Response(status=400)
|
|
574
|
+
return Response(status=200)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# --- SOCKET EVENTS ---
|
|
578
|
+
|
|
579
|
+
@socketio.on('updating_channel')
|
|
580
|
+
def updated_channel():
|
|
581
|
+
emit('channel_updated')
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@socketio.on('sw_channel')
|
|
585
|
+
def sw_channel_handler(json):
|
|
586
|
+
try:
|
|
587
|
+
if json['token'] != online[json['nick']]['token']:
|
|
588
|
+
return
|
|
589
|
+
except:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
user = json['nick']
|
|
593
|
+
to = json['to']
|
|
594
|
+
is_admin = online[user]['is_admin']
|
|
595
|
+
res = conn.execute('SELECT id FROM CHANNEL' +
|
|
596
|
+
(' WHERE ADMIN_ONLY=0;', '')[is_admin])
|
|
597
|
+
ids = [x[0] for x in res.fetchall()]
|
|
598
|
+
|
|
599
|
+
if to not in ids:
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
current_channel = online[user]['channel']
|
|
603
|
+
if current_channel != 0:
|
|
604
|
+
leave_room(current_channel)
|
|
605
|
+
emit('leaving', {'target': json['nick']}, to=current_channel)
|
|
606
|
+
join_room(to)
|
|
607
|
+
online[user]['channel'] = to
|
|
608
|
+
emit('joining', {'target': json['nick']}, to=to)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@socketio.on('msg_send')
|
|
612
|
+
def msg_send_handler(json):
|
|
613
|
+
author: str = json['author']
|
|
614
|
+
token: str = json['token']
|
|
615
|
+
if not author in online.keys():
|
|
616
|
+
return
|
|
617
|
+
if online[author]['token'] != token:
|
|
618
|
+
return
|
|
619
|
+
if json['body'] == '':
|
|
620
|
+
return
|
|
621
|
+
body: str = bbcode.render_html(json['body'])
|
|
622
|
+
attachments: list = json.get('attachments', [])
|
|
623
|
+
time: str = datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')
|
|
624
|
+
msg = {
|
|
625
|
+
'author': author,
|
|
626
|
+
'datetime': time,
|
|
627
|
+
'body': body,
|
|
628
|
+
'attachments': attachments
|
|
629
|
+
}
|
|
630
|
+
cur = conn.execute('INSERT INTO CHAT (BODY, CHANNEL_ID, AUTHOR) VALUES (?,?,?) RETURNING ID;', (
|
|
631
|
+
body,
|
|
632
|
+
online[author]['channel'],
|
|
633
|
+
author
|
|
634
|
+
))
|
|
635
|
+
(chat_id, ) = cur.fetchone()
|
|
636
|
+
msg.update({'id': chat_id})
|
|
637
|
+
if attachments != []:
|
|
638
|
+
for a in attachments:
|
|
639
|
+
conn.execute(
|
|
640
|
+
'INSERT INTO ATTACHMENT (CHAT_ID, RESOURCE_ID) VALUES (?,?);', (chat_id, a))
|
|
641
|
+
emit('msg_deliver', msg, to=online[author]['channel'])
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@socketio.on('connect')
|
|
645
|
+
def connect_handler(auth):
|
|
646
|
+
try:
|
|
647
|
+
if online[auth['nick']]['token'] != auth['token']:
|
|
648
|
+
raise ConnectionRefusedError('Not Authorized')
|
|
649
|
+
except:
|
|
650
|
+
raise ConnectionRefusedError('Missing Authorization')
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
# to make static checking happy
|
|
654
|
+
online[auth['nick']]['sid'] = getattr(request, 'sid', None)
|
|
655
|
+
online[auth['nick']]['namespace'] = getattr(request, 'namespace', None)
|
|
656
|
+
except:
|
|
657
|
+
raise ConnectionRefusedError('No sid or namespace')
|
|
658
|
+
else:
|
|
659
|
+
if (time_milisecond() - online[auth['nick']]['last_heartbeat']) > app.config['app']['timeout']:
|
|
660
|
+
clean_user(auth['nick'])
|
|
661
|
+
raise ConnectionRefusedError('Not Authorized')
|
|
662
|
+
if online[auth['nick']]['is_alive']:
|
|
663
|
+
raise ConnectionRefusedError('Duplicate Session')
|
|
664
|
+
logger.info(f'User {auth['nick']} connected.')
|
|
665
|
+
online[auth['nick']]['is_alive'] = True
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@socketio.on('disconnect')
|
|
669
|
+
def disconnect_handler():
|
|
670
|
+
nick = None
|
|
671
|
+
for k, v in online.items():
|
|
672
|
+
if v["sid"] == getattr(request, 'sid', None):
|
|
673
|
+
nick = k
|
|
674
|
+
if nick:
|
|
675
|
+
logger.info(f'User {nick} disconnected.')
|
|
676
|
+
online[nick]['is_alive'] = False
|
|
677
|
+
online[nick]['last_heartbeat'] = time_milisecond()
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@socketio.on('heartbeat')
|
|
681
|
+
def heartbeat_handler(json):
|
|
682
|
+
try:
|
|
683
|
+
if online[json['nick']]['token'] != json['token']:
|
|
684
|
+
return False
|
|
685
|
+
except:
|
|
686
|
+
return False
|
|
687
|
+
online[json['nick']]['is_alive'] = True
|
|
688
|
+
online[json['nick']]['last_heartbeat'] = time_milisecond()
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def exit():
|
|
692
|
+
try:
|
|
693
|
+
conn.close()
|
|
694
|
+
except:
|
|
695
|
+
pass
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
atexit.register(exit)
|