Compare commits

...

102 Commits
v1.0.0 ... main

Author SHA1 Message Date
aronwk-aaron
823ec2008f util command to get mission/ach awards that aren't in honor accollade 2024-05-26 22:49:32 -05:00
aronwk-aaron
f3e2254330 fix messed up table 2024-03-11 12:33:33 -05:00
Aaron Kimbrell
d6b0a91e4d
Merge pull request #91 from DarkflameUniverse/fix/libsass
Reintroduce libsass
2024-01-10 18:06:41 -06:00
Xiphoseer
d698e650ad feat: upgrade to python3.11; debian 12 (bookworm) 2024-01-11 01:02:31 +01:00
Xiphoseer
cde585fad8 fix: re-add libsass 2024-01-11 01:01:33 +01:00
Aaron Kimbrell
8b70f259c0
Merge pull request #88 from DarkflameUniverse/fix/requirements
fix: remove transitive deps from requirements.txt
2024-01-07 16:22:06 -06:00
Xiphoseer
de50bc7278 fix: remove transitive deps from requirements.txt 2024-01-07 22:12:32 +01:00
Aaron Kimbrell
2e4bd04d09
Merge pull request #87 from DarkflameUniverse/fix/empty-play-key
fix: don't break on empty play key
2024-01-07 15:00:26 -06:00
Xiphoseer
ccc793a129 fix: don't break on empty play key 2024-01-07 21:56:56 +01:00
Aaron Kimbrell
69823be5c8
Merge pull request #86 from DarkflameUniverse/fix/migrate-env
Fix DB connection URL in env.py
2024-01-07 13:57:38 -06:00
Daniel Seiler
3027534b16
Fix DB connection URL in env.py
Previously, this line was returning `***` as the DB password
2024-01-07 20:50:46 +01:00
aronwk-aaron
09096fe1c4 bump action versions 2023-12-31 01:57:16 -06:00
Aaron Kimbrell
9bfa55ac8e
Merge pull request #81 from Ramen2X/main
fix readme typo (exmaple -> example)
2023-12-18 10:11:00 -06:00
Ramen2X
1dee96c04f
fix typo (exmaple -> example) 2023-12-17 16:42:01 -05:00
aronwk-aaron
d005b497e6 compress the reports :D 2023-11-18 05:46:50 -06:00
aronwk-aaron
259efc81fd fix: don't show count of 1 on items
fix: tooltips on lazy loaded items
2023-11-18 01:13:05 -06:00
aronwk-aaron
b9fc039a7e LAZY LOADING
updated a bunch of packages
2023-11-18 00:41:35 -06:00
aronwk-aaron
abc8af89c5 update packages 2023-11-17 22:51:12 -06:00
Aaron Kimbrell
5ae2769ad2
Merge pull request #78 from DarkflameUniverse/captcha
feat: add recaptcha support
2023-11-17 18:29:41 -06:00
aronwk-aaron
b7e48bb656 extend the recaptha validator to make it optional 2023-11-17 17:40:01 -06:00
aronwk-aaron
7633053490 it works 2023-11-17 16:38:01 -06:00
aronwk-aaron
e6c452d000 feat: add recaptcha support 2023-11-17 00:55:18 -06:00
aronwk-aaron
13376d0c1f maybe fix it 2023-11-11 00:17:59 -06:00
aronwk-aaron
bb63cfb8f5 format xml and mke the warning better 2023-11-11 00:09:34 -06:00
aronwk-aaron
a10b8d7975 fix it 2023-11-10 00:30:03 -06:00
aronwk-aaron
1c9ee91b78 check for null 2023-11-10 00:09:07 -06:00
aronwk-aaron
5c2721cd65 fixt copy/paste error 2023-11-10 00:04:10 -06:00
aronwk-aaron
8ec3803786 include ET 2023-11-10 00:00:18 -06:00
aronwk-aaron
a578f8a53c register test command 2023-11-09 23:56:32 -06:00
aronwk-aaron
7ca62fe478 show clone id on props page 2023-11-09 23:48:28 -06:00
aronwk-aaron
01e304a041 fix last commit 2023-10-28 00:45:37 -05:00
aronwk-aaron
368c0819bd lvl sanity check 2023-10-28 00:38:33 -05:00
aronwk-aaron
ab8119c5b8 more sanity checks 2023-10-28 00:30:23 -05:00
aronwk-aaron
c9ad415f13 add sanity checks for inventory to stop crashing 2023-10-28 00:27:17 -05:00
aronwk-aaron
a7a68d2fe1 fix nav highlight and tooltip rendering 2023-06-15 23:02:47 -05:00
aronwk-aaron
b17928b050 fix fk stuff being outside of the main migration check 2023-03-04 00:09:35 -06:00
aronwk-aaron
ee65f67fe3 actually fix mail port 2023-02-14 16:13:27 -06:00
aronwk-aaron
5d1b79334a force mail_port to be int
resolves #62
2023-02-14 13:39:44 -06:00
aronwk-aaron
e726f59114 log mount 2023-01-05 15:21:11 -06:00
aronwk-aaron
8826a34ebc fix docker command more 2023-01-05 15:13:43 -06:00
aronwk-aaron
a3d492df91 ro is readonly not r 2023-01-05 15:12:46 -06:00
aronwk-aaron
4a58e963a5 revery wsgi file for now, need to replace gunicorn
for a cross-platform solution
2023-01-02 13:46:44 -06:00
aronwk-aaron
8012780eba limit scope, update container in readme 2022-12-31 19:39:37 -06:00
aronwk-aaron
f403d7dcb0 cleanup 2022-12-31 19:28:04 -06:00
aronwk-aaron
ceed592342 no 2022-12-31 19:24:47 -06:00
aronwk-aaron
bf7fb3d159 dolla bills? 2022-12-31 19:24:02 -06:00
aronwk-aaron
ef55b8f9f2 update to ci 2022-12-31 19:20:16 -06:00
aronwk-aaron
3d47b265c9 add workflow 2022-12-31 18:24:19 -06:00
Aaron Kimbrell
3f7a382dbc
Merge pull request #58 from DarkflameUniverse/configuration-cleanup
Configuration Cleanup and Additions
2022-12-31 00:16:48 -06:00
Jett
bc6bbdfaa7 I didn't break anything, what are you talking about...? 2022-12-31 04:54:46 +00:00
Jett
9cda62cef7 A few mistakes 2022-12-31 04:39:54 +00:00
Jett
99087eb30a Add Windows documentation 2022-12-31 04:37:42 +00:00
aronwk-aaron
760936a01f f 2022-12-30 22:26:06 -06:00
aronwk-aaron
c96174fcbe fix ldddb location 2022-12-30 22:24:04 -06:00
Jett
bec8233aad Update README and document WSGI file more. 2022-12-31 04:14:08 +00:00
aronwk-aaron
ae46e6d382 remove analytics
add note about securing it
2022-12-29 15:10:36 -06:00
aronwk-aaron
98c61bcaf1 cap password length at 40 on registration
due to client limitations
2022-12-29 13:12:22 -06:00
aronwk-aaron
b8bd7c6cba make needs rename not show up in unmoderated names 2022-12-19 09:58:26 -06:00
aronwk-aaron
e44872e523 delete temporary imports 2022-12-17 01:55:55 -06:00
aronwk-aaron
535a07425b update entrypioint and settings example 2022-12-17 01:50:25 -06:00
aronwk-aaron
785357475d use cache more modified files
so that the client dir never has to be written to
2022-12-17 01:47:10 -06:00
aronwk-aaron
0fea032938 fix config defaults 2022-12-17 01:21:39 -06:00
aronwk-aaron
3b5b478815 configurable client and sqlite 2022-12-17 01:15:27 -06:00
aronwk-aaron
708fbfb9db fix icon rendering stuff 2022-12-17 00:59:47 -06:00
aronwk-aaron
53dda2fd8a move css to subfolder 2022-12-16 22:57:05 -06:00
aronwk-aaron
24fc6a0826 move logs to folder 2022-12-16 22:50:43 -06:00
aronwk-aaron
5e59d3b43c fix init db, force play_key_id to be nullable 2022-11-29 19:23:50 -06:00
aronwk-aaron
19f38b379e make play key perms make sense
adjust GM level 1 name and some related displays
2022-11-28 18:48:28 -06:00
aronwk-aaron
4aff169967 add line break to make it more readable 2022-11-28 14:05:29 -06:00
aronwk-aaron
4d007d66ac display count in tooltip if > 999 2022-11-28 14:01:26 -06:00
aronwk-aaron
8652d6dc13 fix behavior inv tab 2022-11-28 13:34:32 -06:00
aronwk-aaron
ba157a3715 Fixes #55
fall back to CDServer.sqlite if cdclient.sqlite doesn't exist
2022-11-28 13:17:26 -06:00
aronwk-aaron
e69d25594a fix command and activity log confusion 2022-11-28 13:05:52 -06:00
aronwk-aaron
cc4adfcbfe fix large item count displaying 2022-11-23 13:43:28 -06:00
Aaron Kimbrell
77acd7615a
Fix upload button view 2022-11-20 11:34:02 -07:00
aronwk-aaron
f643f428ea apeed up about page loading
add some neat info
2022-11-17 22:41:43 -06:00
aronwk-aaron
37af644078 make upload gm8 2022-11-17 21:59:15 -06:00
aronwk-aaron
64ccb29972 fix upload button showing up for any GM level 2022-11-09 21:09:22 -06:00
aronwk-aaron
000a8c47bf add sdo support for bbb models 2022-10-24 20:57:04 -05:00
aronwk-aaron
3fa8bd4651 fix handling new flag 2022-10-23 17:39:20 -05:00
aronwk-aaron
b87481e803 fix single item in inventory displaying
fix parsing of initial char xml to have some sane output
initial inventory does not show, but it should not crash
2022-10-23 17:25:41 -05:00
aronwk-aaron
eb7a820b54 copy paste error 2022-10-17 18:18:30 -05:00
aronwk-aaron
a9c53254f2 forgor button 2022-10-17 18:00:50 -05:00
aronwk-aaron
d06bad4641 hotfix 2022-10-16 22:16:27 -05:00
Aaron Kimbrell
1f2673d7fc
Merge pull request #53 from DarkflameUniverse/dev-upload-char-xml
hacky charxml uploader until i do it the right way
2022-10-16 22:14:30 -05:00
aronwk-aaron
dce4466487 hacky charxml uploader until i do it the right way 2022-10-16 22:14:12 -05:00
aronwk-aaron
f54e9bf9b4 hotfix 2022-10-16 20:49:45 -05:00
Aaron Kimbrell
3a034de45a
Merge pull request #52 from DarkflameUniverse/admin-password-rest
add the ability to reset user's password
2022-10-16 20:41:19 -05:00
aronwk-aaron
a5f7024211 add the ability to reset user's password
admin only and will randomly generate a password
2022-10-16 20:39:20 -05:00
Aaron Kimbrell
a7419679d0
Merge pull request #50 from DarkflameUniverse/dependabot/pip/mako-1.2.2
Bump mako from 1.1.6 to 1.2.2
2022-09-26 11:39:18 -05:00
dependabot[bot]
e2ca21136e
Bump mako from 1.1.6 to 1.2.2
Bumps [mako](https://github.com/sqlalchemy/mako) from 1.1.6 to 1.2.2.
- [Release notes](https://github.com/sqlalchemy/mako/releases)
- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/mako/commits)

---
updated-dependencies:
- dependency-name: mako
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-16 18:58:10 +00:00
Aaron Kimbre
d7333490d1 fix it lol 2022-07-12 18:06:41 -05:00
Aaron Kimbre
21b6932f48 delete friends now that they work 2022-07-12 18:02:38 -05:00
Aaron Kimbre
6ff2caf039 Quick link changes 2022-07-03 08:17:38 -05:00
Aaron Kimbre
c1307af49c detailed item reports
Resolves #40
2022-06-08 23:09:14 -05:00
Aaron Kimbre
b561bcb60d fix bulk key creation 2022-06-07 21:33:42 -05:00
Aaron Kimbre
34302006a9 fix incorrect logging in name approval 2022-06-06 23:09:42 -05:00
Aaron Kimbre
a5ea052027 fix account deletion 2022-05-23 14:18:55 -05:00
Aaron Kimbre
ad237b121b fix mor formatting in read me 2022-05-20 15:47:14 -05:00
Aaron Kimbrell
4966d6b029
Merge pull request #39 from HailStorm32/main
Fixed README formatting error
2022-05-20 15:44:37 -05:00
Demetri Van Sickle
6c3c0c4888
Fixed README formatting error 2022-05-20 13:23:34 -07:00
Aaron Kimbre
ae0847aba9 fix gm level form 2022-05-18 06:25:07 -05:00
53 changed files with 1302 additions and 772 deletions

54
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,54 @@
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 }}

4
.gitignore vendored
View File

@ -4,7 +4,7 @@ resources.py
__pycache__/
venv/
static/policy/
app/static/site.css
app/static/css/site.css
app/static/.webassets-cache/**/*
app/static/brickdb/*
locale.json
@ -17,3 +17,5 @@ property_files/*
*.log
app/settings.py
*.exe
*.csv
*.sql

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
FROM python:3.8-slim-buster
FROM python:3.11-slim-bookworm
RUN apt update
RUN apt -y install zip

218
README.md
View File

@ -65,28 +65,30 @@
* 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 Linux**
> **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)
## 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/unpacked/client:/app/luclient:rw \
-e REQUIRE_PLAY_KEY=True \
-p 8000:8000/tcp \
-v /path/to/logs:/logs:rw /
-v /path/to/unpacked/client:/app/luclient:ro \
-v /path/to/cachedir:/app/cache:rw \
aronwk/nexus-dashboard:latest
ghcr.io/darkflameuniverse/nexusdashboard:latest
```
* `/app/luclient` must be mapped to the location of an unpacked client
@ -96,43 +98,48 @@ docker run -d \
### Environmental Variables
Please Reference `app/settings_exmaple.py` to see all the variables
Please Reference `app/settings_example.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
## Manual
## Manual Linux Installation
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
First you will want to install the following packages by executing the following commands presuming you are on a Debian based system.
`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 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.
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>
`cd` *make sure you are in 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
`git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
You should now have a directory called `NexusDashboard`
You should now have a directory called `NexusDashboard` present in your home directory
### 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
`vim ~/NexusDashboard/app/settings.py`
>*Feel free to use any text editor you are more comfortable with instead of vim*
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*
<br>
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: Enabling the email option will require further setup that is outside the scope of this tutorial*
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`
@ -162,65 +169,166 @@ Once you are done making the changes, save and close the file
We will need the following folders from the client
```
locale (all of the files inside)
locale
└───locale.xml
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` after you have unzipped it
Remove the `.zip` file after you have unzipped it, you can do that with
`rm brickdb.zip`
In the `luclient` directory you should now have a file structure that looks like this
```
local
|_locale.xml
locale
└───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`
`mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite`
Once the file is moved over, you will need to rename it to `cdclient.sqlite`, this can be done with the following command
```bash
mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite
```
##### Remaining Setup
Run the following commands one at a time
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
```bash
cd ~/NexusDashboard
pip install -r requirements.txt
pip install gunicorn
flask db upgrade
```
##### Running the site
Once all of the above is complete, you can run the site with the command
`gunicorn -b :8000 -w 4 wsgi:app`
`cd ~/NexusDashboard`
`pip install -r requirements.txt`
`pip install gunicorn`
`flask db upgrade`
## 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
You can run the site with
`gunicorn -b :8000 -w 4 wsgi:app`
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/)
Please use [Editor Config](https://editorconfig.org/) to maintain a consistent coding style between different editors and different contributors.
* `flask run` to run local dev server
* `python3 -m flask run` to run a local dev server

View File

@ -5,12 +5,12 @@ 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
import pathlib
from app.commands import (
init_db,
@ -18,7 +18,9 @@ from app.commands import (
load_property,
gen_image_cache,
gen_model_cache,
fix_clone_ids
fix_clone_ids,
remove_buffs,
find_missing_commendation_items
)
from app.models import Account, AccountInvitation, AuditLog
@ -82,6 +84,11 @@ def create_app():
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)
@ -89,6 +96,8 @@ 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)
register_logging(app)
register_settings(app)
@ -96,6 +105,19 @@ def create_app():
register_blueprints(app)
register_luclient_jinja_helpers(app)
# Extract the brickdb if it's not already extracted
materials = pathlib.Path(f'{app.config["CACHE_LOCATION"]}Materials.xml')
if not materials.is_file():
# unzip the brickdb, and remove the import after
from zipfile import ZipFile
with ZipFile(f"{app.config['CLIENT_LOCATION']}res/brickdb.zip","r") as zip_ref:
zip_ref.extractall(app.config["CACHE_LOCATION"])
del ZipFile
# copy over the brick primitives, and remove the import after
from shutil import copytree
copytree(f"{app.config['CLIENT_LOCATION']}res/brickprimitives", f"{app.config['CACHE_LOCATION']}brickprimitives")
del copytree
return app
@ -107,8 +129,6 @@ def register_extensions(app):
"""
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
scheduler.init_app(app)
scheduler.start()
@ -120,7 +140,7 @@ def register_extensions(app):
assets = Environment(app)
assets.url = app.static_url_path
scss = Bundle('scss/site.scss', filters='libsass', output='site.css')
scss = Bundle('scss/site.scss', filters='libsass', output='css/site.css')
assets.register('scss_all', scss)
@ -159,7 +179,7 @@ def register_blueprints(app):
def register_logging(app):
# file logger
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
file_handler = RotatingFileHandler('logs/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)
@ -183,19 +203,23 @@ 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']
)
# 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']
@ -220,25 +244,15 @@ 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['SQLALCHEMY_ENGINE_OPTIONS'] = {
"pool_pre_ping": True,
"pool_size": 10,
"max_overflow": 2,
"pool_recycle": 300,
"pool_pre_ping": True,
"pool_use_lifo": True
}
app.config['MAIL_SERVER'] = os.getenv(
'MAIL_SERVER',
app.config['MAIL_SERVER']
)
app.config['MAIL_PORT'] = os.getenv(
'MAIL_USE_SSL',
app.config['MAIL_PORT'] = int(
os.getenv(
'MAIL_PORT',
app.config['MAIL_PORT']
)
)
app.config['MAIL_USE_SSL'] = os.getenv(
'MAIL_USE_SSL',
@ -265,6 +279,71 @@ 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']
)
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

View File

@ -1,7 +1,9 @@
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,
@ -14,17 +16,15 @@ from app.models import (
AuditLog,
BugReport,
AccountInvitation,
db
db,
Friends
)
from app.schemas import AccountSchema
from app import gm_level, log_audit
from app.forms import EditGMLevelForm, EditEmailForm
from sqlalchemy import or_
accounts_blueprint = Blueprint('accounts', __name__)
account_schema = AccountSchema()
@accounts_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)
@ -150,10 +150,14 @@ def delete(id):
message = f"Deleted Account ({account.id}){account.username}"
chars = CharacterInfo.query.filter(CharacterInfo.account_id == id).all()
for char in chars:
activities = ActivityLog.query.filter(ActivityLog.character_id == char.id).all()
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()
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()
@ -161,21 +165,29 @@ def delete(id):
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()
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()
friends = Friends.query.filter(
or_(Friends.player_id == char.id, Friends.friend_id == char.id)
).all()
for friend in friends:
friend.delete()
char.delete()
# This is for GM stuff, it will be permnently delete logs
bugs = BugReport.query.filter(BugReport.resolve_by_id == id).all()
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()
invites = AccountInvitation.query.filter(
AccountInvitation.invited_by_user_id == id).all()
for invite in invites:
invite.delete()
account.delete()
@ -184,6 +196,27 @@ def delete(id):
return redirect(url_for("main.index"))
@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)
@ -219,6 +252,9 @@ 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:
@ -234,20 +270,11 @@ def get():
else:
account["6"] = '''<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>'''
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>'''
else:
# 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"]
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
return data

View File

@ -1,21 +1,19 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response, current_app
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.schemas import CharacterInfoSchema
from app.forms import RescueForm
from app.forms import RescueForm, CharXMLUploadForm
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)
@ -30,14 +28,20 @@ def approve_name(id, action):
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
if action == "approve":
log_audit(f"Approved ({character.id}){character.pending_name} from {character.name}")
flash(
f"Approved ({character.id}){character.pending_name} from {character.name}",
"success"
)
if character.pending_name:
character.name = character.pending_name
character.pending_name = ""
log_audit(f"Approved ({character.id}){character.pending_name} from {character.name}")
flash(
f"Approved ({character.id}){character.pending_name} from {character.name}",
"success"
)
else:
log_audit("Cannot make character name empty")
flash(
"Cannot make character name empty",
"danger"
)
character.needs_rename = False
elif action == "rename":
@ -72,7 +76,7 @@ def view(id):
character_json = xmltodict.parse(
CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data,
).first().xml_data.replace("\"stt=", "\" stt="),
attr_prefix="attr_"
)
@ -85,9 +89,10 @@ def view(id):
# stupid fix for jinja parsing
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
# sort by items slot index
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']))
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(
'character/view.html.j2',
@ -95,6 +100,88 @@ 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
@ -113,7 +200,7 @@ def view_xml(id):
character_xml = CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data
).first().xml_data.replace("\"stt=", "\" stt=")
response = make_response(character_xml)
response.headers.set('Content-Type', 'text/xml')
@ -184,7 +271,7 @@ def rescue(id):
CharacterXML.id == id
).first()
character_xml = ET.XML(character_data.xml_data)
character_xml = ET.XML(character_data.xml_data.replace("\"stt=", "\" stt="))
for zone in character_xml.findall('.//r'):
if int(zone.attrib["w"]) % 100 == 0:
form.save_world.choices.append(
@ -210,6 +297,30 @@ 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)
@ -228,7 +339,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 == True))
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") & (CharacterInfo.needs_rename == False))
else:
query = db.session.query().select_from(CharacterInfo).join(Account)

View File

@ -4,8 +4,8 @@ import random
import string
import datetime
from flask_user import current_app
from app import db
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
from app import db, luclient
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail, CharacterXML
import pathlib
import zlib
from wand import image
@ -15,7 +15,7 @@ from multiprocessing import Pool
from functools import partial
from sqlalchemy import func
import time
import xml.etree.ElementTree as ET
@click.command("init_db")
@click.argument('drop_tables', nargs=1)
@ -180,6 +180,20 @@ def load_property(zone, player):
)
new_prop_content.save()
@click.command("remove_buffs")
@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()
@click.command("gen_image_cache")
def gen_image_cache():
@ -267,3 +281,41 @@ 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)

View File

@ -1,17 +1,15 @@
from flask_wtf import FlaskForm
from flask_wtf import FlaskForm, Recaptcha, RecaptchaField
from flask import current_app
from flask_user.forms import (
unique_email_validator,
password_validator,
unique_username_validator
LoginForm,
RegisterForm
)
from flask_user import UserManager
from wtforms.widgets import TextArea, NumberInput
from wtforms import (
StringField,
HiddenField,
PasswordField,
BooleanField,
SubmitField,
validators,
@ -24,67 +22,43 @@ from app.models import PlayKey
def validate_play_key(form, field):
"""Validates a field for a valid phone number
"""Validates a field for a valid play kyey
Args:
form: REQUIRED, the field's parent form
field: REQUIRED, the field with data
Returns:
None, raises ValidationError if failed
"""
# jank to get the fireign key that we need back into the field
# jank to get the foreign 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
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)
class CustomUserManager(UserManager):
def customize(self, app):
self.RegisterFormClass = CustomRegisterForm
self.LoginFormClass = CustomLoginForm
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,
]
)
class CustomRegisterForm(RegisterForm):
play_key_id = StringField(
'Play Key',
validators=[
Optional(),
validate_play_key,
]
validators=[validate_play_key]
)
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 CustomLoginForm(LoginForm):
recaptcha = RecaptchaField(
validators=[CustomRecaptcha()]
)
class CreatePlayKeyForm(FlaskForm):
@ -119,7 +93,7 @@ class EditPlayKeyForm(FlaskForm):
class EditGMLevelForm(FlaskForm):
email = IntegerField(
gm_level = IntegerField(
'GM Level',
widget=NumberInput(min=0, max=9)
)
@ -209,3 +183,13 @@ class RejectPropertyForm(FlaskForm):
)
submit = SubmitField('Submit')
class CharXMLUploadForm(FlaskForm):
char_xml = StringField(
'Paste minified charxml here:',
widget=TextArea(),
validators=[validators.DataRequired()]
)
submit = SubmitField('Submit')

View File

@ -25,7 +25,7 @@ def command():
@log_blueprint.route('/system')
@gm_level(8)
def system():
with open('nexus_dashboard.log', 'r') as file:
with open('logs/nexus_dashboard.log', 'r') as file:
logs = '</br>'.join(file.read().split('\n')[-100:])
return render_template('logs/system.html.j2', logs=logs)

View File

@ -5,7 +5,8 @@ from flask import (
redirect,
url_for,
make_response,
abort
abort,
current_app
)
from flask_user import login_required
from app.models import CharacterInfo
@ -32,7 +33,7 @@ def get_dds_as_png(filename):
cache = f'cache/{filename.split(".")[0]}.png'
if not os.path.exists(cache):
root = 'app/luclient/res/'
root = f"{current_app.config['CLIENT_LOCATION']}res/"
path = glob.glob(
root + f'**/{filename}',
@ -41,7 +42,7 @@ def get_dds_as_png(filename):
with image.Image(filename=path) as img:
img.compression = "no"
img.save(filename='app/cache/' + filename.split('.')[0] + '.png')
img.save(filename=current_app.config["CACHE_LOCATION"] + filename.split('.')[0] + '.png')
return send_file(cache)
@ -52,7 +53,7 @@ def get_dds(filename):
if filename.split('.')[-1] != 'dds':
return 404
root = 'app/luclient/res/'
root = f"{current_app.config['CLIENT_LOCATION']}res/"
dds = glob.glob(
root + f'**/{filename}',
@ -65,12 +66,17 @@ 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
)[0]
)
if render_component_id is not None:
render_component_id = render_component_id[0]
else:
return redirect(url_for('luclient.unknown'))
# find the asset from rendercomponent given the component id
filename = query_cdclient(
@ -84,10 +90,10 @@ def get_icon_lot(id):
else:
return redirect(url_for('luclient.unknown'))
cache = f'app/cache/{filename.split(".")[0]}.png'
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
if not os.path.exists(cache):
root = 'app/luclient/res/'
root = f"{current_app.config['CLIENT_LOCATION']}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:
@ -111,10 +117,10 @@ def get_icon_iconid(id):
filename = filename.replace("..\\", "").replace("\\", "/")
cache = f'app/cache/{filename.split(".")[0]}.png'
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
if not os.path.exists(cache):
root = 'app/luclient/res/'
root = f"{current_app.config['CLIENT_LOCATION']}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:
@ -132,14 +138,14 @@ def brick_list():
brick_list = []
if len(brick_list) == 0:
suffixes = [".g", ".g1", ".g2", ".g3", ".xml"]
res = pathlib.Path('app/luclient/res/')
cache = pathlib.Path(f"{current_app.config['CACHE_LOCATION']}")
# Load g files
for path in res.rglob("*.*"):
for path in cache.rglob("*.*"):
if str(path.suffix) in suffixes:
brick_list.append(
{
"type": "file",
"name": str(path.as_posix()).replace("app/luclient/res/", "")
"name": str(path.as_posix()).replace(f"{current_app.config['CACHE_LOCATION']}", "")
}
)
response = make_response(json.dumps(brick_list))
@ -151,7 +157,7 @@ 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'app/luclient/res/{req_path}').resolve()))
rel_path = pathlib.Path(str(pathlib.Path(f"{current_app.config['CACHE_LOCATION']}/{req_path}").resolve()))
# Return 404 if path doesn't exist
if not rel_path.exists():
return abort(404)
@ -168,10 +174,10 @@ def dir_listing(req_path):
def unknown():
filename = "textures/ui/inventory/unknown.dds"
cache = f'app/cache/{filename.split(".")[0]}.png'
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
if not os.path.exists(cache):
root = 'app/luclient/res/'
root = f"{current_app.config['CLIENT_LOCATION']}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:
@ -191,7 +197,16 @@ def get_cdclient():
"""
cdclient = getattr(g, '_cdclient', None)
if cdclient is None:
cdclient = g._database = sqlite3.connect('app/luclient/res/cdclient.sqlite')
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
@ -223,7 +238,7 @@ def translate_from_locale(trans_string):
locale_data = ""
if not locale:
locale_path = "app/luclient/locale/locale.xml"
locale_path = f"{current_app.config['CLIENT_LOCATION']}locale/locale.xml"
with open(locale_path, 'r') as file:
locale_data = file.read()
@ -281,6 +296,7 @@ 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)),
@ -308,7 +324,9 @@ def register_luclient_jinja_helpers(app):
'select component_id from ComponentsRegistry where component_type = 11 and id = ?',
[lot_id],
one=True
)[0]
)
if render_component_id:
render_component_id = render_component_id[0]
rarity = query_cdclient(
'select rarity from ItemComponent where id = ?',

View File

@ -69,8 +69,7 @@ def send():
form.recipient.choices.append((character.id, character.name))
items = query_cdclient(
'Select id, name, displayName from Objects where type = ?',
["Loot"]
'Select id, name, displayName from Objects where type = "Loot"'
)
for item in items:

View File

@ -2,13 +2,12 @@ from flask import render_template, Blueprint, send_from_directory
from flask_user import current_user, login_required
from app.models import Account, CharacterInfo, ActivityLog
from app.schemas import AccountSchema, CharacterInfoSchema
import datetime
import time
main_blueprint = Blueprint('main', __name__)
account_schema = AccountSchema()
char_info_schema = CharacterInfoSchema()
@main_blueprint.route('/', methods=['GET'])
def index():
@ -28,12 +27,16 @@ def index():
@login_required
def about():
"""About Page"""
mods = Account.query.filter(Account.gm_level > 0).order_by(Account.gm_level.desc()).all()
mods = Account.query.filter(Account.gm_level > 1).order_by(Account.gm_level.desc()).all()
online = 0
chars = CharacterInfo.query.all()
users = []
zones = {}
twodaysago = time.mktime((datetime.datetime.now() - datetime.timedelta(days=2)).timetuple())
chars = CharacterInfo.query.filter(CharacterInfo.last_login >= twodaysago).all()
for char in chars:
last_log = ActivityLog.query.with_entities(
ActivityLog.activity
ActivityLog.activity, ActivityLog.map_id
).filter(
ActivityLog.character_id == char.id
).order_by(ActivityLog.id.desc()).first()
@ -41,8 +44,13 @@ 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)
return render_template('main/about.html.j2', mods=mods, online=online, users=users, zones=zones)
@main_blueprint.route('/favicon.ico')

View File

@ -4,10 +4,9 @@ from flask_user import UserMixin
from wtforms import ValidationError
import logging
from flask_sqlalchemy import BaseQuery
from flask_sqlalchemy.query import Query
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import OperationalError, StatementError
from sqlalchemy.types import JSON
from time import sleep
import random
import string
@ -15,7 +14,7 @@ import string
# retrying query to work around python trash collector
# killing connections of other gunicorn workers
class RetryingQuery(BaseQuery):
class RetryingQuery(Query):
__retry_count__ = 3
def __init__(self, *args, **kwargs):
@ -114,7 +113,7 @@ class PlayKey(db.Model):
)
db.session.add(new_key)
db.session.commit()
return key
return key
def delete(self):
db.session.delete(self)
@ -1018,7 +1017,7 @@ class Reports(db.Model):
__tablename__ = 'reports'
data = db.Column(
JSON(),
mysql.MEDIUMBLOB(),
nullable=False
)

View File

@ -11,14 +11,14 @@ play_keys_blueprint = Blueprint('play_keys', __name__)
# Key creation page
@play_keys_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(9)
@gm_level(5)
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(9)
@gm_level(5)
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(9)
@gm_level(5)
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(9)
@gm_level(5)
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(9)
@gm_level(5)
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(9)
@gm_level(5)
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(9)
@gm_level(5)
def get():
columns = [
ColumnDT(PlayKey.id),

View File

@ -13,7 +13,6 @@ 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.schemas import PropertySchema
from app import gm_level, log_audit
from app.luclient import query_cdclient
from app.forms import RejectPropertyForm
@ -24,9 +23,6 @@ import pathlib
property_blueprint = Blueprint('properties', __name__)
property_schema = PropertySchema()
@property_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)
@ -411,25 +407,37 @@ def download_model(id):
def ugc(content):
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
uncompressed_lxfml = zlib.decompress(ugc_data.lxfml)
uncompressed_lxfml = 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 prebuilt(content, file_format, lod):
# translate LOT to component id
# we need to get a type of 2 because reasons
# 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
# find the asset from rendercomponent given the component id
filename = query_cdclient(
'select render_asset from RenderComponent where id = ?',
[render_component_id],
one=True
)
# if we have a valie filename, coerce it
if filename:
filename = filename[0].split("\\\\")[-1].lower().split(".")[0]
if "/" in filename:
@ -437,25 +445,29 @@ def prebuilt(content, file_format, lod):
else:
return f"No filename for LOT {content.lot}"
lxfml = pathlib.Path(f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml')
# 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')
if file_format == "lxfml":
with open(lxfml, 'r') as file:
lxfml_data = file.read()
response = make_response(lxfml_data)
# else we handle getting the files for lddviewer
elif file_format in ["obj", "mtl"]:
cache = pathlib.Path(f'app/cache/BrickModels/{filename}.lod{lod}.{file_format}')
# check to see if the file exists
cache = pathlib.Path(f'{current_app.config["CACHE_LOCATION"]}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}")
# then just read it
with open(str(cache.as_posix()), 'r') as file:
cache_data = file.read()
# and serve it
response = make_response(cache_data)
else:

View File

@ -7,6 +7,7 @@ import math
import struct
import zipfile
from xml.dom import minidom
from flask import current_app
PRIMITIVEPATH = '/Primitives/'
GEOMETRIEPATH = PRIMITIVEPATH
@ -968,8 +969,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="app/luclient/res/", lod=lod)
converter.LoadDBFolder(dbfolderlocation="app/luclient/res/")
setDBFolderVars(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}", lod=lod)
converter.LoadDBFolder(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}")
converter.LoadScene(filename=lxf_filename)
converter.Export(filename=obj_filename)

View File

@ -4,9 +4,7 @@ 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 datetime
import xmltodict
import random
import xmltodict, gzip, json, datetime
reports_blueprint = Blueprint('reports', __name__)
@ -32,7 +30,6 @@ def index():
).filter(
Reports.report_type == "uscore"
).group_by(Reports.date).options(load_only(Reports.date)).all()
return render_template(
'reports/index.html.j2',
reports_items=reports_items,
@ -46,6 +43,8 @@ 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)
@ -64,25 +63,31 @@ 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)
# make it
for key, value in items.items():
data = []
for entry in entries:
if key in entry.data.keys():
data.append(entry.data[key])
else:
data.append(0)
color = "#" + value.encode("utf-8").hex()[1:7]
if max(data) > 10:
datasets.append({
"label": value,
"data": data,
"backgroundColor": color,
"borderColor": color
})
if value:
data = []
for entry in entries:
if key in entry.data.keys():
if not isinstance(entry.data[key], int):
data.append(entry.data[key]["item_count"])
else:
data.append(entry.data[key])
else:
data.append(0)
color = "#" + value.encode("utf-8").hex()[1:7]
if max(data) > 10:
datasets.append({
"label": value,
"data": data,
"backgroundColor": color,
"borderColor": color
})
return render_template(
'reports/graph.html.j2',
@ -102,6 +107,8 @@ 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)
@ -119,6 +126,8 @@ 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 = []
@ -153,6 +162,8 @@ 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)
@ -170,6 +181,8 @@ 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 = []
@ -224,6 +237,7 @@ def gen_item_report():
report_data = {}
for char_xml in char_xmls:
name = CharacterInfo.query.filter(CharacterInfo.id == char_xml.id).first().name
try:
character_json = xmltodict.parse(
char_xml.xml_data,
@ -233,15 +247,31 @@ def gen_item_report():
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"]] = report_data[item["attr_l"]] + int(item["attr_c"])
if ("attr_c" in item):
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"] = report_data[item["attr_l"]]["item_count"] + 1
else:
report_data[item["attr_l"]] = int(item["attr_c"])
if ("attr_c" in item):
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
else:
report_data[item["attr_l"]] = {"item_count": 1, "chars": {}}
if name in report_data[item["attr_l"]]["chars"]:
if ("attr_c" in item):
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] = report_data[item["attr_l"]]["chars"][name] + 1
else:
if ("attr_c" in item):
report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
else:
report_data[item["attr_l"]]["chars"][name] = 1
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=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="items",
date=date
)
@ -289,7 +319,7 @@ def gen_currency_report():
current_app.logger.error(f"REPORT::CURRENCY - {e}")
new_report = Reports(
data=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="currency",
date=date
)
@ -337,7 +367,7 @@ def gen_uscore_report():
current_app.logger.error(f"REPORT::U-SCORE - {e}")
new_report = Reports(
data=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="uscore",
date=date
)

View File

@ -1,130 +0,0 @@
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())

View File

@ -7,8 +7,14 @@ APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
APP_SECRET_KEY = ""
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
# Send Analytics for Developers to better fix issues
ALLOW_ANALYTICS = False
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 = ""
# Flask settings
CSRF_ENABLED = True
@ -52,3 +58,16 @@ 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'}

0
app/static/css/.gitkeep Normal file
View File

View File

@ -14,17 +14,13 @@
<tr>
<th>Actions</th>
<th>Name</th>
{% if config.USER_ENABLE_EMAIL %}
<th>Email</th>
{% endif %}
<th>Email</th>
<th>GM Level</th>
<th>Locked</th>
<th>Banned</th>
<th>Muted</th>
<th>Registered</th>
{% if config.USER_ENABLE_EMAIL %}
<th>Email Confirmed</th>
{% endif %}
<th>Email Confirmed</th>
</tr>
</thead>
<tbody></tbody>

View File

@ -1,188 +0,0 @@
{% 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 %}}

View File

@ -90,30 +90,29 @@
let target_nav = '#{{request.endpoint}}'.replace('\.', '-');
$(target_nav).addClass('active');
});
// make tooltips with data work
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
{% 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 %}
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);
});
}
</script>
{% if config.ALLOW_ANALYTICS %}
<!-- Matomo no js analytics -->
<noscript><p><img src="https://matomo.aronwk.com/matomo.php?idsite=3&amp;rec=1" style="border:0;" alt="" /></p></noscript>
{% endif %}
{% endblock %}
</body>

View File

@ -0,0 +1,34 @@
{% 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 %}

View File

@ -15,8 +15,9 @@
{% include 'partials/_character.html.j2' %}
{% endwith %}
</div>
<div class="col-sm">
{% include 'partials/_charxml.html.j2'%}
<div class="col-sm" id="charxml">
Loading Character Data
{% include 'partials/_loading.html' %}
</div>
</div>
{% endblock content %}
@ -32,3 +33,14 @@
{% 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 %}

View File

@ -60,7 +60,12 @@
{# Remember me #}
{% if user_manager.USER_ENABLE_REMEMBER_ME %}
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
{% endif %}
{# recaptcha #}
{% if config.RECAPTCHA_ENABLE %}
{{ render_field(form.recaptcha, tabindex=250) }}
{% endif %}
{# Submit button #}

View File

@ -29,6 +29,11 @@
{{ 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>
@ -63,6 +68,11 @@
{{ 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>

View File

@ -47,6 +47,11 @@
{{ 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>

View File

@ -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 == 9 and config.REQUIRE_PLAY_KEY %}
{% if current_user.is_authenticated and current_user.gm_level >= 5 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.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.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.audit') }}'>Audit Log</a>
<a class="dropdown-item text-center" href='{{ url_for('log.system') }}'>System Log</a>
{% endif %}

View File

@ -35,29 +35,6 @@
<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&amp;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'

View File

@ -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>

View File

@ -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>

View File

@ -4,9 +4,47 @@
{% 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>
@ -24,7 +62,26 @@
</div>
{% endfor %}
<hr>
</div>
</div>
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
<div class="card-body">
<h4 class="text-center">Links</h4>
{% if config.CONFIG_LINK %}
<div class="row">
<div class="col text-right">
{{ config.CONFIG_LINK_TITLE }}
</div>
<div class="col">
<a href="{{ url_for('static', filename=config.CONFIG_LINK_HREF) }}">
{{ config.CONFIG_LINK_TEXT }}
</a>
</div>
</div>
{% endif %}
<div class="row">
<div class="col text-right">
Source

View File

@ -108,6 +108,12 @@
</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 current_user.gm_level > 3 and config.REQUIRE_PLAY_KEY %}

View File

@ -41,29 +41,35 @@
</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>
<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>
</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>
<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>
{% 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>
{% endif %}
{% if current_user.gm_level > 2 %}

View File

@ -11,9 +11,11 @@
<div class="col text-right">
U-Score: {{ character_json.obj.char.attr_ls }}
</div>
<div class="col">
Level: {{ character_json.obj.lvl.attr_l }}
</div>
{% if "lvl" in character_json.obj %}
<div class="col">
Level: {{ character_json.obj.lvl.attr_l }}
</div>
{% endif %}
</div>
<br/>
<div class="row">
@ -63,9 +65,13 @@
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"/>
@ -108,63 +114,33 @@
<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 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
<div class="tab-pane fade" id="nav-vault" role="tabpanel" aria-labelledby="nav-vault-tab">
{# Inv ID 1 - Index: 1 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
<div class="tab-pane fade" id="nav-vault-models" role="tabpanel" aria-labelledby="nav-vault-models-tab">
{# Inv ID 14 - Index: 10 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
<div class="tab-pane fade" id="nav-bricks" role="tabpanel" aria-labelledby="nav-bricks-tab">
{# Inv ID 2 - Index: 2 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
<div class="tab-pane fade" id="nav-models" role="tabpanel" aria-labelledby="nav-models-tab">
{# Inv ID 5 - Index: 6 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
<div class="tab-pane fade" id="nav-behaviors" role="tabpanel" aria-labelledby="nav-behaviors-tab">
{# Inv ID 7 - Index: 8 #}
{% 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 %}
Loading Inventory
{% include 'partials/_loading.html' %}
</div>
</div>
</div>
@ -187,10 +163,14 @@
</button>
</div>
<div class="modal-body">
{% 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 %}
{% 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 %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
@ -219,3 +199,40 @@
</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>

View File

@ -14,7 +14,7 @@
{% if gm_level==0 %}
Player
{% elif gm_level==1 %}
Key Distributor
Elevevated Civilan {# Unused #}
{% elif gm_level==2 %}
Junior Moderator
{% elif gm_level==3 %}

View File

@ -0,0 +1,21 @@
<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>

View File

@ -77,6 +77,14 @@
{{ 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">

View File

@ -4,21 +4,29 @@
alt="{{ inv_item.attr_l|get_lot_name }}"
class="border p-1 border-primary rounded m-1"
width="60"
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
{% if 'attr_eq' in inv_item %}
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
{% endif %}
height="60"
data-html="true"
data-toggle="tooltip"
data-placement="left"
title="{% include 'partials/charxml/_item_tooltip.html.j2' %}"
>
{% if inv_item.attr_c != "1" %}
{% if 'attr_c' in inv_item %}
<span class="inventory-count text-bold">
{{ inv_item.attr_c }}
{%if inv_item.attr_c|int > 999 %}
+999
{% elif inv_item.attr_c|int > 1 %}
{{ inv_item.attr_c }}
{% endif %}
</span>
{% endif %}
{% if inv_item.attr_b == "true" %}
<span class="inventory-lock">
<i class='fas fa-lock'></i>
</span>
{% if 'attr_b' in inv_item %}
{% if inv_item.attr_b == "true" %}
<span class="inventory-lock">
<i class='fas fa-lock'></i>
</span>
{% endif %}
{% endif %}
</div>

View File

@ -0,0 +1,9 @@
{% 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 %}

View File

@ -66,3 +66,8 @@
{% endwith %}
{% endif %}
{% if 'attr_c' in inv_item %}
{%if inv_item.attr_c|int > 999 %}
<br />Count: {{ inv_item.attr_c|numberFormat }}
{% endif %}
{% endif %}

View File

@ -20,18 +20,34 @@
<th scope="col">
Count
</th>
<th scope="col">
Breakdown
</th>
<th scope="col">
Rarity
</th>
</thead>
<tbody>
{% for lot, count in data.items() %}
{% for lot, details in data.items() %}
<tr>
<td>
{{ lot|get_lot_name }}
</td>
<td>
{{ count }}
{% if details.chars %}
{{ details.item_count }}
{% else %}
{{ details }}
{% endif %}
</td>
<td>
{% if details.chars %}
{% for char, value in details.chars|dictsort(false, 'value')|reverse %}
{{char}}: {{value}}<br/>
{% endfor %}
{% else %}
Missing
{% endif %}
</td>
<td>
{{ lot|get_lot_rarity }}

2
entrypoint.bat Normal file
View File

@ -0,0 +1,2 @@
python3 -m flask db upgrade
python3 wsgi.py

View File

@ -1,8 +1,5 @@
#!/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

0
logs/.gitkeep Normal file
View File

View File

@ -22,8 +22,8 @@ logger = logging.getLogger('alembic.env')
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
current_app.config.get('SQLALCHEMY_DATABASE_URI')
)
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,

View File

@ -0,0 +1,164 @@
"""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 ###

View File

@ -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,14 +103,13 @@ 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'])
op.create_foreign_key(None, 'bug_reports', 'accounts', ['resoleved_by_id'], ['id'], ondelete='CASCADE')
if 'charinfo' not in tables:
op.create_table('charinfo',

View File

@ -0,0 +1,33 @@
"""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 ###

View File

@ -1,61 +1,20 @@
alembic==1.7.5
APScheduler==3.8.1
astroid==2.9.1
autopep8==1.6.0
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==2.0.1
Flask==3.0.0
Flask-APScheduler==1.12.3
Flask-Assets==2.0
Flask-Login==0.5.0
Flask-Mail==0.9.1
flask-marshmallow==0.14.0
Flask-Assets==2.1.0
Flask-Migrate==3.1.0
Flask-SQLAlchemy==2.5.1
Flask-SQLAlchemy==3.1.1
Flask-User==1.0.2.2
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
Flask-WTF==1.2.1
gunicorn==21.2.0
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
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==2.0.23
sqlalchemy-datatables==2.0.1
toml==0.10.2
tzdata==2021.5
tzlocal==4.1
visitor==0.1.3
Wand==0.6.7
webassets==2.0
Werkzeug==2.0.1
wrapt==1.13.3
Werkzeug==3.0.1
WTForms==3.0.0
xmltodict==0.12.0

View File

@ -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('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
file_handler = RotatingFileHandler('logs/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)