mirror of
https://github.com/DarkflameUniverse/NexusDashboard.git
synced 2026-11-07 02:57:14 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59dad04c60 | ||
|
|
92f7e5ae52 | ||
|
|
62fb9a3c01 | ||
|
|
200370709c | ||
|
|
0b04dab1d2 | ||
|
|
f7703abe5e | ||
|
|
f8332dc065 | ||
|
|
1b4887f73e |
54
.github/workflows/ci.yml
vendored
54
.github/workflows/ci.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,3 @@ app/settings.py
|
||||
*.exe
|
||||
*.csv
|
||||
*.sql
|
||||
bin
|
||||
lib
|
||||
include
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.11-slim-bookworm
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
RUN apt update
|
||||
RUN apt -y install zip
|
||||
|
||||
208
README.md
208
README.md
@@ -65,30 +65,28 @@
|
||||
* Reports how much currency that characters posses
|
||||
* U-Score:
|
||||
* Reports how much U-Score that characters posses
|
||||
* Analytics:
|
||||
* Provide reporting to Developers to help better solve issues
|
||||
* Disabled by default. Set `ALLOW_ANALYTICS` to true to enable.
|
||||
|
||||
# Deployment
|
||||
|
||||
> **NOTE: This tutorial assumes you have a working DLU server instance and**
|
||||
> **some knowledge of command line interfaces on your chosen platform**
|
||||
|
||||
|
||||
**It is highly recommended to setup a reverse proxy via Nginx or some other tool and use SSL to secure your Nexus Dashboard instance if you are going to be opening it up to any non-LANs**
|
||||
* [How to setup Nginx](https://www.digitalocean.com/community/tutorials/how-to-configure-nginx-as-a-reverse-proxy-on-ubuntu-22-04)
|
||||
* [How to use certbot for SSL](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04)
|
||||
|
||||
> **some knowledge of Linux**
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
|
||||
docker run -d \
|
||||
-e APP_SECRET_KEY='<secret_key>' \
|
||||
-e APP_DATABASE_URI='mysql+pymysql://<username>:<password>@<host>:<port>/<database>' \
|
||||
# you can include other optional Environment Variables from below like this
|
||||
-e REQUIRE_PLAY_KEY=True \
|
||||
-p 8000:8000/tcp \
|
||||
-v /path/to/logs:/logs:rw /
|
||||
-v /path/to/unpacked/client:/app/luclient:ro \
|
||||
-e REQUIRE_PLAY_KEY=True
|
||||
-p 8000:8000/tcp
|
||||
-v /path/to/unpacked/client:/app/luclient:rw \
|
||||
-v /path/to/cachedir:/app/cache:rw \
|
||||
ghcr.io/darkflameuniverse/nexusdashboard:latest
|
||||
aronwk/nexus-dashboard:latest
|
||||
|
||||
```
|
||||
|
||||
* `/app/luclient` must be mapped to the location of an unpacked client
|
||||
@@ -98,53 +96,44 @@ docker run -d \
|
||||
|
||||
### Environmental Variables
|
||||
|
||||
Please Reference `app/settings_example.py` to see all the variables
|
||||
Please Reference `app/settings_exmaple.py` to see all the variables
|
||||
|
||||
* Required:
|
||||
* APP_SECRET_KEY (Must be provided)
|
||||
* APP_DATABASE_URI (Must be provided)
|
||||
* Everything else is optional and has defaults
|
||||
* Property model viewer:
|
||||
* COMPRESS_MODEL_RESPONSES (default `True`): gzip model and brick database files for browsers that support it
|
||||
* CACHE_COMPRESSED_MODELS (default `False`): store a `.gz` copy next to each cached file instead of compressing on every request (uses more disk space, saves CPU)
|
||||
* PROPERTY_VIEWER_LOAD_REDRAW (default `none`): property viewer redraw while models load; `none` draws once when all models have loaded, `interval` redraws periodically so models appear progressively
|
||||
* PROPERTY_VIEWER_REDRAW_INTERVAL_MS (default `500`): minimum time between redraws while loading when PROPERTY_VIEWER_LOAD_REDRAW is `interval`
|
||||
|
||||
## Manual Linux Installation
|
||||
## Manual
|
||||
|
||||
Thanks to [HailStorm32](https://github.com/HailStorm32) for this manual install guide!
|
||||
|
||||
### Setting Up The Environment
|
||||
First you will want to install the following packages by executing the following commands presuming you are on a Debian based system.
|
||||
|
||||
First you will want to install the following packages by executing the following commands
|
||||
`sudo apt-get update`
|
||||
|
||||
`sudo apt-get install -y python3 python3-pip sqlite3 git unzip libmagickwand-dev`
|
||||
|
||||
> *Note: If you are having issues with installing `sqlite3`, change it to `sqlite`*
|
||||
|
||||
<br>
|
||||
Next you will want to clone the repository. You can clone it anywhere, but for the purpose of this tutorial, we will be cloning it to the home directory.'
|
||||
<br></br>
|
||||
Next we will clone the repository. You can clone it anywhere, but for the purpose of this tutorial, we will be cloning it to the home directory.
|
||||
|
||||
Run `cd ~` to ensure that you are currently in the home directory then run the following command to clone the repository into our home directory
|
||||
`cd` *make sure you are in the home directory*
|
||||
`git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
|
||||
|
||||
You should now have a directory called `NexusDashboard` present in your home directory
|
||||
You should now have a directory called `NexusDashboard`
|
||||
|
||||
### Setting up
|
||||
|
||||
Rename the example settings file
|
||||
`cp ~/NexusDashboard/app/settings_example.py ~/NexusDashboard/app/settings.py`
|
||||
|
||||
Now let's open the settings file we just created and configure some of the settings with nano as it is a simple text editor that is easy to use
|
||||
`nano ~/NexusDashboard/app/settings.py`
|
||||
>*Obviously you can replace this with a text editor of your choice, nano is just the most simple to use out of the ones available by default on most Linux distros*
|
||||
Now let's open the settings file we just created and configure some of the settings
|
||||
`vim ~/NexusDashboard/app/settings.py`
|
||||
>*Feel free to use any text editor you are more comfortable with instead of vim*
|
||||
|
||||
<br>
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial we will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like to fit your needs.
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial I will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like
|
||||
|
||||
>*Note: There are options in here that are related to email registration and password recovery among other features however those require extra setup not covered by this tutorial*
|
||||
>*Note: Enabling the email option will require further setup that is outside the scope of this tutorial*
|
||||
|
||||
The two important settings to configure are `APP_SECRET_KEY` and `APP_DATABASE_URI`
|
||||
|
||||
@@ -174,56 +163,55 @@ Once you are done making the changes, save and close the file
|
||||
|
||||
We will need the following folders from the client
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
locale (all of the files inside)
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
├───brickprimitives
|
||||
├───textures
|
||||
├───ui
|
||||
├───brickdb.zip
|
||||
|_BrickModels
|
||||
|_brickprimitives
|
||||
|_textures
|
||||
|_ui
|
||||
|_brickdb.zip
|
||||
```
|
||||
Put the two folders in `~/NexusDashboard/app/luclient`
|
||||
|
||||
Unzip the `brickdb.zip` in place
|
||||
`unzip brickdb.zip`
|
||||
|
||||
Remove the `.zip` file after you have unzipped it, you can do that with
|
||||
Remove the `.zip` after you have unzipped it
|
||||
`rm brickdb.zip`
|
||||
|
||||
In the `luclient` directory you should now have a file structure that looks like this
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
local
|
||||
|_locale.xml
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
│ └─── ...
|
||||
├───brickprimitives
|
||||
│ └─── ...
|
||||
├───textures
|
||||
│ └─── ...
|
||||
├───ui
|
||||
│ └─── ...
|
||||
├───Assemblies
|
||||
│ └─── ...
|
||||
├───Primitives
|
||||
│ └─── ...
|
||||
├───Materials.xml
|
||||
└───info.xml
|
||||
|_BrickModels
|
||||
|_...
|
||||
|_brickprimitives
|
||||
|_...
|
||||
|_textures
|
||||
|_...
|
||||
|_ui
|
||||
|_...
|
||||
|_Assemblies
|
||||
|_...
|
||||
|_Primitives
|
||||
|_...
|
||||
|_Materials.xml
|
||||
|_info.xml
|
||||
```
|
||||
|
||||
We will also need to copy the `CDServer.sqlite` database file from the server to the `~/NexusDashboard/app/luclient/res` folder
|
||||
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`, this can be done with the following command
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`
|
||||
```bash
|
||||
mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite
|
||||
```
|
||||
|
||||
|
||||
##### Remaining Setup
|
||||
To finish this, we will need to install the python dependencies and run the database migrations, simply run the following commands one at a time
|
||||
Run the following commands one at a time
|
||||
```bash
|
||||
cd ~/NexusDashboard
|
||||
pip install -r requirements.txt
|
||||
@@ -231,109 +219,11 @@ pip install gunicorn
|
||||
flask db upgrade
|
||||
```
|
||||
##### Running the site
|
||||
Once all of the above is complete, you can run the site with the command
|
||||
You can run the site with
|
||||
`gunicorn -b :8000 -w 4 wsgi:app`
|
||||
|
||||
## Manual Windows Setup
|
||||
|
||||
While a lot of the setup on Windows is the same a lot of it can be completed with GUI interfaces and requires installing things from websites instead of the command line.
|
||||
|
||||
### Setting Up The Environment
|
||||
You need to install the following prerequisites:
|
||||
|
||||
* [Python 3.8](https://www.python.org/downloads/release/python-380/)
|
||||
* [Git](https://git-scm.com/downloads)
|
||||
* [ImageMagick](https://docs.wand-py.org/en/latest/guide/install.html#install-imagemagick-on-windows)
|
||||
* [7-Zip](https://www.7-zip.org/download.html)
|
||||
|
||||
Next you will need to clone the repository. You can clone it anywhere, but for the purpose of this tutorial, you will want to clone it to your desktop just for simplicity, it can be moved after.
|
||||
|
||||
Open a command prompt and run `cd Desktop` (The command line should place you in your Home directory be default) to ensure that you are currently in the desktop directory then run the following command to clone the repository into our desktop directory
|
||||
|
||||
Run the following command to clone the repository `git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
|
||||
|
||||
You should now have a directory called `NexusDashboard` present on your desktop.
|
||||
|
||||
### Setting up
|
||||
Now that we have the repository cloned you need to rename the example settings file, you can perform this manually in the GUI or you can use the command line, to do the latter run the following commands
|
||||
* `cd NexusDashboard\app`
|
||||
* `copy settings_example.py settings.py`
|
||||
|
||||
Now let's open the settings file we just created and configure some of the settings with the Windows default notepad.
|
||||
* `notepad settings.py`
|
||||
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial we will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like to fit your needs.
|
||||
|
||||
> *Note: There are options in here that are related to email registration and password recovery among other features however those require extra setup not covered by this tutorial*
|
||||
|
||||
The two important settings to configure are `APP_SECRET_KEY` and `APP_DATABASE_URI`
|
||||
|
||||
For `APP_SECRET_KEY` you can just fill in any random 32 character string and for `APP_DATABASE_URI` you will need to fill in a connection string to your database. The connection string will look similar to this. You will need to fill in your own information for the username, password, host, port and database name.
|
||||
```
|
||||
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
|
||||
```
|
||||
and the rest of the file can be left at the default values other than the `APP_SECRET_KEY` which you will need to fill in with random characters.
|
||||
|
||||
Once you are done making the changes, save and close the file
|
||||
|
||||
##### Client related files
|
||||
We will need the following folders from the client
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
├───brickprimitives
|
||||
├───textures
|
||||
├───ui
|
||||
└───brickdb.zip
|
||||
```
|
||||
Put the two folders in `Desktop/NexusDashboard/app/luclient`
|
||||
|
||||
Unzip the `brickdb.zip` in place using 7-Zip, you can do this by right clicking the file and selecting `7-Zip > Extract Here`.
|
||||
|
||||
After doing this you can remove the `.zip`, simply delete the file.
|
||||
|
||||
In the `luclient` directory you should now have a file structure that looks like this
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
│ └─── ...
|
||||
├───brickprimitives
|
||||
│ └─── ...
|
||||
├───textures
|
||||
│ └─── ...
|
||||
├───ui
|
||||
│ └─── ...
|
||||
├───Assemblies
|
||||
│ └─── ...
|
||||
├───Primitives
|
||||
│ └─── ...
|
||||
├───Materials.xml
|
||||
└───info.xml
|
||||
```
|
||||
|
||||
We will also need to copy the `CDServer.sqlite` database file from the server to the `Desktop/NexusDashboard/app/luclient/res` folder
|
||||
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`, this can be done by right clicking the file and selecting `Rename` and then changing the name to `cdclient.sqlite`
|
||||
|
||||
##### Remaining Setup
|
||||
To finish this, we will need to install the python dependencies and run the database migrations, simply run the following commands one at a time in the root directory of the site, if you are not in the root directory you can run `cd Desktop/NexusDashboard` to get there (assuming you have opened a new terminal window)
|
||||
```bat
|
||||
pip install -r requirements.txt
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
##### Running the site
|
||||
Once all of the above is complete, you can run the site with the command
|
||||
`flask run` however bare in mind that this is a development version of the site, at the moment running a production version of the site on Windows is not supported.
|
||||
|
||||
# Development
|
||||
|
||||
Please use [Editor Config](https://editorconfig.org/) to maintain a consistent coding style between different editors and different contributors.
|
||||
Please use [Editor Config](https://editorconfig.org/)
|
||||
|
||||
* `python3 -m flask run` to run a local dev server
|
||||
* `flask run` to run local dev server
|
||||
|
||||
239
app/__init__.py
239
app/__init__.py
@@ -1,27 +1,16 @@
|
||||
import os
|
||||
from flask import Flask, url_for, g, redirect
|
||||
from flask import Flask, url_for, redirect
|
||||
from functools import wraps
|
||||
from flask_assets import Environment
|
||||
from webassets import Bundle
|
||||
import time
|
||||
from app.models import db, migrate, PlayKey
|
||||
from app.schemas import ma
|
||||
from app.forms import CustomUserManager
|
||||
from flask_user import user_registered, current_user, user_logged_in
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from flask_apscheduler import APScheduler
|
||||
from app.luclient import register_luclient_jinja_helpers
|
||||
from app.compressed_files import register_compressed_files
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from zipfile import ZipFile
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError: # Windows
|
||||
fcntl = None
|
||||
import msvcrt
|
||||
|
||||
from app.commands import (
|
||||
init_db,
|
||||
@@ -30,16 +19,16 @@ from app.commands import (
|
||||
gen_image_cache,
|
||||
gen_model_cache,
|
||||
fix_clone_ids,
|
||||
remove_buffs,
|
||||
find_missing_commendation_items
|
||||
parse_lucache,
|
||||
makeup_unlisted_objects,
|
||||
gen_new_locales,
|
||||
xref_scripts
|
||||
)
|
||||
from app.models import Account, AccountInvitation, AuditLog
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
# Instantiate Flask extensions
|
||||
csrf_protect = CSRFProtect()
|
||||
scheduler = APScheduler()
|
||||
@@ -89,17 +78,6 @@ def create_app():
|
||||
def debug(text):
|
||||
print(text)
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
if cdclient is not None:
|
||||
cdclient.close()
|
||||
|
||||
@app.template_filter()
|
||||
def numberFormat(value):
|
||||
return format(int(value), ',d')
|
||||
|
||||
|
||||
# add the commands to flask cli
|
||||
app.cli.add_command(init_db)
|
||||
app.cli.add_command(init_accounts)
|
||||
@@ -107,101 +85,20 @@ def create_app():
|
||||
app.cli.add_command(gen_image_cache)
|
||||
app.cli.add_command(gen_model_cache)
|
||||
app.cli.add_command(fix_clone_ids)
|
||||
app.cli.add_command(remove_buffs)
|
||||
app.cli.add_command(find_missing_commendation_items)
|
||||
app.cli.add_command(parse_lucache)
|
||||
app.cli.add_command(makeup_unlisted_objects)
|
||||
app.cli.add_command(gen_new_locales)
|
||||
app.cli.add_command(xref_scripts)
|
||||
|
||||
register_logging(app)
|
||||
register_settings(app)
|
||||
register_compressed_files(app)
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_luclient_jinja_helpers(app)
|
||||
|
||||
# Extract the brickdb if it's not already extracted
|
||||
unpack_brick_cache(app.config["CLIENT_LOCATION"], app.config["CACHE_LOCATION"])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
BRICK_CACHE_MARKER = ".brickdb_unpacked"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _exclusive_lock(lock_path):
|
||||
"""Cross-process exclusive lock, held for the duration of the block."""
|
||||
with open(lock_path, "a+b") as lock_file:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
||||
else: # Windows
|
||||
lock_file.seek(0)
|
||||
while True:
|
||||
try:
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
else:
|
||||
lock_file.seek(0)
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
|
||||
|
||||
def _make_owner_writable(path):
|
||||
"""Add owner write permission to a directory tree (copytree keeps source modes)."""
|
||||
for root, dirs, files in os.walk(path):
|
||||
for name in [root] + [os.path.join(root, d) for d in dirs]:
|
||||
os.chmod(name, os.stat(name).st_mode | stat.S_IWUSR | stat.S_IXUSR)
|
||||
|
||||
|
||||
def unpack_brick_cache(client_location, cache_location):
|
||||
"""Unpack brickdb.zip and brickprimitives into the cache exactly once.
|
||||
|
||||
Several gunicorn workers run create_app() at the same time, so the
|
||||
unpack is serialized with a file lock, extracted into a temporary
|
||||
directory and moved into place, and only marked complete at the end.
|
||||
A crashed or partial unpack is redone on the next start.
|
||||
"""
|
||||
cache = pathlib.Path(cache_location)
|
||||
marker = cache / BRICK_CACHE_MARKER
|
||||
if marker.is_file():
|
||||
return
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
with _exclusive_lock(cache / ".brickdb_unpack.lock"):
|
||||
# another process may have finished while we waited for the lock
|
||||
if marker.is_file():
|
||||
return
|
||||
tmp = pathlib.Path(tempfile.mkdtemp(prefix=".brickdb_tmp_", dir=cache))
|
||||
try:
|
||||
with ZipFile(f"{client_location}res/brickdb.zip", "r") as zip_ref:
|
||||
zip_ref.extractall(tmp)
|
||||
# copy file contents only: the client files are often read-only,
|
||||
# and read-only directories can't be moved into place or cleaned up
|
||||
shutil.copytree(
|
||||
f"{client_location}res/brickprimitives",
|
||||
tmp / "brickprimitives",
|
||||
copy_function=shutil.copyfile
|
||||
)
|
||||
_make_owner_writable(tmp)
|
||||
for entry in tmp.iterdir():
|
||||
target = cache / entry.name
|
||||
# leftovers from an older or interrupted unpack get replaced
|
||||
if target.is_dir() and not target.is_symlink():
|
||||
_make_owner_writable(target)
|
||||
shutil.rmtree(target)
|
||||
elif target.exists() or target.is_symlink():
|
||||
target.unlink()
|
||||
os.replace(entry, target)
|
||||
finally:
|
||||
if tmp.exists():
|
||||
_make_owner_writable(tmp)
|
||||
shutil.rmtree(tmp)
|
||||
marker.touch()
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
"""Register extensions for Flask app
|
||||
|
||||
@@ -210,6 +107,8 @@ def register_extensions(app):
|
||||
"""
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
ma.init_app(app)
|
||||
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
@@ -221,7 +120,7 @@ def register_extensions(app):
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
scss = Bundle('scss/site.scss', filters='libsass', output='css/site.css')
|
||||
scss = Bundle('scss/site.scss', filters='libsass', output='site.css')
|
||||
assets.register('scss_all', scss)
|
||||
|
||||
|
||||
@@ -260,7 +159,7 @@ def register_blueprints(app):
|
||||
|
||||
def register_logging(app):
|
||||
# file logger
|
||||
file_handler = RotatingFileHandler('logs/nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
@@ -284,23 +183,22 @@ def register_settings(app):
|
||||
# Load environment specific settings
|
||||
app.config['TESTING'] = False
|
||||
app.config['DEBUG'] = False
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
"pool_pre_ping": True,
|
||||
"pool_size": 10,
|
||||
"max_overflow": 2,
|
||||
"pool_recycle": 300,
|
||||
"pool_pre_ping": True,
|
||||
"pool_use_lifo": True
|
||||
}
|
||||
|
||||
# always pull these two from the env
|
||||
app.config['SECRET_KEY'] = os.getenv(
|
||||
'APP_SECRET_KEY',
|
||||
app.config['APP_SECRET_KEY']
|
||||
|
||||
)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
|
||||
'APP_DATABASE_URI',
|
||||
app.config['APP_DATABASE_URI']
|
||||
)
|
||||
app.config['SQLALCHEMY_BINDS'] = {
|
||||
'cdclient': 'sqlite:///luclient/res/cdclient.sqlite'
|
||||
}
|
||||
|
||||
# try to get overides, otherwise just use what we have already
|
||||
app.config['USER_ENABLE_REGISTER'] = os.getenv(
|
||||
'USER_ENABLE_REGISTER',
|
||||
app.config['USER_ENABLE_REGISTER']
|
||||
@@ -325,15 +223,17 @@ def register_settings(app):
|
||||
'USER_REQUIRE_INVITATION',
|
||||
app.config['USER_REQUIRE_INVITATION']
|
||||
)
|
||||
app.config['ALLOW_ANALYTICS'] = os.getenv(
|
||||
'ALLOW_ANALYTICS',
|
||||
app.config['ALLOW_ANALYTICS']
|
||||
)
|
||||
app.config['MAIL_SERVER'] = os.getenv(
|
||||
'MAIL_SERVER',
|
||||
app.config['MAIL_SERVER']
|
||||
)
|
||||
app.config['MAIL_PORT'] = int(
|
||||
os.getenv(
|
||||
'MAIL_PORT',
|
||||
app.config['MAIL_PORT'] = os.getenv(
|
||||
'MAIL_USE_SSL',
|
||||
app.config['MAIL_PORT']
|
||||
)
|
||||
)
|
||||
app.config['MAIL_USE_SSL'] = os.getenv(
|
||||
'MAIL_USE_SSL',
|
||||
@@ -360,91 +260,6 @@ def register_settings(app):
|
||||
app.config['USER_EMAIL_SENDER_EMAIL']
|
||||
)
|
||||
|
||||
if "ENABLE_CHAR_XML_UPLOAD" not in app.config:
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD'] = False
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD'] = os.getenv(
|
||||
'ENABLE_CHAR_XML_UPLOAD',
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD']
|
||||
)
|
||||
|
||||
# Property model viewer compression (see settings_example.py)
|
||||
for key, default in (
|
||||
("COMPRESS_MODEL_RESPONSES", True), ("CACHE_COMPRESSED_MODELS", False)
|
||||
):
|
||||
value = os.getenv(key, app.config.get(key, default))
|
||||
if isinstance(value, str):
|
||||
value = value.strip().lower() in ("1", "true", "yes", "on")
|
||||
app.config[key] = bool(value)
|
||||
|
||||
# Property model viewer redraw while models load (see settings_example.py)
|
||||
key = "PROPERTY_VIEWER_LOAD_REDRAW"
|
||||
redraw = str(os.getenv(key, app.config.get(key, "none"))).strip().lower()
|
||||
app.config[key] = redraw if redraw in ("none", "interval") else "none"
|
||||
key = "PROPERTY_VIEWER_REDRAW_INTERVAL_MS"
|
||||
try:
|
||||
interval = int(os.getenv(key, app.config.get(key, 500)))
|
||||
except (TypeError, ValueError):
|
||||
interval = 500
|
||||
app.config[key] = max(interval, 16)
|
||||
|
||||
if "CLIENT_LOCATION" not in app.config:
|
||||
app.config['CLIENT_LOCATION'] = 'app/luclient/'
|
||||
app.config['CLIENT_LOCATION'] = os.getenv(
|
||||
'CLIENT_LOCATION',
|
||||
app.config['CLIENT_LOCATION']
|
||||
)
|
||||
|
||||
if "CD_SQLITE_LOCATION" not in app.config:
|
||||
app.config['CD_SQLITE_LOCATION'] = 'app/luclient/res/'
|
||||
app.config['CD_SQLITE_LOCATION'] = os.getenv(
|
||||
'CD_SQLITE_LOCATION',
|
||||
app.config['CD_SQLITE_LOCATION']
|
||||
)
|
||||
|
||||
if "CACHE_LOCATION" not in app.config:
|
||||
app.config['CACHE_LOCATION'] = 'app/cache/'
|
||||
app.config['CACHE_LOCATION'] = os.getenv(
|
||||
'CACHE_LOCATION',
|
||||
app.config['CACHE_LOCATION']
|
||||
)
|
||||
|
||||
# Recaptcha settings
|
||||
if "RECAPTCHA_ENABLE" not in app.config:
|
||||
app.config['RECAPTCHA_ENABLE'] = False
|
||||
app.config['RECAPTCHA_ENABLE'] = os.getenv(
|
||||
'RECAPTCHA_ENABLE',
|
||||
app.config['RECAPTCHA_ENABLE']
|
||||
)
|
||||
if "RECAPTCHA_PUBLIC_KEY" not in app.config:
|
||||
app.config['RECAPTCHA_PUBLIC_KEY'] = ''
|
||||
app.config['RECAPTCHA_PUBLIC_KEY'] = os.getenv(
|
||||
'RECAPTCHA_PUBLIC_KEY',
|
||||
app.config['RECAPTCHA_PUBLIC_KEY']
|
||||
)
|
||||
if "RECAPTCHA_PRIVATE_KEY" not in app.config:
|
||||
app.config['RECAPTCHA_PRIVATE_KEY'] = ''
|
||||
app.config['RECAPTCHA_PRIVATE_KEY'] = os.getenv(
|
||||
'RECAPTCHA_PRIVATE_KEY',
|
||||
app.config['RECAPTCHA_PRIVATE_KEY']
|
||||
)
|
||||
# Optional
|
||||
if "RECAPTCHA_API_SERVER" in app.config:
|
||||
app.config['RECAPTCHA_API_SERVER'] = os.getenv(
|
||||
'RECAPTCHA_API_SERVER',
|
||||
app.config['RECAPTCHA_API_SERVER']
|
||||
)
|
||||
if "RECAPTCHA_PARAMETERS" in app.config:
|
||||
app.config['RECAPTCHA_PARAMETERS'] = os.getenv(
|
||||
'RECAPTCHA_PARAMETERS',
|
||||
app.config['RECAPTCHA_PARAMETERS']
|
||||
)
|
||||
if "RECAPTCHA_DATA_ATTRS" in app.config:
|
||||
app.config['RECAPTCHA_DATA_ATTRS'] = os.getenv(
|
||||
'RECAPTCHA_DATA_ATTRS',
|
||||
app.config['RECAPTCHA_DATA_ATTRS']
|
||||
)
|
||||
|
||||
|
||||
|
||||
def gm_level(gm_level):
|
||||
"""Decorator for handling permissions based on the user's GM Level
|
||||
|
||||
188
app/accounts.py
188
app/accounts.py
@@ -1,9 +1,7 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, current_app, flash
|
||||
from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import bcrypt
|
||||
import datetime
|
||||
import secrets
|
||||
from app.models import (
|
||||
Account,
|
||||
CharacterInfo,
|
||||
@@ -16,17 +14,17 @@ from app.models import (
|
||||
AuditLog,
|
||||
BugReport,
|
||||
AccountInvitation,
|
||||
db,
|
||||
Friends,
|
||||
CommandLog,
|
||||
CharacterXML
|
||||
db
|
||||
)
|
||||
from app.schemas import AccountSchema
|
||||
from app import gm_level, log_audit
|
||||
from app.forms import EditGMLevelForm, EditEmailForm
|
||||
from sqlalchemy import or_, inspect, text, bindparam
|
||||
|
||||
accounts_blueprint = Blueprint('accounts', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
|
||||
|
||||
@accounts_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -149,135 +147,43 @@ def mute(id, days=0):
|
||||
@gm_level(9)
|
||||
def delete(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
if account is None:
|
||||
flash("Account not found", "danger")
|
||||
return redirect(url_for("main.index"))
|
||||
message = f"Deleted Account ({account.id}){account.username}"
|
||||
try:
|
||||
_delete_account_data(account)
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
current_app.logger.exception(f"Failed to delete account {id}")
|
||||
flash(f"Failed to delete Account ({account.id}){account.username}", "danger")
|
||||
return redirect(url_for("accounts.view", id=id))
|
||||
chars = CharacterInfo.query.filter(CharacterInfo.account_id == id).all()
|
||||
for char in chars:
|
||||
activities = ActivityLog.query.filter(ActivityLog.character_id == char.id).all()
|
||||
for activity in activities:
|
||||
activity.delete()
|
||||
lb_entries = Leaderboard.query.filter(Leaderboard.character_id == char.id).all()
|
||||
for lb_entry in lb_entries:
|
||||
lb_entry.delete()
|
||||
mails = Mail.query.filter(Mail.receiver_id == char.id).all()
|
||||
for mail in mails:
|
||||
mail.delete()
|
||||
props = Property.query.filter(Property.owner_id == char.id).all()
|
||||
for prop in props:
|
||||
prop_contents = PropertyContent.query.filter(PropertyContent.property_id == prop.id).all()
|
||||
for prop_content in prop_contents:
|
||||
if prop_content.lot == "14":
|
||||
UGC.query.filter(UGC.id == prop.ugc_id).first().delete()
|
||||
prop_content.delete()
|
||||
prop.delete()
|
||||
char.delete()
|
||||
# This is for GM stuff, it will be permnently delete logs
|
||||
bugs = BugReport.query.filter(BugReport.resoleved_by_id == id).all()
|
||||
for bug in bugs:
|
||||
bug.delete()
|
||||
audits = AuditLog.query.filter(AuditLog.account_id == id).all()
|
||||
for audit in audits:
|
||||
audit.delete()
|
||||
invites = AccountInvitation.query.filter(AccountInvitation.invited_by_user_id == id).all()
|
||||
for invite in invites:
|
||||
invite.delete()
|
||||
account.delete()
|
||||
flash(message, "danger")
|
||||
log_audit(message)
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
def _bulk_delete(query):
|
||||
# Bulk deletes bypass the ORM relationship handling, which would otherwise
|
||||
# try to NULL out non-nullable foreign keys (e.g. ugc.character_id)
|
||||
# instead of deleting the dependent rows.
|
||||
query.delete(synchronize_session=False)
|
||||
|
||||
|
||||
def _delete_account_data(account):
|
||||
"""Delete an account and every row that references it or its characters.
|
||||
|
||||
The DLU schema uses ON DELETE RESTRICT for most of these foreign keys,
|
||||
so dependents have to be removed explicitly, children first, and all
|
||||
in one transaction so a failure leaves nothing half-deleted.
|
||||
"""
|
||||
char_ids = [
|
||||
c.id for c in CharacterInfo.query.with_entities(CharacterInfo.id)
|
||||
.filter(CharacterInfo.account_id == account.id).all()
|
||||
]
|
||||
clone_ids = [
|
||||
c.prop_clone_id for c in CharacterInfo.query.with_entities(CharacterInfo.prop_clone_id)
|
||||
.filter(CharacterInfo.account_id == account.id).all()
|
||||
]
|
||||
existing_tables = set(inspect(db.engine).get_table_names())
|
||||
|
||||
if char_ids:
|
||||
prop_ids = [
|
||||
p.id for p in Property.query.with_entities(Property.id).filter(
|
||||
or_(Property.owner_id.in_(char_ids), Property.clone_id.in_(clone_ids))
|
||||
).all()
|
||||
]
|
||||
ugc_ids = [
|
||||
u.id for u in UGC.query.with_entities(UGC.id).filter(
|
||||
or_(UGC.account_id == account.id, UGC.character_id.in_(char_ids))
|
||||
).all()
|
||||
]
|
||||
# property contents placed on this account's properties, or using its UGC models
|
||||
content_filter = []
|
||||
if prop_ids:
|
||||
content_filter.append(PropertyContent.property_id.in_(prop_ids))
|
||||
if ugc_ids:
|
||||
content_filter.append(PropertyContent.ugc_id.in_(ugc_ids))
|
||||
if content_filter:
|
||||
_bulk_delete(PropertyContent.query.filter(or_(*content_filter)))
|
||||
if prop_ids:
|
||||
_bulk_delete(Property.query.filter(Property.id.in_(prop_ids)))
|
||||
if "ugc_modular_build" in existing_tables:
|
||||
db.session.execute(
|
||||
text("DELETE FROM ugc_modular_build WHERE character_id IN :ids")
|
||||
.bindparams(bindparam("ids", expanding=True)),
|
||||
{"ids": char_ids}
|
||||
)
|
||||
if "ignore_list" in existing_tables:
|
||||
db.session.execute(
|
||||
text("DELETE FROM ignore_list WHERE player_id IN :ids OR ignored_player_id IN :ids")
|
||||
.bindparams(bindparam("ids", expanding=True)),
|
||||
{"ids": char_ids}
|
||||
)
|
||||
_bulk_delete(ActivityLog.query.filter(ActivityLog.character_id.in_(char_ids)))
|
||||
_bulk_delete(Leaderboard.query.filter(Leaderboard.character_id.in_(char_ids)))
|
||||
_bulk_delete(Mail.query.filter(Mail.receiver_id.in_(char_ids)))
|
||||
_bulk_delete(CommandLog.query.filter(CommandLog.character_id.in_(char_ids)))
|
||||
_bulk_delete(Friends.query.filter(
|
||||
or_(Friends.player_id.in_(char_ids), Friends.friend_id.in_(char_ids))
|
||||
))
|
||||
_bulk_delete(CharacterXML.query.filter(CharacterXML.id.in_(char_ids)))
|
||||
|
||||
# UGC can reference the account even without a (remaining) character
|
||||
_bulk_delete(UGC.query.filter(
|
||||
or_(UGC.account_id == account.id, UGC.character_id.in_(char_ids))
|
||||
if char_ids else UGC.account_id == account.id
|
||||
))
|
||||
if char_ids:
|
||||
_bulk_delete(CharacterInfo.query.filter(CharacterInfo.id.in_(char_ids)))
|
||||
|
||||
if "player_cheat_detections" in existing_tables:
|
||||
db.session.execute(
|
||||
text("DELETE FROM player_cheat_detections WHERE account_id = :id"),
|
||||
{"id": account.id}
|
||||
)
|
||||
if "accounts_rewardcodes" in existing_tables:
|
||||
db.session.execute(
|
||||
text("DELETE FROM accounts_rewardcodes WHERE account_id = :id"),
|
||||
{"id": account.id}
|
||||
)
|
||||
# This is for GM stuff, it will be permnently delete logs
|
||||
_bulk_delete(BugReport.query.filter(BugReport.resoleved_by_id == account.id))
|
||||
_bulk_delete(AuditLog.query.filter(AuditLog.account_id == account.id))
|
||||
_bulk_delete(AccountInvitation.query.filter(AccountInvitation.invited_by_user_id == account.id))
|
||||
_bulk_delete(Account.query.filter(Account.id == account.id))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@accounts_blueprint.route('/pass_reset/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def pass_reset(id):
|
||||
# get the account
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
# make a random pass of length 12 using secrets
|
||||
raw_pass = secrets.token_urlsafe(12)
|
||||
# generate the hash
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(str.encode(raw_pass), salt)
|
||||
# save the has
|
||||
account.password = hashed
|
||||
account.save()
|
||||
# display for the admin to get and log that the action was done
|
||||
flash(f"Set password for account {account.username} to {raw_pass}", "success")
|
||||
log_audit(f"Reset password for {account.username}")
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -313,9 +219,6 @@ def get():
|
||||
# Delete
|
||||
# </a>
|
||||
|
||||
if not current_app.config["USER_ENABLE_EMAIL"]:
|
||||
account["2"] = '''N/A'''
|
||||
|
||||
if account["4"]:
|
||||
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
@@ -331,11 +234,20 @@ def get():
|
||||
else:
|
||||
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if not current_app.config["USER_ENABLE_EMAIL"]:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-muted"></h2>'''
|
||||
elif account["8"]:
|
||||
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
if current_app.config["USER_ENABLE_EMAIL"]:
|
||||
if account["8"]:
|
||||
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
else:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
# shift columns to fill in gap of 2
|
||||
account["2"] = account["3"]
|
||||
account["3"] = account["4"]
|
||||
account["4"] = account["5"]
|
||||
account["5"] = account["6"]
|
||||
account["6"] = account["7"]
|
||||
# remove last two columns
|
||||
del account["7"]
|
||||
del account["8"]
|
||||
|
||||
return data
|
||||
|
||||
7495
app/cdclient.py
Normal file
7495
app/cdclient.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,21 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response, current_app
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response
|
||||
from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import CharacterInfo, CharacterXML, Account, db
|
||||
from app.forms import RescueForm, CharXMLUploadForm
|
||||
from app.schemas import CharacterInfoSchema
|
||||
from app.forms import RescueForm
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import translate_from_locale
|
||||
import xmltodict
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
character_blueprint = Blueprint('characters', __name__)
|
||||
|
||||
character_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@character_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -76,7 +78,7 @@ def view(id):
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
).first().xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
@@ -89,10 +91,9 @@ def view(id):
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
|
||||
return render_template(
|
||||
'character/view.html.j2',
|
||||
@@ -100,88 +101,6 @@ def view(id):
|
||||
character_json=character_json
|
||||
)
|
||||
|
||||
@character_blueprint.route('/chardata/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def chardata(id):
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if character_data.account_id and character_data.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
# print json for reference
|
||||
# with open("errorchar.json", "a") as file:
|
||||
# file.write(
|
||||
# json.dumps(character_json, indent=4)
|
||||
# )
|
||||
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
|
||||
return render_template(
|
||||
'partials/_charxml.html.j2',
|
||||
character_data=character_data,
|
||||
character_json=character_json
|
||||
)
|
||||
|
||||
@character_blueprint.route('/inventory/<id>/<inventory_id>', methods=['GET'])
|
||||
@login_required
|
||||
def inventory(id, inventory_id):
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if character_data.account_id and character_data.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
# print json for reference
|
||||
# with open("errorchar.json", "a") as file:
|
||||
# file.write(
|
||||
# json.dumps(character_json, indent=4)
|
||||
# )
|
||||
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
for inventory in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if inventory["attr_t"] == inventory_id:
|
||||
return render_template(
|
||||
'partials/charxml/_inventory.html.j2',
|
||||
inventory=inventory
|
||||
)
|
||||
return "No Items in Inventory", 404
|
||||
|
||||
@character_blueprint.route('/view_xml/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@@ -200,7 +119,7 @@ def view_xml(id):
|
||||
|
||||
character_xml = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt=")
|
||||
).first().xml_data
|
||||
|
||||
response = make_response(character_xml)
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
@@ -271,7 +190,7 @@ def rescue(id):
|
||||
CharacterXML.id == id
|
||||
).first()
|
||||
|
||||
character_xml = ET.XML(character_data.xml_data.replace("\"stt=", "\" stt="))
|
||||
character_xml = ET.XML(character_data.xml_data)
|
||||
for zone in character_xml.findall('.//r'):
|
||||
if int(zone.attrib["w"]) % 100 == 0:
|
||||
form.save_world.choices.append(
|
||||
@@ -297,30 +216,6 @@ def rescue(id):
|
||||
return render_template("character/rescue.html.j2", form=form)
|
||||
|
||||
|
||||
@character_blueprint.route('/upload/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def upload(id):
|
||||
if not current_app.config["ENABLE_CHAR_XML_UPLOAD"]:
|
||||
flash("You must enable this setting to do this", "danger")
|
||||
return redirect(url_for('characters.view', id=id))
|
||||
|
||||
form = CharXMLUploadForm()
|
||||
|
||||
character_data = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first()
|
||||
|
||||
if form.validate_on_submit():
|
||||
character_data.xml_data = form.char_xml.data
|
||||
character_data.save()
|
||||
flash("You accept all consequences from these actions", "danger")
|
||||
log_audit(f"Updated {character_data.id}'s xml data")
|
||||
return redirect(url_for('characters.view', id=id))
|
||||
form.char_xml.data = minidom.parseString(character_data.xml_data).toprettyxml(indent=" ")
|
||||
return render_template("character/upload.html.j2", form=form)
|
||||
|
||||
|
||||
@character_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -339,7 +234,7 @@ def get(status):
|
||||
if status == "approved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name == "") & (CharacterInfo.needs_rename == False))
|
||||
elif status == "unapproved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") & (CharacterInfo.needs_rename == False))
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") | (CharacterInfo.needs_rename == True))
|
||||
else:
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account)
|
||||
|
||||
|
||||
247
app/commands.py
247
app/commands.py
@@ -4,8 +4,8 @@ import random
|
||||
import string
|
||||
import datetime
|
||||
from flask_user import current_app
|
||||
from app import db, luclient
|
||||
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail, CharacterXML
|
||||
from app import db
|
||||
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
|
||||
import pathlib
|
||||
import zlib
|
||||
from wand import image
|
||||
@@ -15,7 +15,17 @@ from multiprocessing import Pool
|
||||
from functools import partial
|
||||
from sqlalchemy import func
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
import csv
|
||||
import json
|
||||
from app.cdclient import (
|
||||
ComponentsRegistry,
|
||||
RenderComponent,
|
||||
ItemComponent,
|
||||
Objects,
|
||||
ScriptComponent,
|
||||
)
|
||||
|
||||
from app.luclient import translate_from_locale
|
||||
|
||||
@click.command("init_db")
|
||||
@click.argument('drop_tables', nargs=1)
|
||||
@@ -180,20 +190,187 @@ def load_property(zone, player):
|
||||
)
|
||||
new_prop_content.save()
|
||||
|
||||
@click.command("remove_buffs")
|
||||
@click.command("parse_lucache")
|
||||
@with_appcontext
|
||||
def remove_buffs():
|
||||
"""Clears all buff from all characters"""
|
||||
chars = CharacterXML.query.all()
|
||||
for char in chars:
|
||||
character_xml = ET.XML(char.xml_data.replace("\"stt=", "\" stt="))
|
||||
dest = character_xml.find(".//dest")
|
||||
if dest:
|
||||
buff = character_xml.find(".//buff")
|
||||
if buff:
|
||||
dest.remove(buff)
|
||||
char.xml_data = ET.tostring(character_xml)
|
||||
char.save()
|
||||
def parse_lucache():
|
||||
"""Parses lucache csv file dump from nexus hq"""
|
||||
unlisted_ids = [146, 147, 938, 1180, 1692, 1715, 1797, 1799, 1824, 1846, 1847, 1848, 1849, 1850, 1872, 1877, 1887, 1928, 1937, 1968, 1970, 1971, 1972, 1974, 1976, 1977, 1978, 1979, 1980, 1981, 1983, 1984, 2189, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2416, 2417, 2418, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2529, 2530, 2553, 2583, 2655, 2656, 2669, 2947, 2948, 3009, 3058, 3068, 3078, 3807, 3812, 3937, 4828, 4874, 4875, 4876, 4877, 4943, 4954, 5839, 5840, 6196, 6218, 6219, 6221, 6433, 6471, 6696, 6821, 6877, 6888, 6889, 6891, 6892, 6893, 6894, 6896, 6897, 6983, 7277, 7551, 7552, 7553, 7554, 7609, 7701, 7713, 7723, 7753, 7754, 7755, 7756, 7760, 7777, 7791, 7824, 7872, 8046, 8053, 8146, 9865, 9866, 9867, 9868, 10126, 10291, 10292, 10293, 10294, 10518, 10630, 10631, 10987, 11511, 11512, 11513, 11514, 11515, 11516, 11517, 11518, 11519, 11520, 11521, 11522, 11523, 11524, 11525, 12096, 12097, 12099, 12100, 12104, 12105, 12111, 12112, 12113, 12324, 12325, 12326, 12553, 12666, 12668, 12670, 12671, 12673, 12674, 12676, 12679, 12680, 12683, 12684, 12685, 12687, 12692, 12694, 12697, 12699, 12701, 12703, 12704, 12713, 12716, 12717, 12727, 12736, 12738, 12739, 12745, 12746, 12750, 12751, 12752, 12757, 12787, 12790, 12791, 12794, 12795, 12799, 12800, 12803, 12887, 12888, 12902, 12904, 12905, 12906, 12907, 12941, 13060, 13061, 13071, 13075, 13076, 13077, 13092, 13093, 13094, 13106, 13118, 13121, 13126, 13127, 13150, 13191, 13192, 13275, 13276, 13277, 13278, 13280, 13295, 13410, 13411, 13510, 13638, 13740, 13742, 13776, 13782, 13905, 13925, 13926, 13927, 13928, 13929, 13930, 13931, 13932, 13953, 13958, 13974, 13996, 13997, 13998, 13999, 14000, 14001, 14002, 14056, 14057, 14058, 14059, 14060, 14061, 14062, 14063, 14064, 14065, 14066, 14067, 14068, 14069, 14070, 14071, 14072, 14073, 14074, 14075, 14076, 14077, 14078, 14079, 14080, 14081, 14090, 14094, 14111, 14135, 14140, 14170, 14171, 14188, 14200, 14202, 14206, 14207, 14208, 14209, 14210, 14211, 14212, 14213, 14228, 14229, 14314, 14428, 14483, 14515, 14522, 14531, 14535, 14536, 14538, 14548, 14554, 14587, 14588, 14589, 14597, 14598, 14599, 14605, 14607, 14608, 14609, 14610, 14611, 14612, 14613, 14614, 14615, 14616, 14617, 14618, 14619, 14620, 14621, 14622, 14623, 14624, 14625, 14626, 14627, 14628, 14629, 14630, 14631, 14632, 14633, 14634, 14635, 14636, 14637, 14638, 14639, 14640, 14641, 14642, 14643, 14644, 14645, 14646, 14647, 14648, 14649, 14650, 14651, 14652, 14653, 14654, 14655, 14656, 14657, 14658, 14659, 14660, 14661, 14662, 14663, 14664, 14665, 14666, 14667, 14668, 14686, 14687, 14688, 14689, 14690, 14704, 14706, 14707, 14716, 14717, 14721, 14722, 14727, 14728, 14729, 14779, 14795, 14799, 14800, 14803, 14815, 14820, 14821, 14822, 14823, 14824, 14825, 14826, 14827, 14831, 14832, 14838, 14839, 15852, 15853, 15854, 15855, 15856, 15857, 15858, 15859, 15860, 15861, 15862, 15863, 15864, 15865, 15885, 15886, 15887, 15888, 15889, 15893, 15894, 15898, 15921, 15923, 15925, 15928, 15930, 15931, 15932, 15933, 15934, 15938, 15939, 15940, 15941, 15942, 15945, 15958, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15981, 15984, 15985, 15986, 15987, 15988, 15989, 15996, 15997, 15998, 15999, 16000, 16001, 16002, 16003, 16004, 16005, 16007, 16008, 16009, 16010, 16011, 16025, 16026, 16027, 16028, 16036, 16039, 16042, 16046, 16051, 16056, 16071, 16072, 16073, 16074, 16075, 16077, 16078, 16079, 16080, 16081, 16089, 16090, 16091, 16092, 16108, 16109, 16110, 16111, 16112, 16113, 16114, 16115, 16116, 16117, 16124, 16125, 16126, 16127, 16128, 16129, 16130, 16137, 16138, 16139, 16140, 16142, 16145, 16167, 16168, 16169, 16170, 16171, 16172, 16173, 16174, 16175, 16176, 16177, 16200, 16201, 16202, 16204, 16212, 16253, 16254, 16418, 16437, 16469, 16479, 16489, 16505, 16641, 16645, 16646, 16648, 16655, 16658, 16659, 16660, 16661, 16662, 16665, 16666, 16667, 16668, 16669, 16670, 16671, 16672, 16673, 16674, 16675, 16676, 16677, 16678, 16679, 16680, 16681, 16685, 16686, 16687, 16688, 16689, 16690, 16691, 16692, 16693, 16694, 16695, 16696, 16697, 16698, 16699, 16700, 16701, 16702, 16703, 16704, 16705, 16706, 16707, 16708, 16709, 16712, 16714, 16717, 16718, 16719, 16720, 16721, 16722, 16724, 16725, 16726, 16727, 16732, 16733, 16734, 16735] # noqa
|
||||
with open("lucache.csv") as cache_file:
|
||||
csv_reader = csv.reader(cache_file, delimiter=',')
|
||||
line_count = 0
|
||||
for row in csv_reader:
|
||||
if row[0] == "id":
|
||||
continue
|
||||
if int(row[0]) in unlisted_ids:
|
||||
json_data = json.loads(row[2])
|
||||
components = ComponentsRegistry.query.filter(ComponentsRegistry.id == int(row[0])).all()
|
||||
obj_type = "Environmental"
|
||||
desc = json_data["Description"]
|
||||
if desc in ["None", None, ""]:
|
||||
desc = row[1]
|
||||
nametag = 0
|
||||
npcTemplateID = "null"
|
||||
for comp in components:
|
||||
if comp.component_type == 7: # Item
|
||||
obj_type = "Smashable"
|
||||
if comp.component_type == 11: # Item
|
||||
obj_type = "Loot"
|
||||
if comp.component_type == 35: # minifig
|
||||
obj_type = "NPC"
|
||||
npcTemplateID = comp.component_id
|
||||
nametag = 1
|
||||
if comp.component_type == 42: # b3
|
||||
obj_type = "Behavior"
|
||||
if comp.component_type == 60: # base combat ai
|
||||
obj_type = "Enemy"
|
||||
if comp.component_type == 73: # mission giver
|
||||
if obj_type != "NPC":
|
||||
obj_type = "Structure"
|
||||
desc = f"__MG__{desc}"
|
||||
if "vendor" in row[1].lower():
|
||||
obj_type = "NPC"
|
||||
print(f"""INSERT INTO "Objects" ("id","name","placeable","type","description","localize","npcTemplateID","displayName","interactionDistance","nametag","_internalNotes","locStatus","gate_version","HQ_valid") VALUES ("{row[0]}", "{row[1].replace("_", " ")}", 1, "{obj_type}", "{desc}", 1, {npcTemplateID} , "{json_data["DisplayName"]}", null , {nametag}, "Unlisted Object", 0, null, 1);""")
|
||||
if obj_type in ["NPC", "Smashable", "Loot"]:
|
||||
print(f""" <phrase id="Objects_{row[0]}_name">
|
||||
<translation locale="en_US">{row[1]}</translation>
|
||||
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
|
||||
<translation locale="en_GB">{row[1]}</translation>
|
||||
</phrase>
|
||||
<phrase id="Objects_{row[0]}_description">
|
||||
<translation locale="en_US">{desc}</translation>
|
||||
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
|
||||
<translation locale="en_GB">{desc}</translation>
|
||||
</phrase>""")
|
||||
# print(f'{row[0]}: {json_data["DisplayName"]}')
|
||||
line_count += 1
|
||||
# print(f'Processed {line_count} lines.')
|
||||
|
||||
|
||||
@click.command("makeup_unlisted_objects")
|
||||
@with_appcontext
|
||||
def makeup_unlisted_objects():
|
||||
objs_left = []
|
||||
for obj in objs_left:
|
||||
obj_type = "Environmental"
|
||||
nametag = 0
|
||||
name = "Name Missing"
|
||||
desc = "null"
|
||||
npcTemplateID = "null"
|
||||
components = ComponentsRegistry.query.filter(ComponentsRegistry.id == obj).all()
|
||||
for comp in components:
|
||||
if comp.component_type == 2: # render
|
||||
render = RenderComponent.query.filter(RenderComponent.id == comp.component_id).first()
|
||||
if render is not None:
|
||||
|
||||
if render.render_asset not in [None, ""]:
|
||||
name = render.render_asset.replace("_", " ").split('\\')[-1].split('/')[-1].split('.')[0].lower()
|
||||
if name == "Name Missing":
|
||||
if render.icon_asset not in [None, ""]:
|
||||
name = render.icon_asset.replace("_", " ").split('\\')[-1].split('/')[-1].split('.')[0].lower()
|
||||
name = name.replace("env ", "").replace("obj ", "").replace("minifig accessory ", "").replace("", "").replace("mf ", "").replace("cre ", "")
|
||||
# print(f"{obj}: {name} : {alt_name}")
|
||||
obj_type = "Smashable"
|
||||
# else:
|
||||
# print(f"{obj}: No Render")
|
||||
if comp.component_type == 7: # destroyable
|
||||
obj_type = "Smashable"
|
||||
if comp.component_type == 11: # Item
|
||||
item = ItemComponent.query.filter(ItemComponent.id == comp.component_id).first()
|
||||
if item.itemType == 24:
|
||||
obj_type = "Mount"
|
||||
else:
|
||||
obj_type = "Loot"
|
||||
if comp.component_type == 35: # minifig
|
||||
obj_type = "NPC"
|
||||
npcTemplateID = comp.component_id
|
||||
nametag = 1
|
||||
if comp.component_type == 42: # b3
|
||||
obj_type = "Behavior"
|
||||
if comp.component_type == 60: # base combat ai
|
||||
obj_type = "Enemy"
|
||||
if comp.component_type == 73: # mission giver
|
||||
if obj_type != "NPC":
|
||||
obj_type = "Structure"
|
||||
desc = f"__MG__{name}"
|
||||
# print(f"""INSERT INTO "Objects" ("id","name","placeable","type","description","localize","npcTemplateID","displayName","interactionDistance","nametag","_internalNotes","locStatus","gate_version","HQ_valid") VALUES ("{obj}", "{name}", 1, "{obj_type}", "{desc}", 1, {npcTemplateID} , "{name}", null , {nametag}, "Unlisted Object", 0, null, 1);""")
|
||||
if name != "Name Missing" and obj_type in ["Mount"]:
|
||||
print(f""" <phrase id="Objects_{obj}_name">
|
||||
<translation locale="en_US">{name}</translation>
|
||||
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
|
||||
<translation locale="en_GB">{name}</translation>
|
||||
</phrase>""")
|
||||
|
||||
|
||||
@click.command("gen_new_locales")
|
||||
@with_appcontext
|
||||
def gen_new_locales():
|
||||
objects = Objects.query.order_by(Objects.id).all()
|
||||
for obj in objects:
|
||||
if obj.type == "Loot":
|
||||
if obj.name not in ["Name Missing", None, "None"] and obj.name[:1] != "m":
|
||||
name_to_trans = f"Object_{obj.id}_name"
|
||||
name_transed = translate_from_locale(name_to_trans)
|
||||
if name_to_trans == name_transed:
|
||||
print(f""" <phrase id="Objects_{obj.id}_name">
|
||||
<translation locale="en_US">{obj.name}</translation>
|
||||
<translation locale="de_DE">TRASNSLATE OLD</translation>
|
||||
<translation locale="en_GB">{obj.name}</translation>
|
||||
</phrase>""")
|
||||
if obj.description not in ["None", None, ""]:
|
||||
description_to_trans = f"Object_{obj.id}_description"
|
||||
description_transed = translate_from_locale(description_to_trans)
|
||||
if description_to_trans == description_transed:
|
||||
print(f""" <phrase id="Objects_{obj.id}_description">
|
||||
<translation locale="en_US">{obj.description}</translation>
|
||||
<translation locale="de_DE">TRASNSLATE OLD</translation>
|
||||
<translation locale="en_GB">{obj.description}</translation>
|
||||
</phrase>""")
|
||||
|
||||
|
||||
@click.command("xref_scripts")
|
||||
@with_appcontext
|
||||
def xref_scripts():
|
||||
"""cross refernce scripts dir with script component table"""
|
||||
scripts = ScriptComponent.query.all()
|
||||
base = 'app/luclient/res/'
|
||||
server = 0
|
||||
server_total = 0
|
||||
client = 0
|
||||
client_total = 0
|
||||
server_used = 0
|
||||
client_used = 0
|
||||
used_total = 0
|
||||
disk_scripts = [path for path in pathlib.Path('app/luclient/res/scripts').rglob("*.lua") if path.is_file()]
|
||||
|
||||
for script in scripts:
|
||||
script_comps = ComponentsRegistry.query.filter(ComponentsRegistry.component_type == 5).filter(ComponentsRegistry.component_id == script.id).all()
|
||||
if len(script_comps) > 0:
|
||||
used_total += 1
|
||||
if script.client_script_name not in [None, ""]:
|
||||
cleaned_name = script.client_script_name.replace('\\', '/').lower()
|
||||
client_script = pathlib.Path(f"{base}{cleaned_name}")
|
||||
client_total += 1
|
||||
if not client_script.is_file():
|
||||
print(f"Missing Server Script: {client_script.as_posix()}")
|
||||
client += 1
|
||||
if len(script_comps) > 0:
|
||||
client_used += 1
|
||||
if script.script_name not in [None, ""]:
|
||||
cleaned_name = script.script_name.replace('\\', '/').lower()
|
||||
server_script = pathlib.Path(f"{base}{cleaned_name}")
|
||||
server_total += 1
|
||||
if not server_script.is_file():
|
||||
print(f"Missing Client Script: {server_script.as_posix()}")
|
||||
server += 1
|
||||
if len(script_comps) > 0:
|
||||
server_used += 1
|
||||
|
||||
print(f"Missing {server}/{server_total} server scripts")
|
||||
print(f"Missing {client}/{client_total} client scripts")
|
||||
print(f"Missing {server_used}/{used_total} used server scripts")
|
||||
print(f"Missing {client_used}/{used_total} used client scripts")
|
||||
print(f"Total cdclient scripts {server_total + client_total}\nTotal disk scripts {len(disk_scripts)}")
|
||||
|
||||
|
||||
@click.command("gen_image_cache")
|
||||
def gen_image_cache():
|
||||
@@ -281,41 +458,3 @@ def find_or_create_account(name, email, password, gm_level=9):
|
||||
db.session.add(play_key)
|
||||
db.session.commit()
|
||||
return # account
|
||||
|
||||
@click.command("find_missing_commendation_items")
|
||||
@with_appcontext
|
||||
def find_missing_commendation_items():
|
||||
data = dict()
|
||||
lots = set()
|
||||
reward_items = luclient.query_cdclient("Select reward_item1 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item2 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item3 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item4 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
lots.remove(0)
|
||||
lots.remove(-1)
|
||||
|
||||
for lot in lots:
|
||||
itemcompid = luclient.query_cdclient(
|
||||
"Select component_id from ComponentsRegistry where component_type = 11 and id = ?;",
|
||||
[lot],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
itemcomp = luclient.query_cdclient(
|
||||
"Select commendationLOT, commendationCost from ItemComponent where id = ?;",
|
||||
[itemcompid],
|
||||
one=True
|
||||
)
|
||||
if itemcomp[0] is None or itemcomp[1] is None:
|
||||
data[lot] = {"name": luclient.get_lot_name(lot)}
|
||||
print(data)
|
||||
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
"""Helpers for serving model / brick database files with validators and gzip.
|
||||
|
||||
Responses carry a strong ETag (derived from the source file's mtime and size,
|
||||
or from a content hash for in-memory data) plus Last-Modified where available,
|
||||
and honour conditional requests so repeat visits get cheap 304 responses.
|
||||
|
||||
When the client accepts gzip and ``COMPRESS_MODEL_RESPONSES`` is enabled the
|
||||
body is gzip encoded. With ``CACHE_COMPRESSED_MODELS`` enabled a ``.gz``
|
||||
copy is
|
||||
stored on disk (regenerated whenever the source is newer) so no CPU is spent
|
||||
per request; otherwise the body is compressed in memory at a fast level.
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Response, current_app, g, request, send_file
|
||||
from flask.sessions import SecureCookieSessionInterface
|
||||
|
||||
|
||||
# Fast level for on-the-fly compression, better level for stored copies
|
||||
# (stored copies are only written once).
|
||||
ON_THE_FLY_LEVEL = 1
|
||||
STORED_LEVEL = 6
|
||||
|
||||
|
||||
def _compression_enabled():
|
||||
return bool(current_app.config.get("COMPRESS_MODEL_RESPONSES", True))
|
||||
|
||||
|
||||
def _store_compressed():
|
||||
return bool(current_app.config.get("CACHE_COMPRESSED_MODELS", False))
|
||||
|
||||
|
||||
def _client_accepts_gzip():
|
||||
return _compression_enabled() and request.accept_encodings["gzip"] > 0
|
||||
|
||||
|
||||
def _gz_path_for(path):
|
||||
"""Location of the stored gzip copy for ``path``.
|
||||
|
||||
Files inside the cache directory get a sibling ``.gz``; files that live
|
||||
elsewhere (e.g. the client's res/ folder, which may be read-only or shared)
|
||||
get a copy under ``<CACHE_LOCATION>/compressed/``.
|
||||
"""
|
||||
cache_root = pathlib.Path(current_app.config["CACHE_LOCATION"]).resolve()
|
||||
path = path.resolve()
|
||||
try:
|
||||
path.relative_to(cache_root)
|
||||
return path.with_name(path.name + ".gz")
|
||||
except ValueError:
|
||||
digest = hashlib.sha1(str(path).encode()).hexdigest()
|
||||
return cache_root / "compressed" / f"{digest}-{path.name}.gz"
|
||||
|
||||
|
||||
def _ensure_gz(path, src_stat):
|
||||
"""Create/refresh the stored gzip copy of ``path`` and return its path."""
|
||||
gz_path = _gz_path_for(path)
|
||||
try:
|
||||
if gz_path.stat().st_mtime_ns >= src_stat.st_mtime_ns:
|
||||
return gz_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
gz_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "rb") as src:
|
||||
data = gzip.compress(src.read(), compresslevel=STORED_LEVEL, mtime=0)
|
||||
# write atomically so concurrent workers never serve a partial file
|
||||
fd, tmp = tempfile.mkstemp(
|
||||
dir=gz_path.parent, prefix=".tmp-", suffix=".gz"
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as out:
|
||||
out.write(data)
|
||||
os.chmod(tmp, 0o644) # mkstemp creates 0600, match other cache files
|
||||
os.replace(tmp, gz_path)
|
||||
except BaseException:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
return gz_path
|
||||
|
||||
|
||||
def _skip_session_cookie():
|
||||
g.compressed_files_user_independent = True
|
||||
|
||||
|
||||
class _SessionInterface(SecureCookieSessionInterface):
|
||||
"""Skip refreshing the session cookie for user independent files.
|
||||
|
||||
Flask-User refreshes the session cookie on every request, and Flask adds
|
||||
"Vary: Cookie" whenever it sets the cookie. As the cookie value changes on
|
||||
every response, the browser then has to revalidate every cached copy,
|
||||
defeating max-age. These files don't depend on the session, so there is
|
||||
nothing to save for them.
|
||||
"""
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
if g.get("compressed_files_user_independent"):
|
||||
return None
|
||||
return super().save_session(app, session, response)
|
||||
|
||||
|
||||
def register_compressed_files(app):
|
||||
if type(app.session_interface) is SecureCookieSessionInterface:
|
||||
app.session_interface = _SessionInterface()
|
||||
|
||||
|
||||
def _finish(response, etag, last_modified, cache_control, vary):
|
||||
response.set_etag(etag)
|
||||
if last_modified is not None:
|
||||
response.last_modified = last_modified
|
||||
response.headers["Cache-Control"] = cache_control
|
||||
if vary:
|
||||
response.vary.add("Accept-Encoding")
|
||||
return response.make_conditional(request)
|
||||
|
||||
|
||||
def _not_modified(etag, last_modified, cache_control, vary, mimetype):
|
||||
"""Answer a matching conditional request without reading/compressing."""
|
||||
if etag in request.if_none_match:
|
||||
return _finish(
|
||||
Response(status=304, mimetype=mimetype),
|
||||
etag, last_modified, cache_control, vary
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def serve_file(path, mimetype, cache_control, user_independent=False):
|
||||
"""Serve a file from disk with validators and optional gzip encoding.
|
||||
|
||||
``user_independent`` marks content that does not depend on the logged in
|
||||
user; the session cookie is not refreshed on these responses so no
|
||||
``Vary: Cookie`` is added (see ``_SessionInterface``).
|
||||
"""
|
||||
if user_independent:
|
||||
_skip_session_cookie()
|
||||
# absolute: send_file resolves relative paths against the app package
|
||||
path = pathlib.Path(path).resolve()
|
||||
if mimetype is None:
|
||||
mimetype = (
|
||||
mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
||||
)
|
||||
st = path.stat()
|
||||
base_etag = f"{st.st_mtime_ns:x}-{st.st_size:x}"
|
||||
last_modified = datetime.fromtimestamp(int(st.st_mtime), tz=timezone.utc)
|
||||
vary = _compression_enabled()
|
||||
|
||||
if not _client_accepts_gzip():
|
||||
response = send_file(
|
||||
path, mimetype=mimetype, etag=base_etag,
|
||||
last_modified=last_modified, conditional=True
|
||||
)
|
||||
response.headers["Cache-Control"] = cache_control
|
||||
if vary:
|
||||
response.vary.add("Accept-Encoding")
|
||||
return response
|
||||
|
||||
etag = base_etag + "-gz"
|
||||
early = _not_modified(etag, last_modified, cache_control, vary, mimetype)
|
||||
if early is not None:
|
||||
return early
|
||||
|
||||
if _store_compressed():
|
||||
gz_path = _ensure_gz(path, st)
|
||||
response = send_file(
|
||||
gz_path, mimetype=mimetype, etag=etag,
|
||||
last_modified=last_modified, conditional=True
|
||||
)
|
||||
response.headers["Cache-Control"] = cache_control
|
||||
response.headers["Content-Encoding"] = "gzip"
|
||||
response.vary.add("Accept-Encoding")
|
||||
return response
|
||||
|
||||
with open(path, "rb") as src:
|
||||
data = gzip.compress(
|
||||
src.read(), compresslevel=ON_THE_FLY_LEVEL, mtime=0
|
||||
)
|
||||
response = Response(data, mimetype=mimetype)
|
||||
response.headers["Content-Encoding"] = "gzip"
|
||||
return _finish(response, etag, last_modified, cache_control, vary)
|
||||
|
||||
|
||||
def serve_bytes(data, mimetype, cache_control, etag_seed=None):
|
||||
"""Serve in-memory data (e.g. UGC lxfml from the database).
|
||||
|
||||
``etag_seed`` should be cheap, stable bytes identifying the content (for
|
||||
example the stored compressed blob); defaults to ``data``. ``data`` may
|
||||
be a callable producing the body, so a 304 can skip building it (this
|
||||
requires ``etag_seed``).
|
||||
"""
|
||||
if callable(data):
|
||||
if etag_seed is None:
|
||||
data = data()
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
base_etag = hashlib.sha1(
|
||||
etag_seed if etag_seed is not None else data
|
||||
).hexdigest()
|
||||
vary = _compression_enabled()
|
||||
gz = _client_accepts_gzip()
|
||||
etag = base_etag + ("-gz" if gz else "")
|
||||
|
||||
early = _not_modified(etag, None, cache_control, vary, mimetype)
|
||||
if early is not None:
|
||||
return early
|
||||
|
||||
if callable(data):
|
||||
data = data()
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
if gz:
|
||||
data = gzip.compress(data, compresslevel=ON_THE_FLY_LEVEL, mtime=0)
|
||||
response = Response(data, mimetype=mimetype)
|
||||
if gz:
|
||||
response.headers["Content-Encoding"] = "gzip"
|
||||
return _finish(response, etag, None, cache_control, vary)
|
||||
116
app/forms.py
116
app/forms.py
@@ -1,92 +1,90 @@
|
||||
from flask_wtf import FlaskForm, Recaptcha, RecaptchaField
|
||||
from flask_wtf import FlaskForm
|
||||
from flask import current_app
|
||||
|
||||
from flask_user.forms import (
|
||||
unique_email_validator,
|
||||
LoginForm,
|
||||
RegisterForm,
|
||||
ChangePasswordForm
|
||||
password_validator,
|
||||
unique_username_validator
|
||||
)
|
||||
from flask_user import UserManager
|
||||
from wtforms.widgets import TextArea, NumberInput
|
||||
from wtforms import (
|
||||
StringField,
|
||||
HiddenField,
|
||||
PasswordField,
|
||||
BooleanField,
|
||||
SubmitField,
|
||||
validators,
|
||||
IntegerField,
|
||||
SelectField,
|
||||
PasswordField
|
||||
SelectField
|
||||
)
|
||||
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from app.models import PlayKey
|
||||
|
||||
def password_check(form, field):
|
||||
"""
|
||||
Validates that the password does not contain a colon, is between 6 and 40 characters long and has an uppercase letter, lowercase letter and a number
|
||||
"""
|
||||
error_msg = "Password must be between 6 and 40 characters long, contain a lowercase letter, an uppercase letter, a number, and cannot contain a colon"
|
||||
password = field.data
|
||||
pass_len = len(password)
|
||||
if pass_len < 6:
|
||||
raise validators.ValidationError(error_msg)
|
||||
if ':' in password:
|
||||
raise validators.ValidationError(error_msg)
|
||||
if not any(c.islower() for c in password):
|
||||
raise validators.ValidationError(error_msg)
|
||||
if not any(c.isupper() for c in password):
|
||||
raise validators.ValidationError(error_msg)
|
||||
if not any(c.isdigit() for c in password):
|
||||
raise validators.ValidationError(error_msg)
|
||||
if pass_len > 40:
|
||||
raise validators.ValidationError(error_msg)
|
||||
return True
|
||||
|
||||
|
||||
def validate_play_key(form, field):
|
||||
"""Validates a field for a valid play kyey
|
||||
"""Validates a field for a valid phone number
|
||||
Args:
|
||||
form: REQUIRED, the field's parent form
|
||||
field: REQUIRED, the field with data
|
||||
Returns:
|
||||
None, raises ValidationError if failed
|
||||
"""
|
||||
# jank to get the foreign key that we need back into the field
|
||||
# jank to get the fireign key that we need back into the field
|
||||
if current_app.config["REQUIRE_PLAY_KEY"]:
|
||||
field.data = PlayKey.key_is_valid(key_string=field.data)
|
||||
return True
|
||||
|
||||
class CustomRecaptcha(Recaptcha):
|
||||
def __call__(self, form, field):
|
||||
if not current_app.config.get("RECAPTCHA_ENABLE", False):
|
||||
return True
|
||||
return super(CustomRecaptcha, self).__call__(form, field)
|
||||
return
|
||||
|
||||
|
||||
class CustomUserManager(UserManager):
|
||||
def customize(self, app):
|
||||
self.RegisterFormClass = CustomRegisterForm
|
||||
self.LoginFormClass = CustomLoginForm
|
||||
self.ChangePasswordFormClass = ColonlessChangePasswordForm
|
||||
|
||||
class CustomRegisterForm(RegisterForm):
|
||||
|
||||
class CustomRegisterForm(FlaskForm):
|
||||
"""Registration form"""
|
||||
next = HiddenField()
|
||||
reg_next = HiddenField()
|
||||
|
||||
# Login Info
|
||||
email = StringField(
|
||||
'E-Mail',
|
||||
validators=[
|
||||
Optional(),
|
||||
validators.Email('Invalid email address'),
|
||||
unique_email_validator,
|
||||
]
|
||||
)
|
||||
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
unique_username_validator,
|
||||
]
|
||||
)
|
||||
|
||||
play_key_id = StringField(
|
||||
'Play Key',
|
||||
validators=[validate_play_key]
|
||||
validators=[
|
||||
Optional(),
|
||||
validate_play_key,
|
||||
]
|
||||
)
|
||||
recaptcha = RecaptchaField(
|
||||
validators=[CustomRecaptcha()]
|
||||
)
|
||||
password=PasswordField(
|
||||
'Password',
|
||||
validators=[DataRequired(), password_check]
|
||||
)
|
||||
|
||||
class CustomLoginForm(LoginForm):
|
||||
recaptcha = RecaptchaField(
|
||||
validators=[CustomRecaptcha()]
|
||||
)
|
||||
password = PasswordField('Password', validators=[
|
||||
DataRequired(),
|
||||
password_validator
|
||||
])
|
||||
retype_password = PasswordField('Retype Password', validators=[
|
||||
validators.EqualTo('password', message='Passwords did not match')
|
||||
])
|
||||
|
||||
invite_token = HiddenField('Token')
|
||||
|
||||
submit = SubmitField('Register')
|
||||
|
||||
|
||||
class CreatePlayKeyForm(FlaskForm):
|
||||
|
||||
@@ -211,19 +209,3 @@ class RejectPropertyForm(FlaskForm):
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class CharXMLUploadForm(FlaskForm):
|
||||
char_xml = StringField(
|
||||
'Paste minified charxml here:',
|
||||
widget=TextArea(),
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
class ColonlessChangePasswordForm(ChangePasswordForm):
|
||||
new_password = PasswordField(
|
||||
'New Password',
|
||||
validators=[validators.DataRequired(), password_check]
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ def command():
|
||||
@log_blueprint.route('/system')
|
||||
@gm_level(8)
|
||||
def system():
|
||||
with open('logs/nexus_dashboard.log', 'r') as file:
|
||||
with open('nexus_dashboard.log', 'r') as file:
|
||||
logs = '</br>'.join(file.read().split('\n')[-100:])
|
||||
return render_template('logs/system.html.j2', logs=logs)
|
||||
|
||||
|
||||
249
app/luclient.py
249
app/luclient.py
@@ -5,12 +5,21 @@ from flask import (
|
||||
redirect,
|
||||
url_for,
|
||||
make_response,
|
||||
abort,
|
||||
current_app
|
||||
abort
|
||||
)
|
||||
from flask_user import login_required
|
||||
from app.models import CharacterInfo
|
||||
from app.compressed_files import serve_file
|
||||
from app.cdclient import (
|
||||
Objects,
|
||||
Icons,
|
||||
ItemSets,
|
||||
ComponentsRegistry,
|
||||
ComponentType,
|
||||
RenderComponent,
|
||||
ItemComponent,
|
||||
ObjectSkills,
|
||||
SkillBehavior
|
||||
)
|
||||
import glob
|
||||
import os
|
||||
from wand import image
|
||||
@@ -18,8 +27,8 @@ from wand.exceptions import BlobError as BE
|
||||
import pathlib
|
||||
import json
|
||||
|
||||
import sqlite3
|
||||
import xml.etree.ElementTree as ET
|
||||
from sqlalchemy import or_
|
||||
|
||||
luclient_blueprint = Blueprint('luclient', __name__)
|
||||
locale = {}
|
||||
@@ -34,7 +43,7 @@ def get_dds_as_png(filename):
|
||||
cache = f'cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
root = 'app/luclient/res/'
|
||||
|
||||
path = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
@@ -43,7 +52,7 @@ def get_dds_as_png(filename):
|
||||
|
||||
with image.Image(filename=path) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=current_app.config["CACHE_LOCATION"] + filename.split('.')[0] + '.png')
|
||||
img.save(filename='app/cache/' + filename.split('.')[0] + '.png')
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
@@ -54,7 +63,7 @@ def get_dds(filename):
|
||||
if filename.split('.')[-1] != 'dds':
|
||||
return 404
|
||||
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
root = 'app/luclient/res/'
|
||||
|
||||
dds = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
@@ -67,37 +76,24 @@ def get_dds(filename):
|
||||
@luclient_blueprint.route('/get_icon_lot/<id>')
|
||||
@login_required
|
||||
def get_icon_lot(id):
|
||||
if id is None:
|
||||
redirect(url_for('luclient.unknown'))
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[id],
|
||||
one=True
|
||||
)
|
||||
if render_component_id is not None:
|
||||
render_component_id = render_component_id[0]
|
||||
icon_path = RenderComponent.query.filter(
|
||||
RenderComponent.id == ComponentsRegistry.query.filter(
|
||||
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_RENDER
|
||||
).filter(ComponentsRegistry.id == id).first().component_id
|
||||
).first().icon_asset
|
||||
|
||||
if icon_path:
|
||||
icon_path = icon_path.replace("..\\", "").replace("\\", "/")
|
||||
else:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient(
|
||||
'select icon_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
if filename:
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
else:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
cache = f'app/cache/{icon_path.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
root = 'app/luclient/res/'
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
with image.Image(filename=f'{root}{icon_path}'.lower()) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=cache)
|
||||
except BE:
|
||||
@@ -110,18 +106,14 @@ def get_icon_lot(id):
|
||||
@login_required
|
||||
def get_icon_iconid(id):
|
||||
|
||||
filename = query_cdclient(
|
||||
'select IconPath from Icons where IconID = ?',
|
||||
[id],
|
||||
one=True
|
||||
)[0]
|
||||
filename = Icons.query.filter(Icons.IconID == id).first().IconPath
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
cache = f'app/cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
root = 'app/luclient/res/'
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
@@ -139,14 +131,14 @@ def brick_list():
|
||||
brick_list = []
|
||||
if len(brick_list) == 0:
|
||||
suffixes = [".g", ".g1", ".g2", ".g3", ".xml"]
|
||||
cache = pathlib.Path(f"{current_app.config['CACHE_LOCATION']}")
|
||||
res = pathlib.Path('app/luclient/res/')
|
||||
# Load g files
|
||||
for path in cache.rglob("*.*"):
|
||||
for path in res.rglob("*.*"):
|
||||
if str(path.suffix) in suffixes:
|
||||
brick_list.append(
|
||||
{
|
||||
"type": "file",
|
||||
"name": str(path.as_posix()).replace(f"{current_app.config['CACHE_LOCATION']}", "")
|
||||
"name": str(path.as_posix()).replace("app/luclient/res/", "")
|
||||
}
|
||||
)
|
||||
response = make_response(json.dumps(brick_list))
|
||||
@@ -158,19 +150,14 @@ def brick_list():
|
||||
@luclient_blueprint.route('/ldddb/<path:req_path>')
|
||||
def dir_listing(req_path):
|
||||
# Joining the base and the requested path
|
||||
rel_path = pathlib.Path(str(pathlib.Path(f"{current_app.config['CACHE_LOCATION']}/{req_path}").resolve()))
|
||||
rel_path = pathlib.Path(str(pathlib.Path(f'app/luclient/res/{req_path}').resolve()))
|
||||
# Return 404 if path doesn't exist
|
||||
if not rel_path.exists():
|
||||
return abort(404)
|
||||
|
||||
# Check if path is a file and serve
|
||||
if rel_path.is_file():
|
||||
# Thousands of small, effectively immutable brick primitives are
|
||||
# fetched
|
||||
# per viewer load; a modest max-age avoids a 304 round trip for each.
|
||||
return serve_file(
|
||||
rel_path, None, "private, max-age=86400", user_independent=True
|
||||
)
|
||||
return send_file(rel_path)
|
||||
else:
|
||||
return abort(404)
|
||||
|
||||
@@ -180,10 +167,10 @@ def dir_listing(req_path):
|
||||
def unknown():
|
||||
filename = "textures/ui/inventory/unknown.dds"
|
||||
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
cache = f'app/cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
root = 'app/luclient/res/'
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
@@ -195,41 +182,6 @@ def unknown():
|
||||
return send_file(pathlib.Path(cache).resolve())
|
||||
|
||||
|
||||
def get_cdclient():
|
||||
"""Connect to CDClient from file system Relative Path
|
||||
|
||||
Args:
|
||||
None
|
||||
"""
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
if cdclient is None:
|
||||
path = pathlib.Path(f"{current_app.config['CD_SQLITE_LOCATION']}cdclient.sqlite")
|
||||
if path.is_file():
|
||||
cdclient = g._database = sqlite3.connect(f"{current_app.config['CD_SQLITE_LOCATION']}cdclient.sqlite")
|
||||
return cdclient
|
||||
|
||||
path = pathlib.Path(f"{current_app.config['CD_SQLITE_LOCATION']}CDServer.sqlite")
|
||||
if path.is_file():
|
||||
cdclient = g._database = sqlite3.connect(f"{current_app.config['CD_SQLITE_LOCATION']}CDServer.sqlite")
|
||||
return cdclient
|
||||
|
||||
return cdclient
|
||||
|
||||
|
||||
def query_cdclient(query, args=(), one=False):
|
||||
"""Run sql queries on CDClient
|
||||
|
||||
Args:
|
||||
query (string) : SQL query
|
||||
args (list) : List of args to place in query
|
||||
one (bool) : Return only on result or all results
|
||||
"""
|
||||
cur = get_cdclient().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
cur.close()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
def translate_from_locale(trans_string):
|
||||
"""Finds the string translation from locale.xml
|
||||
|
||||
@@ -244,7 +196,7 @@ def translate_from_locale(trans_string):
|
||||
locale_data = ""
|
||||
|
||||
if not locale:
|
||||
locale_path = f"{current_app.config['CLIENT_LOCATION']}locale/locale.xml"
|
||||
locale_path = "app/luclient/locale/locale.xml"
|
||||
|
||||
with open(locale_path, 'r') as file:
|
||||
locale_data = file.read()
|
||||
@@ -268,13 +220,11 @@ def get_lot_name(lot_id):
|
||||
return "Missing"
|
||||
name = translate_from_locale(f'Objects_{lot_id}_name')
|
||||
if name == f'Objects_{lot_id}_name':
|
||||
intermed = query_cdclient(
|
||||
'select * from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
intermed = Objects.query.filter(Objects.id == lot_id).first()
|
||||
if intermed:
|
||||
name = intermed[7] if (intermed[7] != "None" and intermed[7] != "" and intermed[7] is None) else intermed[1]
|
||||
name = intermed.displayName if (intermed.displayName != "None" or intermed.displayName != "" or intermed.displayName == None) else intermed.name
|
||||
if not name:
|
||||
name = f'Objects_{lot_id}_name'
|
||||
return name
|
||||
|
||||
|
||||
@@ -302,7 +252,6 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('parse_lzid')
|
||||
def parse_lzid(lzid):
|
||||
if not lzid: return [1000, 1000, 1000]
|
||||
return[
|
||||
(int(lzid) & ((1 << 16) - 1)),
|
||||
((int(lzid) >> 16) & ((1 << 16) - 1)),
|
||||
@@ -326,21 +275,11 @@ def register_luclient_jinja_helpers(app):
|
||||
def get_lot_rarity(lot_id):
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 11 and id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
if render_component_id:
|
||||
render_component_id = render_component_id[0]
|
||||
|
||||
rarity = query_cdclient(
|
||||
'select rarity from ItemComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
if rarity:
|
||||
rarity = rarity[0]
|
||||
rarity = ItemComponent.query.filter(
|
||||
ItemComponent.id == ComponentsRegistry.query.filter(
|
||||
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_ITEM
|
||||
).filter(ComponentsRegistry.id == id).first().component_id
|
||||
).first().rarity
|
||||
return rarity
|
||||
|
||||
@app.template_filter('get_lot_desc')
|
||||
@@ -349,15 +288,12 @@ def register_luclient_jinja_helpers(app):
|
||||
return "Missing"
|
||||
desc = translate_from_locale(f'Objects_{lot_id}_description')
|
||||
if desc == f'Objects_{lot_id}_description':
|
||||
desc = query_cdclient(
|
||||
'select description from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
desc = Objects.query.filter(Objects.id == lot_id).first()
|
||||
|
||||
if desc in ("", None):
|
||||
desc = None
|
||||
else:
|
||||
desc = desc[0]
|
||||
desc = desc.description
|
||||
if desc in ("", None):
|
||||
desc = None
|
||||
if desc:
|
||||
@@ -368,11 +304,14 @@ def register_luclient_jinja_helpers(app):
|
||||
def check_if_in_set(lot_id):
|
||||
if not lot_id:
|
||||
return None
|
||||
item_set = query_cdclient(
|
||||
'select * from ItemSets where itemIDs like ? or itemIDs like ? or itemIDs like ?',
|
||||
[f'{lot_id}%', f'%, {lot_id}%', f'%,{lot_id}%'],
|
||||
one=True
|
||||
)
|
||||
item_set = ItemSets.query.filter(
|
||||
or_(
|
||||
ItemSets.itemIDs.like(f'{lot_id}%'),
|
||||
ItemSets.itemIDs.like(f'%, {lot_id}%'),
|
||||
ItemSets.itemIDs.like(f'%,{lot_id}%')
|
||||
)
|
||||
).first()
|
||||
|
||||
if item_set in ("", None):
|
||||
return None
|
||||
else:
|
||||
@@ -382,12 +321,19 @@ def register_luclient_jinja_helpers(app):
|
||||
def get_lot_stats(lot_id):
|
||||
if not lot_id:
|
||||
return None
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ObjectSkills WHERE objectTemplate=?\
|
||||
)',
|
||||
[lot_id]
|
||||
)
|
||||
stats = SkillBehavior.query.with_entities(
|
||||
SkillBehavior.imBonusUI,
|
||||
SkillBehavior.lifeBonusUI,
|
||||
SkillBehavior.armorBonusUI,
|
||||
SkillBehavior.skillID,
|
||||
SkillBehavior.skillIcon
|
||||
).filter(
|
||||
SkillBehavior.skillID in ObjectSkills.query.with_entities(
|
||||
ObjectSkills.skillID
|
||||
).filter(
|
||||
ObjectSkills.objectTemplate == lot_id
|
||||
).all()
|
||||
).all()
|
||||
|
||||
return consolidate_stats(stats)
|
||||
|
||||
@@ -395,23 +341,22 @@ def register_luclient_jinja_helpers(app):
|
||||
def get_set_stats(lot_id):
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ItemSetSkills WHERE SkillSetID=?\
|
||||
)',
|
||||
[lot_id]
|
||||
)
|
||||
stats = SkillBehavior.query.with_entities(
|
||||
SkillBehavior.imBonusUI,
|
||||
SkillBehavior.lifeBonusUI,
|
||||
SkillBehavior.armorBonusUI,
|
||||
SkillBehavior.skillID,
|
||||
SkillBehavior.skillIcon
|
||||
).filter(
|
||||
SkillBehavior.skillID == ObjectSkills.query.with_entities(
|
||||
ObjectSkills.skillID
|
||||
).filter(
|
||||
ObjectSkills.objectTemplate == lot_id
|
||||
).all()
|
||||
).all()
|
||||
|
||||
return consolidate_stats(stats)
|
||||
|
||||
@app.template_filter('query_cdclient')
|
||||
def jinja_query_cdclient(query, items):
|
||||
print(query, items)
|
||||
return query_cdclient(
|
||||
query,
|
||||
items,
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
@app.template_filter('lu_translate')
|
||||
def lu_translate(to_translate):
|
||||
@@ -420,26 +365,18 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
def consolidate_stats(stats):
|
||||
|
||||
if len(stats) > 1:
|
||||
if stats:
|
||||
consolidated_stats = {"im": 0, "life": 0, "armor": 0, "skill": []}
|
||||
for stat in stats:
|
||||
if stat[0]:
|
||||
consolidated_stats["im"] += stat[0]
|
||||
if stat[1]:
|
||||
consolidated_stats["life"] += stat[1]
|
||||
if stat[2]:
|
||||
consolidated_stats["armor"] += stat[2]
|
||||
if stat[3]:
|
||||
consolidated_stats["skill"].append([stat[3], stat[4]])
|
||||
|
||||
if stat.imBonusUI:
|
||||
consolidated_stats["im"] += stat.imBonusUI
|
||||
if stat.lifeBonusUI:
|
||||
consolidated_stats["life"] += stat.lifeBonusUI
|
||||
if stat.armorBonusUI:
|
||||
consolidated_stats["armor"] += stat.armorBonusUI
|
||||
if stat.skillID:
|
||||
consolidated_stats["skill"].append([stat.skillID, stat.skillIcon])
|
||||
stats = consolidated_stats
|
||||
elif len(stats) == 1:
|
||||
stats = {
|
||||
"im": stats[0][0] if stats[0][0] else 0,
|
||||
"life": stats[0][1] if stats[0][1] else 0,
|
||||
"armor": stats[0][2] if stats[0][2] else 0,
|
||||
"skill": [[stats[0][3], stats[0][4]]] if stats[0][3] else None,
|
||||
}
|
||||
else:
|
||||
stats = None
|
||||
return stats
|
||||
|
||||
11
app/mail.py
11
app/mail.py
@@ -3,7 +3,8 @@ from flask_user import login_required, current_user
|
||||
from app.models import Mail, CharacterInfo
|
||||
from app.forms import SendMailForm
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import translate_from_locale, query_cdclient
|
||||
from app.luclient import translate_from_locale
|
||||
from app.cdclient import Objects
|
||||
import time
|
||||
|
||||
mail_blueprint = Blueprint('mail', __name__)
|
||||
@@ -68,14 +69,12 @@ def send():
|
||||
for character in recipients:
|
||||
form.recipient.choices.append((character.id, character.name))
|
||||
|
||||
items = query_cdclient(
|
||||
'Select id, name, displayName from Objects where type = "Loot"'
|
||||
)
|
||||
items = Objects.query.filter(Objects.type == "Loot").all()
|
||||
|
||||
for item in items:
|
||||
name = translate_from_locale(f'Objects_{item[0]}_name')
|
||||
if name == f'Objects_{item[0]}_name':
|
||||
name = (item[2] if (item[2] != "None" and item[2] != "" and item[2] is not None) else item[1])
|
||||
if name == f'Objects_{item.id}_name':
|
||||
name = (item.displayName if (item.displayName != "None" and item.displayName != "" and item.displayName is not None) else item.name)
|
||||
form.attachment.choices.append(
|
||||
(
|
||||
item[0],
|
||||
|
||||
24
app/main.py
24
app/main.py
@@ -2,12 +2,13 @@ from flask import render_template, Blueprint, send_from_directory
|
||||
from flask_user import current_user, login_required
|
||||
|
||||
from app.models import Account, CharacterInfo, ActivityLog
|
||||
|
||||
import datetime
|
||||
import time
|
||||
from app.schemas import AccountSchema, CharacterInfoSchema
|
||||
|
||||
main_blueprint = Blueprint('main', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
char_info_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@main_blueprint.route('/', methods=['GET'])
|
||||
def index():
|
||||
@@ -27,16 +28,12 @@ def index():
|
||||
@login_required
|
||||
def about():
|
||||
"""About Page"""
|
||||
mods = Account.query.filter(Account.gm_level > 1).order_by(Account.gm_level.desc()).all()
|
||||
mods = Account.query.filter(Account.gm_level > 0).order_by(Account.gm_level.desc()).all()
|
||||
online = 0
|
||||
users = []
|
||||
zones = {}
|
||||
twodaysago = time.mktime((datetime.datetime.now() - datetime.timedelta(days=2)).timetuple())
|
||||
chars = CharacterInfo.query.filter(CharacterInfo.last_login >= twodaysago).all()
|
||||
|
||||
chars = CharacterInfo.query.all()
|
||||
for char in chars:
|
||||
last_log = ActivityLog.query.with_entities(
|
||||
ActivityLog.activity, ActivityLog.map_id
|
||||
ActivityLog.activity
|
||||
).filter(
|
||||
ActivityLog.character_id == char.id
|
||||
).order_by(ActivityLog.id.desc()).first()
|
||||
@@ -44,13 +41,8 @@ def about():
|
||||
if last_log:
|
||||
if last_log[0] == 0:
|
||||
online += 1
|
||||
if current_user.gm_level >= 8: users.append([char.name, last_log[1]])
|
||||
if str(last_log[1]) not in zones:
|
||||
zones[str(last_log[1])] = 1
|
||||
else:
|
||||
zones[str(last_log[1])] += 1
|
||||
|
||||
return render_template('main/about.html.j2', mods=mods, online=online, users=users, zones=zones)
|
||||
return render_template('main/about.html.j2', mods=mods, online=online)
|
||||
|
||||
|
||||
@main_blueprint.route('/favicon.ico')
|
||||
|
||||
@@ -4,9 +4,10 @@ from flask_user import UserMixin
|
||||
from wtforms import ValidationError
|
||||
|
||||
import logging
|
||||
from flask_sqlalchemy.query import Query
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.exc import OperationalError, StatementError
|
||||
from sqlalchemy.types import JSON
|
||||
from time import sleep
|
||||
import random
|
||||
import string
|
||||
@@ -14,7 +15,7 @@ import string
|
||||
|
||||
# retrying query to work around python trash collector
|
||||
# killing connections of other gunicorn workers
|
||||
class RetryingQuery(Query):
|
||||
class RetryingQuery(BaseQuery):
|
||||
__retry_count__ = 3
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -139,7 +140,6 @@ class Account(db.Model, UserMixin):
|
||||
unique=True
|
||||
)
|
||||
|
||||
# ND Exclusive
|
||||
email = db.Column(
|
||||
db.Unicode(255),
|
||||
nullable=True,
|
||||
@@ -147,7 +147,6 @@ class Account(db.Model, UserMixin):
|
||||
unique=False
|
||||
)
|
||||
|
||||
# ND Exclusive
|
||||
email_confirmed_at = db.Column(db.DateTime())
|
||||
|
||||
password = db.Column(
|
||||
@@ -168,7 +167,6 @@ class Account(db.Model, UserMixin):
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
# ND Exclusive
|
||||
active = db.Column(
|
||||
mysql.BOOLEAN,
|
||||
nullable=False,
|
||||
@@ -218,7 +216,7 @@ class Account(db.Model, UserMixin):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
# ND Exclusive
|
||||
|
||||
class AccountInvitation(db.Model):
|
||||
__tablename__ = 'account_invites'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -459,34 +457,17 @@ class Leaderboard(db.Model):
|
||||
passive_deletes=True
|
||||
)
|
||||
|
||||
primaryScore = db.Column(
|
||||
time = db.Column(
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
secondaryScore = db.Column(
|
||||
score = db.Column(
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
tertiaryScore = db.Column(
|
||||
mysql.BIGINT(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
numWins = db.Column(
|
||||
mysql.INTEGER(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
timesPlayed = db.Column(
|
||||
mysql.INTEGER(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
@@ -506,7 +487,7 @@ class Mail(db.Model):
|
||||
)
|
||||
|
||||
sender_id = db.Column(
|
||||
mysql.BIGINT,
|
||||
mysql.INTEGER,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
@@ -604,15 +585,12 @@ class PetNames(db.Model):
|
||||
mysql.TEXT,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# ND Exclusive
|
||||
approved = db.Column(
|
||||
mysql.INTEGER(unsigned=True),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
# ND Exclusive
|
||||
owner_id = db.Column(
|
||||
mysql.BIGINT,
|
||||
nullable=True
|
||||
@@ -744,7 +722,7 @@ class Property(db.Model):
|
||||
class UGC(db.Model):
|
||||
__tablename__ = 'ugc'
|
||||
id = db.Column(
|
||||
mysql.BIGINT,
|
||||
mysql.INTEGER,
|
||||
primary_key=True
|
||||
)
|
||||
account_id = db.Column(
|
||||
@@ -824,7 +802,7 @@ class PropertyContent(db.Model):
|
||||
)
|
||||
|
||||
ugc_id = db.Column(
|
||||
db.BIGINT,
|
||||
db.INT,
|
||||
db.ForeignKey(UGC.id, ondelete='CASCADE'),
|
||||
nullable=True
|
||||
)
|
||||
@@ -875,39 +853,6 @@ class PropertyContent(db.Model):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
model_name = db.Column(
|
||||
mysql.TEXT,
|
||||
nullable=False,
|
||||
server_default=''
|
||||
)
|
||||
|
||||
model_description = db.Column(
|
||||
mysql.TEXT,
|
||||
nullable=False,
|
||||
server_default=''
|
||||
)
|
||||
|
||||
behavior_1 = db.Column(
|
||||
mysql.BIGINT(),
|
||||
server_default='0'
|
||||
)
|
||||
behavior_2 = db.Column(
|
||||
mysql.BIGINT(),
|
||||
server_default='0'
|
||||
)
|
||||
behavior_3 = db.Column(
|
||||
mysql.BIGINT(),
|
||||
server_default='0'
|
||||
)
|
||||
behavior_4 = db.Column(
|
||||
mysql.BIGINT(),
|
||||
server_default='0'
|
||||
)
|
||||
behavior_5 = db.Column(
|
||||
mysql.BIGINT(),
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
@@ -1012,7 +957,7 @@ class BugReport(db.Model):
|
||||
)
|
||||
|
||||
reporter_id = db.Column(
|
||||
mysql.BIGINT,
|
||||
mysql.INTEGER(),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
@@ -1073,7 +1018,7 @@ class Reports(db.Model):
|
||||
__tablename__ = 'reports'
|
||||
|
||||
data = db.Column(
|
||||
mysql.MEDIUMBLOB(),
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ play_keys_blueprint = Blueprint('play_keys', __name__)
|
||||
# Key creation page
|
||||
@play_keys_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def index():
|
||||
return render_template('play_keys/index.html.j2')
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/create/<count>/<uses>', methods=['GET'], defaults={'count': 1, 'uses': 1})
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def create(count=1, uses=1):
|
||||
key = PlayKey.create(count=count, uses=uses)
|
||||
log_audit(f"Created {count} Play Key(s) with {uses} uses!")
|
||||
@@ -28,7 +28,7 @@ def create(count=1, uses=1):
|
||||
|
||||
@play_keys_blueprint.route('/create/bulk', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def bulk_create():
|
||||
form = CreatePlayKeyForm()
|
||||
if form.validate_on_submit():
|
||||
@@ -42,7 +42,7 @@ def bulk_create():
|
||||
|
||||
@play_keys_blueprint.route('/delete/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def delete(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
# associated_accounts = Account.query.filter(Account.play_key_id==id).all()
|
||||
@@ -54,7 +54,7 @@ def delete(id):
|
||||
|
||||
@play_keys_blueprint.route('/edit/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def edit(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
form = EditPlayKeyForm()
|
||||
@@ -81,7 +81,7 @@ def edit(id):
|
||||
|
||||
@play_keys_blueprint.route('/view/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def view(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
accounts = Account.query.filter(Account.play_key_id == id).all()
|
||||
@@ -90,7 +90,7 @@ def view(id):
|
||||
|
||||
@play_keys_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(5)
|
||||
@gm_level(9)
|
||||
def get():
|
||||
columns = [
|
||||
ColumnDT(PlayKey.id),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
from flask import (
|
||||
render_template,
|
||||
Blueprint,
|
||||
@@ -14,19 +13,21 @@ from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail
|
||||
from app.cdclient import ComponentsRegistry, ComponentType, RenderComponent
|
||||
from app.schemas import PropertySchema
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import query_cdclient
|
||||
from app.compressed_files import serve_bytes, serve_file
|
||||
from app.cdclient import ZoneTable
|
||||
from app.forms import RejectPropertyForm
|
||||
|
||||
import zlib
|
||||
import json
|
||||
import hashlib
|
||||
import app.pylddlib as ldd
|
||||
import pathlib
|
||||
|
||||
property_blueprint = Blueprint('properties', __name__)
|
||||
|
||||
property_schema = PropertySchema()
|
||||
|
||||
|
||||
@property_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -49,11 +50,9 @@ def approve(id):
|
||||
|
||||
if property_data.mod_approved:
|
||||
message = f"""Approved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
{property_data.name if property_data.name else ZoneTable.query.filter(
|
||||
ZoneTable.zoneID == property_data.zone_id
|
||||
).first().DisplayDescription}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
|
||||
log_audit(message)
|
||||
flash(
|
||||
@@ -62,11 +61,9 @@ def approve(id):
|
||||
)
|
||||
else:
|
||||
message = f"""Unapproved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
{property_data.name if property_data.name else ZoneTable.query.filter(
|
||||
ZoneTable.zoneID == property_data.zone_id
|
||||
).first().DisplayDescription}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
|
||||
log_audit(message)
|
||||
flash(
|
||||
@@ -100,11 +97,9 @@ def reject(id):
|
||||
|
||||
if form.validate_on_submit():
|
||||
char_name = CharacterInfo.query.filter(CharacterInfo.id == property_data.owner_id).first().name
|
||||
zone_name = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]
|
||||
zone_name = ZoneTable.query.filter(
|
||||
ZoneTable.zoneID == property_data.zone_id
|
||||
).first().DisplayDescription
|
||||
property_data.mod_approved = False
|
||||
property_data.rejection_reason = form.rejection_reason.data
|
||||
message = f"""Rejected Property
|
||||
@@ -210,7 +205,7 @@ def get(status="all"):
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
print(data)
|
||||
|
||||
for property_data in data["data"]:
|
||||
id = property_data["0"]
|
||||
|
||||
@@ -251,11 +246,9 @@ def get(status="all"):
|
||||
"""
|
||||
|
||||
if property_data["4"] == "":
|
||||
property_data["4"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["13"]],
|
||||
one=True
|
||||
)
|
||||
property_data["4"] = ZoneTable.query.filter(
|
||||
ZoneTable.zoneID == property_data["13"]
|
||||
).first().DisplayDescription
|
||||
|
||||
if property_data["6"] == 0:
|
||||
property_data["6"] = "Private"
|
||||
@@ -272,11 +265,9 @@ def get(status="all"):
|
||||
else:
|
||||
property_data["7"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
property_data["13"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["13"]],
|
||||
one=True
|
||||
)
|
||||
property_data["13"] = ZoneTable.query.filter(
|
||||
ZoneTable.zoneID == property_data["13"]
|
||||
).first().DisplayDescription
|
||||
|
||||
return data
|
||||
|
||||
@@ -308,30 +299,10 @@ def view_model(id, lod):
|
||||
return render_template(
|
||||
'ldd/ldd.html.j2',
|
||||
content=formatted_data,
|
||||
lod=lod,
|
||||
**viewer_redraw_options()
|
||||
lod=lod
|
||||
)
|
||||
|
||||
|
||||
def viewer_redraw_options():
|
||||
"""Redraw-while-loading settings for the model viewer template.
|
||||
|
||||
?redraw=none|interval and ?redraw_ms=N override the config for testing.
|
||||
"""
|
||||
config = current_app.config
|
||||
mode = request.args.get(
|
||||
"redraw", config.get("PROPERTY_VIEWER_LOAD_REDRAW", "none")
|
||||
)
|
||||
if mode not in ("none", "interval"):
|
||||
mode = "none"
|
||||
interval = config.get("PROPERTY_VIEWER_REDRAW_INTERVAL_MS", 500)
|
||||
try:
|
||||
interval = max(int(request.args.get("redraw_ms", interval)), 16)
|
||||
except ValueError:
|
||||
pass
|
||||
return {"load_redraw": mode, "redraw_interval_ms": interval}
|
||||
|
||||
|
||||
property_center = {
|
||||
1150: "(-17, 432, -60)",
|
||||
1151: "(0, 455, -110)",
|
||||
@@ -391,9 +362,7 @@ def view_models(id, lod):
|
||||
property_data=property_data,
|
||||
content=consolidated_list,
|
||||
center=property_center[property_data.zone_id],
|
||||
lod=lod,
|
||||
ugc_batch_url=url_for('properties.get_ugc_models', id=id),
|
||||
**viewer_redraw_options()
|
||||
lod=lod
|
||||
)
|
||||
|
||||
|
||||
@@ -403,53 +372,13 @@ def get_model(id, file_format, lod):
|
||||
content = PropertyContent.query.filter(PropertyContent.id == id).first()
|
||||
if not(0 <= int(lod) <= 2):
|
||||
abort(404)
|
||||
# Large model files: let the browser keep a copy but always revalidate it
|
||||
# (cheap 304 via the ETag) instead of storing long-lived entries.
|
||||
cache_control = "private, no-cache"
|
||||
if content.lot == 14: # ugc model
|
||||
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
|
||||
# decompress lazily so a matching If-None-Match skips the work
|
||||
return serve_bytes(
|
||||
lambda: decompress(ugc_data.lxfml), 'text/xml', cache_control,
|
||||
etag_seed=ugc_data.lxfml
|
||||
)
|
||||
response = ugc(content)[0]
|
||||
else: # prebuilt model
|
||||
path = prebuilt_path(content, file_format, lod)
|
||||
if not isinstance(path, pathlib.Path):
|
||||
return path
|
||||
return serve_file(path, 'text/xml', cache_control)
|
||||
response = prebuilt(content, file_format, lod)[0]
|
||||
|
||||
|
||||
@property_blueprint.route('/get_ugc_models/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_ugc_models(id):
|
||||
"""All UGC (brick-built) LXFML of a property in one JSON response.
|
||||
|
||||
Returns ``{"<property content id>": "<lxfml>", ...}`` so the viewer does
|
||||
not need one request per model (a property can have hundreds).
|
||||
"""
|
||||
rows = db.session.query(PropertyContent.id, UGC.lxfml).join(
|
||||
UGC, UGC.id == PropertyContent.ugc_id
|
||||
).filter(
|
||||
PropertyContent.property_id == id, PropertyContent.lot == 14
|
||||
).order_by(PropertyContent.id).all()
|
||||
|
||||
# ETag over the whole set: content ids plus the stored (compressed) blobs
|
||||
seed = hashlib.sha1()
|
||||
for content_id, lxfml in rows:
|
||||
seed.update(f"{content_id}:{len(lxfml)}:".encode())
|
||||
seed.update(lxfml)
|
||||
|
||||
def body():
|
||||
return json.dumps({
|
||||
str(content_id): decompress(lxfml).decode("utf-8", "replace")
|
||||
for content_id, lxfml in rows
|
||||
})
|
||||
|
||||
return serve_bytes(
|
||||
body, 'application/json', "private, no-cache",
|
||||
etag_seed=seed.digest()
|
||||
)
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
return response
|
||||
|
||||
|
||||
@property_blueprint.route('/download_model/<id>', methods=['GET'])
|
||||
@@ -460,7 +389,7 @@ def download_model(id):
|
||||
if content.lot == 14: # ugc model
|
||||
response, filename = ugc(content)
|
||||
else: # prebuilt model
|
||||
response, filename = prebuilt(content, "lxfml", 0)
|
||||
response, filename = prebuilt(content, "lxfml")
|
||||
|
||||
response.headers.set('Content-Type', 'attachment/xml')
|
||||
response.headers.set(
|
||||
@@ -473,78 +402,49 @@ def download_model(id):
|
||||
|
||||
def ugc(content):
|
||||
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
|
||||
uncompressed_lxfml = decompress(ugc_data.lxfml)
|
||||
uncompressed_lxfml = zlib.decompress(ugc_data.lxfml)
|
||||
response = make_response(uncompressed_lxfml)
|
||||
return response, ugc_data.filename
|
||||
|
||||
def decompress(data):
|
||||
assert data[:5] == b"sd0\x01\xff"
|
||||
pos = 5
|
||||
out = b""
|
||||
while pos < len(data):
|
||||
length = int.from_bytes(data[pos:pos+4], "little")
|
||||
pos += 4
|
||||
out += zlib.decompress(data[pos:pos+length])
|
||||
pos += length
|
||||
return out
|
||||
|
||||
def render_asset_basename(render_asset):
|
||||
"""Return the lowercase file name without extension of a client asset path.
|
||||
|
||||
render_asset paths in the cdclient mix separators: single backslashes
|
||||
(mesh\\ftcommunityitems\\x.nif), escaped double backslashes, forward
|
||||
slashes, or combinations of those, and mixed case.
|
||||
"""
|
||||
name = re.split(r"[\\/]+", render_asset.strip())[-1]
|
||||
return name.split(".")[0].lower()
|
||||
|
||||
|
||||
def prebuilt_path(content, file_format, lod):
|
||||
def prebuilt(content, file_format, lod):
|
||||
# translate LOT to component id
|
||||
# we need to get a type of 2 for the render component to find the filename
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[content.lot],
|
||||
one=True
|
||||
)[0]
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient(
|
||||
'select render_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
# we need to get a type of 2 because reasons
|
||||
filename = RenderComponent.query.filter(
|
||||
RenderComponent.id == ComponentsRegistry.query.filter(
|
||||
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_RENDER
|
||||
).filter(ComponentsRegistry.id == id).first().component_id
|
||||
).first().render_asset
|
||||
|
||||
# if we have a valie filename, coerce it
|
||||
if filename and filename[0]:
|
||||
filename = render_asset_basename(filename[0])
|
||||
if not filename:
|
||||
abort(404)
|
||||
if filename:
|
||||
filename = filename[0].split("\\\\")[-1].lower().split(".")[0]
|
||||
if "/" in filename:
|
||||
filename = filename.split("/")[-1].lower()
|
||||
else:
|
||||
return f"No filename for LOT {content.lot}"
|
||||
|
||||
# if we just want the lxfml, fine t and return it
|
||||
lxfml = pathlib.Path(f'{current_app.config["CLIENT_LOCATION"]}res/BrickModels/{filename.split(".")[0]}.lxfml')
|
||||
lxfml = pathlib.Path(f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml')
|
||||
if file_format == "lxfml":
|
||||
return lxfml
|
||||
# else we handle getting the files for lddviewer
|
||||
|
||||
with open(lxfml, 'r') as file:
|
||||
lxfml_data = file.read()
|
||||
response = make_response(lxfml_data)
|
||||
|
||||
elif file_format in ["obj", "mtl"]:
|
||||
# check to see if the file exists
|
||||
cache = pathlib.Path(f'{current_app.config["CACHE_LOCATION"]}BrickModels/{filename}.lod{lod}.{file_format}')
|
||||
cache = pathlib.Path(f'app/cache/BrickModels/{filename}.lod{lod}.{file_format}')
|
||||
if not cache.is_file():
|
||||
# if not make it an store it for later
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
ldd.main(str(lxfml.as_posix()), str(cache.with_suffix("").as_posix()), lod) # convert to OBJ
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"ERROR on {cache}:\n {e}")
|
||||
return cache
|
||||
|
||||
with open(str(cache.as_posix()), 'r') as file:
|
||||
cache_data = file.read()
|
||||
|
||||
response = make_response(cache_data)
|
||||
|
||||
else:
|
||||
raise(Exception("INVALID FILE FORMAT"))
|
||||
|
||||
|
||||
def prebuilt(content, file_format, lod):
|
||||
path = prebuilt_path(content, file_format, lod)
|
||||
if not isinstance(path, pathlib.Path):
|
||||
return path
|
||||
with open(path, 'r') as file:
|
||||
response = make_response(file.read())
|
||||
|
||||
return response, path.name
|
||||
return response, f"{filename}.{file_format}"
|
||||
|
||||
@@ -7,7 +7,6 @@ import math
|
||||
import struct
|
||||
import zipfile
|
||||
from xml.dom import minidom
|
||||
from flask import current_app
|
||||
|
||||
PRIMITIVEPATH = '/Primitives/'
|
||||
GEOMETRIEPATH = PRIMITIVEPATH
|
||||
@@ -969,8 +968,8 @@ def main(lxf_filename, obj_filename, lod="2"):
|
||||
GEOMETRIEPATH = GEOMETRIEPATH + f"LOD{lod}/"
|
||||
converter = Converter()
|
||||
# print("Found DB folder. Will use this instead of db.lif!")
|
||||
setDBFolderVars(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}", lod=lod)
|
||||
converter.LoadDBFolder(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}")
|
||||
setDBFolderVars(dbfolderlocation="app/luclient/res/", lod=lod)
|
||||
converter.LoadDBFolder(dbfolderlocation="app/luclient/res/")
|
||||
converter.LoadScene(filename=lxf_filename)
|
||||
converter.Export(filename=obj_filename)
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ from app.models import CharacterInfo, Account, CharacterXML, Reports
|
||||
from app.luclient import get_lot_name
|
||||
from app import gm_level, scheduler
|
||||
from sqlalchemy.orm import load_only
|
||||
import xmltodict, gzip, json, datetime
|
||||
from collections import OrderedDict
|
||||
import xml.etree.ElementTree as ET
|
||||
import datetime
|
||||
import xmltodict
|
||||
|
||||
reports_blueprint = Blueprint('reports', __name__)
|
||||
|
||||
@@ -45,8 +44,6 @@ def index():
|
||||
@gm_level(3)
|
||||
def items_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/items/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@@ -65,8 +62,6 @@ def items_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for key in entry.data:
|
||||
items[key] = get_lot_name(key)
|
||||
@@ -109,8 +104,6 @@ def items_graph(start, end):
|
||||
@gm_level(3)
|
||||
def currency_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/currency/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@@ -128,8 +121,6 @@ def currency_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
@@ -139,7 +130,7 @@ def currency_graph(start, end):
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + character.name.encode("utf-8").hex()[1:7]
|
||||
if data and max(data) > 10000:
|
||||
if max(data) > 10000:
|
||||
datasets.append({
|
||||
"label": character.name,
|
||||
"data": data,
|
||||
@@ -164,8 +155,6 @@ def currency_graph(start, end):
|
||||
@gm_level(3)
|
||||
def uscore_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@@ -183,8 +172,6 @@ def uscore_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
@@ -194,7 +181,7 @@ def uscore_graph(start, end):
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + character.name.encode("utf-8").hex()[1:7]
|
||||
if data and max(data) > 1000:
|
||||
if max(data) > 1000:
|
||||
datasets.append({
|
||||
"label": character.name,
|
||||
"data": data,
|
||||
@@ -241,27 +228,27 @@ def gen_item_report():
|
||||
for char_xml in char_xmls:
|
||||
name = CharacterInfo.query.filter(CharacterInfo.id == char_xml.id).first().name
|
||||
try:
|
||||
xml_data = ET.fromstring(char_xml.xml_data)
|
||||
inventories = xml_data.findall(".//inv/items/in")
|
||||
for inv in inventories:
|
||||
if inv.attrib["t"] in ["0", "1"]:
|
||||
for item in inv.findall("i"):
|
||||
item_attr_l = item.attrib["l"]
|
||||
item_attr_c = int(item.attrib["c"]) if "c" in item.attrib else 1
|
||||
if item_attr_l in report_data:
|
||||
report_data[item_attr_l]["item_count"] = report_data[item_attr_l]["item_count"] + item_attr_c
|
||||
character_json = xmltodict.parse(
|
||||
char_xml.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
for inv in character_json["obj"]["inv"]["items"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"]) == 0 or int(inv["attr_t"]) == 1):
|
||||
for item in inv["i"]:
|
||||
if item["attr_l"] in report_data:
|
||||
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
|
||||
else:
|
||||
report_data[item_attr_l] = {"item_count": item_attr_c, "chars": {}}
|
||||
if name in report_data[item_attr_l]["chars"]:
|
||||
report_data[item_attr_l]["chars"][name] = report_data[item_attr_l]["chars"][name] + item_attr_c
|
||||
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
|
||||
if name in report_data[item["attr_l"]]["chars"]:
|
||||
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + int(item["attr_c"])
|
||||
else:
|
||||
report_data[item_attr_l]["chars"][name] = item_attr_c
|
||||
report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::ITEMS - ERROR PARSING CHARACTER {char_xml.id}")
|
||||
current_app.logger.error(f"REPORT::ITEMS - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
data=report_data,
|
||||
report_type="items",
|
||||
date=date
|
||||
)
|
||||
@@ -299,15 +286,17 @@ def gen_currency_report():
|
||||
|
||||
for character in characters:
|
||||
try:
|
||||
xml_data = ET.fromstring(character.xml_data)
|
||||
char = xml_data.find(".//char")
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(char.attrib.get("cc"))
|
||||
character_json = xmltodict.parse(
|
||||
character.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_cc"])
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::CURRENCY - ERROR PARSING CHARACTER {character.id}")
|
||||
current_app.logger.error(f"REPORT::CURRENCY - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
data=report_data,
|
||||
report_type="currency",
|
||||
date=date
|
||||
)
|
||||
@@ -345,15 +334,17 @@ def gen_uscore_report():
|
||||
|
||||
for character in characters:
|
||||
try:
|
||||
xml_data = ET.fromstring(character.xml_data)
|
||||
char = xml_data.find(".//char")
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(char.attrib.get("ls"))
|
||||
character_json = xmltodict.parse(
|
||||
character.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_ls"])
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::U-SCORE - ERROR PARSING CHARACTER {character.id}")
|
||||
current_app.logger.error(f"REPORT::U-SCORE - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
data=report_data,
|
||||
report_type="uscore",
|
||||
date=date
|
||||
)
|
||||
|
||||
130
app/schemas.py
Normal file
130
app/schemas.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from flask_marshmallow import Marshmallow
|
||||
from app.models import (
|
||||
PlayKey,
|
||||
PetNames,
|
||||
Mail,
|
||||
UGC,
|
||||
PropertyContent,
|
||||
Property,
|
||||
CharacterXML,
|
||||
CharacterInfo,
|
||||
Account,
|
||||
AccountInvitation,
|
||||
ActivityLog,
|
||||
CommandLog
|
||||
)
|
||||
ma = Marshmallow()
|
||||
|
||||
|
||||
class PlayKeySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PlayKey
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
|
||||
class PetNamesSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PetNames
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class MailSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Mail
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class UGCSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = UGC
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PropertyContent
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
ugc = ma.Nested(UGCSchema)
|
||||
|
||||
|
||||
class PropertySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Property
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
properties_contents = ma.Nested(PropertyContentSchema, many=True)
|
||||
|
||||
|
||||
class CharacterXMLSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterXML
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class CharacterInfoSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterInfo
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
charxml = ma.Nested(CharacterXMLSchema)
|
||||
properties_owner = ma.Nested(PropertySchema, many=True)
|
||||
pets = ma.Nested(PetNamesSchema, many=True)
|
||||
mail = ma.Nested(MailSchema, many=True)
|
||||
|
||||
|
||||
class AccountSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Account
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
play_key = ma.Nested(PlayKeySchema)
|
||||
charinfo = ma.Nested(CharacterInfoSchema, many=True)
|
||||
|
||||
|
||||
class AccountInvitationSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = AccountInvitation
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
invite_by_user = ma.Nested(AccountSchema)
|
||||
|
||||
|
||||
class ActivityLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = ActivityLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
||||
|
||||
|
||||
class CommandLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = CommandLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
||||
@@ -7,15 +7,14 @@ APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
|
||||
APP_SECRET_KEY = ""
|
||||
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
|
||||
|
||||
CLIENT_LOCATION = 'app/luclient/'
|
||||
CD_SQLITE_LOCATION = 'app/luclient/res/'
|
||||
CACHE_LOCATION = 'app/cache/'
|
||||
|
||||
CONFIG_LINK = False
|
||||
CONFIG_LINK_TITLE = ""
|
||||
CONFIG_LINK_HREF = ""
|
||||
CONFIG_LINK_TEXT = ""
|
||||
|
||||
# Send Analytics for Developers to better fix issues
|
||||
ALLOW_ANALYTICS = False
|
||||
|
||||
# Flask settings
|
||||
CSRF_ENABLED = True
|
||||
|
||||
@@ -58,36 +57,3 @@ USER_PASSLIB_CRYPTCONTEXT_SCHEMES = ['bcrypt'] # bcrypt for password hashing
|
||||
# Flask-User routing settings
|
||||
USER_AFTER_LOGIN_ENDPOINT = "main.index"
|
||||
USER_AFTER_LOGOUT_ENDPOINT = "main.index"
|
||||
|
||||
# Option will be removed once this feature is full implemeted
|
||||
ENABLE_CHAR_XML_UPLOAD = False
|
||||
|
||||
# Recaptcha settings
|
||||
# See: https://flask-wtf.readthedocs.io/en/1.2.x/form/#recaptcha
|
||||
RECAPTCHA_ENABLE = False
|
||||
RECAPTCHA_PUBLIC_KEY = ''
|
||||
RECAPTCHA_PRIVATE_KEY = ''
|
||||
# Optional
|
||||
# RECAPTCHA_API_SERVER = ''
|
||||
# RECAPTCHA_PARAMETERS = ''
|
||||
RECAPTCHA_DATA_ATTRS = {'theme': 'white', 'size': 'invisible'}
|
||||
|
||||
# Property model viewer response compression
|
||||
# Gzip model (OBJ/MTL/LXFML) and LDD brick database responses for clients that
|
||||
# accept it. Set to False to always send uncompressed files.
|
||||
COMPRESS_MODEL_RESPONSES = True
|
||||
# When True, a gzip copy is written to disk next to each cached file the first
|
||||
# time it is served (regenerated if the source file is newer) and served
|
||||
# directly: uses more disk space, but no CPU per request.
|
||||
# When False, responses are compressed in memory on every request at a fast
|
||||
# compression level and nothing extra is stored.
|
||||
CACHE_COMPRESSED_MODELS = False
|
||||
|
||||
# Property model viewer redraw while models are loading
|
||||
# 'none': do not redraw as individual models finish loading; draw once when
|
||||
# all loads have completed (or failed), then only on camera move/resize.
|
||||
# 'interval': redraw at most every PROPERTY_VIEWER_REDRAW_INTERVAL_MS while
|
||||
# loading so models appear progressively, then a final draw.
|
||||
# Can be overridden per page for testing with ?redraw=none|interval&redraw_ms=N
|
||||
PROPERTY_VIEWER_LOAD_REDRAW = 'none'
|
||||
PROPERTY_VIEWER_REDRAW_INTERVAL_MS = 500
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
{% if config.USER_ENABLE_EMAIL %}
|
||||
<th>Email</th>
|
||||
{% endif %}
|
||||
<th>GM Level</th>
|
||||
<th>Locked</th>
|
||||
<th>Banned</th>
|
||||
<th>Muted</th>
|
||||
<th>Registered</th>
|
||||
<th>Email Confirmed</th>
|
||||
{% if config.USER_ENABLE_EMAIL %}
|
||||
<th>Email Confirmed</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
188
app/templates/admin/dashboard.html.j2
Normal file
188
app/templates/admin/dashboard.html.j2
Normal file
@@ -0,0 +1,188 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
{% block title %}Key Creation{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
</ul>
|
||||
<ul class="nav navbar-nav" style="float: right">
|
||||
<li class="active"><a href="#">Welcome {{ current_user.username }}!</a></li>
|
||||
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock navbar %}}
|
||||
|
||||
{% block content %}
|
||||
{# LOGO #}
|
||||
<div class="container" style="margin-top: 50px">
|
||||
|
||||
{# Display logo #}
|
||||
<div style="margin-bottom: 50px">
|
||||
<img
|
||||
src="{{ url_for('static', filename=resources.LOGO) }}"
|
||||
class="center-block img-responsive mx-auto d-block"
|
||||
alt="Logo"
|
||||
width="100"
|
||||
height="100"
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# Key creation #}
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h3>Key Creation</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
|
||||
{# If the error value is set, display the error in red text #}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# If the message value is set, display the message in green text #}
|
||||
{% if message %}
|
||||
<div class="alert alert-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Form which takes in Admin Username, Admin Password, and the amount of keys to create. #}
|
||||
<form action="{{ url_for('dashboard') }}" method="post">
|
||||
{# Key count input #}
|
||||
<div class="form-group">
|
||||
<label for="key_count">Generate keys</label>
|
||||
<input type="number" class="form-control" name="key_count" placeholder="Enter number of keys...">
|
||||
<small class="form-text text-muted">Number of keys to create.</small>
|
||||
</div>
|
||||
|
||||
{# Submit button #}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Generate Keys</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# If the keys value is set, create a list for each key in keys #}
|
||||
{% if keys %}
|
||||
<div class="alert alert-success">
|
||||
<ul>
|
||||
{% for key in keys %}
|
||||
<li>{{ key }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Activity graphs #}
|
||||
<div class="container">
|
||||
|
||||
<div class="text-center">
|
||||
<h3>Activity</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
|
||||
<canvas id="sessions_graph" width="400" height="400"></canvas>
|
||||
<canvas id="play_time_graph" width="400" height="400"></canvas>
|
||||
<canvas id="zone_play_time_graph" width="400" height="400"></canvas>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Make a get request to the server to load the activity data
|
||||
$.get("{{ url_for('load_activities') }}", function(data) {
|
||||
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "sessions"
|
||||
$.get("{{ url_for('activity_data', name='sessions') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('sessions_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "play_time"
|
||||
$.get("{{ url_for('activity_data', name='play_time') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('play_time_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "zone_play_time"
|
||||
$.get("{{ url_for('activity_data', name='zone_play_time') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('zone_play_time_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}}
|
||||
@@ -90,29 +90,30 @@
|
||||
let target_nav = '#{{request.endpoint}}'.replace('\.', '-');
|
||||
$(target_nav).addClass('active');
|
||||
});
|
||||
// make tooltips with data work
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
function setInnerHTML(elm, html) {
|
||||
elm.innerHTML = html;
|
||||
$("body").tooltip({ selector: '[data-toggle=tooltip]' });
|
||||
Array.from(elm.querySelectorAll("script"))
|
||||
.forEach( oldScriptEl => {
|
||||
const newScriptEl = document.createElement("script");
|
||||
|
||||
Array.from(oldScriptEl.attributes).forEach( attr => {
|
||||
newScriptEl.setAttribute(attr.name, attr.value)
|
||||
});
|
||||
|
||||
const scriptText = document.createTextNode(oldScriptEl.innerHTML);
|
||||
newScriptEl.appendChild(scriptText);
|
||||
|
||||
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
|
||||
|
||||
});
|
||||
}
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
// Matomo JS analytics
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.aronwk.com/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<!-- Matomo no js analytics -->
|
||||
<noscript><p><img src="https://matomo.aronwk.com/matomo.php?idsite=3&rec=1" style="border:0;" alt="" /></p></noscript>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Character XML Upload
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Character XML Upload
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.blink {
|
||||
animation: blinker .5s linear infinite;
|
||||
color: red;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<h3 class="text-center blink">PROCEED WITH CAUTION</h3>
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" >
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.char_xml) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -15,9 +15,8 @@
|
||||
{% include 'partials/_character.html.j2' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-sm" id="charxml">
|
||||
Loading Character Data
|
||||
{% include 'partials/_loading.html' %}
|
||||
<div class="col-sm">
|
||||
{% include 'partials/_charxml.html.j2'%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -33,14 +32,3 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content_after %}
|
||||
|
||||
{% block js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
fetch({{ url_for("characters.chardata", id=character_data.id)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("charxml"), text);
|
||||
})
|
||||
</script>
|
||||
{% endblock js %}
|
||||
|
||||
@@ -60,12 +60,7 @@
|
||||
|
||||
{# Remember me #}
|
||||
{% if user_manager.USER_ENABLE_REMEMBER_ME %}
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ render_field(form.recaptcha, tabindex=250) }}
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ form.recaptcha }}
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
{{ render_submit_field(login_form.submit, tabindex=180) }}
|
||||
</form>
|
||||
@@ -68,11 +63,6 @@
|
||||
{{ render_field(register_form.retype_password, tabindex=240) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ register_form.recaptcha }}
|
||||
{% endif %}
|
||||
|
||||
{{ render_submit_field(register_form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -47,11 +47,6 @@
|
||||
{{ render_field(form.retype_password, tabindex=240) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ render_field(form.recaptcha, tabindex=250) }}
|
||||
{% endif %}
|
||||
|
||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<a id='property-index' class='nav-link' href='{{ url_for('properties.index') }}'>Properties</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 5 and config.REQUIRE_PLAY_KEY %}
|
||||
{% if current_user.is_authenticated and current_user.gm_level == 9 and config.REQUIRE_PLAY_KEY %}
|
||||
{# Play Keys #}
|
||||
<a id='play_keys-index' class='nav-link' href='{{ url_for('play_keys.index') }}'>Play Keys</a>
|
||||
{% endif %}
|
||||
@@ -71,8 +71,8 @@
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 8 %}
|
||||
<hr/>
|
||||
<h3 class="text-center">Logs</h3>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.command') }}'>Command Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.activity') }}'>Activity Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.activity') }}'>Command Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.command') }}'>Activity Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.audit') }}'>Audit Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.system') }}'>System Log</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -35,6 +35,29 @@
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='lddviewer/base64-binary.js') }}"></script>
|
||||
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<script>
|
||||
// Matomo JS analytics
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.aronwk.com/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<!-- Matomo no js analytics -->
|
||||
<noscript><p><img src="https://matomo.aronwk.com/matomo.php?idsite=3&rec=1" style="border:0;" alt="" /></p></noscript>
|
||||
{% endif %}
|
||||
|
||||
<script type='module'>
|
||||
import {MTLLoader} from 'https://cdn.jsdelivr.net/npm/three@0.116.0/examples/jsm/loaders/MTLLoader.js'
|
||||
import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/three@0.116.0/examples/jsm/loaders/OBJLoader.js'
|
||||
@@ -279,11 +302,7 @@
|
||||
|
||||
AddScene(file){
|
||||
let lxfmlfile = new DBURLFile(file,file)
|
||||
this.AddSceneData(lxfmlfile.read())
|
||||
}
|
||||
|
||||
AddSceneData(xmldata){
|
||||
this.xmldata = xmldata
|
||||
this.xmldata = lxfmlfile.read()
|
||||
|
||||
if (this.xmldata === ""){
|
||||
alert(this.xmldata)
|
||||
@@ -525,8 +544,6 @@
|
||||
|
||||
constructor(){
|
||||
this.allMaterials = ''
|
||||
// brick geometry is shared by every UGC model converted by this converter
|
||||
this.geometryCache = {}
|
||||
this.scene = new Scene()
|
||||
}
|
||||
|
||||
@@ -539,31 +556,19 @@
|
||||
|
||||
LoadScene(filename){
|
||||
if(this.database.initok){
|
||||
// Each LXFML file is its own model with its own placement,
|
||||
// so start from a fresh scene instead of accumulating bricks
|
||||
this.scene = new Scene()
|
||||
this.scene.AddScene(filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the meshes for the currently loaded LXFML scene.
|
||||
// The brick transforms in a UGC LXFML are relative to the model's own origin,
|
||||
// so the meshes are collected into a group which is then placed in the world
|
||||
// using the position/rotation stored for the model (properties_contents).
|
||||
Export(filename, positions = []){
|
||||
let modelGroup = new THREE.Group()
|
||||
// reuse the Materials.xml parsed once in LoadDBURL
|
||||
if (!this.allMaterials) {
|
||||
this.allMaterials = new Materials(this.database.filelist['Materials.xml'].read())
|
||||
}
|
||||
let allMaterials = this.allMaterials
|
||||
Export(filename){
|
||||
let allMaterials = new Materials(this.database.filelist['Materials.xml'].read())
|
||||
let invert = new Matrix3D()
|
||||
//invert.n33 = -1 //uncomment to invert the Z-Axis
|
||||
|
||||
let indexOffset = 1
|
||||
let textOffset = 1
|
||||
let usedmaterials = []
|
||||
let geometriecache = this.geometryCache
|
||||
let geometriecache = {}
|
||||
|
||||
let total = this.scene.Bricks.length
|
||||
let current = 0
|
||||
@@ -735,26 +740,15 @@
|
||||
let mesh = new THREE.Mesh(geometry, materials);
|
||||
mesh.matrixAutoUpdate = false
|
||||
mesh.matrix = m
|
||||
modelGroup.add(mesh);
|
||||
{% if not center %}
|
||||
brick_pos.setFromMatrixPosition( m );
|
||||
{% endif %}
|
||||
scene.add(mesh);
|
||||
// let vnh = new VertexNormalsHelper( mesh, 5 );
|
||||
// scene.add( vnh );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positions.length === 0) {
|
||||
positions = [{x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0, rw: 1}]
|
||||
}
|
||||
for (const [j, pos] of positions.entries()) {
|
||||
let model = (j === 0) ? modelGroup : modelGroup.clone()
|
||||
model.position.set(pos.x, pos.y, pos.z)
|
||||
model.quaternion.set(pos.rx, pos.ry, pos.rz, pos.rw)
|
||||
model.updateMatrixWorld(true)
|
||||
scene.add(model)
|
||||
{% if not center %}
|
||||
new THREE.Box3().setFromObject(model).getCenter(brick_pos)
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,25 +959,7 @@
|
||||
this.name = name
|
||||
}
|
||||
|
||||
// Fetch the file asynchronously (once) and keep it for read().
|
||||
load() {
|
||||
if (!this.loading) {
|
||||
this.loading = fetch(this.urlHandle).then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status + ' for ' + response.url)
|
||||
return response.arrayBuffer()
|
||||
}).then((buffer) => {
|
||||
this.content = bytesToUserDefinedString(buffer)
|
||||
}).catch((error) => {
|
||||
console.log('Error loading', this.urlHandle, error)
|
||||
this.content = ''
|
||||
})
|
||||
}
|
||||
return this.loading
|
||||
}
|
||||
|
||||
read() {
|
||||
// Prefetched by load(): no request needed
|
||||
if (this.content !== undefined) return this.content
|
||||
let fileContent
|
||||
let self = this;
|
||||
let xhr = new XMLHttpRequest();
|
||||
@@ -1015,72 +991,6 @@
|
||||
}
|
||||
|
||||
|
||||
// Same string the synchronous XHR with the 'x-user-defined' charset hack
|
||||
// produced: ASCII bytes as-is, bytes >= 0x80 mapped to U+F780..U+F7FF.
|
||||
function bytesToUserDefinedString(buffer) {
|
||||
const bytes = new Uint8Array(buffer)
|
||||
const chunk = 8192
|
||||
let out = ''
|
||||
for (let i = 0; i < bytes.length; i += chunk) {
|
||||
const part = bytes.subarray(i, i + chunk)
|
||||
const codes = new Array(part.length)
|
||||
for (let j = 0; j < part.length; j++) {
|
||||
const b = part[j]
|
||||
codes[j] = b < 0x80 ? b : 0xF700 + b
|
||||
}
|
||||
out += String.fromCharCode.apply(null, codes)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Run fn over items with at most `limit` in flight at a time.
|
||||
async function runLimited(items, limit, fn) {
|
||||
let next = 0
|
||||
async function worker() {
|
||||
while (next < items.length) {
|
||||
const item = items[next++]
|
||||
await fn(item)
|
||||
}
|
||||
}
|
||||
const workers = []
|
||||
for (let i = 0; i < Math.min(limit, items.length); i++) workers.push(worker())
|
||||
await Promise.all(workers)
|
||||
}
|
||||
|
||||
const FETCH_CONCURRENCY = 8
|
||||
|
||||
// Asynchronous version of DBURLReader: lists the brick database and
|
||||
// prefetches info.xml / Materials.xml without blocking the main thread.
|
||||
async function loadDBURLReader(dburl) {
|
||||
const reader = Object.create(DBURLReader.prototype)
|
||||
reader.filelist = {}
|
||||
reader.initok = false
|
||||
reader.location = dburl
|
||||
reader.dbinfo = ''
|
||||
async function parse(url) {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
console.log('HTTP error', response.status, response.statusText)
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
const subdirs = []
|
||||
for (const obj of data) {
|
||||
if (obj.type == 'directory') subdirs.push(url + obj.name + '/')
|
||||
else if (obj.type == 'file') reader.filelist[obj.name] = new DBURLFile(url + obj.name, obj.name)
|
||||
else console.log('Strange object parsed: ' + obj.type)
|
||||
}
|
||||
await Promise.all(subdirs.map(parse))
|
||||
}
|
||||
await parse(dburl)
|
||||
if (reader.fileexist('Materials.xml') && reader.fileexist('info.xml')) {
|
||||
await Promise.all([reader.filelist['Materials.xml'].load(), reader.filelist['info.xml'].load()])
|
||||
reader.dbinfo = new DBinfo(reader.filelist['info.xml'].read())
|
||||
reader.initok = true
|
||||
}
|
||||
return reader
|
||||
}
|
||||
|
||||
class DBURLReader {
|
||||
constructor(dburl) {
|
||||
this.filelist = {};
|
||||
@@ -1149,127 +1059,25 @@
|
||||
let lxfml_file_list = [
|
||||
{% for model in content %}
|
||||
{% if model.lot == 14 %}
|
||||
{
|
||||
"id": {{ model.id|string|tojson }},
|
||||
"url": "{{url_for('properties.get_model', id=model.id, file_format='lxfml', lod=lod)}}",
|
||||
"pos": {{ model.pos|tojson }}
|
||||
},
|
||||
"{{url_for('properties.get_model', id=model.id, file_format='lxfml', lod=lod)}}"{{ ", " if not loop.last else "" }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
// All UGC LXFML of a property in one request (view_models); otherwise per model
|
||||
const ugcBatchUrl = {{ ugc_batch_url|default(none)|tojson }}
|
||||
if (lxfml_file_list.length > 0) {
|
||||
let ldddburl = FindDBURL()
|
||||
|
||||
// Load the brick-built (UGC) models without blocking the page: fetch the
|
||||
// LXFML (batched), then each needed brick primitive once with bounded
|
||||
// concurrency, then build the meshes in small slices so the progress badge
|
||||
// keeps updating.
|
||||
async function loadUGCModels() {
|
||||
if (lxfml_file_list.length === 0) return
|
||||
let built = 0
|
||||
try {
|
||||
setProgressDetail('brick database')
|
||||
const database = await loadDBURLReader('/luclient/ldddb/')
|
||||
if (!database.initok) {
|
||||
alert("LDD database not available. Please look for LEGO-Digital-Designer database.")
|
||||
throw new Error('LDD database not available')
|
||||
if (ldddburl) {
|
||||
let converter = new Converter()
|
||||
converter.LoadDBURL(ldddburl)
|
||||
for (let i = 0; i < lxfml_file_list.length; i++) {
|
||||
converter.LoadScene(lxfml_file_list[i])
|
||||
}
|
||||
const converter = new Converter()
|
||||
converter.database = database
|
||||
converter.allMaterials = new Materials(database.filelist['Materials.xml'].read())
|
||||
converter.Export('test.webgl')
|
||||
|
||||
setProgressDetail('model files')
|
||||
let texts
|
||||
if (ugcBatchUrl) {
|
||||
const response = await fetch(ugcBatchUrl)
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status + ' for ' + ugcBatchUrl)
|
||||
const data = await response.json()
|
||||
texts = lxfml_file_list.map(function (m) { return data[m.id] })
|
||||
} else {
|
||||
texts = new Array(lxfml_file_list.length)
|
||||
await runLimited(lxfml_file_list.map(function (m, i) { return i }), FETCH_CONCURRENCY, async function (i) {
|
||||
try {
|
||||
const response = await fetch(lxfml_file_list[i].url)
|
||||
if (response.ok) texts[i] = bytesToUserDefinedString(await response.arrayBuffer())
|
||||
} catch (error) { console.log(error) }
|
||||
})
|
||||
}
|
||||
|
||||
let lastYield = performance.now()
|
||||
async function maybeYield() {
|
||||
if (performance.now() - lastYield > 30) {
|
||||
await new Promise(function (resolve) { setTimeout(resolve) })
|
||||
lastYield = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
// parse the scenes and collect the bricks they use
|
||||
const scenes = []
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
let parsed = null
|
||||
if (texts[i]) {
|
||||
try { parsed = new Scene(); parsed.AddSceneData(texts[i]) } catch (error) { console.log(error); parsed = null }
|
||||
}
|
||||
scenes.push(parsed)
|
||||
await maybeYield()
|
||||
}
|
||||
const files = new Set()
|
||||
const lod = {{ lod }}
|
||||
for (const parsed of scenes) {
|
||||
if (!parsed) continue
|
||||
for (const brick of parsed.Bricks) {
|
||||
for (const part of brick.Parts) {
|
||||
let n = 0
|
||||
let location = `brickprimitives/lod${lod}/${part.designID}.g`
|
||||
while (location in database.filelist) {
|
||||
files.add(database.filelist[location])
|
||||
n += 1
|
||||
location = `brickprimitives/lod${lod}/${part.designID}.g${n}`
|
||||
}
|
||||
const primitive = database.filelist[`Primitives/${part.designID}.xml`]
|
||||
if (primitive) files.add(primitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
// each primitive is fetched once (Set + DBURLFile.load() caching)
|
||||
const fileList = Array.from(files)
|
||||
let fetched = 0
|
||||
setProgressDetail('bricks 0/' + fileList.length)
|
||||
await runLimited(fileList, FETCH_CONCURRENCY, async function (file) {
|
||||
await file.load()
|
||||
fetched += 1
|
||||
setProgressDetail('bricks ' + fetched + '/' + fileList.length)
|
||||
})
|
||||
setProgressDetail('')
|
||||
|
||||
// build the meshes
|
||||
for (let i = 0; i < scenes.length; i++) {
|
||||
built += 1
|
||||
if (!scenes[i]) { modelDone(true); continue }
|
||||
try {
|
||||
converter.scene = scenes[i]
|
||||
converter.Export('test.webgl', lxfml_file_list[i].pos)
|
||||
modelDone(false)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
modelDone(true)
|
||||
}
|
||||
if (performance.now() - lastYield > 30) {
|
||||
loadProgressRender()
|
||||
await maybeYield()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
// whatever was not built counts as failed
|
||||
for (; built < lxfml_file_list.length; built++) modelDone(true)
|
||||
}
|
||||
setProgressDetail('')
|
||||
{% if not center and not content|length > 1 %}
|
||||
// the camera follows the brick-built model, now that its position is known
|
||||
camera.position.set( brick_pos.x + 200, brick_pos.y + 300, brick_pos.z + 200 );
|
||||
controls.update()
|
||||
{% endif %}
|
||||
else {
|
||||
alert("LDD database not available. Please look for LEGO-Digital-Designer database.")
|
||||
}
|
||||
}
|
||||
|
||||
const onProgress = function ( xhr ) {
|
||||
@@ -1285,120 +1093,8 @@
|
||||
|
||||
const onError = function (error) {
|
||||
console.log(error)
|
||||
loadProgressRender()
|
||||
};
|
||||
|
||||
// Render on demand: only draw when something changed (a load finished,
|
||||
// the camera moved or the window resized), at most once per animation frame.
|
||||
let renderPending = false
|
||||
function requestRender() {
|
||||
if (renderPending) return
|
||||
renderPending = true
|
||||
requestAnimationFrame(function () {
|
||||
renderPending = false
|
||||
renderer.render(scene, camera)
|
||||
})
|
||||
}
|
||||
|
||||
// Redraw behaviour while models are still loading (PROPERTY_VIEWER_LOAD_REDRAW):
|
||||
// 'none' - do not redraw for finished loads; draw once when everything is done
|
||||
// 'interval' - redraw at most every PROPERTY_VIEWER_REDRAW_INTERVAL_MS while loading
|
||||
// Camera moves and resizes always redraw. The final draw also happens when loads fail.
|
||||
const loadRedraw = {{ load_redraw|default('none')|tojson }}
|
||||
const redrawIntervalMs = {{ redraw_interval_ms|default(500)|int }}
|
||||
let modelsLoading = true
|
||||
let lastLoadRender = 0
|
||||
let loadRenderTimer = null
|
||||
// Progress indicator (a DOM element, so updating it never redraws the canvas).
|
||||
// Counts models: brick-built (lxfml) models count once their meshes are built,
|
||||
// pre-built models when their OBJ loads or their MTL/OBJ fails.
|
||||
const loadingIndicator = document.createElement('div')
|
||||
loadingIndicator.id = 'loading-progress'
|
||||
loadingIndicator.style.cssText = 'position:fixed;top:8px;left:8px;padding:4px 8px;background:rgba(20,20,20,0.93);color:#fff;font:bold 14px sans-serif;border:1px solid rgba(255,255,255,0.35);border-radius:4px;text-shadow:0 1px 2px rgba(0,0,0,0.8);box-shadow:0 1px 4px rgba(0,0,0,0.5);z-index:10;pointer-events:none;transition:opacity 1s'
|
||||
document.body.appendChild(loadingIndicator)
|
||||
const spinnerStyle = document.createElement('style')
|
||||
spinnerStyle.textContent = '@keyframes loading-spin{to{transform:rotate(360deg)}}#loading-progress .loading-spinner{display:inline-block;width:11px;height:11px;margin-right:7px;vertical-align:-1px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:loading-spin 0.8s linear infinite;will-change:transform}'
|
||||
document.head.appendChild(spinnerStyle)
|
||||
let finalRendering = false
|
||||
let modelsTotal = 0, modelsDone = 0, modelsFailed = 0
|
||||
let progressPending = false
|
||||
let progressDetail = ''
|
||||
let prebuiltDetail = ''
|
||||
function progressText() {
|
||||
let pct = modelsTotal ? Math.floor(modelsDone / modelsTotal * 100) : 100
|
||||
let failed = modelsFailed ? ' (' + modelsFailed + ' failed)' : ''
|
||||
let parts = [prebuiltDetail, progressDetail].filter(Boolean).join(', ')
|
||||
let detail = (modelsLoading && parts) ? ' \u2013 ' + parts : ''
|
||||
return (modelsLoading ? 'Loading models: ' : 'Loaded ') + pct + '% (' + modelsDone + '/' + modelsTotal + ')' + failed + detail
|
||||
}
|
||||
function setProgressDetail(text) {
|
||||
progressDetail = text
|
||||
updateProgress()
|
||||
}
|
||||
function updateProgress() {
|
||||
// Batch DOM updates to one per animation frame
|
||||
if (progressPending) return
|
||||
progressPending = true
|
||||
requestAnimationFrame(function () { progressPending = false; if (!finalRendering) loadingIndicator.textContent = progressText() })
|
||||
}
|
||||
function modelDone(failed) {
|
||||
modelsDone++
|
||||
if (failed) modelsFailed++
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
function loadProgressRender() {
|
||||
if (!modelsLoading) return requestRender()
|
||||
if (loadRedraw !== 'interval' || loadRenderTimer) return
|
||||
let wait = Math.max(0, lastLoadRender + redrawIntervalMs - performance.now())
|
||||
loadRenderTimer = setTimeout(function () {
|
||||
loadRenderTimer = null
|
||||
lastLoadRender = performance.now()
|
||||
requestRender()
|
||||
}, wait)
|
||||
}
|
||||
|
||||
// Done once both the loading manager (pre-built models, ground texture) and
|
||||
// the brick-built models have finished.
|
||||
let managerDone = false
|
||||
let ugcDone = lxfml_file_list.length === 0
|
||||
function partFinished() {
|
||||
if (managerDone && ugcDone && modelsLoading) loadsFinished()
|
||||
}
|
||||
|
||||
function loadsFinished() {
|
||||
modelsLoading = false
|
||||
if (loadRenderTimer) { clearTimeout(loadRenderTimer); loadRenderTimer = null }
|
||||
// The final full render can block the main thread for a while on large
|
||||
// properties. Show a 'Rendering' state with a CSS spinner (a transform
|
||||
// animation, which the compositor keeps running while the main thread is
|
||||
// busy), let it paint, then render and show the result.
|
||||
let parts = 0
|
||||
scene.traverse(function (o) { if (o.isMesh) parts++ })
|
||||
finalRendering = true
|
||||
loadingIndicator.textContent = ''
|
||||
const spinner = document.createElement('span')
|
||||
spinner.className = 'loading-spinner'
|
||||
loadingIndicator.appendChild(spinner)
|
||||
loadingIndicator.appendChild(document.createTextNode(parts ? 'Rendering ' + parts + ' parts\u2026' : 'Rendering\u2026'))
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
renderer.render(scene, camera)
|
||||
finalRendering = false
|
||||
loadingIndicator.textContent = progressText()
|
||||
loadingIndicator.style.background = modelsFailed ? 'rgba(170,70,0,0.94)' : 'rgba(0,105,40,0.94)'
|
||||
// Keep the completion state visible briefly, then fade to a faint badge
|
||||
setTimeout(function () { loadingIndicator.style.opacity = '0.7' }, 3000)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Shared by the model/texture loaders; onLoad fires once all queued items
|
||||
// (successful or failed) have finished.
|
||||
let loadsStarted = false
|
||||
const loadingManager = new THREE.LoadingManager(function () { managerDone = true; partFinished() }, loadProgressRender, loadProgressRender)
|
||||
loadingManager.onStart = function () { loadsStarted = true; managerDone = false; loadingIndicator.style.opacity = '1' }
|
||||
|
||||
// Load in pre-built models
|
||||
let obj_file_list = [
|
||||
{% for model in content %}
|
||||
@@ -1408,25 +1104,12 @@
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
modelsTotal = obj_file_list.length + lxfml_file_list.length
|
||||
modelsDone = 0
|
||||
loadingIndicator.textContent = progressText()
|
||||
|
||||
// Pre-built stage on the badge: materials arrive before any OBJ can load
|
||||
let materialsLoaded = 0
|
||||
function materialLoaded() {
|
||||
materialsLoaded++
|
||||
prebuiltDetail = materialsLoaded < obj_file_list.length ? 'materials ' + materialsLoaded + '/' + obj_file_list.length : ''
|
||||
updateProgress()
|
||||
}
|
||||
if (obj_file_list.length) { prebuiltDetail = 'materials 0/' + obj_file_list.length; updateProgress() }
|
||||
|
||||
for (let i = 0; i < obj_file_list.length; i++) {
|
||||
let mtlLoader = new MTLLoader(loadingManager);
|
||||
let mtlLoader = new MTLLoader();
|
||||
mtlLoader.load( obj_file_list[i].mtl, function( materials ) {
|
||||
materialLoaded()
|
||||
|
||||
materials.preload();
|
||||
let objLoader = new OBJLoader(loadingManager);
|
||||
let objLoader = new OBJLoader();
|
||||
objLoader.setMaterials( materials );
|
||||
|
||||
objLoader.load( obj_file_list[i].obj, function ( object ) {
|
||||
@@ -1448,10 +1131,8 @@
|
||||
|
||||
scene.add( newModel );
|
||||
};
|
||||
modelDone(false)
|
||||
loadProgressRender()
|
||||
}, onProgress, function (error) { modelDone(true); onError(error) } );
|
||||
}, undefined, function (error) { materialLoaded(); modelDone(true); onError(error) });
|
||||
}, onProgress, onError );
|
||||
});
|
||||
}
|
||||
|
||||
// Three.JS stuff
|
||||
@@ -1469,7 +1150,7 @@
|
||||
|
||||
{% if center %}
|
||||
let center = new THREE.Vector3{{ center }};
|
||||
let groundTexture = new THREE.TextureLoader(loadingManager).load( "{{url_for('luclient.get_dds_as_png', filename='env_nim_ag_grass.dds')}}");
|
||||
let groundTexture = new THREE.TextureLoader().load( "{{url_for('luclient.get_dds_as_png', filename='env_nim_ag_grass.dds')}}");
|
||||
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
|
||||
groundTexture.repeat.set( 10, 10 );
|
||||
groundTexture.anisotropy = 16;
|
||||
@@ -1512,20 +1193,13 @@
|
||||
controls.update()
|
||||
// if ( vnh ) vnh.update();
|
||||
|
||||
controls.addEventListener('change', requestRender)
|
||||
let render = function () {
|
||||
requestAnimationFrame(render);
|
||||
// camera.rotation.z += 0.01;
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
requestRender()
|
||||
})
|
||||
|
||||
// Nothing queued on the loading manager (e.g. only brick-built models)
|
||||
if (!loadsStarted) managerDone = true
|
||||
loadUGCModels().then(function () { ugcDone = true; partFinished() })
|
||||
partFinished()
|
||||
requestRender();
|
||||
render();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account:Character</th>
|
||||
<th>Activity</th>
|
||||
<th>Time</th>
|
||||
<th>Map</th>
|
||||
<th>Command</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -40,7 +38,7 @@
|
||||
"order": [[0, "desc"]],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('log.get_activities') }}",
|
||||
"ajax": "{{ url_for('log.get_commands') }}",
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account:Character</th>
|
||||
<th>Command</th>
|
||||
<th>Activity</th>
|
||||
<th>Time</th>
|
||||
<th>Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -38,7 +40,7 @@
|
||||
"order": [[0, "desc"]],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('log.get_commands') }}",
|
||||
"ajax": "{{ url_for('log.get_activities') }}",
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,47 +4,9 @@
|
||||
|
||||
{% block content_before %}
|
||||
Online Players: {{ online }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# show general zone info to everyone #}
|
||||
{% if zones %}
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
{% for zone, players in zones.items() %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ zone|get_zone_name }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ players }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# only show this info to high level admina #}
|
||||
{% if current_user.gm_level >= 8 and users|length > 0 %}
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
{% for user in users %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ user[0] }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ user[1]|get_zone_name }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
<h4 class="text-center">Staff</h4>
|
||||
@@ -82,6 +44,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Source
|
||||
|
||||
@@ -108,15 +108,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif current_user.gm_level == 9 %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-danger btn btn-block" href='{{ url_for('accounts.pass_reset', id= account_data.id) }}'>
|
||||
Reset User's Password
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if account_data.play_key and config.REQUIRE_PLAY_KEY %}
|
||||
{% if account_data.play_key and current_user.gm_level > 3 and config.REQUIRE_PLAY_KEY %}
|
||||
<hr class="bg-primary"/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
@@ -133,28 +127,26 @@
|
||||
{{ account_data.play_key.key_string }}
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.gm_level > 3 %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Uses Left:
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ account_data.play_key.key_uses }}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Uses Left:
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Active:
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if account_data.active %}
|
||||
<h5 class="far fa-check-square text-success"></h5>
|
||||
{% else %}
|
||||
<h5 class="far fa-times-circle text-danger"></h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ account_data.play_key.key_uses }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Active:
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if account_data.active %}
|
||||
<h5 class="far fa-check-square text-success"></h5>
|
||||
{% else %}
|
||||
<h5 class="far fa-times-circle text-danger"></h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.id != account_data.id and current_user.gm_level > 3 %}
|
||||
|
||||
@@ -41,35 +41,29 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if request.endpoint != "characters.view" %}
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view', id=character.id) }}'>
|
||||
View Character
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view', id=character.id) }}'>
|
||||
View Character
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<br/>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view_xml', id=character.id) }}'>
|
||||
View XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.get_xml', id=character.id) }}'>
|
||||
Download XML
|
||||
</a>
|
||||
{% if config.ENABLE_CHAR_XML_UPLOAD and current_user.gm_level >= 8 %}
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.upload', id=character.id) }}'>
|
||||
Upload XML
|
||||
</a>
|
||||
{% endif %}
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('accounts.view', id=character.account_id) }}'>
|
||||
View Account: {{character.account.username}}
|
||||
</a>
|
||||
<br/>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view_xml', id=character.id) }}'>
|
||||
View XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.get_xml', id=character.id) }}'>
|
||||
Download XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('accounts.view', id=character.account_id) }}'>
|
||||
View Account: {{character.account.username}}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.gm_level > 2 %}
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
<div class="col text-right">
|
||||
U-Score: {{ character_json.obj.char.attr_ls }}
|
||||
</div>
|
||||
{% if "lvl" in character_json.obj %}
|
||||
<div class="col">
|
||||
Level: {{ character_json.obj.lvl.attr_l }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col">
|
||||
Level: {{ character_json.obj.lvl.attr_l }}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
@@ -65,13 +63,9 @@
|
||||
Play time:
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if character_json.obj.char.attr_time %}
|
||||
{{ (character_json.obj.char.attr_time|int/60/60/24)|int }} Days
|
||||
{{ (character_json.obj.char.attr_time|int/60/60)|int - ((character_json.obj.char.attr_time|int/60/60/24)|int) * 24}} Hours
|
||||
{{ (character_json.obj.char.attr_time|int/60 - (character_json.obj.char.attr_time|int/60/60)|int*60)|int }} Minutes
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="bg-primary"/>
|
||||
@@ -114,33 +108,63 @@
|
||||
<div class="tab-content mt-3" id="nav-invContent">
|
||||
<div class="tab-pane fade show active" id="nav-items" role="tabpanel" aria-labelledby="nav-items-tab">
|
||||
{# Inv ID 0 - Index: 0 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "0" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-vault" role="tabpanel" aria-labelledby="nav-vault-tab">
|
||||
{# Inv ID 1 - Index: 1 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "1" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-vault-models" role="tabpanel" aria-labelledby="nav-vault-models-tab">
|
||||
{# Inv ID 14 - Index: 10 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "14" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-bricks" role="tabpanel" aria-labelledby="nav-bricks-tab">
|
||||
{# Inv ID 2 - Index: 2 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "2" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-models" role="tabpanel" aria-labelledby="nav-models-tab">
|
||||
{# Inv ID 5 - Index: 6 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "5" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-behaviors" role="tabpanel" aria-labelledby="nav-behaviors-tab">
|
||||
{# Inv ID 7 - Index: 8 #}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "7" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,14 +187,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% if character_json.obj.char.zs %}
|
||||
{% for zone in character_json.obj.char.zs.s %}
|
||||
{% include 'partials/charxml/_zone_stats.html.j2' %}
|
||||
{{ '<hr class="bg-primary"/>' if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No Stats Yet
|
||||
{% endif %}
|
||||
{% for zone in character_json.obj.char.zs.s %}
|
||||
{% include 'partials/charxml/_zone_stats.html.j2' %}
|
||||
{{ '<hr class="bg-primary"/>' if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
@@ -199,40 +219,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=0)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-items"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=1)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-vault"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=14)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-vault-models"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=2)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-bricks"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=5)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-models"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=7)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-behaviors"), text);
|
||||
})
|
||||
</script>
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if gm_level==0 %}
|
||||
Player
|
||||
{% elif gm_level==1 %}
|
||||
Elevevated Civilan {# Unused #}
|
||||
Key Distributor
|
||||
{% elif gm_level==2 %}
|
||||
Junior Moderator
|
||||
{% elif gm_level==3 %}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s;">
|
||||
<span class="sr-only">Loading 0</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 1s;">
|
||||
<span class="sr-only">Loading 1</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 2s;">
|
||||
<span class="sr-only">Loading 2</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 3s;">
|
||||
<span class="sr-only">Loading 3</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 4s;">
|
||||
<span class="sr-only">Loading 4</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 5s;">
|
||||
<span class="sr-only">Loading 5</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 6s;">
|
||||
<span class="sr-only">Loading 6</span>
|
||||
</div>
|
||||
@@ -77,14 +77,6 @@
|
||||
{{ property.performance_cost }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Clone ID:
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ property.clone_id }}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.endpoint != "properties.view" %}
|
||||
<br/>
|
||||
<div class="row">
|
||||
|
||||
@@ -4,29 +4,21 @@
|
||||
alt="{{ inv_item.attr_l|get_lot_name }}"
|
||||
class="border p-1 border-primary rounded m-1"
|
||||
width="60"
|
||||
{% if 'attr_eq' in inv_item %}
|
||||
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
|
||||
{% endif %}
|
||||
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
|
||||
height="60"
|
||||
data-html="true"
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title="{% include 'partials/charxml/_item_tooltip.html.j2' %}"
|
||||
>
|
||||
{% if 'attr_c' in inv_item %}
|
||||
{% if inv_item.attr_c != "1" %}
|
||||
<span class="inventory-count text-bold">
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
+999
|
||||
{% elif inv_item.attr_c|int > 1 %}
|
||||
{{ inv_item.attr_c }}
|
||||
{% endif %}
|
||||
{{ inv_item.attr_c }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if 'attr_b' in inv_item %}
|
||||
{% if inv_item.attr_b == "true" %}
|
||||
<span class="inventory-lock">
|
||||
<i class='fas fa-lock'></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if inv_item.attr_b == "true" %}
|
||||
<span class="inventory-lock">
|
||||
<i class='fas fa-lock'></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{% if inventory.i is iterable and (inventory.i is not string and inventory.i is not mapping) %}
|
||||
{% for inv_item in inventory.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=inventory.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
@@ -66,8 +66,3 @@
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'attr_c' in inv_item %}
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
<br />Count: {{ inv_item.attr_c|numberFormat }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
python3 -m flask db upgrade
|
||||
python3 wsgi.py
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# unzip brickdb from client to the right places
|
||||
unzip -n -q /app/luclient/res/brickdb.zip -d app/luclient/res/
|
||||
|
||||
# TODO: preconvert images options
|
||||
# TODO: preconvery models options
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ logger = logging.getLogger('alembic.env')
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||
)
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
"""compressss reports
|
||||
|
||||
Revision ID: 1164e037907f
|
||||
Revises: a6e42ef03da7
|
||||
Create Date: 2023-11-18 01:38:00.127472
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy.orm.session import Session
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.types import JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
import gzip
|
||||
import json
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1164e037907f'
|
||||
down_revision = 'a6e42ef03da7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
class ReportsUpgradeNew(Base):
|
||||
__tablename__ = 'reports'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
mysql.MEDIUMBLOB(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
def save(self):
|
||||
sa.session.add(self)
|
||||
sa.session.commit()
|
||||
sa.session.refresh(self)
|
||||
|
||||
|
||||
class ReportsUpgradeOld(Base):
|
||||
__tablename__ = 'reports_old'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
class ReportsDowngradeOld(Base):
|
||||
__tablename__ = 'reports'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
mysql.MEDIUMBLOB(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
def save(self):
|
||||
sa.session.add(self)
|
||||
sa.session.commit()
|
||||
sa.session.refresh(self)
|
||||
|
||||
|
||||
class ReportsDowngradeNew(Base):
|
||||
__tablename__ = 'reports_old'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.rename_table('reports', 'reports_old')
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
reports = session.query(ReportsUpgradeOld)
|
||||
op.create_table('reports',
|
||||
sa.Column('data', mysql.MEDIUMBLOB(), nullable=False),
|
||||
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
|
||||
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('report_type', 'date')
|
||||
)
|
||||
# insert records
|
||||
new_reports = []
|
||||
# insert records
|
||||
for report in reports:
|
||||
new_reports.append({
|
||||
"data":gzip.compress(json.dumps(report.data).encode('utf-8')),
|
||||
"report_type":report.report_type,
|
||||
"date":report.date
|
||||
})
|
||||
op.bulk_insert(ReportsUpgradeNew.__table__, new_reports)
|
||||
op.drop_table('reports_old')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('reports')
|
||||
op.create_table('reports',
|
||||
sa.Column('data', JSON(), nullable=False),
|
||||
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
|
||||
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('report_type', 'date')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -77,11 +77,11 @@ def upgrade():
|
||||
sa.Column('created_at', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('play_key_id', mysql.INTEGER(), nullable=True),
|
||||
sa.Column('mute_expire', mysql.BIGINT(unsigned=True), server_default='0', nullable=False),
|
||||
sa.ForeignKeyConstraint(['play_key_id'], ['play_keys.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
|
||||
op.create_foreign_key(None, 'accounts', 'play_keys', ['play_key_id'], ['id'], ondelete='CASCADE')
|
||||
op.add_column('accounts', sa.Column('active', sa.BOOLEAN(), server_default='1', nullable=False))
|
||||
op.add_column('accounts', sa.Column('email_confirmed_at', sa.DateTime(), nullable=True))
|
||||
op.add_column('accounts', sa.Column('email', sa.Unicode(length=255), server_default='', nullable=True))
|
||||
@@ -103,13 +103,14 @@ def upgrade():
|
||||
sa.Column('other_player_id', mysql.TEXT(), nullable=False),
|
||||
sa.Column('selection', mysql.TEXT(), nullable=False),
|
||||
sa.Column('submitted', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['resoleved_by_id'], ['accounts.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
op.add_column('bug_reports', sa.Column('resolved_time', mysql.TIMESTAMP(), nullable=True))
|
||||
op.add_column('bug_reports', sa.Column('resoleved_by_id', sa.Integer(), nullable=True))
|
||||
op.add_column('bug_reports', sa.Column('resolution', mysql.TEXT(), nullable=True))
|
||||
op.create_foreign_key(None, 'bug_reports', 'accounts', ['resoleved_by_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key(None, 'bug_reports', 'accounts', ['resoleved_by_id'], ['id'])
|
||||
|
||||
if 'charinfo' not in tables:
|
||||
op.create_table('charinfo',
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""force play_key_id to be nullable
|
||||
|
||||
Revision ID: a6e42ef03da7
|
||||
Revises: 8e52b5c7568a
|
||||
Create Date: 2022-11-29 19:14:22.645911
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a6e42ef03da7'
|
||||
down_revision = '8e52b5c7568a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('accounts', 'play_key_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('accounts', 'play_key_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
alembic==1.7.5
|
||||
APScheduler==3.8.1
|
||||
astroid==2.9.1
|
||||
autopep8==1.6.0
|
||||
bcrypt==4.0.1
|
||||
bcrypt==3.2.0
|
||||
blinker==1.4
|
||||
cffi==1.14.6
|
||||
click==8.0.1
|
||||
colorama==0.4.4
|
||||
cryptography==36.0.0
|
||||
dnspython==2.1.0
|
||||
dominate==2.6.0
|
||||
email-validator==1.1.3
|
||||
Flask==3.0.0
|
||||
Flask==2.0.1
|
||||
Flask-APScheduler==1.12.3
|
||||
Flask-Assets==2.1.0
|
||||
Flask-Assets==2.0
|
||||
Flask-Login==0.5.0
|
||||
Flask-Mail==0.9.1
|
||||
flask-marshmallow==0.14.0
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-User==1.0.2.2
|
||||
Flask-WTF==1.2.1
|
||||
gunicorn==21.2.0
|
||||
Flask-WTF==1.0.0
|
||||
greenlet==1.1.0
|
||||
idna==3.3
|
||||
isort==5.10.1
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.1
|
||||
lazy-object-proxy==1.7.1
|
||||
libsass==0.21.0
|
||||
Mako==1.1.6
|
||||
MarkupSafe==2.0.1
|
||||
marshmallow==3.14.1
|
||||
marshmallow-sqlalchemy==0.26.1
|
||||
mccabe==0.6.1
|
||||
passlib==1.7.4
|
||||
platformdirs==2.4.1
|
||||
pycodestyle==2.8.0
|
||||
pycparser==2.20
|
||||
pydocstyle==6.1.1
|
||||
pyflakes==2.4.0
|
||||
pylama==8.3.3
|
||||
pylint==2.12.2
|
||||
PyMySQL==1.0.2
|
||||
SQLAlchemy==2.0.40
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.3
|
||||
pytz-deprecation-shim==0.1.0.post0
|
||||
six==1.16.0
|
||||
snowballstemmer==2.2.0
|
||||
SQLAlchemy==1.4.22
|
||||
sqlalchemy-datatables==2.0.1
|
||||
SQLAlchemy-Utils==0.38.2
|
||||
toml==0.10.2
|
||||
tzdata==2021.5
|
||||
tzlocal==4.1
|
||||
visitor==0.1.3
|
||||
Wand==0.6.7
|
||||
webassets==2.0
|
||||
Werkzeug==3.0.1
|
||||
Werkzeug==2.0.1
|
||||
wrapt==1.13.3
|
||||
WTForms==3.0.0
|
||||
xmltodict==0.12.0
|
||||
|
||||
2
wsgi.py
2
wsgi.py
@@ -18,7 +18,7 @@ else:
|
||||
from logging.handlers import RotatingFileHandler
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
file_handler = RotatingFileHandler('logs/nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
Reference in New Issue
Block a user