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.
- {yacscript-0.3.1 → yacscript-0.3.2}/PKG-INFO +15 -11
- {yacscript-0.3.1 → yacscript-0.3.2}/README.md +14 -10
- yacscript-0.3.2/app/__init__.py +113 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/app.py +186 -240
- yacscript-0.3.2/app/db.py +24 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/definitions.py +1 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/room.js +1 -1
- {yacscript-0.3.1 → yacscript-0.3.2}/pyproject.toml +1 -1
- yacscript-0.3.1/app/__init__.py +0 -35
- yacscript-0.3.1/app/db.py +0 -25
- {yacscript-0.3.1 → yacscript-0.3.2}/COPYING +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/admin.js +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/file.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/icon.png +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/join.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/leave.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/msg.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/send.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/style.css +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/tile.gif +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/tile.jpg +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/static/welcome.mp3 +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/templates/index.jinja +0 -0
- {yacscript-0.3.1 → yacscript-0.3.2}/app/templates/room.jinja +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yacscript
|
|
3
|
-
Version: 0.3.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
|
|
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
|
-
|
|
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) >=
|
|
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]) >
|
|
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']) >
|
|
89
|
+
if (time_milisecond() - v['last_heartbeat']) > current_app.config['app']['timeout']:
|
|
140
90
|
clean_user(k)
|
|
141
91
|
|
|
142
92
|
|
|
143
|
-
@
|
|
144
|
-
@
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
@
|
|
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=
|
|
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=
|
|
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
|
-
@
|
|
186
|
+
@views.route('/auth', methods=['POST'])
|
|
228
187
|
@use_args(LOGIN_FORM, location='form')
|
|
229
188
|
def auth(form):
|
|
230
|
-
if form['phrase'] !=
|
|
231
|
-
if form['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'] ==
|
|
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
|
-
@
|
|
216
|
+
@views.route('/room-preview', methods=['POST', 'GET'])
|
|
257
217
|
def room_preview():
|
|
258
|
-
if not
|
|
218
|
+
if not current_app.config['DEBUG']:
|
|
259
219
|
return Response(status=404)
|
|
260
|
-
name = 'test_' + ''.join(random.choices(
|
|
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=
|
|
234
|
+
title=current_app.config['custom']['title'],
|
|
275
235
|
token='test',
|
|
276
236
|
nick=name,
|
|
277
|
-
emotes=
|
|
278
|
-
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
|
-
@
|
|
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']) >
|
|
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=
|
|
306
|
-
emotes=
|
|
265
|
+
title=current_app.config['custom']['title'],
|
|
266
|
+
emotes=current_app.config['custom']['emoticons'],
|
|
307
267
|
nick=nick,
|
|
308
268
|
token=token,
|
|
309
|
-
motd=
|
|
269
|
+
motd=current_app.config['custom']['motd'],
|
|
310
270
|
is_admin=online[nick]['is_admin'],
|
|
311
|
-
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
|
-
@
|
|
278
|
+
@views.route('/channels')
|
|
319
279
|
@login_required
|
|
320
280
|
def get_channels():
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
@
|
|
295
|
+
@views.route('/index_upload', methods=['POST'])
|
|
336
296
|
@login_required
|
|
337
297
|
def upload_index():
|
|
338
|
-
|
|
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
|
-
@
|
|
328
|
+
@views.route('/submit_upload')
|
|
370
329
|
@login_required
|
|
371
330
|
def upload_submit():
|
|
372
|
-
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
@
|
|
410
|
+
@views.route('/fellows')
|
|
452
411
|
@login_required
|
|
453
412
|
def get_fellows():
|
|
454
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
|
|
592
|
-
def
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -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 =
|
|
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}`,
|
yacscript-0.3.1/app/__init__.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|