yacscript 0.3.1__tar.gz → 0.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yacscript
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: yet another chatroom script.
5
5
  Author: Valine0x
6
6
  Author-email: Valine0x <me@valine0x.icu>
@@ -44,19 +44,23 @@ Create a venv with python version 3.13 or greater.
44
44
 
45
45
  ```bash
46
46
  $ python -m venv .venv
47
+ ```
47
48
 
48
- # Activate venv in bash
49
+ ```bash
50
+ # Activate venv in *nix shell (e.g. bash, zsh)
49
51
  $ source .venv/bin/activate
52
+ ```
50
53
 
54
+ ```pwsh
51
55
  # Activate venv in pwsh
52
- > . .venv/Scripts/Activate.ps1"
56
+ > . .venv\Scripts\Activate.ps1"
53
57
  ```
54
58
 
55
59
 
56
60
  Then, install the package inside venv.
57
61
 
58
62
  ```bash
59
- $(.venv) pip install yacscript
63
+ (.venv) pip install yacscript
60
64
  ```
61
65
 
62
66
  Create a configuration file and edit it to your liking.
@@ -64,7 +68,7 @@ Create a configuration file and edit it to your liking.
64
68
  For reference, see the <a href="#configuration">configuration section</a>.
65
69
 
66
70
  ```bash
67
- $(.venv) vi ./config.toml
71
+ (.venv) vi ./config.toml
68
72
  ```
69
73
 
70
74
  Finally, you run the server within the venv
@@ -139,6 +143,7 @@ After=network.target
139
143
  Type=simple
140
144
  WorkingDirectory=/path/to/yacs
141
145
  Environment="PATH=/path/to/yacs/.venv/bin"
146
+ # Notice the difference to deploying from package!
142
147
  ExecStart=/path/to/uv run yacs-run --config /path/to/config.toml
143
148
 
144
149
  [Install]
@@ -176,6 +181,10 @@ timeout = 5000 # in milisecond, use for heartbeat
176
181
  # Make sure X-Forwarded-For and X-Forwarded-Host are properly set!
177
182
  proxy_fix = false
178
183
 
184
+ # For socketio connections
185
+ # Do set a propper value in production enviroment
186
+ cors_allowed_origins = "https://me.example"
187
+
179
188
 
180
189
  [custom]
181
190
  title = "The YACS"
@@ -224,11 +233,6 @@ title = "My YACS Server"
224
233
  motd = "Hello!\nRule No.1!"
225
234
  ```
226
235
 
227
-
228
- ## Customization
229
-
230
- Yes, you can rewrite templates in `./app/templates/` and static assets in `./app/static/` to customize ya site. It's written in jinja2 template syntax.
231
-
232
236
  ## Acknowledgement
233
237
  YACS will not be possible without following existing projects.
234
238
 
@@ -241,7 +245,7 @@ YACS will not be possible without following existing projects.
241
245
 
242
246
 
243
247
  ## Loicense
244
- Oi mate! Got a loicense for that project Govna? Yes, and it is the best one.
248
+ Oi mate! Got a loicense for that project Govna? Yes, and it is the best one!
245
249
 
246
250
  YACS is licensed under WTFPL with NO WARRANTY.
247
251
 
@@ -27,19 +27,23 @@ Create a venv with python version 3.13 or greater.
27
27
 
28
28
  ```bash
29
29
  $ python -m venv .venv
30
+ ```
30
31
 
31
- # Activate venv in bash
32
+ ```bash
33
+ # Activate venv in *nix shell (e.g. bash, zsh)
32
34
  $ source .venv/bin/activate
35
+ ```
33
36
 
37
+ ```pwsh
34
38
  # Activate venv in pwsh
35
- > . .venv/Scripts/Activate.ps1"
39
+ > . .venv\Scripts\Activate.ps1"
36
40
  ```
37
41
 
38
42
 
39
43
  Then, install the package inside venv.
40
44
 
41
45
  ```bash
42
- $(.venv) pip install yacscript
46
+ (.venv) pip install yacscript
43
47
  ```
44
48
 
45
49
  Create a configuration file and edit it to your liking.
@@ -47,7 +51,7 @@ Create a configuration file and edit it to your liking.
47
51
  For reference, see the <a href="#configuration">configuration section</a>.
48
52
 
49
53
  ```bash
50
- $(.venv) vi ./config.toml
54
+ (.venv) vi ./config.toml
51
55
  ```
52
56
 
53
57
  Finally, you run the server within the venv
@@ -122,6 +126,7 @@ After=network.target
122
126
  Type=simple
123
127
  WorkingDirectory=/path/to/yacs
124
128
  Environment="PATH=/path/to/yacs/.venv/bin"
129
+ # Notice the difference to deploying from package!
125
130
  ExecStart=/path/to/uv run yacs-run --config /path/to/config.toml
126
131
 
127
132
  [Install]
@@ -159,6 +164,10 @@ timeout = 5000 # in milisecond, use for heartbeat
159
164
  # Make sure X-Forwarded-For and X-Forwarded-Host are properly set!
160
165
  proxy_fix = false
161
166
 
167
+ # For socketio connections
168
+ # Do set a propper value in production enviroment
169
+ cors_allowed_origins = "https://me.example"
170
+
162
171
 
163
172
  [custom]
164
173
  title = "The YACS"
@@ -207,11 +216,6 @@ title = "My YACS Server"
207
216
  motd = "Hello!\nRule No.1!"
208
217
  ```
209
218
 
210
-
211
- ## Customization
212
-
213
- Yes, you can rewrite templates in `./app/templates/` and static assets in `./app/static/` to customize ya site. It's written in jinja2 template syntax.
214
-
215
219
  ## Acknowledgement
216
220
  YACS will not be possible without following existing projects.
217
221
 
@@ -224,7 +228,7 @@ YACS will not be possible without following existing projects.
224
228
 
225
229
 
226
230
  ## Loicense
227
- Oi mate! Got a loicense for that project Govna? Yes, and it is the best one.
231
+ Oi mate! Got a loicense for that project Govna? Yes, and it is the best one!
228
232
 
229
233
  YACS is licensed under WTFPL with NO WARRANTY.
230
234
 
@@ -0,0 +1,113 @@
1
+ from .app import views, DefaultNamespace
2
+ from os import makedirs, path
3
+ import click
4
+ from flask import Flask
5
+ from .definitions import CONFIG_DEFAULT
6
+ import tomllib
7
+ from werkzeug.middleware.proxy_fix import ProxyFix
8
+ from string import ascii_lowercase, ascii_uppercase
9
+ from flask_socketio import SocketIO
10
+ from .db import init_db, close_db
11
+
12
+
13
+ def deep_update(dst: dict, src: dict) -> None:
14
+ for k, v in src.items():
15
+ if isinstance(v, dict) and isinstance(dst.get(k), dict):
16
+ deep_update(dst[k], v)
17
+ else:
18
+ dst[k] = v
19
+
20
+
21
+ def create_app(config=None) -> tuple[Flask, SocketIO]:
22
+ app = Flask(__name__)
23
+
24
+ # Load config
25
+ app.config.update(CONFIG_DEFAULT)
26
+ if config is not None:
27
+ if path.isfile(path.abspath(config)):
28
+ with open(path.abspath(config), 'rb') as file:
29
+ try:
30
+ conf = tomllib.load(file)
31
+ except tomllib.TOMLDecodeError as e:
32
+ app.logger.critical(
33
+ f'Initialization failed while parsing config file: {e}')
34
+ app.logger.warning('Starting YACS without config file...')
35
+ else:
36
+ top = conf.get('flask', None)
37
+ if top:
38
+ app.config.update(conf.pop('flask'))
39
+ deep_update(app.config, conf)
40
+ else:
41
+ app.logger.warning('Starting YACS without config file...')
42
+
43
+ if not path.isdir(app.config['res']['path']):
44
+ makedirs(app.config['res']['path'], exist_ok=True)
45
+
46
+ # Set pre-calculated values
47
+ app.config.update({'runtime': {}})
48
+ app.config['runtime']['SIZE_MAX_BYTE'] = app.config['res']['size_max'] * 1000000
49
+ options = app.config['captcha']['options']
50
+ app.config['runtime']['challenge_set'] = ['', ascii_lowercase][options['lowercase']] + \
51
+ ['', ascii_uppercase][options['uppercase']] + \
52
+ ['', '0123456789'][options['numbers']]
53
+
54
+ # Configure app instance
55
+ app.logger.setLevel(app.config['app']['log_level'])
56
+ if app.config['app']['proxy_fix']:
57
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)
58
+
59
+ # Create socket.io instance
60
+ socketio = SocketIO(
61
+ app, cors_allowed_origins=app.config['app']['cors_allowed_origins'])
62
+
63
+ # Attach routes
64
+ app.register_blueprint(views)
65
+ socketio.on_namespace(DefaultNamespace())
66
+ app.teardown_appcontext(close_db)
67
+
68
+ # Making sure that db is propperly initialized
69
+ with app.app_context():
70
+ init_db(app.config['db']['path'])
71
+
72
+ return (app, socketio)
73
+
74
+
75
+ @click.command()
76
+ @click.option('-c', '--config', default=None, help='Path to the config file.')
77
+ def main(config):
78
+ '''Entry point for YACS.'''
79
+
80
+ # Validate given path
81
+ if config is not None:
82
+ if not path.isfile(config):
83
+ click.echo('Config file does not exist!', err=True)
84
+ return 0
85
+
86
+ (app, socketio) = create_app(config)
87
+
88
+ app.logger.info(
89
+ f'YACS is now running on http://{app.config["app"]["ip"]}:{app.config["app"]["port"]}{' with DEBUG MODE ON' if app.config['DEBUG'] else ''}.')
90
+
91
+ # Check misconfigurations
92
+ user_phrase = app.config['app']['user_phrase']
93
+ admin_phrase = app.config['app']['admin_phrase']
94
+
95
+ if user_phrase == '' and admin_phrase == 'admin':
96
+ app.logger.warning(
97
+ f'You have not set the passphrases or set exactly as default values, this is not safe!')
98
+ app.logger.warning(
99
+ f'There\'s no passphrase for user and "{admin_phrase}" for admin.')
100
+ if user_phrase == admin_phrase:
101
+ app.logger.critical(
102
+ 'user_phrase and admin_phrase shouldn\'t be exactly same!')
103
+ return 0
104
+ if admin_phrase == '':
105
+ app.logger.critical('admin_phrase shouldn\'t be empty!')
106
+ return 0
107
+
108
+ socketio.run(
109
+ app,
110
+ host=app.config["app"]["ip"],
111
+ port=app.config["app"]["port"],
112
+ debug=app.config["DEBUG"],
113
+ )
@@ -1,33 +1,27 @@
1
- import atexit
2
1
  import random
3
- import tomllib
4
2
  import uuid
5
3
  from base64 import b64encode
6
4
  from datetime import UTC, datetime
7
5
  from functools import wraps
8
- from os import path, mkdir
6
+ from os import path
9
7
  from string import ascii_lowercase, ascii_uppercase
10
8
  from time import time
11
9
  from collections import OrderedDict
12
10
 
13
11
  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)
12
+ from flask import (Response, redirect, render_template,
13
+ request, send_file, jsonify, current_app, Blueprint, g)
14
+ from flask_socketio import (emit, join_room,
15
+ leave_room, disconnect, Namespace)
21
16
  from webargs import fields, validate
22
17
  from webargs.flaskparser import use_args
23
- from werkzeug.middleware.proxy_fix import ProxyFix
24
18
  from werkzeug.utils import secure_filename
19
+ from captcha.image import ImageCaptcha
25
20
 
21
+ from .db import get_db
26
22
 
27
- # --- GLOBAL DEFINITIONS ---
28
23
 
29
- app = Flask(__name__)
30
- socketio = SocketIO(app, cors_allowed_origins='*')
24
+ views = Blueprint('views', __name__)
31
25
 
32
26
  # Caches
33
27
  candidates: OrderedDict = OrderedDict()
@@ -36,50 +30,6 @@ online = dict()
36
30
 
37
31
  # --- HELPER FUNCTIONS ---
38
32
 
39
- def deep_update(dst: dict, src: dict) -> None:
40
- for k, v in src.items():
41
- if isinstance(v, dict) and isinstance(dst.get(k), dict):
42
- deep_update(dst[k], v)
43
- else:
44
- dst[k] = v
45
-
46
- def initialization(config=None):
47
- app.config.update(CONFIG_DEFAULT)
48
- if config is not None:
49
- if path.isfile(path.abspath(config)):
50
- with open(path.abspath(config), 'rb') as file:
51
- try:
52
- conf = tomllib.load(file)
53
- except tomllib.TOMLDecodeError as e:
54
- return e
55
- # read built-in values
56
- top = conf.get('flask', None)
57
- if top:
58
- app.config.update(conf.pop('flask'))
59
- deep_update(app.config, conf)
60
- if app.config['app']['proxy_fix']:
61
- app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)
62
- if not path.isdir(app.config['res']['path']):
63
- mkdir(app.config['res']['path'])
64
-
65
- # Making global variables
66
- # TODO: consider changing them to vars inside app
67
- global logger
68
- global conn
69
- global SIZE_MAX_BYTE
70
- global challenge_set
71
- global image_captcha
72
- app.logger.setLevel(app.config['app']['log_level'])
73
- logger = app.logger
74
- conn = get_db(app.config['db']['path'], logger)
75
- SIZE_MAX_BYTE = app.config['res']['size_max'] * 1000000
76
- options = app.config['captcha']['options']
77
- challenge_set = ['', ascii_lowercase][options['lowercase']] + \
78
- ['', ascii_uppercase][options['uppercase']] + \
79
- ['', '0123456789'][options['numbers']]
80
- image_captcha = ImageCaptcha()
81
- return 'ok'
82
-
83
33
 
84
34
  def get_channel_member(channel_id: int):
85
35
  resp = []
@@ -91,7 +41,7 @@ def get_channel_member(channel_id: int):
91
41
 
92
42
 
93
43
  def push_candidate(identifier, challenge, time):
94
- if len(candidates) >= app.config['captcha']['max_cache']:
44
+ if len(candidates) >= current_app.config['captcha']['max_cache']:
95
45
  candidates.popitem(last=False)
96
46
  candidates[identifier] = (challenge, time)
97
47
 
@@ -101,7 +51,7 @@ def verify_candidate(identifier, challenge, time):
101
51
  candidate = candidates.pop(identifier)
102
52
  except:
103
53
  return False
104
- if (time - candidate[1]) > app.config['captcha']['expire']:
54
+ if (time - candidate[1]) > current_app.config['captcha']['expire']:
105
55
  return False
106
56
  if challenge == candidate[0]:
107
57
  return True
@@ -127,7 +77,7 @@ def clean_user(name):
127
77
  item['sid'],
128
78
  item['namespace']
129
79
  )
130
- logger.info(f'User {name} logged out.')
80
+ current_app.logger.info(f'User {name} logged out.')
131
81
 
132
82
 
133
83
  def time_milisecond():
@@ -136,12 +86,12 @@ def time_milisecond():
136
86
 
137
87
  def garbage_collect():
138
88
  for k, v in list(online.items()):
139
- if (time_milisecond() - v['last_heartbeat']) > app.config['app']['timeout']:
89
+ if (time_milisecond() - v['last_heartbeat']) > current_app.config['app']['timeout']:
140
90
  clean_user(k)
141
91
 
142
92
 
143
- @app.errorhandler(422)
144
- @app.errorhandler(400)
93
+ @views.errorhandler(422)
94
+ @views.errorhandler(400)
145
95
  def handle_error(err):
146
96
  headers = err.data.get('headers', None)
147
97
  messages = err.data.get('messages', ['Invalid request.'])
@@ -159,14 +109,18 @@ def login_required(f):
159
109
  auth = request.headers.get('Authorization', default=None)
160
110
  if not auth:
161
111
  return Response(status=401)
112
+
162
113
  try:
163
114
  _, nick, token = str(auth).split(' ')
164
115
  except:
165
116
  return Response(status=401)
166
- if nick not in online.keys():
167
- return Response(status=401)
168
- if not online[nick]['token'] == token:
169
- return Response(status=401)
117
+ else:
118
+ if nick not in online.keys():
119
+ return Response(status=401)
120
+ if not online[nick]['token'] == token:
121
+ return Response(status=401)
122
+ g.nick = nick
123
+ g.token = token
170
124
  return f(*args, **kwargs)
171
125
  return decorated
172
126
 
@@ -177,16 +131,20 @@ def admin_login_required(f):
177
131
  auth = request.headers.get('Authorization', default=None)
178
132
  if not auth:
179
133
  return Response(status=401)
134
+
180
135
  try:
181
136
  _, nick, token = str(auth).split(' ')
182
137
  except:
183
138
  return Response(status=401)
184
- if nick not in online.keys():
185
- return Response(status=401)
186
- if not online[nick]['token'] == token:
187
- return Response(status=401)
188
- if not online[nick]['is_admin']:
189
- return Response(status=401)
139
+ else:
140
+ if nick not in online.keys():
141
+ return Response(status=401)
142
+ if not online[nick]['token'] == token:
143
+ return Response(status=401)
144
+ if not online[nick]['is_admin']:
145
+ return Response(status=401)
146
+ g.nick = nick
147
+ g.token = token
190
148
  return f(*args, **kwargs)
191
149
  return decorated
192
150
 
@@ -194,7 +152,7 @@ def admin_login_required(f):
194
152
  # --- VIEW ROUTES ---
195
153
  # ROUTES HERE ARE SPECIFIALLY FOR RENDERING VIEWS
196
154
 
197
- @app.route('/')
155
+ @views.route('/')
198
156
  def landing_page():
199
157
  # get possible failure callback
200
158
  failed = request.args.get('failed', default=None)
@@ -203,13 +161,14 @@ def landing_page():
203
161
  # making captcha
204
162
  identifier = str(uuid.uuid4())
205
163
  challenge = ''.join(random.choices(
206
- challenge_set, k=app.config['captcha']['length']))
164
+ current_app.config['runtime']['challenge_set'], k=current_app.config['captcha']['length']))
207
165
  push_candidate(identifier, challenge, time())
166
+ image_captcha = ImageCaptcha()
208
167
  data = image_captcha.generate(challenge).read()
209
168
  captcha_data = b64encode(data).decode()
210
169
  return render_template(
211
170
  'index.jinja',
212
- title=app.config['custom']['title'],
171
+ title=current_app.config['custom']['title'],
213
172
  captcha=captcha_data,
214
173
  identifier=identifier,
215
174
  failed=failed
@@ -224,11 +183,11 @@ LOGIN_FORM = {
224
183
  }
225
184
 
226
185
 
227
- @app.route('/auth', methods=['POST'])
186
+ @views.route('/auth', methods=['POST'])
228
187
  @use_args(LOGIN_FORM, location='form')
229
188
  def auth(form):
230
- if form['phrase'] != app.config['app']['admin_phrase']:
231
- if form['phrase'] != app.config['app']['user_phrase']:
189
+ if form['phrase'] != current_app.config['app']['admin_phrase']:
190
+ if form['phrase'] != current_app.config['app']['user_phrase']:
232
191
  return redirect('/?failed=Wrong+passphrase')
233
192
 
234
193
  if verify_candidate(form['identifier'], form['captcha'], time()):
@@ -236,7 +195,8 @@ def auth(form):
236
195
  if form['nick'] in online.keys():
237
196
  return redirect('/?failed=User+exists')
238
197
  token = str(uuid.uuid4())
239
- is_admin = (form['phrase'] == app.config['app']['admin_phrase'])
198
+ is_admin = (form['phrase'] ==
199
+ current_app.config['app']['admin_phrase'])
240
200
  online[form['nick']] = dict(
241
201
  token=token,
242
202
  is_admin=is_admin,
@@ -248,16 +208,16 @@ def auth(form):
248
208
  is_uploading=False,
249
209
  is_alive=False
250
210
  )
251
- logger.info(f'User {form['nick']} logged in.')
211
+ current_app.logger.info(f'User {form['nick']} logged in.')
252
212
  return redirect(f'/room?nickname={form['nick']}&token={token}')
253
213
  return redirect('/?failed=CAPTCHA+failed')
254
214
 
255
215
 
256
- @app.route('/room-preview', methods=['POST', 'GET'])
216
+ @views.route('/room-preview', methods=['POST', 'GET'])
257
217
  def room_preview():
258
- if not app.config['DEBUG']:
218
+ if not current_app.config['DEBUG']:
259
219
  return Response(status=404)
260
- name = 'test_' + ''.join(random.choices(challenge_set, k=5))
220
+ name = 'test_' + ''.join(random.choices(ascii_lowercase, k=5))
261
221
  online[name] = {
262
222
  'token': 'test',
263
223
  'is_admin': True,
@@ -271,11 +231,11 @@ def room_preview():
271
231
  }
272
232
  return render_template(
273
233
  'room.jinja',
274
- title=app.config['custom']['title'],
234
+ title=current_app.config['custom']['title'],
275
235
  token='test',
276
236
  nick=name,
277
- emotes=app.config['custom']['emoticons'],
278
- motd=app.config['custom']['motd'],
237
+ emotes=current_app.config['custom']['emoticons'],
238
+ motd=current_app.config['custom']['motd'],
279
239
  is_admin=True
280
240
  )
281
241
 
@@ -286,7 +246,7 @@ ROOM_FORM = {
286
246
  }
287
247
 
288
248
 
289
- @app.route('/room')
249
+ @views.route('/room')
290
250
  @use_args(ROOM_FORM, location='query')
291
251
  def room_view(auth):
292
252
  nick = auth['nickname']
@@ -297,29 +257,29 @@ def room_view(auth):
297
257
  except:
298
258
  return redirect('/?failed=Invalid+authentication')
299
259
  if online[nick]['last_heartbeat']:
300
- if (time_milisecond() - online[nick]['last_heartbeat']) > app.config['app']['timeout']:
260
+ if (time_milisecond() - online[nick]['last_heartbeat']) > current_app.config['app']['timeout']:
301
261
  clean_user(nick)
302
262
  return redirect('/?failed=Authentication+expired')
303
263
  return render_template(
304
264
  'room.jinja',
305
- title=app.config['custom']['title'],
306
- emotes=app.config['custom']['emoticons'],
265
+ title=current_app.config['custom']['title'],
266
+ emotes=current_app.config['custom']['emoticons'],
307
267
  nick=nick,
308
268
  token=token,
309
- motd=app.config['custom']['motd'],
269
+ motd=current_app.config['custom']['motd'],
310
270
  is_admin=online[nick]['is_admin'],
311
- timeout=app.config['app']['timeout'],
271
+ timeout=current_app.config['app']['timeout'],
312
272
  )
313
273
 
314
274
  # --- SECRET API ---
315
275
  # ROUTES HERE NEED TO BE CALLED BY LOGGED IN CLIENT SCRIPT
316
276
 
317
277
 
318
- @app.route('/channels')
278
+ @views.route('/channels')
319
279
  @login_required
320
280
  def get_channels():
321
- auth = request.headers.get('Authorization', default=None)
322
- nick = str(auth).split(' ')[1]
281
+ nick = g.get('nick')
282
+ conn = get_db()
323
283
  res = conn.execute(
324
284
  'SELECT * FROM CHANNEL WHERE IS_DELETED=0 AND (ADMIN_ONLY=0 OR ?=1)', (online[nick]['is_admin'],))
325
285
  channels = []
@@ -332,11 +292,10 @@ def get_channels():
332
292
  return channels
333
293
 
334
294
 
335
- @app.route('/index_upload', methods=['POST'])
295
+ @views.route('/index_upload', methods=['POST'])
336
296
  @login_required
337
297
  def upload_index():
338
- auth = request.headers.get('Authorization', default=None)
339
- uploader = str(auth).split(' ')[1]
298
+ uploader = g.get('nick')
340
299
  if online[uploader]['is_uploading']:
341
300
  return Response(status=429)
342
301
  # lock
@@ -355,7 +314,7 @@ def upload_index():
355
314
  name = secure_filename(name)
356
315
  mime = file.mimetype
357
316
  binary = file.read()
358
- if len(binary) > SIZE_MAX_BYTE:
317
+ if len(binary) > current_app.config['runtime']['SIZE_MAX_BYTE']:
359
318
  online[uploader]['is_uploading'] = False
360
319
  return Response(status=400)
361
320
  id = str(uuid.uuid4())
@@ -366,11 +325,10 @@ def upload_index():
366
325
  return {'uuid': id, 'file_name': name}
367
326
 
368
327
 
369
- @app.route('/submit_upload')
328
+ @views.route('/submit_upload')
370
329
  @login_required
371
330
  def upload_submit():
372
- auth = request.headers.get('Authorization', default=None)
373
- user = str(auth).split(' ')[1]
331
+ user = g.get('nick')
374
332
 
375
333
  recall = request.args.get('recall', default=None)
376
334
  submit = request.args.get('submit', default=None)
@@ -389,11 +347,12 @@ def upload_submit():
389
347
  # Write to disk
390
348
  extension = path.splitext(name)[1]
391
349
  file_path = path.abspath(
392
- path.join(app.config['res']['path'], f'{submit}{extension}'))
350
+ path.join(current_app.config['res']['path'], f'{submit}{extension}'))
393
351
  with open(file_path, 'xb') as f:
394
352
  f.write(binary)
395
353
 
396
354
  # Write to DB
355
+ conn = get_db()
397
356
  conn.execute(
398
357
  'INSERT INTO RESOURCE (UUID, FILE_NAME, MIME_TYPE) VALUES (?, ?, ?);',
399
358
  (submit, name, mime)
@@ -402,7 +361,7 @@ def upload_submit():
402
361
  return Response(status=400)
403
362
 
404
363
 
405
- @app.route('/messages/<int:channel_id>')
364
+ @views.route('/messages/<int:channel_id>')
406
365
  @login_required
407
366
  def get_messages(channel_id):
408
367
  if channel_id == 0:
@@ -415,8 +374,8 @@ def get_messages(channel_id):
415
374
  offset = int(request.args['offset'])
416
375
 
417
376
  # check your fucking privilege
418
- auth = request.headers.get('Authorization', default=None)
419
- nick = str(auth).split(' ')[1]
377
+ nick = g.get('nick')
378
+ conn = get_db()
420
379
  cur = conn.execute(
421
380
  'SELECT ADMIN_ONLY FROM CHANNEL WHERE ID=?;', (channel_id,))
422
381
  row = cur.fetchone()
@@ -448,15 +407,10 @@ def get_messages(channel_id):
448
407
  return resp
449
408
 
450
409
 
451
- @app.route('/fellows')
410
+ @views.route('/fellows')
452
411
  @login_required
453
412
  def get_fellows():
454
- auth = request.headers.get('Authorization', default=None)
455
- if auth == None:
456
- # auth is guaranteed to be valid after decoration
457
- # so uh this is merely a trick to disable static checker
458
- return Response(status=400)
459
- user = auth.split(' ')[1]
413
+ user = g.get('nick')
460
414
  channel = online[user]['channel']
461
415
  if channel == 0:
462
416
  return Response(status=404)
@@ -465,8 +419,10 @@ def get_fellows():
465
419
  # --- PUBLIC APIS ---
466
420
 
467
421
 
468
- @app.route('/resource_meta/<resource_id>')
422
+ @views.route('/resource_meta/<resource_id>')
469
423
  def get_resource_meta(resource_id):
424
+
425
+ conn = get_db()
470
426
  row = conn.execute(
471
427
  'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
472
428
  row = row.fetchone()
@@ -478,8 +434,9 @@ def get_resource_meta(resource_id):
478
434
  }
479
435
 
480
436
 
481
- @app.route('/resource/<resource_id>')
437
+ @views.route('/resource/<resource_id>')
482
438
  def get_resource(resource_id):
439
+ conn = get_db()
483
440
  row = conn.execute(
484
441
  'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
485
442
  row = row.fetchone()
@@ -487,13 +444,13 @@ def get_resource(resource_id):
487
444
  return Response(status=404)
488
445
  extension = path.splitext(row[0])[1]
489
446
  file_path = path.abspath(path.join(
490
- app.config['res']['path'],
447
+ current_app.config['res']['path'],
491
448
  f'{resource_id}.{extension}'
492
449
  ))
493
450
  if path.exists(file_path):
494
451
  return send_file(file_path, mimetype=row[1], download_name=row[0])
495
452
  file_path = path.abspath(path.join(
496
- app.config['res']['path'],
453
+ current_app.config['res']['path'],
497
454
  f'{resource_id}{extension}'
498
455
  ))
499
456
  if path.exists(file_path):
@@ -504,16 +461,17 @@ def get_resource(resource_id):
504
461
  (resource_id, )
505
462
  )
506
463
  except Exception as e:
507
- logger.error(
464
+ current_app.logger.error(
508
465
  f'Error deleting resource {resource_id} with exception {e}')
509
466
  return Response(status=404)
510
467
 
511
468
  # --- ADMIN APIS ---
512
469
 
513
470
 
514
- @app.route('/channel', methods=['POST', 'PUT', 'DELETE'])
471
+ @views.route('/channel', methods=['POST', 'PUT', 'DELETE'])
515
472
  @admin_login_required
516
473
  def channel_control():
474
+ conn = get_db()
517
475
  if request.method == 'POST':
518
476
  try:
519
477
  name = request.get_json()['name']
@@ -549,7 +507,7 @@ def channel_control():
549
507
  return Response(status=404)
550
508
 
551
509
 
552
- @app.route('/online/<name>', methods=['DELETE'])
510
+ @views.route('/online/<name>', methods=['DELETE'])
553
511
  @admin_login_required
554
512
  def kick_user(name):
555
513
  if name in online.keys():
@@ -558,153 +516,141 @@ def kick_user(name):
558
516
  return Response(status=400)
559
517
 
560
518
 
561
- @app.route('/resource/<res_id>', methods=['DELETE'])
519
+ @views.route('/resource/<res_id>', methods=['DELETE'])
562
520
  @admin_login_required
563
521
  def delete_resource(res_id):
522
+ conn = get_db()
564
523
  try:
565
524
  conn.execute(
566
525
  'UPDATE RESOURCE SET IS_EXPIRED=1 WHERE UUID=?;',
567
526
  (res_id, )
568
527
  )
569
528
  except Exception as e:
570
- logger.error(f'Error deleting resource {res_id} with exception {e}')
529
+ current_app.logger.error(
530
+ f'Error deleting resource {res_id} with exception {e}')
571
531
  return Response(status=400)
572
532
  return Response(status=200)
573
533
 
574
534
 
575
- @app.route('/message/<int:id>', methods=['DELETE'])
535
+ @views.route('/message/<int:id>', methods=['DELETE'])
576
536
  @admin_login_required
577
537
  def delete_msg(id: int):
538
+ conn = get_db()
578
539
  try:
579
540
  conn.execute(
580
541
  'UPDATE CHAT SET IS_DELETED=1 WHERE ID=?;',
581
542
  (id, )
582
543
  )
583
544
  except Exception as e:
584
- logger.error(f'Error deleting msg {id} with exception {e}')
545
+ current_app.logger.error(f'Error deleting msg {id} with exception {e}')
585
546
  return Response(status=400)
586
547
  return Response(status=200)
587
548
 
588
549
 
589
550
  # --- SOCKET EVENTS ---
590
551
 
591
- @socketio.on('updating_channel')
592
- def updated_channel():
593
- emit('channel_updated')
594
-
595
-
596
- @socketio.on('sw_channel')
597
- def sw_channel_handler(json):
598
- try:
599
- if json['token'] != online[json['nick']]['token']:
600
- return
601
- except:
602
- return
603
-
604
- user = json['nick']
605
- to = json['to']
606
- is_admin = online[user]['is_admin']
607
- res = conn.execute('SELECT id FROM CHANNEL' +
608
- (' WHERE ADMIN_ONLY=0;', '')[is_admin])
609
- ids = [x[0] for x in res.fetchall()]
610
-
611
- if to not in ids:
612
- return
613
-
614
- current_channel = online[user]['channel']
615
- if current_channel != 0:
616
- leave_room(current_channel)
617
- emit('leaving', {'target': json['nick']}, to=current_channel)
618
- join_room(to)
619
- online[user]['channel'] = to
620
- emit('joining', {'target': json['nick']}, to=to)
621
-
622
-
623
- @socketio.on('msg_send')
624
- def msg_send_handler(json):
625
- author: str = json['author']
626
- token: str = json['token']
627
- if not author in online.keys():
628
- return
629
- if online[author]['token'] != token:
630
- return
631
- if json['body'] == '':
632
- return
633
- body: str = bbcode.render_html(json['body'])
634
- attachments: list = json.get('attachments', [])
635
- time: str = datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')
636
- msg = {
637
- 'author': author,
638
- 'datetime': time,
639
- 'body': body,
640
- 'attachments': attachments
641
- }
642
- cur = conn.execute('INSERT INTO CHAT (BODY, CHANNEL_ID, AUTHOR) VALUES (?,?,?) RETURNING ID;', (
643
- body,
644
- online[author]['channel'],
645
- author
646
- ))
647
- (chat_id, ) = cur.fetchone()
648
- msg.update({'id': chat_id})
649
- if attachments != []:
650
- for a in attachments:
651
- conn.execute(
652
- 'INSERT INTO ATTACHMENT (CHAT_ID, RESOURCE_ID) VALUES (?,?);', (chat_id, a))
653
- emit('msg_deliver', msg, to=online[author]['channel'])
654
-
655
-
656
- @socketio.on('connect')
657
- def connect_handler(auth):
658
- try:
659
- if online[auth['nick']]['token'] != auth['token']:
660
- raise ConnectionRefusedError('Not Authorized')
661
- except:
662
- raise ConnectionRefusedError('Missing Authorization')
663
-
664
- try:
665
- # to make static checking happy
666
- online[auth['nick']]['sid'] = getattr(request, 'sid', None)
667
- online[auth['nick']]['namespace'] = getattr(request, 'namespace', None)
668
- except:
669
- raise ConnectionRefusedError('No sid or namespace')
670
- else:
671
- if (time_milisecond() - online[auth['nick']]['last_heartbeat']) > app.config['app']['timeout']:
672
- clean_user(auth['nick'])
673
- raise ConnectionRefusedError('Not Authorized')
674
- if online[auth['nick']]['is_alive']:
675
- raise ConnectionRefusedError('Duplicate Session')
676
- logger.info(f'User {auth['nick']} connected.')
677
- online[auth['nick']]['is_alive'] = True
678
-
679
-
680
- @socketio.on('disconnect')
681
- def disconnect_handler():
682
- nick = None
683
- for k, v in online.items():
684
- if v["sid"] == getattr(request, 'sid', None):
685
- nick = k
686
- if nick:
687
- logger.info(f'User {nick} disconnected.')
688
- online[nick]['is_alive'] = False
689
- online[nick]['last_heartbeat'] = time_milisecond()
690
-
552
+ class DefaultNamespace(Namespace):
553
+ def on_connect(self, auth):
554
+ if online.get(auth['nick'], None) is None:
555
+ return False
556
+ try:
557
+ if online[auth['nick']]['token'] != auth['token']:
558
+ return False
559
+ except:
560
+ return False
691
561
 
692
- @socketio.on('heartbeat')
693
- def heartbeat_handler(json):
694
- try:
695
- if online[json['nick']]['token'] != json['token']:
562
+ try:
563
+ # to make static checking happy
564
+ online[auth['nick']]['sid'] = getattr(request, 'sid', None)
565
+ online[auth['nick']]['namespace'] = getattr(
566
+ request, 'namespace', None)
567
+ except:
568
+ return False
569
+ else:
570
+ if (time_milisecond() - online[auth['nick']]['last_heartbeat']) > current_app.config['app']['timeout']:
571
+ clean_user(auth['nick'])
572
+ return False
573
+ if online[auth['nick']]['is_alive']:
574
+ return False
575
+ current_app.logger.info(f'User {auth['nick']} connected.')
576
+ online[auth['nick']]['is_alive'] = True
577
+
578
+ def on_disconnect(self, reason):
579
+ nick = None
580
+ for k, v in online.items():
581
+ if v["sid"] == getattr(request, 'sid', None):
582
+ nick = k
583
+ if nick:
584
+ current_app.logger.info(f'User {nick} disconnected. {reason}')
585
+ online[nick]['is_alive'] = False
586
+ online[nick]['last_heartbeat'] = time_milisecond()
587
+
588
+ def on_sw_channel(self, json):
589
+ try:
590
+ if json['token'] != online[json['nick']]['token']:
591
+ return False
592
+ except:
696
593
  return False
697
- except:
698
- return False
699
- online[json['nick']]['is_alive'] = True
700
- online[json['nick']]['last_heartbeat'] = time_milisecond()
701
594
 
595
+ user = json['nick']
596
+ to = json['to']
597
+ is_admin = online[user]['is_admin']
598
+ conn = get_db()
599
+ res = conn.execute('SELECT id FROM CHANNEL' +
600
+ (' WHERE ADMIN_ONLY=0;', '')[is_admin])
601
+ ids = [x[0] for x in res.fetchall()]
702
602
 
703
- def cleanup():
704
- try:
705
- conn.close()
706
- except:
707
- pass
603
+ if to not in ids:
604
+ return False
708
605
 
606
+ current_channel = online[user]['channel']
607
+ if current_channel != 0:
608
+ leave_room(current_channel)
609
+ emit('leaving', {'target': json['nick']}, to=current_channel)
610
+ join_room(to)
611
+ online[user]['channel'] = to
612
+ emit('joining', {'target': json['nick']}, to=to)
613
+
614
+ def on_msg_send(self, json):
615
+ author: str = json['author']
616
+ token: str = json['token']
617
+ conn = get_db()
618
+ if not author in online.keys():
619
+ return False
620
+ if online[author]['token'] != token:
621
+ return False
622
+ if json['body'] == '':
623
+ return False
624
+ body: str = bbcode.render_html(json['body'])
625
+ attachments: list = json.get('attachments', [])
626
+ time: str = datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')
627
+ msg = {
628
+ 'author': author,
629
+ 'datetime': time,
630
+ 'body': body,
631
+ 'attachments': attachments
632
+ }
633
+ cur = conn.execute('INSERT INTO CHAT (BODY, CHANNEL_ID, AUTHOR) VALUES (?,?,?) RETURNING ID;', (
634
+ body,
635
+ online[author]['channel'],
636
+ author
637
+ ))
638
+ (chat_id, ) = cur.fetchone()
639
+ msg.update({'id': chat_id})
640
+ if attachments != []:
641
+ for a in attachments:
642
+ conn.execute(
643
+ 'INSERT INTO ATTACHMENT (CHAT_ID, RESOURCE_ID) VALUES (?,?);', (chat_id, a))
644
+ emit('msg_deliver', msg, to=online[author]['channel'])
645
+
646
+ def on_heartbeat(self, json):
647
+ try:
648
+ if online[json['nick']]['token'] != json['token']:
649
+ return False
650
+ except:
651
+ return False
652
+ online[json['nick']]['is_alive'] = True
653
+ online[json['nick']]['last_heartbeat'] = time_milisecond()
709
654
 
710
- atexit.register(cleanup)
655
+ def on_updating_channel(self):
656
+ emit('channel_updated')
@@ -0,0 +1,24 @@
1
+ import sqlite3
2
+ from flask import g, current_app
3
+ from os import path
4
+ from .definitions import SCHEMA
5
+ from logging import Logger
6
+
7
+
8
+ def init_db(db_path: str):
9
+ if not path.exists(db_path):
10
+ current_app.logger.warning("Database not found!")
11
+ conn = sqlite3.connect(db_path, check_same_thread=False, autocommit=True)
12
+ conn.executescript(SCHEMA)
13
+ current_app.logger.info("Database created and initialized.")
14
+ conn.close()
15
+
16
+ def get_db():
17
+ if 'db' not in g:
18
+ g.db = sqlite3.connect(current_app.config['db']['path'], check_same_thread=False, autocommit=True)
19
+ return g.db
20
+
21
+ def close_db(e=None):
22
+ db = g.pop('db', None)
23
+ if db:
24
+ db.close()
@@ -45,6 +45,7 @@ CONFIG_DEFAULT = {
45
45
  'log_level': 'INFO',
46
46
  'timeout': 5000,
47
47
  'proxy_fix': False,
48
+ 'cors_allowed_origins': '*'
48
49
  },
49
50
  'custom': {
50
51
  'title': 'The YACS',
@@ -183,7 +183,7 @@ async function switch_channel(channel_id) {
183
183
  msgbox.scrollTop = msgbox.scrollHeight;
184
184
  }
185
185
 
186
- async function load_more(count = 30) {
186
+ async function load_more(count = 15) {
187
187
  var msgbox = document.getElementById("msgbox");
188
188
  var new_msg = await fetch(
189
189
  `/messages/${current_channel}?count=${count}&offset=${msgbox.childElementCount}`,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "YACScript"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "yet another chatroom script."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,35 +0,0 @@
1
- from .app import app, socketio, initialization
2
- from os.path import isfile
3
- import click
4
-
5
- @click.command()
6
- @click.option('-c', '--config', default=None, help='Path to the config file.')
7
- def main(config):
8
- '''Entry point for YACS.'''
9
- if config is not None:
10
- if not isfile(config):
11
- click.echo('Config file does not exist!', err=True)
12
- return 0
13
- result = initialization(config)
14
- if result != 'ok':
15
- click.echo(f'Initialization failed while parsing config file: {result}')
16
- return 0
17
- app.logger.info(f'YACS is now running on http://{app.config["app"]["ip"]}:{app.config["app"]["port"]}{' with DEBUG MODE ON' if app.config['DEBUG'] else ''}.')
18
- user_phrase = app.config['app']['user_phrase']
19
- admin_phrase = app.config['app']['admin_phrase']
20
-
21
- if user_phrase == '' and admin_phrase == 'admin':
22
- app.logger.warning(f'You have not set the passphrases or set exactly as default values, this is not safe!')
23
- app.logger.warning(f'There\'s no passphrase for user and "{admin_phrase}" for admin.')
24
- if user_phrase == admin_phrase:
25
- app.logger.critical('user_phrase and admin_phrase shouldn\'t be exactly same!')
26
- return 0
27
- if admin_phrase == '':
28
- app.logger.critical('admin_phrase shouldn\'t be empty!')
29
- return 0
30
- socketio.run(
31
- app,
32
- host=app.config["app"]["ip"],
33
- port=app.config["app"]["port"],
34
- debug=app.config["DEBUG"],
35
- )
yacscript-0.3.1/app/db.py DELETED
@@ -1,25 +0,0 @@
1
- import os
2
- import sqlite3
3
- from logging import Logger
4
- from app.definitions import SCHEMA
5
-
6
-
7
- def get_db(path: str, logger: Logger) -> sqlite3.Connection:
8
- if not os.path.exists(path):
9
- logger.warning("Database not found!")
10
- conn = sqlite3.connect(path, check_same_thread=False, autocommit=True)
11
- conn.executescript(SCHEMA)
12
- logger.info("Database created and initialized.")
13
- return conn
14
-
15
- conn = sqlite3.connect(path, check_same_thread=False, autocommit=True)
16
- cursor = conn.cursor()
17
- cursor.execute(
18
- "SELECT name FROM sqlite_master WHERE type='table' AND name='CHAT';")
19
- if cursor.fetchone() is None:
20
- logger.warning("Database not initialized or is CORRUPTED!")
21
- conn.executescript(SCHEMA)
22
- logger.info("Database initialized.")
23
- cursor.close()
24
- logger.info("Database good!")
25
- return conn
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes