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.
- {yacscript-0.3.0 → yacscript-0.3.2}/PKG-INFO +15 -11
- {yacscript-0.3.0 → yacscript-0.3.2}/README.md +14 -10
- yacscript-0.3.2/app/__init__.py +113 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/app.py +191 -233
- yacscript-0.3.2/app/db.py +24 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/definitions.py +3 -2
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/room.js +6 -1
- {yacscript-0.3.0 → yacscript-0.3.2}/pyproject.toml +1 -1
- yacscript-0.3.0/app/__init__.py +0 -24
- yacscript-0.3.0/app/db.py +0 -25
- {yacscript-0.3.0 → yacscript-0.3.2}/COPYING +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/admin.js +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/file.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/icon.png +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/join.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/leave.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/msg.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/send.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/style.css +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/tile.gif +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/tile.jpg +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/static/welcome.mp3 +0 -0
- {yacscript-0.3.0 → yacscript-0.3.2}/app/templates/index.jinja +0 -0
- {yacscript-0.3.0 → 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,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) >=
|
|
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]) >
|
|
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']) >
|
|
89
|
+
if (time_milisecond() - v['last_heartbeat']) > current_app.config['app']['timeout']:
|
|
128
90
|
clean_user(k)
|
|
129
91
|
|
|
130
92
|
|
|
131
|
-
@
|
|
132
|
-
@
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
@
|
|
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=
|
|
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=
|
|
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
|
-
@
|
|
186
|
+
@views.route('/auth', methods=['POST'])
|
|
216
187
|
@use_args(LOGIN_FORM, location='form')
|
|
217
188
|
def auth(form):
|
|
218
|
-
if form['phrase'] !=
|
|
219
|
-
if form['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'] ==
|
|
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
|
-
@
|
|
216
|
+
@views.route('/room-preview', methods=['POST', 'GET'])
|
|
245
217
|
def room_preview():
|
|
246
|
-
if not
|
|
218
|
+
if not current_app.config['DEBUG']:
|
|
247
219
|
return Response(status=404)
|
|
248
|
-
name = 'test_' + ''.join(random.choices(
|
|
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=
|
|
234
|
+
title=current_app.config['custom']['title'],
|
|
263
235
|
token='test',
|
|
264
236
|
nick=name,
|
|
265
|
-
emotes=
|
|
266
|
-
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
|
-
@
|
|
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']) >
|
|
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=
|
|
294
|
-
emotes=
|
|
265
|
+
title=current_app.config['custom']['title'],
|
|
266
|
+
emotes=current_app.config['custom']['emoticons'],
|
|
295
267
|
nick=nick,
|
|
296
268
|
token=token,
|
|
297
|
-
motd=
|
|
269
|
+
motd=current_app.config['custom']['motd'],
|
|
298
270
|
is_admin=online[nick]['is_admin'],
|
|
299
|
-
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
|
-
@
|
|
278
|
+
@views.route('/channels')
|
|
307
279
|
@login_required
|
|
308
280
|
def get_channels():
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
@
|
|
295
|
+
@views.route('/index_upload', methods=['POST'])
|
|
324
296
|
@login_required
|
|
325
297
|
def upload_index():
|
|
326
|
-
|
|
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
|
-
@
|
|
328
|
+
@views.route('/submit_upload')
|
|
358
329
|
@login_required
|
|
359
330
|
def upload_submit():
|
|
360
|
-
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
@
|
|
410
|
+
@views.route('/fellows')
|
|
440
411
|
@login_required
|
|
441
412
|
def get_fellows():
|
|
442
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
|
|
580
|
-
def
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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': '
|
|
44
|
-
'user_phrase': '
|
|
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 =
|
|
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}`,
|
yacscript-0.3.0/app/__init__.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|