yacscript 0.3.0__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.0
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,43 +30,6 @@ online = dict()
36
30
 
37
31
  # --- HELPER FUNCTIONS ---
38
32
 
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
33
 
77
34
  def get_channel_member(channel_id: int):
78
35
  resp = []
@@ -84,7 +41,7 @@ def get_channel_member(channel_id: int):
84
41
 
85
42
 
86
43
  def push_candidate(identifier, challenge, time):
87
- if len(candidates) >= app.config['captcha']['max_cache']:
44
+ if len(candidates) >= current_app.config['captcha']['max_cache']:
88
45
  candidates.popitem(last=False)
89
46
  candidates[identifier] = (challenge, time)
90
47
 
@@ -94,7 +51,7 @@ def verify_candidate(identifier, challenge, time):
94
51
  candidate = candidates.pop(identifier)
95
52
  except:
96
53
  return False
97
- if (time - candidate[1]) > app.config['captcha']['expire']:
54
+ if (time - candidate[1]) > current_app.config['captcha']['expire']:
98
55
  return False
99
56
  if challenge == candidate[0]:
100
57
  return True
@@ -111,11 +68,16 @@ def clean_user(name):
111
68
  item['sid'],
112
69
  item['namespace']
113
70
  )
71
+ emit(
72
+ 'kicked',
73
+ to=item['sid'],
74
+ namespace=item['namespace']
75
+ )
114
76
  disconnect(
115
77
  item['sid'],
116
78
  item['namespace']
117
79
  )
118
- logger.info(f'User {name} logged out.')
80
+ current_app.logger.info(f'User {name} logged out.')
119
81
 
120
82
 
121
83
  def time_milisecond():
@@ -124,12 +86,12 @@ def time_milisecond():
124
86
 
125
87
  def garbage_collect():
126
88
  for k, v in list(online.items()):
127
- if (time_milisecond() - v['last_heartbeat']) > app.config['app']['timeout']:
89
+ if (time_milisecond() - v['last_heartbeat']) > current_app.config['app']['timeout']:
128
90
  clean_user(k)
129
91
 
130
92
 
131
- @app.errorhandler(422)
132
- @app.errorhandler(400)
93
+ @views.errorhandler(422)
94
+ @views.errorhandler(400)
133
95
  def handle_error(err):
134
96
  headers = err.data.get('headers', None)
135
97
  messages = err.data.get('messages', ['Invalid request.'])
@@ -147,14 +109,18 @@ def login_required(f):
147
109
  auth = request.headers.get('Authorization', default=None)
148
110
  if not auth:
149
111
  return Response(status=401)
112
+
150
113
  try:
151
114
  _, nick, token = str(auth).split(' ')
152
115
  except:
153
116
  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)
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
158
124
  return f(*args, **kwargs)
159
125
  return decorated
160
126
 
@@ -165,16 +131,20 @@ def admin_login_required(f):
165
131
  auth = request.headers.get('Authorization', default=None)
166
132
  if not auth:
167
133
  return Response(status=401)
134
+
168
135
  try:
169
136
  _, nick, token = str(auth).split(' ')
170
137
  except:
171
138
  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)
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
178
148
  return f(*args, **kwargs)
179
149
  return decorated
180
150
 
@@ -182,7 +152,7 @@ def admin_login_required(f):
182
152
  # --- VIEW ROUTES ---
183
153
  # ROUTES HERE ARE SPECIFIALLY FOR RENDERING VIEWS
184
154
 
185
- @app.route('/')
155
+ @views.route('/')
186
156
  def landing_page():
187
157
  # get possible failure callback
188
158
  failed = request.args.get('failed', default=None)
@@ -191,13 +161,14 @@ def landing_page():
191
161
  # making captcha
192
162
  identifier = str(uuid.uuid4())
193
163
  challenge = ''.join(random.choices(
194
- challenge_set, k=app.config['captcha']['length']))
164
+ current_app.config['runtime']['challenge_set'], k=current_app.config['captcha']['length']))
195
165
  push_candidate(identifier, challenge, time())
166
+ image_captcha = ImageCaptcha()
196
167
  data = image_captcha.generate(challenge).read()
197
168
  captcha_data = b64encode(data).decode()
198
169
  return render_template(
199
170
  'index.jinja',
200
- title=app.config['custom']['title'],
171
+ title=current_app.config['custom']['title'],
201
172
  captcha=captcha_data,
202
173
  identifier=identifier,
203
174
  failed=failed
@@ -212,11 +183,11 @@ LOGIN_FORM = {
212
183
  }
213
184
 
214
185
 
215
- @app.route('/auth', methods=['POST'])
186
+ @views.route('/auth', methods=['POST'])
216
187
  @use_args(LOGIN_FORM, location='form')
217
188
  def auth(form):
218
- if form['phrase'] != app.config['app']['admin_phrase']:
219
- 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']:
220
191
  return redirect('/?failed=Wrong+passphrase')
221
192
 
222
193
  if verify_candidate(form['identifier'], form['captcha'], time()):
@@ -224,7 +195,8 @@ def auth(form):
224
195
  if form['nick'] in online.keys():
225
196
  return redirect('/?failed=User+exists')
226
197
  token = str(uuid.uuid4())
227
- is_admin = (form['phrase'] == app.config['app']['admin_phrase'])
198
+ is_admin = (form['phrase'] ==
199
+ current_app.config['app']['admin_phrase'])
228
200
  online[form['nick']] = dict(
229
201
  token=token,
230
202
  is_admin=is_admin,
@@ -236,16 +208,16 @@ def auth(form):
236
208
  is_uploading=False,
237
209
  is_alive=False
238
210
  )
239
- logger.info(f'User {form['nick']} logged in.')
211
+ current_app.logger.info(f'User {form['nick']} logged in.')
240
212
  return redirect(f'/room?nickname={form['nick']}&token={token}')
241
213
  return redirect('/?failed=CAPTCHA+failed')
242
214
 
243
215
 
244
- @app.route('/room-preview', methods=['POST', 'GET'])
216
+ @views.route('/room-preview', methods=['POST', 'GET'])
245
217
  def room_preview():
246
- if not app.config['DEBUG']:
218
+ if not current_app.config['DEBUG']:
247
219
  return Response(status=404)
248
- name = 'test_' + ''.join(random.choices(challenge_set, k=5))
220
+ name = 'test_' + ''.join(random.choices(ascii_lowercase, k=5))
249
221
  online[name] = {
250
222
  'token': 'test',
251
223
  'is_admin': True,
@@ -259,11 +231,11 @@ def room_preview():
259
231
  }
260
232
  return render_template(
261
233
  'room.jinja',
262
- title=app.config['custom']['title'],
234
+ title=current_app.config['custom']['title'],
263
235
  token='test',
264
236
  nick=name,
265
- emotes=app.config['custom']['emoticons'],
266
- motd=app.config['custom']['motd'],
237
+ emotes=current_app.config['custom']['emoticons'],
238
+ motd=current_app.config['custom']['motd'],
267
239
  is_admin=True
268
240
  )
269
241
 
@@ -274,7 +246,7 @@ ROOM_FORM = {
274
246
  }
275
247
 
276
248
 
277
- @app.route('/room')
249
+ @views.route('/room')
278
250
  @use_args(ROOM_FORM, location='query')
279
251
  def room_view(auth):
280
252
  nick = auth['nickname']
@@ -285,29 +257,29 @@ def room_view(auth):
285
257
  except:
286
258
  return redirect('/?failed=Invalid+authentication')
287
259
  if online[nick]['last_heartbeat']:
288
- if (time_milisecond() - online[nick]['last_heartbeat']) > app.config['app']['timeout']:
260
+ if (time_milisecond() - online[nick]['last_heartbeat']) > current_app.config['app']['timeout']:
289
261
  clean_user(nick)
290
262
  return redirect('/?failed=Authentication+expired')
291
263
  return render_template(
292
264
  'room.jinja',
293
- title=app.config['custom']['title'],
294
- emotes=app.config['custom']['emoticons'],
265
+ title=current_app.config['custom']['title'],
266
+ emotes=current_app.config['custom']['emoticons'],
295
267
  nick=nick,
296
268
  token=token,
297
- motd=app.config['custom']['motd'],
269
+ motd=current_app.config['custom']['motd'],
298
270
  is_admin=online[nick]['is_admin'],
299
- timeout=app.config['app']['timeout'],
271
+ timeout=current_app.config['app']['timeout'],
300
272
  )
301
273
 
302
274
  # --- SECRET API ---
303
275
  # ROUTES HERE NEED TO BE CALLED BY LOGGED IN CLIENT SCRIPT
304
276
 
305
277
 
306
- @app.route('/channels')
278
+ @views.route('/channels')
307
279
  @login_required
308
280
  def get_channels():
309
- auth = request.headers.get('Authorization', default=None)
310
- nick = str(auth).split(' ')[1]
281
+ nick = g.get('nick')
282
+ conn = get_db()
311
283
  res = conn.execute(
312
284
  'SELECT * FROM CHANNEL WHERE IS_DELETED=0 AND (ADMIN_ONLY=0 OR ?=1)', (online[nick]['is_admin'],))
313
285
  channels = []
@@ -320,11 +292,10 @@ def get_channels():
320
292
  return channels
321
293
 
322
294
 
323
- @app.route('/index_upload', methods=['POST'])
295
+ @views.route('/index_upload', methods=['POST'])
324
296
  @login_required
325
297
  def upload_index():
326
- auth = request.headers.get('Authorization', default=None)
327
- uploader = str(auth).split(' ')[1]
298
+ uploader = g.get('nick')
328
299
  if online[uploader]['is_uploading']:
329
300
  return Response(status=429)
330
301
  # lock
@@ -343,7 +314,7 @@ def upload_index():
343
314
  name = secure_filename(name)
344
315
  mime = file.mimetype
345
316
  binary = file.read()
346
- if len(binary) > SIZE_MAX_BYTE:
317
+ if len(binary) > current_app.config['runtime']['SIZE_MAX_BYTE']:
347
318
  online[uploader]['is_uploading'] = False
348
319
  return Response(status=400)
349
320
  id = str(uuid.uuid4())
@@ -354,11 +325,10 @@ def upload_index():
354
325
  return {'uuid': id, 'file_name': name}
355
326
 
356
327
 
357
- @app.route('/submit_upload')
328
+ @views.route('/submit_upload')
358
329
  @login_required
359
330
  def upload_submit():
360
- auth = request.headers.get('Authorization', default=None)
361
- user = str(auth).split(' ')[1]
331
+ user = g.get('nick')
362
332
 
363
333
  recall = request.args.get('recall', default=None)
364
334
  submit = request.args.get('submit', default=None)
@@ -377,11 +347,12 @@ def upload_submit():
377
347
  # Write to disk
378
348
  extension = path.splitext(name)[1]
379
349
  file_path = path.abspath(
380
- path.join(app.config['res']['path'], f'{submit}{extension}'))
350
+ path.join(current_app.config['res']['path'], f'{submit}{extension}'))
381
351
  with open(file_path, 'xb') as f:
382
352
  f.write(binary)
383
353
 
384
354
  # Write to DB
355
+ conn = get_db()
385
356
  conn.execute(
386
357
  'INSERT INTO RESOURCE (UUID, FILE_NAME, MIME_TYPE) VALUES (?, ?, ?);',
387
358
  (submit, name, mime)
@@ -390,7 +361,7 @@ def upload_submit():
390
361
  return Response(status=400)
391
362
 
392
363
 
393
- @app.route('/messages/<int:channel_id>')
364
+ @views.route('/messages/<int:channel_id>')
394
365
  @login_required
395
366
  def get_messages(channel_id):
396
367
  if channel_id == 0:
@@ -403,8 +374,8 @@ def get_messages(channel_id):
403
374
  offset = int(request.args['offset'])
404
375
 
405
376
  # check your fucking privilege
406
- auth = request.headers.get('Authorization', default=None)
407
- nick = str(auth).split(' ')[1]
377
+ nick = g.get('nick')
378
+ conn = get_db()
408
379
  cur = conn.execute(
409
380
  'SELECT ADMIN_ONLY FROM CHANNEL WHERE ID=?;', (channel_id,))
410
381
  row = cur.fetchone()
@@ -436,15 +407,10 @@ def get_messages(channel_id):
436
407
  return resp
437
408
 
438
409
 
439
- @app.route('/fellows')
410
+ @views.route('/fellows')
440
411
  @login_required
441
412
  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]
413
+ user = g.get('nick')
448
414
  channel = online[user]['channel']
449
415
  if channel == 0:
450
416
  return Response(status=404)
@@ -453,8 +419,10 @@ def get_fellows():
453
419
  # --- PUBLIC APIS ---
454
420
 
455
421
 
456
- @app.route('/resource_meta/<resource_id>')
422
+ @views.route('/resource_meta/<resource_id>')
457
423
  def get_resource_meta(resource_id):
424
+
425
+ conn = get_db()
458
426
  row = conn.execute(
459
427
  'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
460
428
  row = row.fetchone()
@@ -466,8 +434,9 @@ def get_resource_meta(resource_id):
466
434
  }
467
435
 
468
436
 
469
- @app.route('/resource/<resource_id>')
437
+ @views.route('/resource/<resource_id>')
470
438
  def get_resource(resource_id):
439
+ conn = get_db()
471
440
  row = conn.execute(
472
441
  'SELECT FILE_NAME, MIME_TYPE FROM RESOURCE WHERE IS_EXPIRED=0 AND UUID=?;', (resource_id, ))
473
442
  row = row.fetchone()
@@ -475,13 +444,13 @@ def get_resource(resource_id):
475
444
  return Response(status=404)
476
445
  extension = path.splitext(row[0])[1]
477
446
  file_path = path.abspath(path.join(
478
- app.config['res']['path'],
447
+ current_app.config['res']['path'],
479
448
  f'{resource_id}.{extension}'
480
449
  ))
481
450
  if path.exists(file_path):
482
451
  return send_file(file_path, mimetype=row[1], download_name=row[0])
483
452
  file_path = path.abspath(path.join(
484
- app.config['res']['path'],
453
+ current_app.config['res']['path'],
485
454
  f'{resource_id}{extension}'
486
455
  ))
487
456
  if path.exists(file_path):
@@ -492,16 +461,17 @@ def get_resource(resource_id):
492
461
  (resource_id, )
493
462
  )
494
463
  except Exception as e:
495
- logger.error(
464
+ current_app.logger.error(
496
465
  f'Error deleting resource {resource_id} with exception {e}')
497
466
  return Response(status=404)
498
467
 
499
468
  # --- ADMIN APIS ---
500
469
 
501
470
 
502
- @app.route('/channel', methods=['POST', 'PUT', 'DELETE'])
471
+ @views.route('/channel', methods=['POST', 'PUT', 'DELETE'])
503
472
  @admin_login_required
504
473
  def channel_control():
474
+ conn = get_db()
505
475
  if request.method == 'POST':
506
476
  try:
507
477
  name = request.get_json()['name']
@@ -537,7 +507,7 @@ def channel_control():
537
507
  return Response(status=404)
538
508
 
539
509
 
540
- @app.route('/online/<name>', methods=['DELETE'])
510
+ @views.route('/online/<name>', methods=['DELETE'])
541
511
  @admin_login_required
542
512
  def kick_user(name):
543
513
  if name in online.keys():
@@ -546,153 +516,141 @@ def kick_user(name):
546
516
  return Response(status=400)
547
517
 
548
518
 
549
- @app.route('/resource/<res_id>', methods=['DELETE'])
519
+ @views.route('/resource/<res_id>', methods=['DELETE'])
550
520
  @admin_login_required
551
521
  def delete_resource(res_id):
522
+ conn = get_db()
552
523
  try:
553
524
  conn.execute(
554
525
  'UPDATE RESOURCE SET IS_EXPIRED=1 WHERE UUID=?;',
555
526
  (res_id, )
556
527
  )
557
528
  except Exception as e:
558
- 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}')
559
531
  return Response(status=400)
560
532
  return Response(status=200)
561
533
 
562
534
 
563
- @app.route('/message/<int:id>', methods=['DELETE'])
535
+ @views.route('/message/<int:id>', methods=['DELETE'])
564
536
  @admin_login_required
565
537
  def delete_msg(id: int):
538
+ conn = get_db()
566
539
  try:
567
540
  conn.execute(
568
541
  'UPDATE CHAT SET IS_DELETED=1 WHERE ID=?;',
569
542
  (id, )
570
543
  )
571
544
  except Exception as e:
572
- logger.error(f'Error deleting msg {id} with exception {e}')
545
+ current_app.logger.error(f'Error deleting msg {id} with exception {e}')
573
546
  return Response(status=400)
574
547
  return Response(status=200)
575
548
 
576
549
 
577
550
  # --- SOCKET EVENTS ---
578
551
 
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
-
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
679
561
 
680
- @socketio.on('heartbeat')
681
- def heartbeat_handler(json):
682
- try:
683
- 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:
684
593
  return False
685
- except:
686
- return False
687
- online[json['nick']]['is_alive'] = True
688
- online[json['nick']]['last_heartbeat'] = time_milisecond()
689
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()]
690
602
 
691
- def exit():
692
- try:
693
- conn.close()
694
- except:
695
- pass
603
+ if to not in ids:
604
+ return False
696
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()
697
654
 
698
- atexit.register(exit)
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()
@@ -40,11 +40,12 @@ CONFIG_DEFAULT = {
40
40
  'app': {
41
41
  'ip': '0.0.0.0',
42
42
  'port': 8080,
43
- 'admin_phrase': 'CHANGE_ME_ADMIN',
44
- 'user_phrase': 'CHANGE_ME',
43
+ 'admin_phrase': 'admin',
44
+ 'user_phrase': '',
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',
@@ -50,6 +50,11 @@ socket.on("joining", async (e) => {
50
50
  document.getElementById("fellows_list").innerHTML += `<li id="fellow-${e["target"]}">${e["target"]}</li>`
51
51
  });
52
52
 
53
+ socket.on("kicked", async () => {
54
+ window.alert("You're kicked out :(")
55
+ window.location.replace("/");
56
+ })
57
+
53
58
  async function refresh_channels() {
54
59
  var clist = document.getElementById("channel_list");
55
60
  clist.innerHTML = "";
@@ -178,7 +183,7 @@ async function switch_channel(channel_id) {
178
183
  msgbox.scrollTop = msgbox.scrollHeight;
179
184
  }
180
185
 
181
- async function load_more(count = 30) {
186
+ async function load_more(count = 15) {
182
187
  var msgbox = document.getElementById("msgbox");
183
188
  var new_msg = await fetch(
184
189
  `/messages/${current_channel}?count=${count}&offset=${msgbox.childElementCount}`,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "YACScript"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "yet another chatroom script."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,24 +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
- app.logger.info(f'You have set the guest passphrase to "{app.config['app']['user_phrase']}" , and the admin passphrase to "{app.config['app']['admin_phrase']}"')
19
- socketio.run(
20
- app,
21
- host=app.config["app"]["ip"],
22
- port=app.config["app"]["port"],
23
- debug=app.config["DEBUG"],
24
- )
yacscript-0.3.0/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