157 Commits

Author SHA1 Message Date
aronwk-aaron
59dad04c60 update gitignore 2022-12-18 17:23:23 -06:00
Aaron Kimbre
92f7e5ae52 cdclient and some analysis commands 2022-07-14 08:31:31 -05:00
Aaron Kimbre
62fb9a3c01 MORE 2022-06-10 12:40:21 -05:00
Aaron Kimbre
200370709c Merge branch 'main' into issue-31 2022-06-09 09:13:08 -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
0b04dab1d2 remove print, fix some stuff 2022-06-06 23:08:56 -05:00
Aaron Kimbre
f7703abe5e Progress 2022-05-29 20:12:32 -05:00
Aaron Kimbre
f8332dc065 progress 2022-05-29 16:54:10 -05:00
Aaron Kimbre
1b4887f73e IT HAS BEGUN 2022-05-29 01:49:19 -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
Aaron Kimbre
0c18d03aa7 readme update 2022-05-14 09:04:47 -05:00
Aaron Kimbre
ded0e55501 Resolves #33 2022-05-14 08:53:23 -05:00
Aaron Kimbrell
3668ca8792 Merge pull request #32 from DarkflameUniverse/local-ldd-db
Local ldd db
2022-05-11 21:34:17 -05:00
Aaron Kimbre
a07515cdb2 fixes it 2022-05-11 21:33:31 -05:00
Aaron Kimbre
35ee0ed457 first pass, maybe doesn't work 2022-05-11 20:37:22 -05:00
Aaron Kimbre
7b4e11d65b better manual setup guide from HailStorm32 2022-05-11 13:28:55 -05:00
Aaron Kimbre
07136ef283 Resolves #30 2022-05-11 13:05:18 -05:00
Aaron Kimbre
1dbe0ce980 Resolves #28 2022-05-11 12:49:42 -05:00
Aaron Kimbrell
1dbb637053 Merge pull request #29 from FlareCO/main
Update ldd.html.j2
2022-05-02 08:44:19 -05:00
FlareCO
26111d0bcf Update ldd.html.j2 2022-05-01 22:14:05 +02:00
FlareCO
4124d98853 Update ldd.html.j2 2022-05-01 02:51:29 +02:00
Aaron Kimbre
ef312fc744 return models to people 2022-04-13 22:29:57 -05:00
Aaron Kimbre
1790a8b060 extend prop clone id fix to remove duped props 2022-04-13 21:29:08 -05:00
Aaron Kimbre
02bb6bb0aa Don't include reject properties in unapprove items 2022-04-10 18:32:13 -05:00
Aaron Kimbre
8dd4a2c80e no logrithymic 2022-04-08 23:43:55 -05:00
Aaron Kimbre
6f4b2ef574 fix acount banning 2022-04-08 23:13:33 -05:00
Aaron Kimbre
33d24745e1 make key appear in flash if making a single key 2022-04-04 18:10:17 -05:00
Aaron Kimbre
8e58b0bc1f fix pet name associated 2022-04-04 17:51:25 -05:00
Aaron Kimbre
413a2c06a4 explicitly set these not so there's no guessing 2022-04-03 15:54:23 -05:00
Aaron Kimbre
c32da45a16 actually fix it 2022-04-03 15:47:42 -05:00
Aaron Kimbre
8ad6249275 fixed active togglign with locked and ban 2022-04-03 15:47:21 -05:00
Aaron Kimbre
65ce84e090 remove print statement 2022-04-03 15:12:24 -05:00
Aaron Kimbre
d66bdee575 fix item sowing for all users 2022-04-03 13:57:55 -05:00
Aaron Kimbre
72be25f5b5 order by gmlevel 2022-04-03 13:42:06 -05:00
Aaron Kimbre
63d5d2da87 refine it a bit 2022-04-03 13:33:33 -05:00
Aaron Kimbre
e73d1ecd53 online player count and mod list on about page 2022-04-03 13:31:43 -05:00
Aaron Kimbre
5492ae6668 fixed typos in text 2022-04-03 12:30:50 -05:00
Aaron Kimbre
7b27cab25d missed one 2022-04-03 12:25:50 -05:00
Aaron Kimbre
b6cad4acae actually fix bug report view and resolve 2022-04-03 12:25:18 -05:00
Aaron Kimbre
900a3cbf3c fixe resolve status display 2022-04-03 12:18:39 -05:00
Aaron Kimbre
993e5dcfe4 fix resolve as well 2022-04-03 12:16:58 -05:00
Aaron Kimbre
2dc5437fc3 use vw instead of rem 2022-04-03 12:15:45 -05:00
Aaron Kimbre
c94d1cc62c widen bug report card 2022-04-03 12:13:01 -05:00
Aaron Kimbre
f73ce143a1 fix resolve button 2022-04-03 12:11:23 -05:00
Aaron Kimbre
e5428dcd4d switch all to be the default if filter is invalid 2022-04-03 12:00:14 -05:00
Aaron Kimbre
44689aad36 remove gm req on bug get endpoint 2022-04-03 11:56:15 -05:00
Aaron Kimbre
f1d79f1e90 cleanup and fixes 2022-04-03 11:35:53 -05:00
Aaron Kimbre
1113295c5b allow players to view their bug reports 2022-04-02 22:32:22 -05:00
Aaron Kimbre
e45fb3233a fix displaying reporter in bug reports 2022-04-02 21:35:57 -05:00
Aaron Kimbre
a3894c3450 Fix missing column on property moderation 2022-04-02 21:24:59 -05:00
Aaron Kimbre
6fa7ade6a3 add reporter id to bug reports
fix moderation status filters
2022-04-02 18:17:16 -05:00
Aaron Kimbre
70aed00d97 added command to fix legacy property issues 2022-04-02 16:33:23 -05:00
Aaron Kimbre
f8d5928c86 make ui more consistent 2022-04-01 10:49:11 -05:00
Aaron Kimbre
5ac384663b shorten subject 2022-04-01 10:41:48 -05:00
Aaron Kimbre
a79631be8e go back to useing slightly scruffed formatting 2022-04-01 10:40:07 -05:00
Aaron Kimbre
ef7da76629 fix 2022-04-01 10:35:59 -05:00
Aaron Kimbre
520c5f2d40 fix string formatting to db 2022-04-01 10:31:59 -05:00
Aaron Kimbre
27c4f1a7f2 Property rejection handling
fixe some table to be wider
make search and pagination float right
2022-04-01 10:02:02 -05:00
Aaron Kimbre
7f992a5dfa Add property reputation handling 2022-03-31 18:05:28 -05:00
Aaron Kimbre
560afa5e0d add imagemagick note to manual setup 2022-03-31 12:58:39 -05:00
Aaron Kimbre
17c6d5bc1a resolves #27
and fixes caching
2022-03-31 12:56:28 -05:00
Aaron Kimbre
be80d7b2ab uncomment error handler 2022-03-28 13:52:01 -05:00
Aaron Kimbre
521a10756e make some queries not crash on edge cases 2022-03-28 11:47:42 -05:00
Aaron Kimbre
b6905c9235 fix play key times used on view page
Added early API listener enpionts
2022-03-28 11:01:46 -05:00
Aaron Kimbrell
faff08b160 Merge pull request #24 from moliata/main
Rename characterxml.json
2022-03-22 12:38:20 -05:00
Ben
c8b21ae6a6 fix: characterxml.json filename 2022-03-22 15:34:02 +02:00
Aaron Kimbre
179ea43cb8 remove check, let flask handle the failure 2022-03-14 18:47:19 -05:00
Aaron Kimbre
1566df65cb better sanity checking 2022-03-14 18:42:49 -05:00
Aaron Kimbre
4d62e70810 log scale x axis 2022-03-14 13:07:50 -05:00
Aaron Kimbre
24e98190fe graphs
Resolves #21
2022-03-13 22:10:07 -05:00
Aaron Kimbre
6606f1558e fix config sanity check
add chartjs
2022-03-13 19:39:16 -05:00
Aaron Kimbre
5b949e02d1 update readme 2022-03-13 19:28:24 -05:00
Aaron Kimbre
f0faa4c53b fallback to example settings
if no setting file is given
2022-03-13 19:27:23 -05:00
Aaron Kimbre
418c81944b make active useful
Readme update
parity between setting and env settings
2022-03-13 15:09:34 -05:00
Aaron Kimbre
8285f02ed8 comment out excessive pet maint logs 2022-03-13 07:25:34 -05:00
Aaron Kimbre
ede3642efe fix logging issues 2022-03-13 07:03:50 -05:00
Aaron Kimbre
3fed8cb02f fixes from linting changes 2022-03-13 06:27:10 -05:00
Aaron Kimbre
7cb0b94972 fix exception 2022-03-13 06:24:41 -05:00
Aaron Kimbre
1cb64f7594 adjust init an settings to line up with file 2022-03-12 20:38:24 -06:00
Aaron Kimbre
33739456df fraction out settings 2022-03-12 20:34:04 -06:00
Aaron Kimbre
5ce9ac85bc syntax/linting fixes 2022-03-12 20:09:35 -06:00
Aaron Kimbre
70742549b9 make reports look at vault items 2022-03-03 11:18:22 -06:00
Aaron Kimbre
039b45c824 fix edge cases in report generation
with xml parsing
error > system
disable email by default
2022-02-25 11:46:13 -06:00
Aaron Kimbre
9452e8c801 add logging to pet name maintenance
fix petname moderation
2022-02-23 15:14:39 -06:00
Aaron Kimbre
09363fcd06 ui consistency 2022-02-20 08:50:33 -06:00
Aaron Kimbrell
6303a86778 Merge pull request #22 from DarkflameUniverse/character-rescue
Character rescue
2022-02-20 08:39:03 -06:00
Aaron Kimbre
7f37343de4 change the displaed world they are in 2022-02-20 08:35:58 -06:00
Aaron Kimbre
78ab5b93bb use the lwid
which overrides the lzid
2022-02-20 08:18:18 -06:00
Aaron Kimbre
76c862a2ba debug 2022-02-19 22:45:37 -06:00
Aaron Kimbre
04f4e833a4 Merge branch 'main' into character-rescue 2022-02-19 22:37:34 -06:00
Aaron Kimbre
a66e4aaf80 first pass 2022-02-19 22:36:33 -06:00
Aaron Kimbre
e622c44042 fix nulltype object error 2022-02-19 21:48:36 -06:00
Aaron Kimbre
819d51c68a fix import 2022-02-16 13:39:55 -06:00
Aaron Kimbre
94411568ce fix logging when under gunicorn
fis returns in reports
2022-02-16 13:33:12 -06:00
Aaron Kimbre
b829d666c9 fix reports app context 2022-02-16 12:09:40 -06:00
Aaron Kimbre
2e82f94b9d fix context issue 2022-02-14 13:58:14 -06:00
Aaron Kimbre
b66d2748b4 fixed automoderator scheduler 2022-02-13 08:14:13 -06:00
Aaron Kimbre
78ed39905f sensible fix for pet names
and automoderation for pet names
2022-02-12 21:37:11 -06:00
Aaron Kimbre
d63dd807e3 Handle deleted chars in pet name association
Fix pre-built obj generation
2022-02-12 19:09:45 -06:00
Aaron Kimbrell
e9aef0219e Merge pull request #19 from DarkflameUniverse/v1-polish 2022-02-12 15:37:28 -06:00
Aaron Kimbre
d77eaf16d1 Audit log view
move error logs to logs
Add links for new logs in header
2022-02-12 15:30:43 -06:00
Aaron Kimbre
24a398b89c improve flaks/audit for pet names 2022-02-12 00:38:22 -06:00
Aaron Kimbrell
26cc2d431c Merge pull request #18 from DarkflameUniverse/flask-apscheduler-quiet
quiet warnings by setting timeze
2022-02-12 00:31:37 -06:00
Aaron Kimbre
8a019c340e quiet warnings by setting timeze 2022-02-12 00:30:12 -06:00
Aaron Kimbrell
0dbe9d87fc Merge pull request #17 from DarkflameUniverse/pet_owner
Allow pet names to be associated with characters
2022-02-11 23:43:27 -06:00
Aaron Kimbre
d792596926 Allow pet names to be associated with characters 2022-02-11 23:40:15 -06:00
Aaron Kimbrell
9cc3dbb4c4 Merge pull request #15 from DarkflameUniverse/audit-log
Audit log
2022-02-11 23:06:45 -06:00
Aaron Kimbre
de698f86fa Added Audit logs for everything 2022-02-11 23:05:00 -06:00
Aaron Kimbre
2e8ce8ea19 fix 2022-02-11 21:48:16 -06:00
Aaron Kimbre
91e6ed33e7 autdit log model 2022-02-11 21:45:07 -06:00
Aaron Kimbre
f0cbcee100 Fix log viewing 2022-02-11 21:43:38 -06:00
Aaron Kimbrell
506d4ac090 Merge pull request #13 from DarkflameUniverse/logging
Logging
2022-02-11 21:09:48 -06:00
Aaron Kimbre
121c407cdb simple web log viewing for admins 2022-02-11 21:08:06 -06:00
Aaron Kimbre
d3c8c5d77b template 2022-02-11 20:54:11 -06:00
Aaron Kimbre
8aec4a45d2 General Logging
some extra logging around problem areas
2022-02-11 20:31:57 -06:00
Aaron Kimbrell
87384c1b98 Merge pull request #12 from DarkflameUniverse/property-viewer-v2
Property viewer v2
2022-02-11 17:51:40 -06:00
Aaron Kimbre
f0df357258 Fix layout a bit 2022-02-11 17:51:07 -06:00
Aaron Kimbre
869bbdf7e6 Lod picker 2022-02-11 17:37:43 -06:00
Aaron Kimbre
8646c40943 lod differentiation
pre-gen commands
2022-02-10 12:01:51 -06:00
Aaron Kimbre
ba0c4801df remove Audit model 2022-02-04 09:55:32 -06:00
Aaron Kimbre
b0e76ce691 encode quotes 2022-02-03 19:06:25 -06:00
Aaron Kimbre
cf359e2c6b apparently some skills have no icon
add jinja debug helper
restrict char_xml to gm 9
strip quoted from local desc
2022-02-03 16:37:34 -06:00
Aaron Kimbre
c6d624e154 lock getting char_xml behind gm 9 2022-02-02 21:21:19 -06:00
Aaron Kimbre
0066e0ea2d added char_xml download
fix bad call in luclient.get_lot_name
Don't show rank for rank 0
fixes
2022-02-02 21:18:21 -06:00
Aaron Kimbre
3df9f143ed handle searchable when disabling email support 2022-02-02 19:20:58 -06:00
Aaron Kimbre
649c986345 fix non-value error state 2022-02-02 18:16:53 -06:00
Aaron Kimbre
172bbe5b60 prop moderation filters are a bit more sane 2022-01-27 00:10:17 -06:00
Aaron Kimbre
7194a04c5d Property importing, maybe
fix issue with color fallback
2022-01-26 23:11:49 -06:00
Aaron Kimbre
7bb0e46e56 don't use super-three
faster loading through geometry re-use
2022-01-26 19:36:41 -06:00
Aaron Kimbre
2a5fad3c87 Fix ldd loader imports 2022-01-26 19:09:02 -06:00
Aaron Kimbre
ac835de53b fix caching
wrong assumption that every filename was unique
2022-01-21 11:30:43 -06:00
Aaron Kimbre
fba782fd96 reverse order all lists of buttons 2022-01-19 00:22:43 -06:00
Aaron Kimbre
59db1df604 with quotes? 2022-01-18 23:28:28 -06:00
Aaron Kimbre
aede0f4f1a missed references 2022-01-18 23:23:13 -06:00
Aaron Kimbre
1b5f002d6d listsize: 1 2022-01-18 23:18:11 -06:00
Aaron Kimbrell
256ea19bfc Merge pull request #6 from DarkflameUniverse/jenkins-testing
Jenkins testing
2022-01-18 23:05:23 -06:00
Aaron Kimbre
4bda98670b Didn't work
use GIT_BRANCH as branch var
2022-01-18 23:04:32 -06:00
Aaron Kimbre
6deb02b11d single quotes to fix insecure string interpolation 2022-01-18 22:52:11 -06:00
Aaron Kimbrell
d6d8bb085d Merge pull request #5 from DarkflameUniverse/reports-new-to-old
reverse list order while displaying buttons
2022-01-18 22:48:54 -06:00
Aaron Kimbre
0fa91c9cfa sane jenkins description 2022-01-18 22:47:17 -06:00
Aaron Kimbre
8cc630d420 reverse list order while displaying buttons 2022-01-18 22:34:40 -06:00
Aaron Kimbrell
7f7b854b8b Merge pull request #4 from DarkflameUniverse/analytics
Analytics Reporting to Matomo
2022-01-18 20:11:28 -06:00
Aaron Kimbre
81193499f4 for vendored code to not be applicable to linguist 2022-01-17 22:45:42 -06:00
Aaron Kimbre
dbe6da370a update Readme about analytics 2022-01-17 22:30:39 -06:00
Aaron Kimbre
6197f630e9 Analytics to Matomo 2022-01-17 21:55:48 -06:00
Aaron Kimbre
683a1d6f7d push uscore report page 2022-01-17 16:17:08 -06:00
Aaron Kimbre
ab25db182d U-Score reporting 2022-01-16 22:12:23 -06:00
Aaron Kimbre
c183d1ff5a remove some debug code 2022-01-16 20:50:39 -06:00
Aaron Kimbre
de89647421 fix rarity helper 2022-01-16 20:37:08 -06:00
Aaron Kimbre
b2af6d967a Fix reports
Add currency report
make reports use json storage
Make tables nicer
2022-01-16 20:24:47 -06:00
Aaron Kimbre
1a5531eb19 simplier report storing 2022-01-16 18:49:28 -06:00
74 changed files with 10961 additions and 1016 deletions

8
.gitattributes vendored
View File

@@ -1,4 +1,4 @@
app/static/bootstrap-4.2.1/* linguist-vendored
app/static/bootswatch-master/* linguist-vendored
app/static/datatables/* linguist-vendored
app/static/font-awesome/* linguist-vendored
app/static/bootstrap-4.2.1/* linguist-vendored=true
app/static/datatables/* linguist-vendored=true
app/static/font-awesome/* linguist-vendored=true
app/static/chartjs/* linguist-vendored=true

8
.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
@@ -13,3 +13,9 @@ app/static/ldddb/*
locale.xml
app/luclient/*
app/cache/*
property_files/*
*.log
app/settings.py
*.exe
*.csv
*.sql

27
Jenkinsfile vendored
View File

@@ -5,22 +5,25 @@ properties([
branchFilter: 'origin/(.*)',
defaultValue: 'origin/main',
description: '',
name: 'BRANCH',
quickFilterEnabled: false,
name: 'GIT_BRANCH',
quickFilterEnabled: true,
selectedValue: 'DEFAULT',
sortMode: 'NONE',
tagFilter: '*',
useRepository: 'git@github.com:DarkflameUniverse/NexusDashboard.git',
type: 'PT_BRANCH'
type: 'PT_BRANCH',
listSize: "1"
)
])
])
node('worker'){
currentBuild.setDescription(params.GIT_BRANCH)
stage('Clone Code'){
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
branches: [[name: params.GIT_BRANCH]],
extensions: [],
userRemoteConfigs: [
[
@@ -31,19 +34,19 @@ node('worker'){
])
}
def tag = ''
stage("Build Container"){
if (params.BRANCH.contains('main')){
stage('Build Container'){
if (params.GIT_BRANCH.contains('main')){
tag = 'latest'
} else {
tag = params.BRANCH.replace('\\', '-')
tag = params.GIT_BRANCH.replace('\\', '-')
}
sh "docker build -t aronwk/nexus-dashboard:${tag} ."
sh "docker build -t aronwk/nexus-dashboard:$tag ."
}
stage("Push Container"){
stage('Push Container'){
withCredentials([usernamePassword(credentialsId: 'docker-hub-token', passwordVariable: 'password', usernameVariable: 'username')]) {
sh "docker login -u ${username} -p ${password}"
sh "docker push aronwk/nexus-dashboard:${tag}"
sh 'docker logout'
sh "docker login -u $username -p $password"
sh "docker push aronwk/nexus-dashboard:$tag"
sh "docker logout"
}
}
}

240
README.md
View File

@@ -1,69 +1,229 @@
# Nexus Dashboard
**This is a WIP: For Advanced Users**
<p align="center">
<img src="app/static/logo/logo.png" alt="Sublime's custom image"/>
<img src="app/static/logo/logo.png" alt="DLU logo"/>
</p>
## Features
* Account Management:
* Ban, Lock, and Mute accounts (This Mute affects all Characters)
* Account Deletion
* Email ( all optional ):
* Require email verification
* Reset Password via Email
* Edit Email ( by Admin only )
* User Registration
* Invitations ( TODO: Implement this )
* Invitation Only Registration ( TODO: Implement this )
* Play Key Management:
* Create, Edit, and Add notes to play keys
* View accounts Tied to a play key
* Character Management:
* Rescue: Pull character to a previously visited world
* Restrict Trade: Toggle the character's ability to trade
* Restrict Mail: Toggle the character's ability to send mail
* Restrict Chat: Toggle the character's ability to send chat messages
* Inventory viewer
* View backpack contents, vault, models, and more!
* Stats Viewer
* Moderation:
* Character Names:
* Approve and mark as needs rename
* Pet Names:
* Auto-moderation of Pet names based on already moderated names
* This is a scheduled tack that runs in the background every hour
* Character Association, to see who has requested what name
* Name cleanup: remove names of deleted pets/characters
* Properties:
* Approve and Un-approve Properties
* Property/Model viewer
* Pre-built and UGC model rendering
* View Properties in full 360 in the browser!
* View in LOD0 (High), LOD1(Medium), or LOD2(Low) quality
* Download models
* Bug Reports:
* View and Resolve bug reports
* Logs:
* Command: View commands that have been run
* Activity: View character activity of entering and exiting worlds
* Audit:
* View moderation activity (characters, pets, properties)
* View GM Level changes
* View Send Mail usage
* System: View Extra logging of background activities of Nexus Dashboard
* Send Mail:
* Send Mail to characters
* Attach items to Mail
* Economy Reports:
* Reports are generated as a scheduled background task run every day at 2300 UTC
* Accounts with GM Level 3 and above are ignored
* Item reports:
* Reports numbers of items in existence
* Includes backpack and Vault items
* Currency:
* 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**
## 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 \
-v /path/to/cachedir:/app/cache:rw \ # optional for persistent cache for conversions
aronwk/nexus-dashboard:latest
-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 \
-v /path/to/cachedir:/app/cache:rw \
aronwk/nexus-dashboard:latest
```
* /app/luclient must be mapped to the location of an unpacked client
* you only need `res/` and `locale/` from the client, but dropping the whole cleint in there won't hurt
* `/app/luclient` must be mapped to the location of an unpacked client
* you only need `res/` and `locale/` from the client, but dropping the whole client in there won't hurt
* Use `fdb_to_sqlite.py` in lcdr's utilities on `res/cdclient.fdb` in the unpacked client to convert the client database to `cdclient.sqlite`
* Put teh resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite`
* unzip `res/brickdb.zip` in-place
* **Docker will do this for you**
* you should have new folders and files in the following places:
* `res/Assemblies/../..` with a bunch of sub folders
* `res/Primitives/../..` with a bunch of sub folders
* `res/info.xml`
* `res/Materials.xml`
* Put the resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite`
### Environmental Variables
* Required:
Please Reference `app/settings_exmaple.py` to see all the variables
* Required:
* APP_SECRET_KEY (Must be provided)
* APP_DATABASE_URI (Must be provided)
* Optional
* USER_ENABLE_REGISTER (Default: True)
* USER_ENABLE_EMAIL (Default: True, Needs Mail to be configured)
* USER_ENABLE_CONFIRM_EMAIL (Default: True)
* USER_ENABLE_INVITE_USER (Default: False)
* USER_REQUIRE_INVITATION (Default: False)
* REQUIRE_PLAY_KEY (Default: True)
* MAIL_SERVER (Default: smtp.gmail.com)
* MAIL_PORT (Default: 587)
* MAIL_USE_SSL (Default: False)
* MAIL_USE_TLS (Default: True)
* MAIL_USERNAME (Default: None)
* MAIL_PASSWORD (Default: None)
* USER_EMAIL_SENDER_NAME (Default: None)
* USER_EMAIL_SENDER_EMAIL (Default: None)
* Everything else is optional and has defaults
## Manual
Don't, use Docker /s
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
`sudo apt-get update`
`sudo apt-get install -y python3 python3-pip sqlite3 git unzip libmagickwand-dev`
TODO: Make manual deployment easier to configure
> *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.
`cd` *make sure you are in the home directory*
`git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
You should now have a directory called `NexusDashboard`
### 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*
<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*
The two important settings to configure are `APP_SECRET_KEY` and `APP_DATABASE_URI`
For `APP_SECRET_KEY`, fill in any random 32 character string
For `APP_DATABASE_URI`, fill in the respective fields
```
<username> --> database username
<password> --> database password
<host> --> database address
(this will most likely be localhost if you are running the database on the same machine
<port> --> port number of the database
(this can most likely be left out if you are running the database on the same machine)
<database> --> database name
```
>*If you are omitting `<port>`, make sure to also omit the `:`*
For a configuration where the database is running on the same machine, it would similar to this
```
APP_SECRET_KEY = "abcdefghijklmnopqrstuvwxyz123456"
APP_DATABASE_URI = "mysql+pymysql://DBusername:DBpassword@localhost/DBname"
```
The rest of the file is left at the default values
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 (all of the files inside)
res
|_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
`rm brickdb.zip`
In the `luclient` directory you should now have a file structure that looks like this
```
local
|_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 `~/NexusDashboard/app/luclient/res` folder
Once the file is moved over, you will need to rename it to `cdclient.sqlite`
```bash
mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite
```
##### Remaining Setup
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
You can run the site with
`gunicorn -b :8000 -w 4 wsgi:app`
# Development
Please use [Editor Config](https://editorconfig.org/)
* `flask run` to run local dev server
* `flask run` to run local dev server

View File

@@ -1,5 +1,5 @@
import os
from flask import Flask, url_for, g, redirect
from flask import Flask, url_for, redirect
from functools import wraps
from flask_assets import Environment
from webassets import Bundle
@@ -7,13 +7,27 @@ 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
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 query_cdclient, register_luclient_jinja_helpers
from app.luclient import register_luclient_jinja_helpers
from app.commands import init_db, init_accounts
from app.models import Account, AccountInvitation
from app.commands import (
init_db,
init_accounts,
load_property,
gen_image_cache,
gen_model_cache,
fix_clone_ids,
parse_lucache,
makeup_unlisted_objects,
gen_new_locales,
xref_scripts
)
from app.models import Account, AccountInvitation, AuditLog
import logging
from logging.handlers import RotatingFileHandler
# Instantiate Flask extensions
csrf_protect = CSRFProtect()
@@ -22,7 +36,6 @@ scheduler = APScheduler()
def create_app():
app = Flask(__name__, instance_relative_config=True)
# decrement uses on a play key after a successful registration
@@ -33,14 +46,24 @@ def create_app():
play_key_used = PlayKey.query.filter(PlayKey.id == user.play_key_id).first()
play_key_used.key_uses = play_key_used.key_uses - 1
play_key_used.times_used = play_key_used.times_used + 1
app.logger.info(
f"USERS::REGISTRATION User with ID {user.id} and name {user.username} Registered \
using Play Key ID {play_key_used.id} : {play_key_used.key_string}"
)
db.session.add(play_key_used)
db.session.commit()
else:
app.logger.info(f"USERS::REGISTRATION User with ID {user.id} and name {user.username} Registered")
@user_logged_in.connect_via(app)
def _after_login_hook(sender, user, **extra):
app.logger.info(f"{user.username} Logged in")
# A bunch of jinja filters to make things easiers
@app.template_filter('ctime')
def timectime(s):
if s:
return time.ctime(s) # or datetime.datetime.fromtimestamp(s)
return time.ctime(s) # or datetime.datetime.fromtimestamp(s)
else:
return "Never"
@@ -51,16 +74,23 @@ def create_app():
else:
return 0 & (1 << bit)
@app.teardown_appcontext
def close_connection(exception):
cdclient = getattr(g, '_cdclient', None)
if cdclient is not None:
cdclient.close()
@app.template_filter('debug')
def debug(text):
print(text)
# add the commands to flask cli
app.cli.add_command(init_db)
app.cli.add_command(init_accounts)
app.cli.add_command(load_property)
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(parse_lucache)
app.cli.add_command(makeup_unlisted_objects)
app.cli.add_command(gen_new_locales)
app.cli.add_command(xref_scripts)
register_logging(app)
register_settings(app)
register_extensions(app)
register_blueprints(app)
@@ -123,8 +153,18 @@ def register_blueprints(app):
app.register_blueprint(luclient_blueprint, url_prefix='/luclient')
from .reports import reports_blueprint
app.register_blueprint(reports_blueprint, url_prefix='/reports')
from .api import api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/api')
def register_logging(app):
# file logger
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
def register_settings(app):
"""Register setting from setting and env
@@ -134,15 +174,29 @@ def register_settings(app):
"""
# Load common settings
app.config.from_object('app.settings')
try:
app.config.from_object('app.settings')
except Exception:
app.logger.info("No settings.py, loading from example")
app.config.from_object('app.settings_example')
# Load environment specific settings
app.config['TESTING'] = False
app.config['DEBUG'] = False
# always pull these two from the env
app.config['SECRET_KEY'] = os.getenv('APP_SECRET_KEY')
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('APP_DATABASE_URI')
app.config['SECRET_KEY'] = os.getenv(
'APP_SECRET_KEY',
app.config['APP_SECRET_KEY']
)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
'APP_DATABASE_URI',
app.config['APP_DATABASE_URI']
)
app.config['SQLALCHEMY_BINDS'] = {
'cdclient': 'sqlite:///luclient/res/cdclient.sqlite'
}
# try to get overides, otherwise just use what we have already
app.config['USER_ENABLE_REGISTER'] = os.getenv(
@@ -169,22 +223,42 @@ def register_settings(app):
'USER_REQUIRE_INVITATION',
app.config['USER_REQUIRE_INVITATION']
)
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', 'smtp.gmail.com')
app.config['MAIL_PORT'] = os.getenv('MAIL_USE_SSL', 587)
app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', False)
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', True)
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', None)
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', None)
app.config['USER_EMAIL_SENDER_NAME'] = os.getenv('USER_EMAIL_SENDER_NAME', None)
app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv('USER_EMAIL_SENDER_EMAIL', None)
app.config['ALLOW_ANALYTICS'] = os.getenv(
'ALLOW_ANALYTICS',
app.config['ALLOW_ANALYTICS']
)
app.config['MAIL_SERVER'] = os.getenv(
'MAIL_SERVER',
app.config['MAIL_SERVER']
)
app.config['MAIL_PORT'] = os.getenv(
'MAIL_USE_SSL',
app.config['MAIL_PORT']
)
app.config['MAIL_USE_SSL'] = os.getenv(
'MAIL_USE_SSL',
app.config['MAIL_USE_SSL']
)
app.config['MAIL_USE_TLS'] = os.getenv(
'MAIL_USE_TLS',
app.config['MAIL_USE_TLS']
)
app.config['MAIL_USERNAME'] = os.getenv(
'MAIL_USERNAME',
app.config['MAIL_USERNAME']
)
app.config['MAIL_PASSWORD'] = os.getenv(
'MAIL_PASSWORD',
app.config['MAIL_PASSWORD']
)
app.config['USER_EMAIL_SENDER_NAME'] = os.getenv(
'USER_EMAIL_SENDER_NAME',
app.config['USER_EMAIL_SENDER_NAME']
)
app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv(
'USER_EMAIL_SENDER_EMAIL',
app.config['USER_EMAIL_SENDER_EMAIL']
)
def gm_level(gm_level):
@@ -201,3 +275,10 @@ def gm_level(gm_level):
return func(*args, **kwargs)
return wrapper
return decorator
def log_audit(message):
AuditLog(
account_id=current_user.id,
action=message
).save()

View File

@@ -1,18 +1,30 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, current_app, flash
from flask import render_template, Blueprint, redirect, url_for, request, current_app, flash
from flask_user import login_required, current_user
import json
from datatables import ColumnDT, DataTables
import datetime
import time
from app.models import Account, AccountInvitation, db
from app.models import (
Account,
CharacterInfo,
ActivityLog,
Leaderboard,
Mail,
Property,
PropertyContent,
UGC,
AuditLog,
BugReport,
AccountInvitation,
db
)
from app.schemas import AccountSchema
from app import gm_level
from app.forms import EditGMLevelForm
from app import gm_level, log_audit
from app.forms import EditGMLevelForm, EditEmailForm
accounts_blueprint = Blueprint('accounts', __name__)
account_schema = AccountSchema()
@accounts_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)
@@ -38,7 +50,7 @@ def edit_gm_level(id):
if current_user.id == int(id):
flash("You cannot your own GM Level", "danger")
return redirect(request.referrer if request.referrer else url_for("main.index"))
account_data = Account.query.filter(Account.id==id).first()
account_data = Account.query.filter(Account.id == id).first()
if account_data.gm_level >= 8 and current_user.gm_level == 8:
flash("You cannot edit this user's GM Level", "warning")
return redirect(request.referrer if request.referrer else url_for("main.index"))
@@ -46,8 +58,10 @@ def edit_gm_level(id):
form = EditGMLevelForm()
if form.validate_on_submit():
log_audit(f"Changed ({account_data.id}){account_data.username}'s GM Level from {account_data.gm_level} to {form.gm_level.data}")
account_data.gm_level = form.gm_level.data
account_data.save()
return redirect(url_for('accounts.view', id=account_data.id))
form.gm_level.data = account_data.gm_level
@@ -55,17 +69,38 @@ def edit_gm_level(id):
return render_template('accounts/edit_gm_level.html.j2', form=form, username=account_data.username)
@accounts_blueprint.route('/edit_email/<id>', methods=('GET', 'POST'))
@login_required
@gm_level(8)
def edit_email(id):
account_data = Account.query.filter(Account.id == id).first()
form = EditEmailForm()
if form.validate_on_submit():
log_audit(f"Changed ({account_data.id}){account_data.username}'s Email from {account_data.email} to {form.email.data}")
account_data.email = form.email.data
account_data.save()
return redirect(url_for('accounts.view', id=account_data.id))
form.email.data = account_data.email
return render_template('accounts/edit_email.html.j2', form=form, username=account_data.username)
@accounts_blueprint.route('/lock/<id>', methods=['GET'])
@login_required
@gm_level(3)
def lock(id):
account = Account.query.filter(Account.id == id).first()
account.locked = not account.locked
account.save()
if account.locked:
if not account.locked:
account.locked = True
account.active = False
log_audit(f"Locked ({account.id}){account.username}")
flash("Locked Account", "danger")
else:
account.locked = False
account.active = True
log_audit(f"Unlocked ({account.id}){account.username}")
flash("Unlocked account", "success")
account.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@@ -74,12 +109,17 @@ def lock(id):
@gm_level(3)
def ban(id):
account = Account.query.filter(Account.id == id).first()
account.banned = not account.banned
account.save()
if account.banned:
if not account.banned:
account.banned = True
account.active = False
log_audit(f"Banned ({account.id}){account.username}")
flash("Banned Account", "danger")
else:
account.banned = False
account.active = True
log_audit(f"Unbanned ({account.id}){account.username}")
flash("Unbanned account", "success")
account.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@@ -90,16 +130,60 @@ def mute(id, days=0):
account = Account.query.filter(Account.id == id).first()
if days == "0":
account.mute_expire = 0
log_audit(f"Unmuted ({account.id}){account.username}")
flash("Unmuted Account", "success")
else:
muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days))
account.mute_expire = muted_intil.timestamp()
log_audit(f"Muted ({account.id}){account.username} for {days} days")
flash(f"Muted account for {days} days", "danger")
account.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@accounts_blueprint.route('/delete/<id>/', methods=['GET', 'POST'])
@login_required
@gm_level(9)
def delete(id):
account = Account.query.filter(Account.id == id).first()
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()
for activity in activities:
activity.delete()
lb_entries = Leaderboard.query.filter(Leaderboard.character_id == char.id).all()
for lb_entry in lb_entries:
lb_entry.delete()
mails = Mail.query.filter(Mail.receiver_id == char.id).all()
for mail in mails:
mail.delete()
props = Property.query.filter(Property.owner_id == char.id).all()
for prop in props:
prop_contents = PropertyContent.query.filter(PropertyContent.property_id == prop.id).all()
for prop_content in prop_contents:
if prop_content.lot == "14":
UGC.query.filter(UGC.id == prop.ugc_id).first().delete()
prop_content.delete()
prop.delete()
char.delete()
# This is for GM stuff, it will be permnently delete logs
bugs = BugReport.query.filter(BugReport.resoleved_by_id == id).all()
for bug in bugs:
bug.delete()
audits = AuditLog.query.filter(AuditLog.account_id == id).all()
for audit in audits:
audit.delete()
invites = AccountInvitation.query.filter(AccountInvitation.invited_by_user_id == id).all()
for invite in invites:
invite.delete()
account.delete()
flash(message, "danger")
log_audit(message)
return redirect(url_for("main.index"))
@accounts_blueprint.route('/get', methods=['GET'])
@login_required
@gm_level(3)
@@ -130,10 +214,10 @@ def get():
View
</a>
"""
# <a role="button" class="btn btn-danger btn btn-block"
# href='{url_for('acounts.delete', id=account["0"])}'>
# Delete
# </a>
# <a role="button" class="btn btn-danger btn btn-block"
# href='{url_for('acounts.delete', id=account["0"])}'>
# Delete
# </a>
if account["4"]:
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
@@ -146,13 +230,13 @@ def get():
account["5"] = '''<h2 class="far fa-check-square text-success"></h2>'''
if account["6"]:
account["6"] = f'''<h2 class="far fa-times-circle text-danger"></h2>'''
account["6"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
else:
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
if current_app.config["USER_ENABLE_EMAIL"]:
if account["8"]:
account["8"] = f'''<h2 class="far fa-check-square text-success"></h2>'''
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
else:
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
else:
@@ -167,4 +251,3 @@ def get():
del account["8"]
return data

27
app/api.py Normal file
View File

@@ -0,0 +1,27 @@
from flask import Blueprint, current_app, request
api_blueprint = Blueprint('api', __name__)
@api_blueprint.route('/web', methods=['GET', 'POST'])
def web():
current_app.logger.info(f"API::WEB [DATA] {request.data}")
return
@api_blueprint.route('/game', methods=['GET', 'POST'])
def game():
current_app.logger.info(f"API::GAME [DATA] {request.data}")
return
@api_blueprint.route('/game_content', methods=['GET', 'POST'])
def game_content():
current_app.logger.info(f"API::GAME CONTENT [DATA] {request.data}")
return
@api_blueprint.route('/metrics_data_service', methods=['GET', 'POST'])
def metrics_data_service():
current_app.logger.info(f"API::METRICS DATA SERVICE [DATA] {request.data}")
return

View File

@@ -1,4 +1,4 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
from flask import render_template, Blueprint, redirect, url_for, request, flash, redirect
from flask_user import login_required, current_user
from app.models import db, BugReport, CharacterInfo
from datatables import ColumnDT, DataTables
@@ -8,22 +8,28 @@ from app.luclient import translate_from_locale
bug_report_blueprint = Blueprint('bug_reports', __name__)
@bug_report_blueprint.route('/<status>', methods=['GET'])
@login_required
@gm_level(3)
def index(status):
return render_template('bug_reports/index.html.j2', status=status)
@bug_report_blueprint.route('/view/<id>', methods=['GET'])
@login_required
@gm_level(3)
def view(id):
report = BugReport.query.filter(BugReport.id == id).first()
if current_user.gm_level < 3:
chars = CharacterInfo.query.with_entities(CharacterInfo.id).filter(CharacterInfo.account_id == current_user.id).all()
char_ids = []
for char in chars:
char_ids.append(char[0])
if report.reporter_id not in char_ids:
return redirect(url_for('bug_reports.index', status=all))
if report.resoleved_by:
rb = report.resoleved_by.username
else:
rb=""
rb = ""
return render_template('bug_reports/view.html.j2', report=report, resolved_by=rb)
@@ -49,27 +55,37 @@ def resolve(id):
@bug_report_blueprint.route('/get/<status>', methods=['GET'])
@login_required
@gm_level(3)
def get(status):
columns = [
ColumnDT(BugReport.id), # 0
ColumnDT(BugReport.body), # 1
ColumnDT(BugReport.client_version), # 2
ColumnDT(BugReport.other_player_id), # 3
ColumnDT(BugReport.selection), # 4
ColumnDT(BugReport.submitted), # 5
ColumnDT(BugReport.resolved_time), # 6
ColumnDT(BugReport.reporter_id), # 1
ColumnDT(BugReport.body), # 2
ColumnDT(BugReport.client_version), # 3
ColumnDT(BugReport.other_player_id), # 4
ColumnDT(BugReport.selection), # 5
ColumnDT(BugReport.submitted), # 6
ColumnDT(BugReport.resolved_time), # 7
]
query = None
if status=="all":
query = db.session.query().select_from(BugReport)
elif status=="resolved":
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None)
elif status=="unresolved":
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None)
if current_user.gm_level > 0:
if status == "resolved":
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None)
elif status == "unresolved":
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None)
else:
query = db.session.query().select_from(BugReport)
else:
raise Exception("Not a valid filter")
chars = CharacterInfo.query.with_entities(CharacterInfo.id).filter(CharacterInfo.account_id == current_user.id).all()
char_ids = []
for char in chars:
char_ids.append(char[0])
if status == "resolved":
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids)).filter(BugReport.resolved_time != None)
elif status == "unresolved":
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids)).filter(BugReport.resolved_time == None)
else:
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids))
params = request.args.to_dict()
@@ -85,31 +101,46 @@ def get(status):
</a>
"""
if not report["6"]:
if report["7"] is None:
report["0"] += f"""
<a role="button" class="btn btn-danger btn btn-block"
href='{url_for('bug_reports.resolve', id=id)}'>
Resolve
</a>
"""
<a role="button" class="btn btn-danger btn btn-block"
href='{url_for('bug_reports.resolve', id=id)}'>
Resolve
</a>
"""
report["7"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
if report["3"] == "0":
report["3"] = "None"
if not report["1"]:
report["1"] = "None"
else:
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["3"]) & 0xFFFFFFFF).first()
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["1"])).first()
if character:
report["3"] = f"""
report["1"] = f"""
<a role="button" class="btn btn-primary btn btn-block"
href='{url_for('characters.view', id=(int(report["3"]) & 0xFFFFFFFF))}'>
href='{url_for('characters.view', id=report['1'])}'>
{character.name}
</a>
"""
else:
report["3"] = "Player Deleted"
report["1"] = "Player Deleted"
report["4"] = translate_from_locale(report["4"][2:-1])
if report["4"] == "0":
report["4"] = "None"
else:
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["4"]) & 0xFFFFFFFF).first()
if character:
if current_user.gm_level > 3:
report["4"] = f"""
<a role="button" class="btn btn-primary btn btn-block"
href='{url_for('characters.view', id=(int(report["4"]) & 0xFFFFFFFF))}'>
{character.name}
</a>
"""
else:
report["4"] = character.name
else:
report["4"] = "Player Deleted"
if not report["6"]:
report["6"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
report["5"] = translate_from_locale(report["5"][2:-1])
return data

7495
app/cdclient.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,21 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response
from flask_user import login_required, current_user
import json
from datatables import ColumnDT, DataTables
import datetime, time
import time
from app.models import CharacterInfo, CharacterXML, Account, db
from app.schemas import CharacterInfoSchema
from app import gm_level
from app.forms import RescueForm
from app import gm_level, log_audit
from app.luclient import translate_from_locale
import xmltodict
import xml.etree.ElementTree as ET
character_blueprint = Blueprint('characters', __name__)
character_schema = CharacterInfoSchema()
@character_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)
@@ -23,21 +27,33 @@ def index():
@login_required
@gm_level(3)
def approve_name(id, action):
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
if action == "approve":
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
flash(
f"Approved name {character.name}",
"success"
)
elif action == "rename":
character.needs_rename = True
log_audit(
f"Marked character ({character.id}){character.name} \
(Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename")
flash(
f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
f"Marked character {character.name} \
(Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
"danger"
)
@@ -60,11 +76,11 @@ def view(id):
abort(403)
return
character_json = xmltodict.parse(
CharacterXML.query.filter(
CharacterXML.id==id
).first().xml_data,
attr_prefix="attr_"
)
CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data,
attr_prefix="attr_"
)
# print json for reference
# with open("errorchar.json", "a") as file:
@@ -77,8 +93,7 @@ def view(id):
# 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']))
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
return render_template(
'character/view.html.j2',
@@ -87,6 +102,58 @@ def view(id):
)
@character_blueprint.route('/view_xml/<id>', methods=['GET'])
@login_required
def view_xml(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_xml = CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data
response = make_response(character_xml)
response.headers.set('Content-Type', 'text/xml')
return response
@character_blueprint.route('/get_xml/<id>', methods=['GET'])
@login_required
def get_xml(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_xml = CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data
response = make_response(character_xml)
response.headers.set('Content-Type', 'attachment/xml')
response.headers.set(
'Content-Disposition',
'attachment',
filename=f"{character_data.name}.xml"
)
return response
@character_blueprint.route('/restrict/<bit>/<id>', methods=['GET'])
@login_required
@gm_level(3)
@@ -103,12 +170,52 @@ def restrict(id, bit):
abort(404)
return
log_audit(f"Updated ({character_data.id}){character_data.name}'s permission map to \
{character_data.permission_map ^ (1 << int(bit))} from {character_data.permission_map}")
character_data.permission_map ^= (1 << int(bit))
character_data.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@character_blueprint.route('/rescue/<id>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def rescue(id):
form = RescueForm()
character_data = CharacterXML.query.filter(
CharacterXML.id == id
).first()
character_xml = ET.XML(character_data.xml_data)
for zone in character_xml.findall('.//r'):
if int(zone.attrib["w"]) % 100 == 0:
form.save_world.choices.append(
(
zone.attrib["w"],
translate_from_locale(f"ZoneTable_{zone.attrib['w']}_DisplayDescription")
)
)
if form.validate_on_submit():
new_zone = character_xml.find(f'.//r[@w="{form.save_world.data}"]')
char = character_xml.find(".//char")
char.attrib["lzx"] = new_zone.attrib["x"]
char.attrib["lzy"] = new_zone.attrib["y"]
char.attrib["lzz"] = new_zone.attrib["z"]
char.attrib["lwid"] = form.save_world.data
character_data.xml_data = ET.tostring(character_xml)
character_data.save()
return redirect(url_for('characters.view', id=id))
return render_template("character/rescue.html.j2", form=form)
@character_blueprint.route('/get/<status>', methods=['GET'])
@login_required
@gm_level(3)
@@ -124,14 +231,12 @@ def get(status):
]
query = None
if status=="all":
query = db.session.query().select_from(CharacterInfo).join(Account)
elif status=="approved":
if status == "approved":
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name == "") & (CharacterInfo.needs_rename == False))
elif status=="unapproved":
elif status == "unapproved":
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") | (CharacterInfo.needs_rename == True))
else:
raise Exception("Not a valid filter")
query = db.session.query().select_from(CharacterInfo).join(Account)
params = request.args.to_dict()
@@ -190,6 +295,4 @@ def get(status):
if perm_map & (1 << 6):
character["6"] += "Restricted Chat</br>"
return data

View File

@@ -1,10 +1,31 @@
import click
import json
from flask.cli import with_appcontext
import random, string, datetime
import random
import string
import datetime
from flask_user import current_app
from app import db
from app.models import Account, PlayKey
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
import pathlib
import zlib
from wand import image
from wand.exceptions import BlobError as BE
import app.pylddlib as ldd
from multiprocessing import Pool
from functools import partial
from sqlalchemy import func
import time
import csv
import json
from app.cdclient import (
ComponentsRegistry,
RenderComponent,
ItemComponent,
Objects,
ScriptComponent,
)
from app.luclient import translate_from_locale
@click.command("init_db")
@click.argument('drop_tables', nargs=1)
@@ -29,16 +50,384 @@ def init_accounts():
# Add accounts
print('Creating Admin account.')
admin_account = find_or_create_account(
find_or_create_account(
'admin',
'example@example.com',
'Nope',
)
return
@click.command("fix_clone_ids")
@with_appcontext
def fix_clone_ids():
"""
Fix incorrect prop_clone_id's
Remove duplicate properties
Either the one with most models or most recently claimed
Retuen Pre-built models via mail
(May have errors and need to be run multiple times)
"""
properties = Property.query.all()
count = 0
for prop in properties:
char = CharacterInfo.query.filter(CharacterInfo.id == prop.owner_id).first()
if char.prop_clone_id != prop.clone_id:
count += 1
prop.clone_id = char.prop_clone_id
prop.save()
print(f"Fixed {count} props where clone id did not match owner's clone id")
dupes = 0
characters = CharacterInfo.query.all()
for char in characters:
props = Property.query.with_entities(
Property.zone_id, func.count(Property.zone_id)
).group_by(Property.zone_id).filter(
Property.owner_id == char.id
).all()
for prop in props:
if prop[1] != 1:
dupes += 1
print(f"found dupe on {char.name}'s {prop[0]}")
dupe_props = Property.query.filter(
Property.owner_id == char.id
).filter(
Property.zone_id == prop[0]).all()
dupe_data = []
# id, content_count
for dprop in dupe_props:
dupe_data.append(
[
dprop.id,
PropertyContent.query.filter(PropertyContent.property_id == dprop.id).count(),
dprop.time_claimed
]
)
max_models = max(dupe_data, key=lambda x: x[1])
if max_models[1] == 0:
newest = max(dupe_data, key=lambda x: x[2])
for data in dupe_data:
if data[2] != newest[2]:
Property.query.filter(Property.id == data[0]).first().delete()
else:
for data in dupe_data:
if data[1] != max_models[1]:
contents = PropertyContent.query.filter(PropertyContent.property_id == dprop.id).all()
if contents:
for content in contents:
if content.lot == 14:
UGC.query.filter(content.ugc_id).first().delete()
content.delete()
else:
Mail(
sender_id=0,
sender_name="System",
receiver_id=char.id,
receiver_name=char.name,
time_sent=time.time(),
subject="Returned Model",
body="This model was returned to you from a property cleanup script",
attachment_id=0,
attachment_lot=content.lot,
attachment_count=1
).save()
content.delete()
time.sleep(1)
Property.query.filter(Property.id == data[0]).first().delete()
return
@click.command("load_property")
@click.argument('zone')
@click.argument('player')
@with_appcontext
def load_property(zone, player):
"""shoves property data into db"""
char = CharacterInfo.query.filter(CharacterInfo.name == player).first()
if not char:
print("Character not Found")
return 404
prop = Property.query.filter(Property.owner_id == char.id).filter(Property.zone_id == zone).first()
if not prop:
print(f"Property {zone} not claimed by Character: {char.name}")
return 404
prop_files = pathlib.Path('property_files/')
for i in prop_files.glob('**/*'):
if i.suffix == '.lxfml':
lxfml = ""
with open(i, "r") as file:
lxfml = file.read()
compressed_lxfml = zlib.compress(lxfml.encode())
new_ugc = UGC(
account_id=char.account_id,
character_id=char.id,
is_optimized=0,
lxfml=compressed_lxfml,
bake_ao=0,
filename=i.name
)
new_ugc.save()
new_prop_content = PropertyContent(
id=i.stem,
property_id=prop.id,
ugc_id=new_ugc.id,
lot=14,
x=0,
y=0,
z=0,
rx=0,
ry=0,
rz=0,
rw=1
)
new_prop_content.save()
@click.command("parse_lucache")
@with_appcontext
def parse_lucache():
"""Parses lucache csv file dump from nexus hq"""
unlisted_ids = [146, 147, 938, 1180, 1692, 1715, 1797, 1799, 1824, 1846, 1847, 1848, 1849, 1850, 1872, 1877, 1887, 1928, 1937, 1968, 1970, 1971, 1972, 1974, 1976, 1977, 1978, 1979, 1980, 1981, 1983, 1984, 2189, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2416, 2417, 2418, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2529, 2530, 2553, 2583, 2655, 2656, 2669, 2947, 2948, 3009, 3058, 3068, 3078, 3807, 3812, 3937, 4828, 4874, 4875, 4876, 4877, 4943, 4954, 5839, 5840, 6196, 6218, 6219, 6221, 6433, 6471, 6696, 6821, 6877, 6888, 6889, 6891, 6892, 6893, 6894, 6896, 6897, 6983, 7277, 7551, 7552, 7553, 7554, 7609, 7701, 7713, 7723, 7753, 7754, 7755, 7756, 7760, 7777, 7791, 7824, 7872, 8046, 8053, 8146, 9865, 9866, 9867, 9868, 10126, 10291, 10292, 10293, 10294, 10518, 10630, 10631, 10987, 11511, 11512, 11513, 11514, 11515, 11516, 11517, 11518, 11519, 11520, 11521, 11522, 11523, 11524, 11525, 12096, 12097, 12099, 12100, 12104, 12105, 12111, 12112, 12113, 12324, 12325, 12326, 12553, 12666, 12668, 12670, 12671, 12673, 12674, 12676, 12679, 12680, 12683, 12684, 12685, 12687, 12692, 12694, 12697, 12699, 12701, 12703, 12704, 12713, 12716, 12717, 12727, 12736, 12738, 12739, 12745, 12746, 12750, 12751, 12752, 12757, 12787, 12790, 12791, 12794, 12795, 12799, 12800, 12803, 12887, 12888, 12902, 12904, 12905, 12906, 12907, 12941, 13060, 13061, 13071, 13075, 13076, 13077, 13092, 13093, 13094, 13106, 13118, 13121, 13126, 13127, 13150, 13191, 13192, 13275, 13276, 13277, 13278, 13280, 13295, 13410, 13411, 13510, 13638, 13740, 13742, 13776, 13782, 13905, 13925, 13926, 13927, 13928, 13929, 13930, 13931, 13932, 13953, 13958, 13974, 13996, 13997, 13998, 13999, 14000, 14001, 14002, 14056, 14057, 14058, 14059, 14060, 14061, 14062, 14063, 14064, 14065, 14066, 14067, 14068, 14069, 14070, 14071, 14072, 14073, 14074, 14075, 14076, 14077, 14078, 14079, 14080, 14081, 14090, 14094, 14111, 14135, 14140, 14170, 14171, 14188, 14200, 14202, 14206, 14207, 14208, 14209, 14210, 14211, 14212, 14213, 14228, 14229, 14314, 14428, 14483, 14515, 14522, 14531, 14535, 14536, 14538, 14548, 14554, 14587, 14588, 14589, 14597, 14598, 14599, 14605, 14607, 14608, 14609, 14610, 14611, 14612, 14613, 14614, 14615, 14616, 14617, 14618, 14619, 14620, 14621, 14622, 14623, 14624, 14625, 14626, 14627, 14628, 14629, 14630, 14631, 14632, 14633, 14634, 14635, 14636, 14637, 14638, 14639, 14640, 14641, 14642, 14643, 14644, 14645, 14646, 14647, 14648, 14649, 14650, 14651, 14652, 14653, 14654, 14655, 14656, 14657, 14658, 14659, 14660, 14661, 14662, 14663, 14664, 14665, 14666, 14667, 14668, 14686, 14687, 14688, 14689, 14690, 14704, 14706, 14707, 14716, 14717, 14721, 14722, 14727, 14728, 14729, 14779, 14795, 14799, 14800, 14803, 14815, 14820, 14821, 14822, 14823, 14824, 14825, 14826, 14827, 14831, 14832, 14838, 14839, 15852, 15853, 15854, 15855, 15856, 15857, 15858, 15859, 15860, 15861, 15862, 15863, 15864, 15865, 15885, 15886, 15887, 15888, 15889, 15893, 15894, 15898, 15921, 15923, 15925, 15928, 15930, 15931, 15932, 15933, 15934, 15938, 15939, 15940, 15941, 15942, 15945, 15958, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15981, 15984, 15985, 15986, 15987, 15988, 15989, 15996, 15997, 15998, 15999, 16000, 16001, 16002, 16003, 16004, 16005, 16007, 16008, 16009, 16010, 16011, 16025, 16026, 16027, 16028, 16036, 16039, 16042, 16046, 16051, 16056, 16071, 16072, 16073, 16074, 16075, 16077, 16078, 16079, 16080, 16081, 16089, 16090, 16091, 16092, 16108, 16109, 16110, 16111, 16112, 16113, 16114, 16115, 16116, 16117, 16124, 16125, 16126, 16127, 16128, 16129, 16130, 16137, 16138, 16139, 16140, 16142, 16145, 16167, 16168, 16169, 16170, 16171, 16172, 16173, 16174, 16175, 16176, 16177, 16200, 16201, 16202, 16204, 16212, 16253, 16254, 16418, 16437, 16469, 16479, 16489, 16505, 16641, 16645, 16646, 16648, 16655, 16658, 16659, 16660, 16661, 16662, 16665, 16666, 16667, 16668, 16669, 16670, 16671, 16672, 16673, 16674, 16675, 16676, 16677, 16678, 16679, 16680, 16681, 16685, 16686, 16687, 16688, 16689, 16690, 16691, 16692, 16693, 16694, 16695, 16696, 16697, 16698, 16699, 16700, 16701, 16702, 16703, 16704, 16705, 16706, 16707, 16708, 16709, 16712, 16714, 16717, 16718, 16719, 16720, 16721, 16722, 16724, 16725, 16726, 16727, 16732, 16733, 16734, 16735] # noqa
with open("lucache.csv") as cache_file:
csv_reader = csv.reader(cache_file, delimiter=',')
line_count = 0
for row in csv_reader:
if row[0] == "id":
continue
if int(row[0]) in unlisted_ids:
json_data = json.loads(row[2])
components = ComponentsRegistry.query.filter(ComponentsRegistry.id == int(row[0])).all()
obj_type = "Environmental"
desc = json_data["Description"]
if desc in ["None", None, ""]:
desc = row[1]
nametag = 0
npcTemplateID = "null"
for comp in components:
if comp.component_type == 7: # Item
obj_type = "Smashable"
if comp.component_type == 11: # Item
obj_type = "Loot"
if comp.component_type == 35: # minifig
obj_type = "NPC"
npcTemplateID = comp.component_id
nametag = 1
if comp.component_type == 42: # b3
obj_type = "Behavior"
if comp.component_type == 60: # base combat ai
obj_type = "Enemy"
if comp.component_type == 73: # mission giver
if obj_type != "NPC":
obj_type = "Structure"
desc = f"__MG__{desc}"
if "vendor" in row[1].lower():
obj_type = "NPC"
print(f"""INSERT INTO "Objects" ("id","name","placeable","type","description","localize","npcTemplateID","displayName","interactionDistance","nametag","_internalNotes","locStatus","gate_version","HQ_valid") VALUES ("{row[0]}", "{row[1].replace("_", " ")}", 1, "{obj_type}", "{desc}", 1, {npcTemplateID} , "{json_data["DisplayName"]}", null , {nametag}, "Unlisted Object", 0, null, 1);""")
if obj_type in ["NPC", "Smashable", "Loot"]:
print(f""" <phrase id="Objects_{row[0]}_name">
<translation locale="en_US">{row[1]}</translation>
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
<translation locale="en_GB">{row[1]}</translation>
</phrase>
<phrase id="Objects_{row[0]}_description">
<translation locale="en_US">{desc}</translation>
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
<translation locale="en_GB">{desc}</translation>
</phrase>""")
# print(f'{row[0]}: {json_data["DisplayName"]}')
line_count += 1
# print(f'Processed {line_count} lines.')
@click.command("makeup_unlisted_objects")
@with_appcontext
def makeup_unlisted_objects():
objs_left = []
for obj in objs_left:
obj_type = "Environmental"
nametag = 0
name = "Name Missing"
desc = "null"
npcTemplateID = "null"
components = ComponentsRegistry.query.filter(ComponentsRegistry.id == obj).all()
for comp in components:
if comp.component_type == 2: # render
render = RenderComponent.query.filter(RenderComponent.id == comp.component_id).first()
if render is not None:
if render.render_asset not in [None, ""]:
name = render.render_asset.replace("_", " ").split('\\')[-1].split('/')[-1].split('.')[0].lower()
if name == "Name Missing":
if render.icon_asset not in [None, ""]:
name = render.icon_asset.replace("_", " ").split('\\')[-1].split('/')[-1].split('.')[0].lower()
name = name.replace("env ", "").replace("obj ", "").replace("minifig accessory ", "").replace("", "").replace("mf ", "").replace("cre ", "")
# print(f"{obj}: {name} : {alt_name}")
obj_type = "Smashable"
# else:
# print(f"{obj}: No Render")
if comp.component_type == 7: # destroyable
obj_type = "Smashable"
if comp.component_type == 11: # Item
item = ItemComponent.query.filter(ItemComponent.id == comp.component_id).first()
if item.itemType == 24:
obj_type = "Mount"
else:
obj_type = "Loot"
if comp.component_type == 35: # minifig
obj_type = "NPC"
npcTemplateID = comp.component_id
nametag = 1
if comp.component_type == 42: # b3
obj_type = "Behavior"
if comp.component_type == 60: # base combat ai
obj_type = "Enemy"
if comp.component_type == 73: # mission giver
if obj_type != "NPC":
obj_type = "Structure"
desc = f"__MG__{name}"
# print(f"""INSERT INTO "Objects" ("id","name","placeable","type","description","localize","npcTemplateID","displayName","interactionDistance","nametag","_internalNotes","locStatus","gate_version","HQ_valid") VALUES ("{obj}", "{name}", 1, "{obj_type}", "{desc}", 1, {npcTemplateID} , "{name}", null , {nametag}, "Unlisted Object", 0, null, 1);""")
if name != "Name Missing" and obj_type in ["Mount"]:
print(f""" <phrase id="Objects_{obj}_name">
<translation locale="en_US">{name}</translation>
<translation locale="de_DE">TRASNSLATE UNLISTED</translation>
<translation locale="en_GB">{name}</translation>
</phrase>""")
@click.command("gen_new_locales")
@with_appcontext
def gen_new_locales():
objects = Objects.query.order_by(Objects.id).all()
for obj in objects:
if obj.type == "Loot":
if obj.name not in ["Name Missing", None, "None"] and obj.name[:1] != "m":
name_to_trans = f"Object_{obj.id}_name"
name_transed = translate_from_locale(name_to_trans)
if name_to_trans == name_transed:
print(f""" <phrase id="Objects_{obj.id}_name">
<translation locale="en_US">{obj.name}</translation>
<translation locale="de_DE">TRASNSLATE OLD</translation>
<translation locale="en_GB">{obj.name}</translation>
</phrase>""")
if obj.description not in ["None", None, ""]:
description_to_trans = f"Object_{obj.id}_description"
description_transed = translate_from_locale(description_to_trans)
if description_to_trans == description_transed:
print(f""" <phrase id="Objects_{obj.id}_description">
<translation locale="en_US">{obj.description}</translation>
<translation locale="de_DE">TRASNSLATE OLD</translation>
<translation locale="en_GB">{obj.description}</translation>
</phrase>""")
@click.command("xref_scripts")
@with_appcontext
def xref_scripts():
"""cross refernce scripts dir with script component table"""
scripts = ScriptComponent.query.all()
base = 'app/luclient/res/'
server = 0
server_total = 0
client = 0
client_total = 0
server_used = 0
client_used = 0
used_total = 0
disk_scripts = [path for path in pathlib.Path('app/luclient/res/scripts').rglob("*.lua") if path.is_file()]
for script in scripts:
script_comps = ComponentsRegistry.query.filter(ComponentsRegistry.component_type == 5).filter(ComponentsRegistry.component_id == script.id).all()
if len(script_comps) > 0:
used_total += 1
if script.client_script_name not in [None, ""]:
cleaned_name = script.client_script_name.replace('\\', '/').lower()
client_script = pathlib.Path(f"{base}{cleaned_name}")
client_total += 1
if not client_script.is_file():
print(f"Missing Server Script: {client_script.as_posix()}")
client += 1
if len(script_comps) > 0:
client_used += 1
if script.script_name not in [None, ""]:
cleaned_name = script.script_name.replace('\\', '/').lower()
server_script = pathlib.Path(f"{base}{cleaned_name}")
server_total += 1
if not server_script.is_file():
print(f"Missing Client Script: {server_script.as_posix()}")
server += 1
if len(script_comps) > 0:
server_used += 1
print(f"Missing {server}/{server_total} server scripts")
print(f"Missing {client}/{client_total} client scripts")
print(f"Missing {server_used}/{used_total} used server scripts")
print(f"Missing {client_used}/{used_total} used client scripts")
print(f"Total cdclient scripts {server_total + client_total}\nTotal disk scripts {len(disk_scripts)}")
@click.command("gen_image_cache")
def gen_image_cache():
"""generates image cache"""
luclient = pathlib.Path('app/luclient/res')
files = [path for path in luclient.rglob("*.dds") if path.is_file()]
for file in files:
cache = get_cache_file(file).with_suffix(".png")
if not cache.exists():
try:
print(f"Convert {file.as_posix()} to {cache}")
cache.parent.mkdir(parents=True, exist_ok=True)
with image.Image(filename=str(file.as_posix())) as img:
img.compression = "no"
img.save(filename=str(cache.as_posix()))
except BE:
return print(f"Error on {file}")
@click.command("gen_model_cache")
def gen_model_cache():
"""generate model obj cache"""
luclient = pathlib.Path('app/luclient/res')
files = [path for path in luclient.rglob("*.lxfml") if path.is_file()]
pool = Pool(processes=4)
pool.map(partial(convert_lxfml_to_obj, lod=0), files)
pool.map(partial(convert_lxfml_to_obj, lod=1), files)
pool.map(partial(convert_lxfml_to_obj, lod=2), files)
def convert_lxfml_to_obj(file, lod):
mtl = get_cache_file(file).with_suffix(f".lod{lod}.mtl")
if not mtl.exists():
mtl.parent.mkdir(parents=True, exist_ok=True)
print(f"Convert LXFML {file.as_posix()} to obj and mtl @ {mtl}")
try:
ldd.main(str(file.as_posix()), str(mtl.with_suffix("").as_posix()), lod) # convert to OBJ
except Exception as e:
print(f"ERROR on {file}:\n {e}")
else:
# print(f"Already Exists: {file} with LOD {lod}")
return
def get_cache_file(path):
"""helper"""
# convert to list so that we can change elements
parts = list(path.parts)
# replace part that matches src with dst
parts[parts.index("luclient")] = "cache"
del parts[parts.index("res")]
return pathlib.Path(*parts)
def find_or_create_account(name, email, password, gm_level=9):
""" Find existing account or create new account """
account = Account.query.filter(Account.email == email).first()
@@ -56,15 +445,16 @@ def find_or_create_account(name, email, password, gm_level=9):
db.session.commit()
play_key = PlayKey.query.filter(PlayKey.key_string == key).first()
account = Account(email=email,
username=name,
password=current_app.user_manager.password_manager.hash_password(password),
play_key_id=play_key.id,
email_confirmed_at=datetime.datetime.utcnow(),
gm_level=gm_level
)
account = Account(
email=email,
username=name,
password=current_app.user_manager.password_manager.hash_password(password),
play_key_id=play_key.id,
email_confirmed_at=datetime.datetime.utcnow(),
gm_level=gm_level
)
play_key.key_uses = 0
db.session.add(account)
db.session.add(play_key)
db.session.commit()
return # account
return # account

View File

@@ -16,13 +16,13 @@ from wtforms import (
SubmitField,
validators,
IntegerField,
StringField,
SelectField
)
from wtforms.validators import DataRequired, Optional
from app.models import PlayKey
def validate_play_key(form, field):
"""Validates a field for a valid phone number
Args:
@@ -85,6 +85,7 @@ class CustomRegisterForm(FlaskForm):
submit = SubmitField('Register')
class CreatePlayKeyForm(FlaskForm):
count = IntegerField(
@@ -97,6 +98,7 @@ class CreatePlayKeyForm(FlaskForm):
)
submit = SubmitField('Create!')
class EditPlayKeyForm(FlaskForm):
active = BooleanField(
@@ -119,12 +121,24 @@ class EditGMLevelForm(FlaskForm):
gm_level = IntegerField(
'GM Level',
widget=NumberInput(min = 0, max = 9)
widget=NumberInput(min=0, max=9)
)
submit = SubmitField('Submit')
class EditEmailForm(FlaskForm):
email = StringField(
'E-Mail',
validators=[
Optional(),
validators.Email('Invalid email address'),
unique_email_validator,
]
)
submit = SubmitField('Submit')
class ResolveBugReportForm(FlaskForm):
resolution = StringField(
@@ -142,8 +156,8 @@ class SendMailForm(FlaskForm):
'Recipient: ',
coerce=str,
choices=[
("",""),
("0","All Characters"),
("", ""),
("0", "All Characters"),
],
validators=[validators.DataRequired()]
)
@@ -162,7 +176,7 @@ class SendMailForm(FlaskForm):
attachment = SelectField(
"Attachment",
coerce=str,
choices=[(0,"No Attachment")]
choices=[(0, "No Attachment")]
)
attachment_count = IntegerField(
@@ -171,3 +185,27 @@ class SendMailForm(FlaskForm):
)
submit = SubmitField('Submit')
class RescueForm(FlaskForm):
save_world = SelectField(
'Move to:',
coerce=str,
choices=[
("", ""),
],
validators=[validators.DataRequired()]
)
submit = SubmitField('Submit')
class RejectPropertyForm(FlaskForm):
rejection_reason = StringField(
'Rejection Reason',
widget=TextArea(),
validators=[validators.DataRequired()]
)
submit = SubmitField('Submit')

View File

@@ -1,12 +1,13 @@
from flask import render_template, Blueprint, request, url_for
from flask_user import login_required, current_user
from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo
from flask_user import login_required
from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo, AuditLog
from datatables import ColumnDT, DataTables
import time
from app import gm_level
log_blueprint = Blueprint('log', __name__)
@log_blueprint.route('/activities', methods=['GET'])
@login_required
@gm_level(8)
@@ -21,16 +22,30 @@ def command():
return render_template('logs/command.html.j2')
@log_blueprint.route('/system')
@gm_level(8)
def system():
with open('nexus_dashboard.log', 'r') as file:
logs = '</br>'.join(file.read().split('\n')[-100:])
return render_template('logs/system.html.j2', logs=logs)
@log_blueprint.route('/audits')
@gm_level(8)
def audit():
return render_template('logs/audit.html.j2')
@log_blueprint.route('/get_activities', methods=['GET'])
@login_required
@gm_level(8)
def get_activities():
columns = [
ColumnDT(ActivityLog.id), # 0
ColumnDT(ActivityLog.character_id), # 1
ColumnDT(ActivityLog.activity), # 2
ColumnDT(ActivityLog.time), # 3
ColumnDT(ActivityLog.map_id), # 4
ColumnDT(ActivityLog.id), # 0
ColumnDT(ActivityLog.character_id), # 1
ColumnDT(ActivityLog.activity), # 2
ColumnDT(ActivityLog.time), # 3
ColumnDT(ActivityLog.map_id), # 4
]
query = db.session.query().select_from(ActivityLog)
@@ -68,9 +83,9 @@ def get_activities():
@gm_level(8)
def get_commands():
columns = [
ColumnDT(CommandLog.id), # 0
ColumnDT(CommandLog.character_id), # 1
ColumnDT(CommandLog.command), # 2
ColumnDT(CommandLog.id), # 0
ColumnDT(CommandLog.character_id), # 1
ColumnDT(CommandLog.command), # 2
]
query = db.session.query().select_from(CommandLog)
@@ -96,3 +111,33 @@ def get_commands():
"""
return data
@log_blueprint.route('/get_audits', methods=['GET'])
@login_required
@gm_level(8)
def get_audits():
columns = [
ColumnDT(AuditLog.id), # 0
ColumnDT(AuditLog.account_id), # 1
ColumnDT(AuditLog.action), # 2
ColumnDT(AuditLog.date), # 2
]
query = db.session.query().select_from(AuditLog)
params = request.args.to_dict()
rowTable = DataTables(params, query, columns)
data = rowTable.output_result()
for audit in data["data"]:
char_id = audit["1"]
audit["1"] = f"""
<a role="button" class="btn btn-primary btn btn-block"
href='{url_for('accounts.view', id=char_id)}'>
{Account.query.filter(Account.id==audit['1']).first().username}
</a>
"""
return data

View File

@@ -3,21 +3,37 @@ from flask import (
send_file,
g,
redirect,
url_for
url_for,
make_response,
abort
)
from flask_user import login_required
from app.models import CharacterInfo
from app.cdclient import (
Objects,
Icons,
ItemSets,
ComponentsRegistry,
ComponentType,
RenderComponent,
ItemComponent,
ObjectSkills,
SkillBehavior
)
import glob
import os
from wand import image
from wand.exceptions import BlobError as BE
import pathlib
import json
import sqlite3
import xml.etree.ElementTree as ET
from sqlalchemy import or_
luclient_blueprint = Blueprint('luclient', __name__)
locale = {}
@luclient_blueprint.route('/get_dds_as_png/<filename>')
@login_required
def get_dds_as_png(filename):
@@ -26,7 +42,7 @@ def get_dds_as_png(filename):
cache = f'cache/{filename.split(".")[0]}.png'
if not os.path.exists("app/" + cache):
if not os.path.exists(cache):
root = 'app/luclient/res/'
path = glob.glob(
@@ -36,7 +52,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='app/cache/' + filename.split('.')[0] + '.png')
return send_file(cache)
@@ -60,104 +76,110 @@ def get_dds(filename):
@luclient_blueprint.route('/get_icon_lot/<id>')
@login_required
def get_icon_lot(id):
icon_path = RenderComponent.query.filter(
RenderComponent.id == ComponentsRegistry.query.filter(
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_RENDER
).filter(ComponentsRegistry.id == id).first().component_id
).first().icon_asset
render_component_id = query_cdclient(
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
[id],
one=True
)[0]
if icon_path:
icon_path = icon_path.replace("..\\", "").replace("\\", "/")
else:
return redirect(url_for('luclient.unknown'))
# find the asset from rendercomponent given the component id
filename = query_cdclient('select icon_asset from RenderComponent where id = ?',
[render_component_id],
one=True
)[0]
cache = f'app/cache/{icon_path.split(".")[0]}.png'
filename = filename.replace("..\\", "").replace("\\", "/")
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
if not os.path.exists("app/" + cache):
if not os.path.exists(cache):
root = 'app/luclient/res/'
try:
with image.Image(filename=f'{root}{filename}'.lower()) as img:
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
with image.Image(filename=f'{root}{icon_path}'.lower()) as img:
img.compression = "no"
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
img.save(filename=cache)
except BE:
return redirect(url_for('luclient.unknown'))
return send_file(cache)
return send_file(pathlib.Path(cache).resolve())
@luclient_blueprint.route('/get_icon_iconid/<id>')
@login_required
def get_icon_iconid(id):
filename = query_cdclient(
'select IconPath from Icons where IconID = ?',
[id],
one=True
)[0]
filename = Icons.query.filter(Icons.IconID == id).first().IconPath
filename = filename.replace("..\\", "").replace("\\", "/")
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
cache = f'app/cache/{filename.split(".")[0]}.png'
if not os.path.exists("app/" + cache):
if not os.path.exists(cache):
root = 'app/luclient/res/'
try:
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
with image.Image(filename=f'{root}{filename}'.lower()) as img:
img.compression = "no"
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
img.save(filename=cache)
except BE:
return redirect(url_for('luclient.unknown'))
return send_file(cache)
return send_file(pathlib.Path(cache).resolve())
@luclient_blueprint.route('/ldddb/')
@login_required
def brick_list():
brick_list = []
if len(brick_list) == 0:
suffixes = [".g", ".g1", ".g2", ".g3", ".xml"]
res = pathlib.Path('app/luclient/res/')
# Load g files
for path in res.rglob("*.*"):
if str(path.suffix) in suffixes:
brick_list.append(
{
"type": "file",
"name": str(path.as_posix()).replace("app/luclient/res/", "")
}
)
response = make_response(json.dumps(brick_list))
response.headers.set('Content-Type', 'application/json')
return response
@luclient_blueprint.route('/ldddb/', defaults={'req_path': ''})
@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()))
# Return 404 if path doesn't exist
if not rel_path.exists():
return abort(404)
# Check if path is a file and serve
if rel_path.is_file():
return send_file(rel_path)
else:
return abort(404)
@luclient_blueprint.route('/unknown')
@login_required
def unknown():
filename = "textures/ui/inventory/unknown.dds"
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
cache = f'app/cache/{filename.split(".")[0]}.png'
if not os.path.exists("app/" + cache):
if not os.path.exists(cache):
root = 'app/luclient/res/'
try:
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
with image.Image(filename=f'{root}{filename}'.lower()) as img:
img.compression = "no"
img.save(filename=cache)
except BE:
return redirect(url_for('luclient.unknown'))
with image.Image(filename=f'{root}{filename}'.lower()) as img:
img.compression = "no"
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
return send_file(cache)
def get_cdclient():
"""Connect to CDClient from file system Relative Path
Args:
None
"""
cdclient = getattr(g, '_cdclient', None)
if cdclient is None:
cdclient = g._database = sqlite3.connect('app/luclient/res/cdclient.sqlite')
return cdclient
def query_cdclient(query, args=(), one=False):
"""Run sql queries on CDClient
Args:
query (string) : SQL query
args (list) : List of args to place in query
one (bool) : Return only on result or all results
"""
cur = get_cdclient().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
return send_file(pathlib.Path(cache).resolve())
def translate_from_locale(trans_string):
@@ -192,23 +214,41 @@ def translate_from_locale(trans_string):
else:
return trans_string
def get_lot_name(lot_id):
if not lot_id:
return "Missing"
name = translate_from_locale(f'Objects_{lot_id}_name')
if name == f'Objects_{lot_id}_name':
intermed = Objects.query.filter(Objects.id == lot_id).first()
if intermed:
name = intermed.displayName if (intermed.displayName != "None" or intermed.displayName != "" or intermed.displayName == None) else intermed.name
if not name:
name = f'Objects_{lot_id}_name'
return name
def register_luclient_jinja_helpers(app):
@app.template_filter('get_zone_name')
def get_zone_name(zone_id):
if not zone_id:
return "Missing"
return translate_from_locale(f'ZoneTable_{zone_id}_DisplayDescription')
@app.template_filter('get_skill_desc')
def get_skill_desc(skill_id):
return translate_from_locale(f'SkillBehavior_{skill_id}_descriptionUI').replace(
"%(DamageCombo)", "Damage Combo: "
).replace(
"%(AltCombo)", "<br/>Skeleton Combo: "
).replace(
"%(Description)", "<br/>"
).replace(
"%(ChargeUp)", "<br/>Charge-up: "
)
return translate_from_locale(
f'SkillBehavior_{skill_id}_descriptionUI'
).replace(
"%(DamageCombo)", "Damage Combo: "
).replace(
"%(AltCombo)", "<br/>Skeleton Combo: "
).replace(
"%(Description)", "<br/>"
).replace(
"%(ChargeUp)", "<br/>Charge-up: "
)
@app.template_filter('parse_lzid')
def parse_lzid(lzid):
@@ -228,54 +268,50 @@ def register_luclient_jinja_helpers(app):
return None
@app.template_filter('get_lot_name')
def get_lot_name(lot_id):
name = translate_from_locale(f'Objects_{lot_id}_name')
if name == translate_from_locale(f'Objects_{lot_id}_name'):
intermed = query_cdclient(
'select * from Objects where id = ?',
[lot_id],
one=True
)
name = intermed[7] if (intermed[7] != "None" and intermed[7] !="" and intermed[7] != None) else intermed[1]
return name
def jinja_get_lot_name(lot_id):
return get_lot_name(lot_id)
@app.template_filter('get_lot_rarity')
def get_lot_rarity(lot_id):
render_component_id = query_cdclient(
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
[lot_id],
one=True
)[0]
rarity = query_cdclient('select rarity from ItemComponent where id = ?',
[render_component_id],
one=True
)
if rarity:
rarity = rarity[0]
if not lot_id:
return "Missing"
rarity = ItemComponent.query.filter(
ItemComponent.id == ComponentsRegistry.query.filter(
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_ITEM
).filter(ComponentsRegistry.id == id).first().component_id
).first().rarity
return rarity
@app.template_filter('get_lot_desc')
def get_lot_desc(lot_id):
if not lot_id:
return "Missing"
desc = translate_from_locale(f'Objects_{lot_id}_description')
if desc == f'Objects_{lot_id}_description':
desc = query_cdclient(
'select description from Objects where id = ?',
[lot_id],
one=True
)[0]
desc = Objects.query.filter(Objects.id == lot_id).first()
if desc in ("", None):
desc = None
else:
desc = desc.description
if desc in ("", None):
desc = None
if desc:
desc = desc.replace('"', "&#8220;")
return desc
@app.template_filter('get_item_set')
def check_if_in_set(lot_id):
item_set = query_cdclient(
'select * from ItemSets where itemIDs like ? or itemIDs like ? or itemIDs like ?',
[f'{lot_id}%', f'%, {lot_id}%', f'%,{lot_id}%'],
one=True
)
if not lot_id:
return None
item_set = ItemSets.query.filter(
or_(
ItemSets.itemIDs.like(f'{lot_id}%'),
ItemSets.itemIDs.like(f'%, {lot_id}%'),
ItemSets.itemIDs.like(f'%,{lot_id}%')
)
).first()
if item_set in ("", None):
return None
else:
@@ -283,35 +319,44 @@ def register_luclient_jinja_helpers(app):
@app.template_filter('get_lot_stats')
def get_lot_stats(lot_id):
stats = query_cdclient(
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
SELECT skillID FROM ObjectSkills WHERE objectTemplate=?\
)',
[lot_id]
)
if not lot_id:
return None
stats = SkillBehavior.query.with_entities(
SkillBehavior.imBonusUI,
SkillBehavior.lifeBonusUI,
SkillBehavior.armorBonusUI,
SkillBehavior.skillID,
SkillBehavior.skillIcon
).filter(
SkillBehavior.skillID in ObjectSkills.query.with_entities(
ObjectSkills.skillID
).filter(
ObjectSkills.objectTemplate == lot_id
).all()
).all()
return consolidate_stats(stats)
@app.template_filter('get_set_stats')
def get_set_stats(lot_id):
stats = query_cdclient(
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
SELECT skillID FROM ItemSetSkills WHERE SkillSetID=?\
)',
[lot_id]
)
if not lot_id:
return "Missing"
stats = SkillBehavior.query.with_entities(
SkillBehavior.imBonusUI,
SkillBehavior.lifeBonusUI,
SkillBehavior.armorBonusUI,
SkillBehavior.skillID,
SkillBehavior.skillIcon
).filter(
SkillBehavior.skillID == ObjectSkills.query.with_entities(
ObjectSkills.skillID
).filter(
ObjectSkills.objectTemplate == lot_id
).all()
).all()
return consolidate_stats(stats)
@app.template_filter('query_cdclient')
def jinja_query_cdclient(query, items):
print(query, items)
return query_cdclient(
query,
items,
one=True
)[0]
@app.template_filter('lu_translate')
def lu_translate(to_translate):
@@ -320,27 +365,18 @@ def register_luclient_jinja_helpers(app):
def consolidate_stats(stats):
if len(stats) > 1:
consolidated_stats = {"im": 0,"life": 0,"armor": 0, "skill": []}
if stats:
consolidated_stats = {"im": 0, "life": 0, "armor": 0, "skill": []}
for stat in stats:
if stat[0]:
consolidated_stats["im"] += stat[0]
if stat[1]:
consolidated_stats["life"] += stat[1]
if stat[2]:
consolidated_stats["armor"] += stat[2]
if stat[3]:
consolidated_stats["skill"].append([stat[3],stat[4]])
if stat.imBonusUI:
consolidated_stats["im"] += stat.imBonusUI
if stat.lifeBonusUI:
consolidated_stats["life"] += stat.lifeBonusUI
if stat.armorBonusUI:
consolidated_stats["armor"] += stat.armorBonusUI
if stat.skillID:
consolidated_stats["skill"].append([stat.skillID, stat.skillIcon])
stats = consolidated_stats
elif len(stats) == 1:
stats = {
"im": stats[0][0] if stats[0][0] else 0,
"life": stats[0][1] if stats[0][1] else 0,
"armor": stats[0][2] if stats[0][2] else 0,
"skill": [[stats[0][3], stats[0][4]]] if stats[0][3] else None,
}
else:
stats = None
return stats

View File

@@ -1,10 +1,10 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
from flask import render_template, Blueprint, redirect, url_for, flash, request
from flask_user import login_required, current_user
from app.models import db, Mail, CharacterInfo
from datatables import ColumnDT, DataTables
from app.models import Mail, CharacterInfo
from app.forms import SendMailForm
from app import gm_level
from app.luclient import translate_from_locale, query_cdclient
from app import gm_level, log_audit
from app.luclient import translate_from_locale
from app.cdclient import Objects
import time
mail_blueprint = Blueprint('mail', __name__)
@@ -25,54 +25,56 @@ def send():
form = SendMailForm()
if request.method == "POST":
# if form.validate_on_submit():
if form.attachment.data != "0" and form.attachment_count.data == 0:
form.attachment_count.data = 1
if form.recipient.data == "0":
log_audit(f"Sending {form.subject.data}: {form.body.data} to All Characters with {form.attachment_count.data} of item {form.attachment.data}")
for character in CharacterInfo.query.all():
Mail(
sender_id = 0,
sender_name = f"[GM] {current_user.username}",
receiver_id = character.id,
receiver_name = character.name,
time_sent = time.time(),
subject = form.subject.data,
body = form.body.data,
attachment_id = 0,
attachment_lot = form.attachment.data,
attachment_count = form.attachment_count.data
sender_id=0,
sender_name=f"[GM] {current_user.username}",
receiver_id=character.id,
receiver_name=character.name,
time_sent=time.time(),
subject=form.subject.data,
body=form.body.data,
attachment_id=0,
attachment_lot=form.attachment.data,
attachment_count=form.attachment_count.data
).save()
log_audit(f"Sent {form.subject.data}: \
{form.body.data} to ({character.id}){character.name} \
with {form.attachment_count.data} of item {form.attachment.data}")
else:
Mail(
sender_id = 0,
sender_name = f"[GM] {current_user.username}",
receiver_id = form.recipient.data,
receiver_name = CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name,
time_sent = time.time(),
subject = form.subject.data,
body = form.body.data,
attachment_id = 0,
attachment_lot = form.attachment.data,
attachment_count = form.attachment_count.data
sender_id=0,
sender_name=f"[GM] {current_user.username}",
receiver_id=form.recipient.data,
receiver_name=CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name,
time_sent=time.time(),
subject=form.subject.data,
body=form.body.data,
attachment_id=0,
attachment_lot=form.attachment.data,
attachment_count=form.attachment_count.data
).save()
log_audit(f"Sent {form.subject.data}: \
{form.body.data} to ({form.recipient.data}){CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name} \
with {form.attachment_count.data} of item {form.attachment.data}")
flash("Sent Mail", "success")
return redirect(url_for('mail.send'))
recipients = CharacterInfo.query.all()
for character in recipients:
form.recipient.choices.append((character.id, character.name))
items = query_cdclient(
'Select id, name, displayName from Objects where type = ?',
["Loot"]
)
items = Objects.query.filter(Objects.type == "Loot").all()
for item in items:
name = translate_from_locale(f'Objects_{item[0]}_name')
if name == f'Objects_{item[0]}_name':
name = (item[2] if (item[2] != "None" and item[2] !="" and item[2] != None) else item[1])
if name == f'Objects_{item.id}_name':
name = (item.displayName if (item.displayName != "None" and item.displayName != "" and item.displayName is not None) else item.name)
form.attachment.choices.append(
(
item[0],
@@ -80,6 +82,4 @@ def send():
)
)
return render_template('mail/send.html.j2', form=form)

View File

@@ -1,22 +1,19 @@
from flask import render_template, Blueprint, redirect, request, send_from_directory, make_response, send_file
from flask_user import login_required, current_user
import json, glob, os
from wand import image
from flask import render_template, Blueprint, send_from_directory
from flask_user import current_user, login_required
from app.models import Account, AccountInvitation, CharacterInfo
from app.models import Account, CharacterInfo, ActivityLog
from app.schemas import AccountSchema, CharacterInfoSchema
from app.luclient import query_cdclient
main_blueprint = Blueprint('main', __name__)
account_schema = AccountSchema()
char_info_schema = CharacterInfoSchema()
@main_blueprint.route('/', methods=['GET'])
def index():
"""Home/Index Page"""
if current_user.is_authenticated:
account_data = Account.query.filter(Account.id == current_user.id).first()
return render_template(
@@ -28,9 +25,24 @@ def index():
@main_blueprint.route('/about')
@login_required
def about():
"""About Page"""
return render_template('main/about.html.j2')
mods = Account.query.filter(Account.gm_level > 0).order_by(Account.gm_level.desc()).all()
online = 0
chars = CharacterInfo.query.all()
for char in chars:
last_log = ActivityLog.query.with_entities(
ActivityLog.activity
).filter(
ActivityLog.character_id == char.id
).order_by(ActivityLog.id.desc()).first()
if last_log:
if last_log[0] == 0:
online += 1
return render_template('main/about.html.j2', mods=mods, online=online)
@main_blueprint.route('/favicon.ico')

View File

@@ -7,10 +7,12 @@ import logging
from flask_sqlalchemy import BaseQuery
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import OperationalError, StatementError
from sqlalchemy.types import JSON
from time import sleep
import random
import string
# retrying query to work around python trash collector
# killing connections of other gunicorn workers
class RetryingQuery(BaseQuery):
@@ -45,9 +47,11 @@ class RetryingQuery(BaseQuery):
raise
self.session.rollback()
db = SQLAlchemy(query_class=RetryingQuery)
migrate = Migrate()
class PlayKey(db.Model):
__tablename__ = 'play_keys'
id = db.Column(db.Integer, primary_key=True)
@@ -110,6 +114,7 @@ class PlayKey(db.Model):
)
db.session.add(new_key)
db.session.commit()
return key
def delete(self):
db.session.delete(self)
@@ -120,6 +125,7 @@ class PlayKey(db.Model):
db.session.commit()
db.session.refresh(self)
class Account(db.Model, UserMixin):
__tablename__ = 'accounts'
id = db.Column(
@@ -199,7 +205,7 @@ class Account(db.Model, UserMixin):
@staticmethod
def get_user_by_id(*, user_id=None):
return User.query.filter(user_id == User.id).first()
return Account.query.filter(user_id == Account.id).first()
def save(self):
db.session.add(self)
@@ -210,6 +216,7 @@ class Account(db.Model, UserMixin):
db.session.delete(self)
db.session.commit()
class AccountInvitation(db.Model):
__tablename__ = 'account_invites'
id = db.Column(db.Integer, primary_key=True)
@@ -244,14 +251,10 @@ class AccountInvitation(db.Model):
db.session.delete(self)
db.session.commit()
@staticmethod
def get_user_by_id(*, user_id=None):
return User.query.filter(user_id == User.id).first()
return Account.query.filter(user_id == Account.id).first()
def delete(self):
db.session.delete(self)
db.session.commit()
# This table is cursed, see prop_clone_id
class CharacterInfo(db.Model):
@@ -326,6 +329,7 @@ class CharacterInfo(db.Model):
db.session.delete(self)
db.session.commit()
class CharacterXML(db.Model):
__tablename__ = 'charxml'
id = db.Column(
@@ -347,6 +351,7 @@ class CharacterXML(db.Model):
db.session.delete(self)
db.session.commit()
class CommandLog(db.Model):
__tablename__ = 'command_log'
id = db.Column(db.Integer, primary_key=True)
@@ -363,7 +368,7 @@ class CommandLog(db.Model):
passive_deletes=True
)
command = db.Column(
command = db.Column(
mysql.VARCHAR(256),
nullable=False
)
@@ -377,6 +382,7 @@ class CommandLog(db.Model):
db.session.delete(self)
db.session.commit()
class Friends(db.Model):
__tablename__ = 'friends'
player_id = db.Column(
@@ -422,6 +428,7 @@ class Friends(db.Model):
db.session.delete(self)
db.session.commit()
class Leaderboard(db.Model):
__tablename__ = 'leaderboard'
id = db.Column(db.Integer, primary_key=True)
@@ -471,6 +478,7 @@ class Leaderboard(db.Model):
db.session.delete(self)
db.session.commit()
class Mail(db.Model):
__tablename__ = 'mail'
id = db.Column(
@@ -559,6 +567,7 @@ class Mail(db.Model):
db.session.delete(self)
db.session.commit()
class ObjectIDTracker(db.Model):
__tablename__ = 'object_id_tracker'
last_object_id = db.Column(
@@ -568,6 +577,7 @@ class ObjectIDTracker(db.Model):
server_default='0'
)
class PetNames(db.Model):
__tablename__ = 'pet_names'
id = db.Column(mysql.BIGINT, primary_key=True)
@@ -581,6 +591,11 @@ class PetNames(db.Model):
server_default='0'
)
owner_id = db.Column(
mysql.BIGINT,
nullable=True
)
def save(self):
db.session.add(self)
db.session.commit()
@@ -599,7 +614,7 @@ class Property(db.Model):
autoincrement=False
)
owner_id = db.Column(
owner_id = db.Column(
mysql.BIGINT,
db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'),
nullable=False
@@ -617,7 +632,7 @@ class Property(db.Model):
nullable=False,
)
clone_id = db.Column(
clone_id = db.Column(
mysql.BIGINT(unsigned=True),
db.ForeignKey(CharacterInfo.prop_clone_id, ondelete='CASCADE'),
)
@@ -680,6 +695,15 @@ class Property(db.Model):
nullable=False,
)
performance_cost = db.Column(
mysql.DOUBLE(
precision=20,
scale=15,
asdecimal=False
),
server_default='0.0'
)
zone_id = db.Column(
mysql.INTEGER,
nullable=False,
@@ -757,6 +781,7 @@ class UGC(db.Model):
db.session.delete(self)
db.session.commit()
class PropertyContent(db.Model):
__tablename__ = 'properties_contents'
id = db.Column(
@@ -837,6 +862,7 @@ class PropertyContent(db.Model):
db.session.delete(self)
db.session.commit()
class ActivityLog(db.Model):
__tablename__ = 'activity_log'
id = db.Column(mysql.INTEGER, primary_key=True)
@@ -877,6 +903,7 @@ class ActivityLog(db.Model):
db.session.delete(self)
db.session.commit()
class BugReport(db.Model):
__tablename__ = 'bug_reports'
id = db.Column(mysql.INTEGER, primary_key=True)
@@ -929,6 +956,12 @@ class BugReport(db.Model):
nullable=True
)
reporter_id = db.Column(
mysql.INTEGER(),
nullable=False,
server_default='0'
)
def save(self):
db.session.add(self)
db.session.commit()
@@ -938,6 +971,7 @@ class BugReport(db.Model):
db.session.delete(self)
db.session.commit()
class Server(db.Model):
__tablename__ = 'servers'
id = db.Column(
@@ -980,23 +1014,25 @@ class Server(db.Model):
db.session.commit()
class ItemReports(db.Model):
__tablename__ = 'item_reports'
class Reports(db.Model):
__tablename__ = 'reports'
item = db.Column(
db.Integer(),
primary_key=True,
data = db.Column(
JSON(),
nullable=False
)
count = db.Column(
db.Integer(),
nullable=False
report_type = db.Column(
db.VARCHAR(35),
nullable=False,
primary_key=True,
autoincrement=False
)
date = db.Column(
db.Date(),
primary_key=False,
primary_key=True,
autoincrement=False
)
def save(self):
@@ -1008,3 +1044,42 @@ class ItemReports(db.Model):
db.session.delete(self)
db.session.commit()
class AuditLog(db.Model):
__tablename__ = 'audit_logs'
id = db.Column(
mysql.INTEGER,
primary_key=True
)
account_id = db.Column(
db.Integer(),
db.ForeignKey(Account.id, ondelete='CASCADE'),
nullable=False,
)
account = db.relationship(
'Account',
backref="audit_logs",
passive_deletes=True
)
action = db.Column(
mysql.TEXT,
nullable=False
)
date = db.Column(
mysql.TIMESTAMP,
nullable=False,
server_default=db.func.now()
)
def save(self):
db.session.add(self)
db.session.commit()
db.session.refresh(self)
def delete(self):
db.session.delete(self)
db.session.commit()

View File

@@ -1,9 +1,8 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
from flask import render_template, Blueprint, redirect, url_for, request, flash, current_app
from flask_user import login_required
from app.models import PetNames, db
from app.models import PetNames, db, CharacterXML, CharacterInfo
from datatables import ColumnDT, DataTables
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
from app import gm_level
from app import gm_level, log_audit, scheduler
moderation_blueprint = Blueprint('moderation', __name__)
@@ -20,10 +19,11 @@ def index(status):
@gm_level(3)
def approve_pet(id):
pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data.approved = 2
flash(f"Approved pet name {pet_data.pet_name}", "success")
log_audit(f"Approved pet name {pet_data.pet_name} from {pet_data.owner_id}")
flash(f"Approved pet name {pet_data.pet_name} from {pet_data.owner_id}", "success")
pet_data.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@@ -33,10 +33,11 @@ def approve_pet(id):
@gm_level(3)
def reject_pet(id):
pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data = PetNames.query.filter(PetNames.id == id).first()
pet_data.approved = 0
flash(f"Rejected pet name {pet_data.pet_name}", "danger")
log_audit(f"Rejected pet name {pet_data.pet_name} from {pet_data.owner_id}")
flash(f"Rejected pet name {pet_data.pet_name} from {pet_data.owner_id}", "danger")
pet_data.save()
return redirect(request.referrer if request.referrer else url_for("main.index"))
@@ -49,18 +50,16 @@ def get_pets(status="all"):
ColumnDT(PetNames.id),
ColumnDT(PetNames.pet_name),
ColumnDT(PetNames.approved),
ColumnDT(PetNames.owner_id),
]
query = None
if status=="all":
query = db.session.query().select_from(PetNames)
elif status=="approved":
query = db.session.query().select_from(PetNames).filter(PetNames.approved==2)
elif status=="unapproved":
query = db.session.query().select_from(PetNames).filter(PetNames.approved==1)
if status == "approved":
query = db.session.query().select_from(PetNames).filter(PetNames.approved == 2)
elif status == "unapproved":
query = db.session.query().select_from(PetNames).filter(PetNames.approved == 1)
else:
raise Exception("Not a valid filter")
query = db.session.query().select_from(PetNames)
params = request.args.to_dict()
@@ -105,4 +104,47 @@ def get_pets(status="all"):
"""
pet_data["2"] = "<span class='text-danger'>Rejected</span>"
if pet_data["3"]:
try:
pet_data["3"] = f"""
<a role="button" class="btn btn-primary btn btn-block"
href='{url_for('characters.view', id=pet_data["3"])}'>
{CharacterInfo.query.filter(CharacterInfo.id==pet_data['3']).first().name}
</a>
"""
except Exception:
PetNames.query.filter(PetNames.id == id).first().delete()
pet_data["0"] = "<span class='text-danger'>Deleted Refresh to make go away</span>"
pet_data["3"] = "<span class='text-danger'>Character Deleted</span>"
else:
pet_data["3"] = "Pending Character Association"
return data
@scheduler.task("cron", id="pet_name_maintenance", hour="*", timezone="UTC")
def pet_name_maintenance():
with scheduler.app.app_context():
# associate pet names to characters
# current_app.logger.info("Started Pet Name Maintenance")
unassociated_pets = PetNames.query.filter(PetNames.owner_id == None).all()
if unassociated_pets:
current_app.logger.info("Found un-associated pets")
for pet in unassociated_pets:
owner = CharacterXML.query.filter(CharacterXML.xml_data.like(f"%<p id=\"{pet.id}\" l=\"%")).first()
if owner:
pet.owner_id = owner.id
pet.save()
else:
pet.delete()
# auto-moderate based on already moderated names
unmoderated_pets = PetNames.query.filter(PetNames.approved == 1).all()
if unmoderated_pets:
current_app.logger.info("Found un-moderated Pets")
for pet in unmoderated_pets:
existing_pet = PetNames.query.filter(PetNames.approved.in_([0, 2])).filter(PetNames.pet_name == pet.pet_name).first()
if existing_pet:
pet.approved = existing_pet.approved
pet.save()
# current_app.logger.info("Finished Pet Name Maintenance")

View File

@@ -1,14 +1,14 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
from flask_user import login_required, current_user
from app.models import Account, AccountInvitation, PlayKey, db
from flask import render_template, Blueprint, redirect, url_for, request, flash
from flask_user import login_required
from app.models import Account, PlayKey, db
from datatables import ColumnDT, DataTables
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
from app import gm_level
from app import gm_level, log_audit
play_keys_blueprint = Blueprint('play_keys', __name__)
# Key creation page
# Key creation page
@play_keys_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(9)
@@ -20,8 +20,9 @@ def index():
@login_required
@gm_level(9)
def create(count=1, uses=1):
PlayKey.create(count=count, uses=uses)
flash(f"Created {count} Play Key(s) with {uses} uses!", "success")
key = PlayKey.create(count=count, uses=uses)
log_audit(f"Created {count} Play Key(s) with {uses} uses!")
flash(f"Created {count} Play Key(s) with {uses} uses! {key}", "success")
return redirect(url_for('play_keys.index'))
@@ -32,6 +33,8 @@ def bulk_create():
form = CreatePlayKeyForm()
if form.validate_on_submit():
PlayKey.create(count=form.count.data, uses=form.uses.data)
log_audit(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!")
flash(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!", "success")
return redirect(url_for('play_keys.index'))
return render_template('play_keys/bulk.html.j2', form=form)
@@ -42,7 +45,8 @@ def bulk_create():
@gm_level(9)
def delete(id):
key = PlayKey.query.filter(PlayKey.id == id).first()
associated_accounts = Account.query.filter(Account.play_key_id==id).all()
# associated_accounts = Account.query.filter(Account.play_key_id==id).all()
log_audit(f"Deleted Play Key {key.key_string}")
flash(f"Deleted Play Key {key.key_string}", "danger")
key.delete()
return redirect(url_for('play_keys.index'))
@@ -52,14 +56,20 @@ def delete(id):
@login_required
@gm_level(9)
def edit(id):
key = PlayKey.query.filter(PlayKey.id==id).first()
key = PlayKey.query.filter(PlayKey.id == id).first()
form = EditPlayKeyForm()
if form.validate_on_submit():
log_audit(f"Updated Play key {key.id} \
Uses: {key.key_uses}:{form.uses.data} \
Active: {key.active}:{form.active.data} \
Notes: {key.notes}:{form.notes.data} \
")
key.key_uses = form.uses.data
key.active = form.active.data
key.notes = form.notes.data
key.save()
return redirect(url_for('play_keys.index'))
form.uses.data = key.key_uses
@@ -74,7 +84,7 @@ def edit(id):
@gm_level(9)
def view(id):
key = PlayKey.query.filter(PlayKey.id == id).first()
accounts = Account.query.filter(Account.play_key_id==id).all()
accounts = Account.query.filter(Account.play_key_id == id).all()
return render_template('play_keys/view.html.j2', key=key, accounts=accounts)
@@ -115,7 +125,12 @@ def get():
Delete
</a>
<div class="modal fade bd-example-modal-lg" id="delete-{play_key["1"]}-modal" tabindex="-1" role="dialog" aria-labelledby="delete-{play_key["1"]}-modalLabel" aria-hidden="true">
<div class="modal
fade bd-example-modal-lg"
id="delete-{play_key["1"]}-modal"
tabindex="-1" role="dialog"
aria-labelledby="delete-{play_key["1"]}-modalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content bg-dark border-primary">
<div class="modal-header">

View File

@@ -5,29 +5,29 @@ from flask import (
url_for,
request,
abort,
jsonify,
send_from_directory,
make_response,
flash
flash,
current_app
)
from flask_user import login_required, current_user
import json
from datatables import ColumnDT, DataTables
import time
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail
from app.cdclient import ComponentsRegistry, ComponentType, RenderComponent
from app.schemas import PropertySchema
from app import gm_level
from app.luclient import query_cdclient
from app import gm_level, log_audit
from app.cdclient import ZoneTable
from app.forms import RejectPropertyForm
import zlib
import xmltodict
import os
import app.pylddlib as ldd
import pathlib
property_blueprint = Blueprint('properties', __name__)
property_schema = PropertySchema()
@property_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)
@@ -40,7 +40,7 @@ def index():
@gm_level(3)
def approve(id):
property_data = Property.query.filter(Property.id == id).first()
property_data = Property.query.filter(Property.id == id).first()
property_data.mod_approved = not property_data.mod_approved
@@ -49,26 +49,26 @@ def approve(id):
property_data.rejection_reason = ""
if property_data.mod_approved:
message = f"""Approved Property
{property_data.name if property_data.name else ZoneTable.query.filter(
ZoneTable.zoneID == property_data.zone_id
).first().DisplayDescription}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
log_audit(message)
flash(
f"""Approved Property
{property_data.name if property_data.name else query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data.zone_id],
one=True
)[0]}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
message,
"success"
)
else:
message = f"""Unapproved Property
{property_data.name if property_data.name else ZoneTable.query.filter(
ZoneTable.zoneID == property_data.zone_id
).first().DisplayDescription}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
log_audit(message)
flash(
f"""Unapproved Property
{property_data.name if property_data.name else query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data.zone_id],
one=True
)[0]}
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
"danger"
message,
"warning"
)
property_data.save()
@@ -83,11 +83,71 @@ def approve(id):
else:
go_to = url_for('main.index')
return redirect(go_to)
@property_blueprint.route('/reject/<id>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def reject(id):
property_data = Property.query.filter(Property.id == id).first()
form = RejectPropertyForm()
if form.validate_on_submit():
char_name = CharacterInfo.query.filter(CharacterInfo.id == property_data.owner_id).first().name
zone_name = ZoneTable.query.filter(
ZoneTable.zoneID == property_data.zone_id
).first().DisplayDescription
property_data.mod_approved = False
property_data.rejection_reason = form.rejection_reason.data
message = f"""Rejected Property
{property_data.name if property_data.name else zone_name}
from {char_name} with reason \"{form.rejection_reason.data}\""""
log_audit(message)
flash(
message,
"danger"
)
property_data.save()
# send rejection reason to their mailbox
# cause the game doesn't present it otherwise
mail_message = f"""Rejected Property
{property_data.name} on {zone_name}
with reason \"{form.rejection_reason.data}\""""
Mail(
sender_id=0,
sender_name=f"[GM] {current_user.username}",
receiver_id=property_data.owner_id,
receiver_name=char_name,
time_sent=time.time(),
subject="Property Rejected",
body=mail_message,
attachment_id=0,
attachment_lot=0,
attachment_count=0
).save()
go_to = ""
if request.referrer:
if "view_models" in request.referrer:
go_to = url_for('properties.view', id=id)
else:
go_to = url_for('properties.index')
else:
go_to = url_for('main.index')
return redirect(go_to)
form.rejection_reason.data = property_data.rejection_reason
return render_template('properties/reject.html.j2', property_data=property_data, form=form)
@property_blueprint.route('/view/<id>', methods=['GET'])
@login_required
def view(id):
@@ -123,26 +183,29 @@ def get(status="all"):
ColumnDT(Property.time_claimed), # 9
ColumnDT(Property.rejection_reason), # 10
ColumnDT(Property.reputation), # 11
ColumnDT(Property.zone_id), # 12
ColumnDT(Account.username) # 13
ColumnDT(Property.performance_cost), # 12
ColumnDT(Property.zone_id), # 13
ColumnDT(Account.username) # 14
]
query = None
if status=="all":
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account)
elif status=="approved":
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==True)
elif status=="unapproved":
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==False)
if status == "approved":
query = db.session.query().select_from(Property).join(
CharacterInfo, CharacterInfo.id == Property.owner_id
).join(Account).filter(Property.mod_approved == True).filter(Property.privacy_option == 2) # noqa
elif status == "unapproved":
query = db.session.query().select_from(Property).join(
CharacterInfo, CharacterInfo.id == Property.owner_id
).join(Account).filter(Property.mod_approved == False).filter(Property.privacy_option == 2).filter(Property.rejection_reason == "") # noqa
else:
raise Exception("Not a valid filter")
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id == Property.owner_id).join(Account)
params = request.args.to_dict()
rowTable = DataTables(params, query, columns)
data = rowTable.output_result()
for property_data in data["data"]:
id = property_data["0"]
@@ -167,6 +230,13 @@ def get(status="all"):
Unapprove
</a>
"""
if not property_data["10"]:
property_data["0"] += f"""
<a role="button" class="btn btn-danger btn btn-block"
href='{url_for('properties.reject', id=id)}'>
Reject
</a>
"""
property_data["1"] = f"""
<a role="button" class="btn btn-primary btn btn-block"
@@ -176,11 +246,9 @@ def get(status="all"):
"""
if property_data["4"] == "":
property_data["4"] = query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data["12"]],
one=True
)
property_data["4"] = ZoneTable.query.filter(
ZoneTable.zoneID == property_data["13"]
).first().DisplayDescription
if property_data["6"] == 0:
property_data["6"] = "Private"
@@ -197,25 +265,23 @@ def get(status="all"):
else:
property_data["7"] = '''<h2 class="far fa-check-square text-success"></h2>'''
property_data["12"] = query_cdclient(
'select DisplayDescription from ZoneTable where zoneID = ?',
[property_data["12"]],
one=True
)
property_data["13"] = ZoneTable.query.filter(
ZoneTable.zoneID == property_data["13"]
).first().DisplayDescription
return data
@property_blueprint.route('/view_model/<id>', methods=['GET'])
@property_blueprint.route('/view_model/<id>/<lod>', methods=['GET'])
@login_required
def view_model(id):
property_content_data = PropertyContent.query.filter(PropertyContent.id==id).all()
def view_model(id, lod):
property_content_data = PropertyContent.query.filter(PropertyContent.id == id).all()
# TODO: Restrict somehow
formatted_data = [
{
"obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj'),
"mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl'),
"obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj', lod=lod),
"mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl', lod=lod),
"lot": property_content_data[0].lot,
"id": property_content_data[0].id,
"pos": [{
@@ -232,9 +298,11 @@ def view_model(id):
return render_template(
'ldd/ldd.html.j2',
content=formatted_data
content=formatted_data,
lod=lod
)
property_center = {
1150: "(-17, 432, -60)",
1151: "(0, 455, -110)",
@@ -245,11 +313,11 @@ property_center = {
}
@property_blueprint.route('/view_models/<id>', methods=['GET'])
@property_blueprint.route('/view_models/<id>/<lod>', methods=['GET'])
@login_required
def view_models(id):
def view_models(id, lod):
property_content_data = PropertyContent.query.filter(
PropertyContent.property_id==id
PropertyContent.property_id == id
).order_by(PropertyContent.lot).all()
consolidated_list = []
@@ -273,8 +341,8 @@ def view_models(id):
# add new lot
consolidated_list.append(
{
"obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj'),
"mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl'),
"obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj', lod=lod),
"mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl', lod=lod),
"lot": property_content_data[item].lot,
"id": property_content_data[item].id,
"pos": [{
@@ -288,37 +356,39 @@ def view_models(id):
}]
}
)
property_data = Property.query.filter(Property.id==id).first()
property_data = Property.query.filter(Property.id == id).first()
return render_template(
'ldd/ldd.html.j2',
property_data=property_data,
content=consolidated_list,
center=property_center[property_data.zone_id]
center=property_center[property_data.zone_id],
lod=lod
)
@property_blueprint.route('/get_model/<id>/<file_format>', methods=['GET'])
@login_required
def get_model(id, file_format):
content = PropertyContent.query.filter(PropertyContent.id==id).first()
if content.lot == 14: # ugc model
@property_blueprint.route('/get_model/<id>/<file_format>/<lod>', methods=['GET'])
@login_required
def get_model(id, file_format, lod):
content = PropertyContent.query.filter(PropertyContent.id == id).first()
if not(0 <= int(lod) <= 2):
abort(404)
if content.lot == 14: # ugc model
response = ugc(content)[0]
else: # prebuild model
response = prebuilt(content, file_format)[0]
else: # prebuilt model
response = prebuilt(content, file_format, lod)[0]
response.headers.set('Content-Type', 'text/xml')
return response
@property_blueprint.route('/download_model/<id>', methods=['GET'])
@login_required
def download_model(id):
content = PropertyContent.query.filter(PropertyContent.id==id).first()
content = PropertyContent.query.filter(PropertyContent.id == id).first()
if content.lot == 14: # ugc model
if content.lot == 14: # ugc model
response, filename = ugc(content)
else: # prebuild model
else: # prebuilt model
response, filename = prebuilt(content, "lxfml")
response.headers.set('Content-Type', 'attachment/xml')
@@ -331,55 +401,48 @@ def download_model(id):
def ugc(content):
ugc_data = UGC.query.filter(UGC.id==content.ugc_id).first()
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
uncompressed_lxfml = zlib.decompress(ugc_data.lxfml)
response = make_response(uncompressed_lxfml)
return response, ugc_data.filename
def prebuilt(content, file_format):
def prebuilt(content, file_format, lod):
# translate LOT to component id
# we need to get a type of 2 because reasons
render_component_id = query_cdclient(
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
[content.lot],
one=True
)[0]
# find the asset from rendercomponent given the component id
filename = query_cdclient('select render_asset from RenderComponent where id = ?',
[render_component_id],
one=True
)
filename = RenderComponent.query.filter(
RenderComponent.id == ComponentsRegistry.query.filter(
ComponentsRegistry.component_type == ComponentType.COMPONENT_TYPE_RENDER
).filter(ComponentsRegistry.id == id).first().component_id
).first().render_asset
if filename:
filename = filename[0].split("\\\\")[-1].lower().split(".")[0]
if "/" in filename:
filename = filename.split("/")[-1].lower()
else:
return f"No filename for LOT {content.lot}"
lxfml = pathlib.Path(f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml')
if file_format == "lxfml":
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
with open(lxfml, 'r') as file:
lxfml_data = file.read()
# print(lxfml_data)
response = make_response(lxfml_data)
elif file_format in ["obj", "mtl"]:
cache = pathlib.Path(f'app/cache/BrickModels/{filename}.lod{lod}.{file_format}')
if not cache.is_file():
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}")
cache = f"app/cache/{filename}.{file_format}"
with open(str(cache.as_posix()), 'r') as file:
cache_data = file.read()
if os.path.exists(cache):
with open(cache, 'r') as file:
cache_data = file.read()
response = make_response(cache_data)
else:
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
ldd.main(lxfml, cache.split('.')[0]) # convert to OBJ
if os.path.exists(cache):
with open(cache, 'r') as file:
cache_data = file.read()
response = make_response(cache_data)
response = make_response(cache_data)
else:
raise(Exception("INVALID FILE FORMAT"))

View File

@@ -1,43 +1,23 @@
#!/usr/bin/env python
# pylddlib version 0.4.9.7
# based on pyldd2obj version 0.4.8 - Copyright (c) 2019 by jonnysp
#
# Updates:
# 0.4.9.8 Make work with LEGO Universe brickdb
# 0.4.9.7 corrected bug of incorrectly parsing the primitive xml file, specifically with comments. Add support LDDLIFTREE envirnment variable to set location of db.lif.
# 0.4.9.6 preliminary Linux support
# 0.4.9.5 corrected bug of incorrectly Bounding / GeometryBounding parsing the primitive xml file.
# 0.4.9.4 improved lif.db checking for crucial files (because of the infamous botched 4.3.12 LDD Windows update).
# 0.4.9.3 improved Windows and Python 3 compatibility
# 0.4.9.2 changed handling of material = 0 for a part. Now a 0 will choose the 1st material (the base material of a part) and not the previous material of the subpart before. This will fix "Chicken Helmet Part 11262". It may break other parts and this change needs further regression.
# 0.4.9.1 improved custom2DField handling, fixed decorations bug, improved material assignments handling
# 0.4.9 updates to support reading extracted db.lif from db folder
#
# License: MIT License
#
import os
import platform
import sys
import math
import struct
import zipfile
from xml.dom import minidom
import time
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf-8')
PRIMITIVEPATH = '/Primitives/'
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
GEOMETRIEPATH = PRIMITIVEPATH
DECORATIONPATH = '/Decorations/'
MATERIALNAMESPATH = '/MaterialNames/'
LOGOONSTUDSCONNTYPE = {"0:4", "0:4:1", "0:4:2", "0:4:33", "2:4:1", "2:4:34"}
class Matrix3D:
def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1):
def __init__(self, n11=1, n12=0, n13=0, n14=0, n21=0, n22=1, n23=0, n24=0, n31=0, n32=0, n33=1, n34=0, n41=0, n42=0, n43=0, n44=1):
self.n11 = n11
self.n12 = n12
self.n13 = n13
@@ -56,9 +36,26 @@ class Matrix3D:
self.n44 = n44
def __str__(self):
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(self.n11, self.n12, self.n13,self.n14,self.n21, self.n22, self.n23,self.n24,self.n31, self.n32, self.n33,self.n34,self.n41, self.n42, self.n43,self.n44)
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(
self.n11,
self.n12,
self.n13,
self.n14,
self.n21,
self.n22,
self.n23,
self.n24,
self.n31,
self.n32,
self.n33,
self.n34,
self.n41,
self.n42,
self.n43,
self.n44
)
def rotate(self,angle=0,axis=0):
def rotate(self, angle=0, axis=0):
c = math.cos(angle)
s = math.sin(angle)
t = 1 - c
@@ -109,21 +106,22 @@ class Matrix3D:
self.n12 * other.n41 + self.n22 * other.n42 + self.n32 * other.n43 + self.n42 * other.n44,
self.n13 * other.n41 + self.n23 * other.n42 + self.n33 * other.n43 + self.n43 * other.n44,
self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44
)
)
class Point3D:
def __init__(self, x=0,y=0,z=0):
def __init__(self, x=0, y=0, z=0):
self.x = x
self.y = y
self.z = z
def __str__(self):
return '[{0},{1},{2}]'.format(self.x, self.y,self.z)
return '[{0},{1},{2}]'.format(self.x, self.y, self.z)
def string(self,prefix = "v"):
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z)
def string(self, prefix="v"):
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix, self.x, self.y, self.z)
def transformW(self,matrix):
def transformW(self, matrix):
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z
@@ -131,7 +129,7 @@ class Point3D:
self.y = y
self.z = z
def transform(self,matrix):
def transform(self, matrix):
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43
@@ -140,41 +138,58 @@ class Point3D:
self.z = z
def copy(self):
return Point3D(x=self.x,y=self.y,z=self.z)
return Point3D(x=self.x, y=self.y, z=self.z)
class Point2D:
def __init__(self, x=0,y=0):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '[{0},{1}]'.format(self.x, self.y * -1)
def string(self,prefix="t"):
return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 )
def string(self, prefix="t"):
return '{0} {1:f} {2:f}\n'.format(prefix, self.x, self.y * -1)
def copy(self):
return Point2D(x=self.x,y=self.y)
return Point2D(x=self.x, y=self.y)
class Face:
def __init__(self,a=0,b=0,c=0):
def __init__(self, a=0, b=0, c=0):
self.a = a
self.b = b
self.c = c
def string(self,prefix="f", indexOffset=0 ,textureoffset=0):
def string(self, prefix="f", indexOffset=0, textureoffset=0):
if textureoffset == 0:
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset)
else:
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset)
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(
self.a + indexOffset,
self.b + indexOffset,
self.c + indexOffset,
self.a + textureoffset,
self.b + textureoffset,
self.c + textureoffset
)
def __str__(self):
return '[{0},{1},{2}]'.format(self.a, self.b, self.c)
class Group:
def __init__(self, node):
self.partRefs = node.getAttribute('partRefs').split(',')
class Bone:
def __init__(self, node):
self.refID = node.getAttribute('refID')
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
self.matrix = Matrix3D(n11=a, n12=b, n13=c, n14=0, n21=d, n22=e, n23=f, n24=0, n31=g, n32=h, n33=i, n34=0, n41=x, n42=y, n43=z, n44=1)
class Part:
def __init__(self, node):
@@ -185,19 +200,17 @@ class Part:
self.designID = node.getAttribute('designID')
self.materials = list(map(str, node.getAttribute('materials').split(',')))
lastm = '0'
for i, m in enumerate(self.materials):
if (m == '0'):
# self.materials[i] = lastm
self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material
else:
lastm = m
self.materials[i] = self.materials[0] # in case of 0 choose the 'base' material
if node.hasAttribute('decoration'):
self.decoration = list(map(str,node.getAttribute('decoration').split(',')))
self.decoration = list(map(str, node.getAttribute('decoration').split(',')))
for childnode in node.childNodes:
if childnode.nodeName == 'Bone':
self.Bones.append(Bone(node=childnode))
class Brick:
def __init__(self, node):
self.refID = node.getAttribute('refID')
@@ -207,14 +220,16 @@ class Brick:
if childnode.nodeName == 'Part':
self.Parts.append(Part(node=childnode))
class SceneCamera:
def __init__(self, node):
self.refID = node.getAttribute('refID')
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
self.matrix = Matrix3D(n11=a, n12=b, n13=c, n14=0, n21=d, n22=e, n23=f, n24=0, n31=g, n32=h, n33=i, n34=0, n41=x, n42=y, n43=z, n44=1)
self.fieldOfView = float(node.getAttribute('fieldOfView'))
self.distance = float(node.getAttribute('distance'))
class Scene:
def __init__(self, file):
self.Bricks = []
@@ -238,10 +253,10 @@ class Scene:
for childnode in node.childNodes:
if childnode.nodeName == 'BrickSet':
self.Version = str(childnode.getAttribute('version'))
elif node.nodeName == 'Cameras':
for childnode in node.childNodes:
if childnode.nodeName == 'Camera':
self.Scenecamera.append(SceneCamera(node=childnode))
# elif node.nodeName == 'Cameras':
# for childnode in node.childNodes:
# if childnode.nodeName == 'Camera':
# self.Scenecamera.append(SceneCamera(node=childnode))
elif node.nodeName == 'Bricks':
for childnode in node.childNodes:
if childnode.nodeName == 'Brick':
@@ -262,6 +277,7 @@ class Scene:
# print('Scene "'+ self.Name + '" Brickversion: ' + str(self.Version))
class GeometryReader:
def __init__(self, data):
self.offset = 0
@@ -282,10 +298,10 @@ class GeometryReader:
options = self.readInt()
for i in range(0, self.valueCount):
self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
self.positions.append(Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat()))
for i in range(0, self.valueCount):
self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
self.normals.append(Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat()))
if (options & 3) == 3:
self.texCount = self.valueCount
@@ -293,7 +309,7 @@ class GeometryReader:
self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat()))
for i in range(0, self.faceCount):
self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt()))
self.faces.append(Face(a=self.readInt(), b=self.readInt(), c=self.readInt()))
if (options & 48) == 48:
num = self.readInt()
@@ -311,7 +327,7 @@ class GeometryReader:
boneoffset = self.readInt() + 4
self.bonemap[i] = self.read_Int(datastart + boneoffset)
def read_Int(self,_offset):
def read_Int(self, _offset):
if sys.version_info < (3, 0):
return int(struct.unpack_from('i', self.data, _offset)[0])
else:
@@ -330,6 +346,7 @@ class GeometryReader:
self.offset += 4
return ret
class Geometry:
def __init__(self, designID, database):
self.designID = designID
@@ -337,21 +354,25 @@ class Geometry:
self.maxGeoBounding = -1
self.studsFields2D = []
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID,'.g')
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID, '.g')
GeometryCount = 0
while str(GeometryLocation) in database.filelist:
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
GeometryCount += 1
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID,'.g',GeometryCount)
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID, '.g', GeometryCount)
primitive = Primitive(data = database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
primitive = Primitive(data=database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
self.Partname = primitive.Designname
self.studsFields2D = primitive.Fields2D
try:
geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))]
geoBoundingList = [
abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])),
abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])),
abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))
]
geoBoundingList.sort()
self.maxGeoBounding = geoBoundingList[-1]
except KeyError as e:
except KeyError:
# print('\nBounding errror in part {0}: {1}\n'.format(designID, e))
pass
@@ -386,25 +407,33 @@ class Geometry:
count += self.Parts[part].texCount
return count
class Bone2:
def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
def __init__(self, boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
self.boneId = boneId
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty, z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
rotationMatrix.n43 -= p.z
self.matrix = rotationMatrix
class Field2D:
def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'):
self.type = type
self.field2DRawData = field2DRawData
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty, z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
@@ -413,7 +442,7 @@ class Field2D:
self.matrix = rotationMatrix
self.custom2DField = []
#The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
# The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
rows_count = height + 1
cols_count = width + 1
# creation looks reverse
@@ -432,18 +461,23 @@ class Field2D:
def __str__(self):
return '[type="{0}" transform="{1}" custom2DField="{2}"]'.format(self.type, self.matrix, self.custom2DField)
class CollisionBox:
def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
rotationMatrix = Matrix3D()
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
p = Point3D(x=tx,y=ty,z=tz)
rotationMatrix.rotate(
angle=(-angle * math.pi / 180.0),
axis=Point3D(x=ax, y=ay, z=az)
)
p = Point3D(x=tx, y=ty,
z=tz)
p.transformW(rotationMatrix)
rotationMatrix.n41 -= p.x
rotationMatrix.n42 -= p.y
rotationMatrix.n43 -= p.z
self.matrix = rotationMatrix
self.corner = Point3D(x=sX,y=sY,z=sZ)
self.corner = Point3D(x=sX, y=sY, z=sZ)
self.positions = []
self.positions.append(Point3D(x=0, y=0, z=0))
@@ -452,11 +486,14 @@ class CollisionBox:
self.positions.append(Point3D(x=sX, y=sY, z=0))
self.positions.append(Point3D(x=0, y=0, z=sZ))
self.positions.append(Point3D(x=0, y=sY, z=sZ))
self.positions.append(Point3D(x=sX ,y=0, z=sZ))
self.positions.append(Point3D(x=sX ,y=sY, z=sZ))
self.positions.append(Point3D(x=sX, y=0, z=sZ))
self.positions.append(Point3D(x=sX, y=sY, z=sZ))
def __str__(self):
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(self.corner.x, self.corner.y, self.corner.z)
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(
self.corner.x, self.corner.y, self.corner.z
)
class Primitive:
def __init__(self, data):
@@ -475,7 +512,18 @@ class Primitive:
if node.nodeName == 'Flex':
for node in node.childNodes:
if node.nodeName == 'Bone':
self.Bones.append(Bone2(boneId=int(node.getAttribute('boneId')), angle=float(node.getAttribute('angle')), ax=float(node.getAttribute('ax')), ay=float(node.getAttribute('ay')), az=float(node.getAttribute('az')), tx=float(node.getAttribute('tx')), ty=float(node.getAttribute('ty')), tz=float(node.getAttribute('tz'))))
self.Bones.append(
Bone2(
boneId=int(node.getAttribute('boneId')),
angle=float(node.getAttribute('angle')),
ax=float(node.getAttribute('ax')),
ay=float(node.getAttribute('ay')),
az=float(node.getAttribute('az')),
tx=float(node.getAttribute('tx')),
ty=float(node.getAttribute('ty')),
tz=float(node.getAttribute('tz'))
)
)
elif node.nodeName == 'Annotations':
for childnode in node.childNodes:
if childnode.nodeName == 'Annotation' and childnode.hasAttribute('designname'):
@@ -483,23 +531,73 @@ class Primitive:
elif node.nodeName == 'Collision':
for childnode in node.childNodes:
if childnode.nodeName == 'Box':
self.CollisionBoxes.append(CollisionBox(sX=float(childnode.getAttribute('sX')), sY=float(childnode.getAttribute('sY')), sZ=float(childnode.getAttribute('sZ')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz'))))
self.CollisionBoxes.append(
CollisionBox(
sX=float(childnode.getAttribute('sX')),
sY=float(childnode.getAttribute('sY')),
sZ=float(childnode.getAttribute('sZ')),
angle=float(childnode.getAttribute('angle')),
ax=float(childnode.getAttribute('ax')),
ay=float(childnode.getAttribute('ay')),
az=float(childnode.getAttribute('az')),
tx=float(childnode.getAttribute('tx')),
ty=float(childnode.getAttribute('ty')),
tz=float(childnode.getAttribute('tz'))
)
)
elif node.nodeName == 'PhysicsAttributes':
self.PhysicsAttributes = {"inertiaTensor": node.getAttribute('inertiaTensor'),"centerOfMass": node.getAttribute('centerOfMass'),"mass": node.getAttribute('mass'),"frictionType": node.getAttribute('frictionType')}
self.PhysicsAttributes = {
"inertiaTensor": node.getAttribute('inertiaTensor'),
"centerOfMass": node.getAttribute('centerOfMass'),
"mass": node.getAttribute('mass'),
"frictionType": node.getAttribute('frictionType')
}
elif node.nodeName == 'Bounding':
for childnode in node.childNodes:
if childnode.nodeName == 'AABB':
self.Bounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
self.Bounding = {
"minX": childnode.getAttribute('minX'),
"minY": childnode.getAttribute('minY'),
"minZ": childnode.getAttribute('minZ'),
"maxX": childnode.getAttribute('maxX'),
"maxY": childnode.getAttribute('maxY'),
"maxZ": childnode.getAttribute('maxZ')
}
elif node.nodeName == 'GeometryBounding':
for childnode in node.childNodes:
if childnode.nodeName == 'AABB':
self.GeometryBounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
self.GeometryBounding = {
"minX": childnode.getAttribute('minX'),
"minY": childnode.getAttribute('minY'),
"minZ": childnode.getAttribute('minZ'),
"maxX": childnode.getAttribute('maxX'),
"maxY": childnode.getAttribute('maxY'),
"maxZ": childnode.getAttribute('maxZ')
}
elif node.nodeName == 'Connectivity':
for childnode in node.childNodes:
if childnode.nodeName == 'Custom2DField':
self.Fields2D.append(Field2D(type=int(childnode.getAttribute('type')), width=int(childnode.getAttribute('width')), height=int(childnode.getAttribute('height')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')), field2DRawData=str(childnode.firstChild.data)))
self.Fields2D.append(
Field2D(
type=int(childnode.getAttribute('type')),
width=int(childnode.getAttribute('width')),
height=int(childnode.getAttribute('height')),
angle=float(childnode.getAttribute('angle')),
ax=float(childnode.getAttribute('ax')),
ay=float(childnode.getAttribute('ay')),
az=float(childnode.getAttribute('az')),
tx=float(childnode.getAttribute('tx')),
ty=float(childnode.getAttribute('ty')),
tz=float(childnode.getAttribute('tz')),
field2DRawData=str(childnode.firstChild.data)
)
)
elif node.nodeName == 'Decoration':
self.Decoration = {"faces": node.getAttribute('faces'), "subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')}
self.Decoration = {
"faces": node.getAttribute('faces'),
"subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')
}
class Materials:
def __init__(self, data):
@@ -514,13 +612,14 @@ class Materials:
b=int(node.getAttribute('Blue')),
a=int(node.getAttribute('Alpha')),
mtype=str(node.getAttribute('MaterialType'))
)
)
def getMaterialbyId(self, mid):
return self.Materials[mid]
class Material:
def __init__(self,id, r, g, b, a, mtype):
def __init__(self, id, r, g, b, a, mtype):
self.id = id
self.name = id
self.mattype = mtype
@@ -528,18 +627,25 @@ class Material:
self.g = float(g)
self.b = float(b)
self.a = float(a)
def string(self):
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format( self.r / 255, self.g / 255,self.b / 255)
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format(
self.r / 255,
self.g / 255,
self.b / 255
)
if self.a < 255:
out += 'Ni 1.575\n' + 'd {0}'.format(0.05) + '\n' + 'Tr {0}\n'.format(0.05)
return out
class DBinfo:
def __init__(self, data):
xml = minidom.parseString(data)
self.Version = xml.getElementsByTagName('Bricks')[0].attributes['version'].value
# print('DB Version: ' + str(self.Version))
class DBFolderFile:
def __init__(self, name, handle):
self.handle = handle
@@ -554,6 +660,7 @@ class DBFolderFile:
finally:
reader.close()
class LIFFile:
def __init__(self, name, offset, size, handle):
self.handle = handle
@@ -565,6 +672,7 @@ class LIFFile:
self.handle.seek(self.offset, 0)
return self.handle.read(self.size)
class DBFolderReader:
def __init__(self, folder):
self.filelist = {}
@@ -574,14 +682,14 @@ class DBFolderReader:
try:
os.path.isdir(self.location)
except Exception as e:
except Exception:
self.initok = False
# print("db folder read FAIL")
return
else:
self.parse()
if self.fileexist(os.path.join(self.location,'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location,'info.xml')].read())
if self.fileexist(os.path.join(self.location, 'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location, 'info.xml')].read())
# print("DB folder OK.")
self.initok = True
else:
@@ -593,7 +701,6 @@ class DBFolderReader:
# print(MATERIALNAMESPATH)
pass
def fileexist(self, filename):
return filename in self.filelist
@@ -603,6 +710,7 @@ class DBFolderReader:
entryName = os.path.join(path, name)
self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName)
class LIFReader:
def __init__(self, file):
self.packedFilesOffset = 84
@@ -614,7 +722,7 @@ class LIFReader:
try:
self.filehandle = open(self.location, "rb")
self.filehandle.seek(0, 0)
except Exception as e:
except Exception:
self.initok = False
# print("Database FAIL")
return
@@ -632,7 +740,7 @@ class LIFReader:
# print("Database FAIL")
self.initok = False
def fileexist(self,filename):
def fileexist(self, filename):
return filename in self.filelist
def parse(self, prefix='', offset=0):
@@ -648,7 +756,7 @@ class LIFReader:
entryType = self.readShort(offset=offset)
offset += 6
entryName = '{0}{1}'.format(prefix,'/');
entryName = '{0}{1}'.format(prefix, '/')
self.filehandle.seek(offset + 1, 0)
if sys.version_info < (3, 0):
t = ord(self.filehandle.read(1))
@@ -656,7 +764,7 @@ class LIFReader:
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
while not t == 0:
entryName ='{0}{1}'.format(entryName,chr(t))
entryName = '{0}{1}'.format(entryName, chr(t))
self.filehandle.seek(1, 1)
if sys.version_info < (3, 0):
t = ord(self.filehandle.read(1))
@@ -692,36 +800,37 @@ class LIFReader:
else:
return int.from_bytes(self.filehandle.read(2), byteorder='big')
class Converter:
def LoadDBFolder(self, dbfolderlocation):
self.database = DBFolderReader(folder=dbfolderlocation)
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation,'Materials.xml')):
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation,'Materials.xml')].read());
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation, 'Materials.xml')):
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation, 'Materials.xml')].read())
def LoadDatabase(self,databaselocation):
def LoadDatabase(self, databaselocation):
self.database = LIFReader(file=databaselocation)
if self.database.initok and self.database.fileexist('/Materials.xml'):
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read());
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read())
def LoadScene(self,filename):
def LoadScene(self, filename):
if self.database.initok:
self.scene = Scene(file=filename)
def Export(self,filename):
def Export(self, filename):
invert = Matrix3D()
#invert.n33 = -1 #uncomment to invert the Z-Axis
# invert.n33 = -1 #uncomment to invert the Z-Axis
indexOffset = 1
textOffset = 1
usedmaterials = []
geometriecache = {}
start_time = time.time()
out = open(filename + ".obj.tmp", "w+")
out.truncate(0)
out.write("mtllib " + filename + ".mtl" + '\n\n')
outtext = open(filename + ".mtl.tmp", "w+")
outtext.truncate(0)
total = len(self.scene.Bricks)
current = 0
@@ -733,12 +842,12 @@ class Converter:
if pa.designID not in geometriecache:
geo = Geometry(designID=pa.designID, database=self.database)
progress(current ,total , "(" + geo.designID + ") " + geo.Partname, ' ')
progress(current, total, "(" + geo.designID + ") " + geo.Partname, ' ')
geometriecache[pa.designID] = geo
else:
geo = geometriecache[pa.designID]
progress(current ,total , "(" + geo.designID + ") " + geo.Partname ,'-')
progress(current, total, "(" + geo.designID + ") " + geo.Partname, '-')
out.write("o\n")
@@ -750,11 +859,11 @@ class Converter:
# positions
for j, p in enumerate(geo.Parts[part].outpositions):
if (geo.Parts[part].bonemap[j] == i):
p.transform( invert * b.matrix)
p.transform(invert * b.matrix)
# normals
for k, n in enumerate(geo.Parts[part].outnormals):
if (geo.Parts[part].bonemap[k] == i):
n.transformW( invert * b.matrix)
n.transformW(invert * b.matrix)
for point in geo.Parts[part].outpositions:
out.write(point.string("v"))
@@ -770,13 +879,12 @@ class Converter:
last_color = 0
for part in geo.Parts:
#try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
# try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
try:
materialCurrentPart = pa.materials[part]
last_color = pa.materials[part]
except IndexError:
# print('WARNING: {0}.g{1} has NO material assignment in lxf. Replaced with color {2}. Fix {0}.xml faces values.'.format(pa.designID, part, last_color))
materialCurrentPart = last_color
lddmat = self.allMaterials.getMaterialbyId(materialCurrentPart)
@@ -784,7 +892,7 @@ class Converter:
deco = '0'
if hasattr(pa, 'decoration') and len(geo.Parts[part].textures) > 0:
#if decoCount <= len(pa.decoration):
# if decoCount <= len(pa.decoration):
if decoCount < len(pa.decoration):
deco = pa.decoration[decoCount]
decoCount += 1
@@ -799,7 +907,7 @@ class Converter:
f.write(self.database.filelist[decofilename].read())
f.close()
if not matname in usedmaterials:
if matname not in usedmaterials:
usedmaterials.append(matname)
outtext.write("newmtl " + matname + '\n')
outtext.write(lddmat.string())
@@ -809,9 +917,9 @@ class Converter:
out.write("usemtl " + matname + '\n')
for face in geo.Parts[part].faces:
if len(geo.Parts[part].textures) > 0:
out.write(face.string("f",indexOffset,textOffset))
out.write(face.string("f", indexOffset, textOffset))
else:
out.write(face.string("f",indexOffset))
out.write(face.string("f", indexOffset))
indexOffset += len(geo.Parts[part].outpositions)
textOffset += len(geo.Parts[part].textures)
@@ -823,55 +931,20 @@ class Converter:
sys.stdout.write('%s\r' % (' '))
# print("--- %s seconds ---" % (time.time() - start_time))
def setDBFolderVars(dbfolderlocation):
def setDBFolderVars(dbfolderlocation, lod):
global PRIMITIVEPATH
global GEOMETRIEPATH
global DECORATIONPATH
global MATERIALNAMESPATH
PRIMITIVEPATH = os.path.join(dbfolderlocation, 'Primitives', '')
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', 'lod0', '')
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', f'lod{lod}', '')
DECORATIONPATH = os.path.join(dbfolderlocation, 'Decorations', '')
MATERIALNAMESPATH = os.path.join(dbfolderlocation, 'MaterialNames', '')
# print(MATERIALNAMESPATH)
def FindDatabase():
lddliftree = os.getenv('LDDLIFTREE')
if lddliftree is not None:
if os.path.isdir(str(lddliftree)): #LDDLIFTREE points to folder
return str(lddliftree)
elif os.path.isfile(str(lddliftree)): #LDDLIFTREE points to file (should be db.lif)
return str(lddliftree)
else: #Env variable LDDLIFTREE not set. Check for default locations per different platform.
if platform.system() == 'Darwin':
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))
else:
# print("no LDD database found please install LEGO-Digital-Designer")
os._exit()
elif platform.system() == 'Windows':
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))
else:
# print("no LDD database found please install LEGO-Digital-Designer")
os._exit()
elif platform.system() == 'Linux':
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))):
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))
else:
# print("no LDD database found please install LEGO-Digital-Designer")
os._exit()
else:
# print('Your OS {0} is not supported yet.'.format(platform.system()))
os._exit()
def progress(count, total, status='', suffix = ''):
def progress(count, total, status='', suffix=''):
bar_len = 40
filled_len = int(round(bar_len * count / float(total)))
percents = round(100.0 * count / float(total), 1)
@@ -880,7 +953,8 @@ def progress(count, total, status='', suffix = ''):
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, status))
sys.stdout.flush()
def main(lxf_filename, obj_filename):
def main(lxf_filename, obj_filename, lod="2"):
# print("- - - pylddlib - - -")
# print(" _ ")
# print(" [_]")
@@ -890,13 +964,15 @@ def main(lxf_filename, obj_filename):
# print(" [=|=]")
# print("")
# print("- - - - - - - - - - - -")
global GEOMETRIEPATH
GEOMETRIEPATH = GEOMETRIEPATH + f"LOD{lod}/"
converter = Converter()
# print("Found DB folder. Will use this instead of db.lif!")
setDBFolderVars(dbfolderlocation = "app/luclient/res/")
converter.LoadDBFolder(dbfolderlocation = "app/luclient/res/")
setDBFolderVars(dbfolderlocation="app/luclient/res/", lod=lod)
converter.LoadDBFolder(dbfolderlocation="app/luclient/res/")
converter.LoadScene(filename=lxf_filename)
converter.Export(filename=obj_filename)
if __name__ == "__main__":
main()

View File

@@ -1,58 +1,356 @@
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
from flask_user import login_required, current_user
from app.models import db, CharacterInfo, Account, CharacterXML, ItemReports
from flask import render_template, Blueprint, current_app
from flask_user import login_required
from app.models import CharacterInfo, Account, CharacterXML, Reports
from app.luclient import get_lot_name
from app import gm_level, scheduler
import datetime, xmltodict
from sqlalchemy.orm import load_only
import datetime
import xmltodict
reports_blueprint = Blueprint('reports', __name__)
@reports_blueprint.route('/', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def index():
items = ItemReports.query.distinct(ItemReports.date).group_by(ItemReports.date).all()
reports_items = Reports.query.distinct(
Reports.date
).filter(
Reports.report_type == "items"
).group_by(Reports.date).options(load_only(Reports.date)).all()
reports_currency = Reports.query.distinct(
Reports.date
).filter(
Reports.report_type == "currency"
).group_by(Reports.date).options(load_only(Reports.date)).all()
reports_uscore = Reports.query.distinct(
Reports.date
).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,
reports_currency=reports_currency,
reports_uscore=reports_uscore,
)
return render_template('reports/index.html.j2', items=items)
@reports_blueprint.route('/items/by_date/<date>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def items_by_date(date):
items = ItemReports.query.filter(ItemReports.date==date).order_by(ItemReports.count.desc()).all()
return render_template('reports/items/by_date.html.j2', items=items, date=date)
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
return render_template('reports/items/by_date.html.j2', data=data, date=date)
@scheduler.task("cron", id="gen_item_report", hour=3)
def gen_item_report():
char_xmls = CharacterXML.query.join(
CharacterInfo,
CharacterInfo.id==CharacterXML.id
).join(
Account,
CharacterInfo.account_id==Account.id
).filter(Account.gm_level < 3).all()
date = datetime.date.today().strftime('%Y-%m-%d')
for char_xml in char_xmls:
character_json = xmltodict.parse(
char_xml.xml_data,
attr_prefix="attr_"
)
for inv in character_json["obj"]["inv"]["items"]["in"]:
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"])==0 or int(inv["attr_t"])==0):
for item in inv["i"]:
entry = ItemReports.query.filter(
ItemReports.item == int(item["attr_l"]) and \
ItemReports.date == date
).first()
if entry:
entry.count = entry.count + int(item["attr_c"])
entry.save()
@reports_blueprint.route('/items/graph/<start>/<end>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def items_graph(start, end):
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
entries = Reports.query.filter(
Reports.report_type == "items"
).filter(Reports.date.between(start_date, end_date)).all()
# transform data for chartjs
labels = []
items = dict()
datasets = []
# get stuff ready
for entry in entries:
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():
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:
new_entry = ItemReports(
item=int(item["attr_l"]),
count=int(item["attr_c"]),
date=date
)
new_entry.save()
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 "Done"
return render_template(
'reports/graph.html.j2',
labels=labels,
datasets=datasets,
name="Item",
start=start,
end=end,
data_type="items",
start_date=start_date,
end_date=end_date
)
@reports_blueprint.route('/currency/by_date/<date>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def currency_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
return render_template('reports/currency/by_date.html.j2', data=data, date=date)
@reports_blueprint.route('/currency/graph/<start>/<end>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def currency_graph(start, end):
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
entries = Reports.query.filter(
Reports.report_type == "currency"
).filter(Reports.date.between(start_date, end_date)).all()
characters = CharacterInfo.query.options(load_only(CharacterInfo.name)).all()
labels = []
datasets = []
# get stuff ready
for entry in entries:
labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters:
data = []
for entry in entries:
if character.name in entry.data.keys():
data.append(entry.data[character.name])
else:
data.append(0)
color = "#" + character.name.encode("utf-8").hex()[1:7]
if max(data) > 10000:
datasets.append({
"label": character.name,
"data": data,
"backgroundColor": color,
"borderColor": color
})
return render_template(
'reports/graph.html.j2',
labels=labels,
datasets=datasets,
name="Currency",
start=start,
end=end,
data_type="currency",
start_date=start_date,
end_date=end_date
)
@reports_blueprint.route('/uscore/by_date/<date>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def uscore_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
@reports_blueprint.route('/uscore/graph/<start>/<end>', methods=['GET', 'POST'])
@login_required
@gm_level(3)
def uscore_graph(start, end):
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
entries = Reports.query.filter(
Reports.report_type == "uscore"
).filter(Reports.date.between(start_date, end_date)).all()
characters = CharacterInfo.query.options(load_only(CharacterInfo.name)).all()
labels = []
datasets = []
# get stuff ready
for entry in entries:
labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters:
data = []
for entry in entries:
if character.name in entry.data.keys():
data.append(entry.data[character.name])
else:
data.append(0)
color = "#" + character.name.encode("utf-8").hex()[1:7]
if max(data) > 1000:
datasets.append({
"label": character.name,
"data": data,
"backgroundColor": color,
"borderColor": color
})
return render_template(
'reports/graph.html.j2',
labels=labels,
datasets=datasets,
name="U-Score",
start=start,
end=end,
data_type="uscore",
start_date=start_date,
end_date=end_date
)
@scheduler.task("cron", id="gen_item_report", hour=23, timezone="UTC")
def gen_item_report():
with scheduler.app.app_context():
try:
current_app.logger.info("Start Item Report Generation")
date = datetime.date.today().strftime('%Y-%m-%d')
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first()
# Only one report per day
if report is not None:
current_app.logger.info(f"Item Report Already Generated for {date}")
return
char_xmls = CharacterXML.query.join(
CharacterInfo,
CharacterInfo.id == CharacterXML.id
).join(
Account,
CharacterInfo.account_id == Account.id
).filter(Account.gm_level < 3).all()
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,
attr_prefix="attr_"
)
for inv in character_json["obj"]["inv"]["items"]["in"]:
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"]) == 0 or int(inv["attr_t"]) == 1):
for item in inv["i"]:
if item["attr_l"] in report_data:
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
else:
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
if name in report_data[item["attr_l"]]["chars"]:
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + int(item["attr_c"])
else:
report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
except Exception as e:
current_app.logger.error(f"REPORT::ITEMS - ERROR PARSING CHARACTER {char_xml.id}")
current_app.logger.error(f"REPORT::ITEMS - {e}")
new_report = Reports(
data=report_data,
report_type="items",
date=date
)
new_report.save()
current_app.logger.info(f"Generated Item Report for {date}")
except Exception as e:
current_app.logger.critical(f"REPORT::ITEMS - {e}")
return
@scheduler.task("cron", id="gen_currency_report", hour=23, timezone="UTC")
def gen_currency_report():
with scheduler.app.app_context():
try:
current_app.logger.info("Start Currency Report Generation")
date = datetime.date.today().strftime('%Y-%m-%d')
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first()
# Only one report per day
if report is not None:
current_app.logger.info(f"Currency Report Already Generated for {date}")
return
characters = CharacterXML.query.join(
CharacterInfo,
CharacterInfo.id == CharacterXML.id
).join(
Account,
CharacterInfo.account_id == Account.id
).filter(Account.gm_level < 3).all()
report_data = {}
for character in characters:
try:
character_json = xmltodict.parse(
character.xml_data,
attr_prefix="attr_"
)
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_cc"])
except Exception as e:
current_app.logger.error(f"REPORT::CURRENCY - ERROR PARSING CHARACTER {character.id}")
current_app.logger.error(f"REPORT::CURRENCY - {e}")
new_report = Reports(
data=report_data,
report_type="currency",
date=date
)
new_report.save()
current_app.logger.info(f"Generated Currency Report for {date}")
except Exception as e:
current_app.logger.critical(f"REPORT::CURRENCY - {e}")
return
@scheduler.task("cron", id="gen_uscore_report", hour=23, timezone="UTC")
def gen_uscore_report():
with scheduler.app.app_context():
try:
current_app.logger.info("Start U-Score Report Generation")
date = datetime.date.today().strftime('%Y-%m-%d')
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first()
# Only one report per day
if report is not None:
current_app.logger.info(f"U-Score Report Already Generated for {date}")
return
characters = CharacterXML.query.join(
CharacterInfo,
CharacterInfo.id == CharacterXML.id
).join(
Account,
CharacterInfo.account_id == Account.id
).filter(Account.gm_level < 3).all()
report_data = {}
for character in characters:
try:
character_json = xmltodict.parse(
character.xml_data,
attr_prefix="attr_"
)
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_ls"])
except Exception as e:
current_app.logger.error(f"REPORT::U-SCORE - ERROR PARSING CHARACTER {character.id}")
current_app.logger.error(f"REPORT::U-SCORE - {e}")
new_report = Reports(
data=report_data,
report_type="uscore",
date=date
)
new_report.save()
current_app.logger.info(f"Generated U-Score Report for {date}")
except Exception as e:
current_app.logger.critical(f"REPORT::U-SCORE - {e}")
return

View File

@@ -1,7 +1,21 @@
from flask_marshmallow import Marshmallow
from app.models import *
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
@@ -44,7 +58,6 @@ class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
ugc = ma.Nested(UGCSchema)
class PropertySchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Property

View File

@@ -4,6 +4,17 @@
APP_NAME = "Nexus Dashboard"
APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
APP_SECRET_KEY = ""
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
CONFIG_LINK = False
CONFIG_LINK_TITLE = ""
CONFIG_LINK_HREF = ""
CONFIG_LINK_TEXT = ""
# Send Analytics for Developers to better fix issues
ALLOW_ANALYTICS = False
# Flask settings
CSRF_ENABLED = True
@@ -14,7 +25,7 @@ WTF_CSRF_TIME_LIMIT = 86400
# Flask-User settings
USER_APP_NAME = APP_NAME
USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password
USER_ENABLE_CHANGE_USERNAME = True # Allow users to change their username
USER_ENABLE_CHANGE_USERNAME = False # Allow users to change their username
USER_ENABLE_REGISTER = False # Allow new users to register
# Should alwyas be set to true
@@ -22,16 +33,25 @@ USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password`
USER_ENABLE_USERNAME = True # Register and Login with username
# Email Related Settings
USER_ENABLE_EMAIL = True # Register with Email WILL - DISABLE OTHER THINGS TOO
USER_ENABLE_EMAIL = False # Register with Email WILL - DISABLE OTHER THINGS TOO
USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email
USER_ENABLE_INVITE_USER = False # Allow users to be invited
USER_REQUIRE_INVITATION = False # Only invited users may - WILL DISABLE REGISTRATION
USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 587
MAIL_USE_SSL = False
MAIL_USE_TLS = True
MAIL_USERNAME = None
MAIL_PASSWORD = None
USER_EMAIL_SENDER_NAME = None
USER_EMAIL_SENDER_EMAIL = None
# Require Play Key
REQUIRE_PLAY_KEY = True
# Password hashing settings
# Password hashing settings DO NOT CHANGE
USER_PASSLIB_CRYPTCONTEXT_SCHEMES = ['bcrypt'] # bcrypt for password hashing
# Flask-User routing settings

13
app/static/chartjs/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@ $theme-colors: (
$link-color: #005ac2;
@import "../bootstrap-4.2.1/scss/bootstrap";
@import url(http://fonts.googleapis.com/css?family=Nunito:700);
@import url(https://fonts.googleapis.com/css?family=Nunito:700);
body { font-family:'Nunito', Helvetica, Arial, sans-serif; }
@@ -92,3 +92,13 @@ body { font-family:'Nunito', Helvetica, Arial, sans-serif; }
color: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.5);
}
div.dataTables_paginate {
float: right;
margin: 0;
}
div.dataTables_filter{
float: right;
margin: 0;
}

View File

@@ -0,0 +1,21 @@
{% extends 'base.html.j2' %}
{% block title %}
Edit E-mail for User {{ username }}
{% endblock title %}
{% block content_before %}
Edit E-mail for User {{ username }}
{% endblock content_before %}
{% block content %}
<form method=post>
{{ form.csrf_token }}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
<div class="card-body">
{{ helper.render_field(form.email) }}
{{ helper.render_submit_field(form.submit) }}
</div>
</div>
</form>
{% endblock content %}

View File

@@ -9,7 +9,7 @@
{% endblock content_before %}
{% block content %}
<table class="table" id="accounts_table">
<table class="table table-dark table-striped table-bordered table-hover" id="accounts_table">
<thead>
<tr>
<th>Actions</th>
@@ -41,7 +41,11 @@
"serverSide": true,
"ajax": "{{ url_for('accounts.get') }}",
"columnDefs": [
{ "searchable": false, "targets": [0,7] },
{% if config.USER_ENABLE_EMAIL %}
{ "searchable": false, "targets": [0,7] },
{% else %}
{ "searchable": false, "targets": [0,6] },
{% endif %}
{ "orderable": false, "targets": [0] }
]
});

View File

@@ -93,7 +93,27 @@
$(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 %}
</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

@@ -8,11 +8,13 @@
{{ status|capitalize }} Bug Reports
{% endblock content_before %}
{% block content %}
<table class="table" id="accounts_table">
<thead>
{% block content_override %}
<div class="mx-5">
<table class="table table-dark table-striped table-bordered table-hover" id="accounts_table">
<thead>
<tr>
<th>Actions</th>
<th>Reporter</th>
<th>Body</th>
<th>Client Version</th>
<th>Other Player</th>
@@ -20,9 +22,10 @@
<th>Submitted</th>
<th>Resolved</th>
</tr>
</thead>
<tbody></tbody>
</table>
</thead>
<tbody></tbody>
</table>
</div>
{% endblock %}

View File

@@ -8,10 +8,10 @@
Resolve Report {{ report.id }}
{% endblock content_before %}
{% block content %}
{% block content_override %}
<form method=post>
{{ form.csrf_token }}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 80vw;">
<div class="card-body">
<div class="row">
<div class="col text-right">
@@ -75,4 +75,4 @@
</div>
</div>
</form>
{% endblock content %}
{% endblock content_override %}

View File

@@ -8,8 +8,8 @@
View Report {{ report.id }}
{% endblock content_before %}
{% block content %}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
{% block content_override %}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 80vw;">
<div class="card-body">
<div class="row">
@@ -102,4 +102,4 @@
</div>
</div>
{% endblock content %}
{% endblock content_override %}

View File

@@ -1,11 +1,11 @@
{% extends 'base.html.j2' %}
{% block title %}
Account Management
Character Management
{% endblock title %}
{% block content_before %}
Account Management
Character Management
{% endblock content_before %}
{% block content %}
@@ -17,7 +17,7 @@
</div>
<br/>
{% endif %}
<table class="table" id="characters_table">
<table class="table table-dark table-striped table-bordered table-hover" id="characters_table">
<thead>
<tr>
<th>Actions</th>

View File

@@ -0,0 +1,21 @@
{% extends 'base.html.j2' %}
{% block title %}
Character Rescue
{% endblock title %}
{% block content_before %}
Character Rescue
{% endblock content_before %}
{% block content %}
<form method=post>
{{ form.csrf_token }}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
<div class="card-body">
{{ helper.render_field(form.save_world) }}
{{ helper.render_submit_field(form.submit) }}
</div>
</div>
</form>
{% endblock %}

View File

@@ -46,42 +46,55 @@
<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 >= 2 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
<div class="dropdown-menu">
<a class="dropdown-item" href='{{ url_for('mail.send') }}'>Send Mail</a>
<hr/>
<h6 class="text-center">Moderation</h6>
<a class="dropdown-item" href='{{ url_for('moderation.index', status='unapproved') }}'>View Unapproved Items</a>
<a class="dropdown-item" href='{{ url_for('moderation.index', status='approved') }}'>View Approved Items</a>
<a class="dropdown-item" href='{{ url_for('moderation.index', status='all') }}'>View All Items</a>
<hr/>
<h6 class="text-center">Bug Reports</h6>
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='unresolved') }}'>View Unresolved Reports</a>
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='resolved') }}'>View Resolved Reports</a>
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='all') }}'>View All Reports</a>
{% if current_user.is_authenticated and current_user.gm_level >= 8 %}
<hr/>
<h6 class="text-center">Logs</h6>
<a class="dropdown-item" href='{{ url_for('log.activity') }}'>Command Log</a>
<a class="dropdown-item" href='{{ url_for('log.command') }}'>Activity Log</a>
{% endif %}
</div>
</li>
{% endif %}
{% if current_user.is_authenticated and current_user.gm_level == 9 and config.REQUIRE_PLAY_KEY %}
{# Play Keys #}
<a id='play_keys-index' class='nav-link' href='{{ url_for('play_keys.index') }}'>Play Keys</a>
{% endif %}
{# About always right most #}
<a id='main-about' class='nav-link' href='{{ url_for('main.about') }}'>About</a>
{% if current_user.is_authenticated and current_user.gm_level >= 2 %}
<a id='report-index' class='nav-link' href='{{ url_for('reports.index') }}'>Reports</a>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
<div class="dropdown-menu">
<a class="dropdown-item text-center" href='{{ url_for('mail.send') }}'>Send Mail</a>
<hr/>
<h3 class="text-center">Moderation</h3>
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='unapproved') }}'>Unapproved Items</a>
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='approved') }}'>Approved Items</a>
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='all') }}'>All Items</a>
<hr/>
<h3 class="text-center">Bug Reports</h3>
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='unresolved') }}'>Unresolved Reports</a>
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='resolved') }}'>Resolved Reports</a>
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='all') }}'>All Reports</a>
{% 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.audit') }}'>Audit Log</a>
<a class="dropdown-item text-center" href='{{ url_for('log.system') }}'>System Log</a>
{% endif %}
</div>
</li>
{% endif %}
{% if current_user.is_authenticated %}
{% if current_user.gm_level == 0 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Bug Reports</a>
<div class="dropdown-menu">
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='unresolved') }}'>Unresolved Reports</a>
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='resolved') }}'>Resolved Reports</a>
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='all') }}'>All Reports</a>
</div>
</li>
{% endif %}
{# About always right most #}
<a id='main-about' class='nav-link' href='{{ url_for('main.about') }}'>About</a>
{% endif %}
{# Only show logout if unauthenticated #}
{% if current_user.is_authenticated %}

View File

@@ -21,7 +21,7 @@
Credit to sttng
</a>
<br/>
{% if property_data %}
{% if (property_data and current_user.gm_level >= 3) %}
<a role="button" class="btn text-{% if property_data.mod_approved %}danger{% else %}success{% endif %} btn-block"
href='{{url_for('properties.approve', id=property_data.id)}}'>
{% if property_data.mod_approved %} Unapprove {% else %} Approve {% endif %}
@@ -29,16 +29,38 @@
{% endif %}
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/super-three@0.116.0/build/three.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/super-three@0.116.0/examples/js/controls/OrbitControls.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/three@0.116.0/build/three.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/three@0.116.0/examples/js/controls/OrbitControls.js"></script>
<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/gh/mrdoob/three.js/examples/jsm/loaders/MTLLoader.js'
import {OBJLoader} from 'https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/jsm/loaders/OBJLoader.js'
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'
//Three.js stuff
const scene = new THREE.Scene();
let cammatr = new THREE.Matrix4();
@@ -270,7 +292,6 @@
class Scene{
//partially done - need zip file handling
constructor(){
this.Bricks = []
this.Scenecamera = []
@@ -446,16 +467,16 @@
constructor(designID, database){
this.designID = designID
this.Parts = []
this.studsFields2D = []
let GeometryLocation = `${designID}.g`
let PrimitiveLocation = `${designID}.xml`
let lod = {{ lod }}
let GeometryLocation = `brickprimitives/lod${lod}/${designID}.g`
let PrimitiveLocation = `Primitives/${designID}.xml`
let GeometryCount = 0
while (GeometryLocation in database.filelist) {
this.Parts[GeometryCount] = new GeometryReader(database.filelist[GeometryLocation].read())
GeometryCount = GeometryCount + 1
GeometryLocation = `${designID}.g${GeometryCount}`
GeometryLocation = `brickprimitives/lod${lod}/${designID}.g${GeometryCount}`
}
let primitive = new Primitive(database.filelist[PrimitiveLocation].read())
this.Partname = primitive.Designname
@@ -528,7 +549,7 @@
LoadDBURL(dbURLlocation){
this.database = new DBURLReader(dbURLlocation)
if(this.database.initok && this.database.fileexist('Materials.xml') && this.database.fileexist('localizedStrings.loc')){
if(this.database.initok && this.database.fileexist('Materials.xml')){
this.allMaterials = new Materials(this.database.filelist['Materials.xml'].read())
}
}
@@ -568,13 +589,15 @@
for (const bri of this.scene.Bricks){
current += 1
for (const pa of bri.Parts){
let geo = new Geometry(pa.designID, this.database)
if (!(pa.designID in geometriecache)) {
let geo = new Geometry(pa.designID, this.database)
geometriecache[pa.designID] = geo
let geo = 0
if (geometriecache.hasOwnProperty(pa.designID)) {
// console.log(`Re-use brick ${pa.designID}`)
geo = geometriecache[pa.designID]
}
else {
geo = geometriecache[pa.designID]
// console.log(`New brick ${pa.designID}`)
geo = new Geometry(pa.designID, this.database)
geometriecache[pa.designID] = geo
}
let ind = 0
@@ -675,7 +698,7 @@
console.log('partindex: ' + partindex)
console.log(pa.materials)
let lddmat = allMaterials.getMaterialbyId(21)
lddmat = allMaterials.getMaterialbyId(21)
}
let deco = '0'
@@ -724,11 +747,8 @@
// let vnh = new VertexNormalsHelper( mesh, 5 );
// scene.add( vnh );
}
}
}
}
}
@@ -861,7 +881,6 @@
class Materials {
//done
constructor(data) {
this.Materials = {}
let parser = new DOMParser();
@@ -908,13 +927,11 @@
function FindDBURL(){
let dburl = 'https://json.aronwk.com/LDD-DB/'
let dburl = '/luclient/ldddb/'
let xhr = new XMLHttpRequest();
xhr.open('GET', dburl, false); // `false` makes the request synchronous
// request state change event
xhr.onreadystatechange = function() {
// request completed?
if (xhr.readyState !== 4) {//return;
dburl = false;
@@ -930,7 +947,6 @@
console.log('HTTP error in FindDBURL:', xhr.status, xhr.statusText);
}
};
// start request
xhr.send();
return dburl
@@ -999,7 +1015,7 @@
return self.filelist[filename];
}
parse(dburl) {
parse(dburl, folder="") {
let self = this;
let xhr = new XMLHttpRequest();
xhr.open('GET', dburl, false);
@@ -1018,7 +1034,7 @@
let obj = data[i];
if (obj.type == 'directory'){
// parse subdirs
self.parse(dburl + obj.name + '/')
self.parse(dburl + obj.name + '/', obj.name)
}
else if (obj.type == 'file'){
self.filelist[obj.name] = new DBURLFile(dburl + obj.name, obj.name)
@@ -1043,7 +1059,7 @@
let lxfml_file_list = [
{% for model in content %}
{% if model.lot == 14 %}
"{{url_for('properties.get_model', id=model.id, file_format='lxfml')}}"{{ ", " if not loop.last else "" }}
"{{url_for('properties.get_model', id=model.id, file_format='lxfml', lod=lod)}}"{{ ", " if not loop.last else "" }}
{% endif %}
{% endfor %}
]

View File

@@ -17,7 +17,7 @@
</div>
<br/>
{% endif %}
<table class="table" id="command_table">
<table class="table table-dark table-striped table-bordered table-hover" id="command_table">
<thead>
<tr>
<th>ID</th>

View File

@@ -0,0 +1,46 @@
{% extends 'base.html.j2' %}
{% block title %}
Audit Log
{% endblock title %}
{% block content_before %}
Audit Log
{% endblock content_before %}
{% block content %}
{% if message %}
<div class="row">
<div class="col text-center">
<h3>{{ message }}</h3>
</div>
</div>
<br/>
{% endif %}
<table class="table table-dark table-striped table-bordered table-hover" id="audit_table">
<thead>
<tr>
<th>ID</th>
<th>Account</th>
<th>Command</th>
<th>Date</th>
</tr>
</thead>
<tbody></tbody>
</table>
{% endblock %}
{% block js %}
{{ super () }}
<script>
$(document).ready(function(){
let audit_table = $('#audit_table').DataTable({
"order": [[0, "desc"]],
"processing": true,
"serverSide": true,
"ajax": "{{ url_for('log.get_audits') }}",
});
});
</script>
{% endblock %}

View File

@@ -17,7 +17,7 @@
</div>
<br/>
{% endif %}
<table class="table" id="activity_table">
<table class="table table-dark table-striped table-bordered table-hover" id="activity_table">
<thead>
<tr>
<th>ID</th>

View File

@@ -0,0 +1,15 @@
{% extends 'base.html.j2' %}
{% block title %}LOGS{% endblock %}
{% block content_before %}
LOGS - {{ config.APP_NAME }}
{% endblock %}
{% block content_override %}
<div class="bg-white mx-5 p-3 rounded">
<code>
{{ logs }}
</code>
</div>
{% endblock %}

View File

@@ -3,66 +3,54 @@
{% block title %}About{% endblock %}
{% block content_before %}
About {{ config.APP_NAME }}
Online Players: {{ online }}
{% endblock %}
{% block content %}
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
<div class="card-body">
<h4 class="text-center">Contributors</h4>
<h4 class="text-center">Staff</h4>
<div class="row">
<div class="col text-right">
Developer:
{% for mod in mods %}
<div class="row">
<div class="col text-right">
{{ mod.username }}
</div>
<div class="col">
{% with gm_level=mod.gm_level %}
{% include 'partials/_gm_level.html.j2' %}
{% endwith %}
</div>
</div>
<div class="col">
Aronwk (Aaron Kimbrell)
</div>
</div>
{% endfor %}
<div class="row">
<div class="col text-right">
Developer:
</div>
<div class="col">
Wincent
</div>
</div>
</div>
</div>
<div class="row">
<div class="col text-right">
Logo Designer:
</div>
<div class="col">
BlasterBuilder
</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>
<div class="row">
<div class="col text-right">
LDD/LXFML Rendering:
{% 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>
<div class="col">
m2m/sttng
</div>
</div>
{% endif %}
<div class="row">
<div class="col text-right">
TODO: add more Contributors
</div>
<div class="col">
Add more
</div>
</div>
<hr>
<div class="row">
<div class="col text-right">
Source
</div>
<div class="col">
<a href="https://github.com/DarkflameUniverse/AccountManager">
<a href="https://github.com/DarkflameUniverse/NexusDashboard">
Github
</a>
</div>

View File

@@ -8,59 +8,63 @@
Moderation of {{ status|capitalize }} Items
{% endblock content_before %}
{% block content %}
<h4> Characters </h4>
<hr class="bg-primary"/>
<table class="table" id="character_table">
<thead>
<tr>
<th>Actions</th>
<th>Account</th>
<th>Name</th>
<th>Pending Name</th>
<th>Needs Rename</th>
<th>Last Login</th>
<th>Permission Map</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br/>
<h4> Pets </h4>
<hr class="bg-primary"/>
<table class="table" id="pet_table">
<thead>
<tr>
<th>Actions</th>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br/>
<h4> Properties </h4>
<hr class="bg-primary"/>
<table class="table" id="property_table">
<thead>
<tr>
<th>Actions</th>
<th>Owner</th>
<th>Template ID</th>
<th>Clone ID</th>
<th>Name</th>
<th>Description</th>
<th>Privacy</th>
<th>Approved</th>
<th>Updated</th>
<th>Claimed</th>
<th>Rejection Reason</th>
<th>Reputation</th>
<th>Location</th>
</tr>
</thead>
<tbody></tbody>
</table>
{% block content_override %}
<div class="mx-5">
<h4> Characters </h4>
<hr class="bg-primary"/>
<table class="table table-dark table-striped table-bordered table-hover" id="character_table">
<thead>
<tr>
<th>Actions</th>
<th>Account</th>
<th>Name</th>
<th>Pending Name</th>
<th>Needs Rename</th>
<th>Last Login</th>
<th>Permission Map</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br/>
<h4> Pets </h4>
<hr class="bg-primary"/>
<table class="table table-dark table-striped table-bordered table-hover" id="pet_table">
<thead>
<tr>
<th>Actions</th>
<th>Name</th>
<th>Status</th>
<th>Owner</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br/>
<h4> Properties </h4>
<hr class="bg-primary"/>
<table class="table table-dark table-striped table-bordered table-hover" id="property_table">
<thead>
<tr>
<th>Actions</th>
<th>Owner</th>
<th>Template ID</th>
<th>Clone ID</th>
<th>Name</th>
<th>Description</th>
<th>Privacy</th>
<th>Approved</th>
<th>Updated</th>
<th>Claimed</th>
<th>Rejection Reason</th>
<th>Reputation</th>
<th>Performance Cost</th>
<th>Location</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
{% endblock %}
{% block js %}

View File

@@ -23,6 +23,12 @@
<br/>
<div class="col">
{{ account_data.email }}
{% if current_user.gm_level >= 8 and not(current_user.gm_level == 8 and account_data.gm_level == 8)%}
<a role="button" class="btn btn-primary"
href='{{ url_for('accounts.edit_email', id=account_data.id) }}'>
Edit
</a>
{% endif %}
</div>
</div>
{% endif %}
@@ -141,9 +147,9 @@
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% if current_user.id != account_data.id and current_user.gm_level > 3 %}
{% if current_user.id != account_data.id and current_user.gm_level > 3 %}
<hr class="bg-primary"/>
<div class="row">
<div class="col text-center">
@@ -191,6 +197,42 @@
Mute for 1 year
</a>
{% endif %}
{% if current_user.gm_level == 9 %}
<button type="button" class="btn btn-danger btn-block" data-toggle="modal" data-target="#deleteModal">
Delete Account
</button>
{% endif %}
</div>
{% endif %}
</div>
{% if current_user.gm_level == 9 %}
{# delete Modal #}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content bg-dark">
<div class="modal-header">
<h1 class="modal-title" id="deleteModalLabel">
Permanently Delete this Account?
</h1>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h2> This can NOT be undone! </h2>
<br/>
This user is a GM {{account_data.gm_level}} !!!<br/>
This will delete their everything, including but not limited to:<br/>
Properties, Audit Logs, Bug Reports, and Invitations!
</div>
<div class="modal-footer">
<a role="button" class="btn btn-danger btn-block"
href='{{ url_for('accounts.delete', id=account_data.id) }}'>
Permanently Delete
</a>
</div>
</div>
</div>
</div>
{% endif %}

View File

@@ -52,17 +52,21 @@
</div>
{% else %}
<br/>
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('accounts.view', id=character.account_id) }}'>
View Account: {{character.account.username}}
</a>
</div>
</div>
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('characters.view_xml', id=character.id) }}'>
View XML
</a>
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('characters.get_xml', id=character.id) }}'>
Download XML
</a>
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('accounts.view', id=character.account_id) }}'>
View Account: {{character.account.username}}
</a>
{% endif %}
{% if current_user.id != character.account_id and current_user.gm_level > 2 %}
{% if current_user.gm_level > 2 %}
<hr class="bg-primary"/>
<div class="row">
<div class="col text-center">
@@ -83,6 +87,10 @@
</a>
{% endif %}
<a role="button" class="btn btn-warning btn-block"
href='{{ url_for('characters.rescue', id=character.id) }}'>
Rescue
</a>
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('characters.restrict', id=character.id, bit=4) }}'>
{% if character.permission_map|check_perm_map(4) %}Unrestrict{% else %}Restrict{% endif %} Trade

View File

@@ -3,7 +3,7 @@
<div class="row">
<div class="col text-center">
<h4>
Chatacter Data
Character Data
</h4>
</div>
</div>
@@ -19,7 +19,7 @@
<div class="row">
<div class="col text-center">
{% set parsed_lzid = character_json.obj.char.attr_lzid|parse_lzid %}
Zone: {{ parsed_lzid[0]|get_zone_name }}<br>
Zone: {{ character_json.obj.char.attr_lwid|get_zone_name }}<br>
Zone Instance: {{ parsed_lzid[1] }}<br>
Zone Clone: {{ parsed_lzid[2] }}<br>
</div>

View File

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

View File

@@ -69,6 +69,14 @@
{{ property.reputation }}
</div>
</div>
<div class="row">
<div class="col text-right">
Performance Cost:
</div>
<div class="col">
{{ property.performance_cost }}
</div>
</div>
{% if request.endpoint != "properties.view" %}
<br/>
<div class="row">
@@ -81,11 +89,24 @@
</div>
{% else %}
<br/>
<h5 class="text-center">Render Quality</h5>
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_models', id=property.id) }}'>
Render Property
href='{{ url_for('properties.view_models', id=property.id, lod=0) }}'>
High (0)
</a>
</div>
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_models', id=property.id, lod=1) }}'>
Med (1)
</a>
</div>
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_models', id=property.id, lod=2) }}'>
Low (2)
</a>
</div>
</div>
@@ -96,6 +117,13 @@
href='{{url_for('properties.approve', id=property.id)}}'>
{% if property.mod_approved %} Unapprove {% else %} Approve {% endif %}
</a>
{% if not property.rejection_reason %}
<br/>
<a role="button" class="btn btn-danger btn-block"
href='{{url_for('properties.reject', id=property.id)}}'>
Reject
</a>
{% endif %}
{% endif %}
</div>
</div>

View File

@@ -44,21 +44,12 @@
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.get_model', id=item.id, file_format="lxfml") }}'>
href='{{ url_for('properties.get_model', id=item.id, file_format="lxfml", lod=0) }}'>
View Model XML
</a>
</div>
</div>
<br/>
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_model', id=item.id) }}'>
Render Model
</a>
</div>
</div>
<br/>
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
@@ -67,5 +58,27 @@
</a>
</div>
</div>
<br/>
<h5 class="text-center">Render Quality</h5>
<div class="row">
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_model', id=item.id, lod=0) }}'>
High (0)
</a>
</div>
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_model', id=item.id, lod=1) }}'>
Med (1)
</a>
</div>
<div class="col text-center">
<a role="button" class="btn btn-primary btn-block"
href='{{ url_for('properties.view_model', id=item.id, lod=2) }}'>
Low (2)
</a>
</div>
</div>
</div>
</div>

View File

@@ -8,7 +8,8 @@
<br/>
--------------------------------
<br/>
{{ ("ItemSets_" ~ item_set[0] ~ "_kitName")|lu_translate }}: Rank {{ item_set[4] }}<br/>
{{ ("ItemSets_" ~ item_set[0] ~ "_kitName")|lu_translate }}{% if item_set[4]|int > 0%}: Rank {{ item_set[4] }}{% endif %}<br/>
{% if item_set[5] %}
<img src='/luclient/get_icon_iconid/{{item_set[5]}}'
alt='Kit Image'

View File

@@ -27,10 +27,12 @@
{% set skill_desc = skill[0]|get_skill_desc %}
{% if "IP" not in skill_desc and "AP" not in skill_desc and "LP" not in skill_desc and skill[0]|string not in skill_desc %}
<br/>
<img src='{{ url_for('luclient.get_icon_iconid', id=skill[1]) }}'
alt='Skill: '
width='32'
height='32'>
{% if skill[1]%}
<img src='{{ url_for('luclient.get_icon_iconid', id=skill[1]) }}'
alt='Skill: '
width='32'
height='32'>
{% endif %}
{{ skill[0]|get_skill_desc }}
<br/>
{% endif %}

View File

@@ -26,7 +26,7 @@
Bulk Create Play Keys
</a>
<hr class="bg-primary"/>
<table class="table" id="play_key_table">
<table class="table table-dark table-striped table-bordered table-hover" id="play_key_table">
<thead>
<tr>
<th>Actions</th>

View File

@@ -28,11 +28,11 @@
</div>
<div class="row">
<div class="col text-right">
Uses Left:
Times Used:
</div>
<br/>
<div class="col">
{{ key.key_uses}}
{{ key.times_used}}
</div>
</div>
<div class="row">

View File

@@ -8,35 +8,38 @@
Property Management
{% endblock content_before %}
{% block content %}
{% if message %}
<div class="row">
<div class="col text-center">
<h3>{{ message }}</h3>
{% block content_override %}
<div class="mx-5">
{% if message %}
<div class="row">
<div class="col text-center">
<h3>{{ message }}</h3>
</div>
</div>
</div>
<br/>
{% endif %}
<table class="table" id="properties_table">
<thead>
<tr>
<th>Actions</th>
<th>Owner</th>
<th>Template ID</th>
<th>Clone ID</th>
<th>Name</th>
<th>Description</th>
<th>Privacy</th>
<th>Approved</th>
<th>Updated</th>
<th>Claimed</th>
<th>Rejection Reason</th>
<th>Reputation</th>
<th>Location</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br/>
{% endif %}
<table class="table table-dark table-striped table-bordered table-hover" id="properties_table">
<thead>
<tr>
<th>Actions</th>
<th>Owner</th>
<th>Template ID</th>
<th>Clone ID</th>
<th>Name</th>
<th>Description</th>
<th>Privacy</th>
<th>Approved</th>
<th>Updated</th>
<th>Claimed</th>
<th>Rejection Reason</th>
<th>Reputation</th>
<th>Performance Cost</th>
<th>Location</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends 'base.html.j2' %}
{% block title %}
Viewing {{property_data.owner.name}}'s
{% if property_data.name %}
{{ property_data.name }}
{% else %}
{{ property_data.zone_id|get_zone_name }}
{% endif %}
{% endblock %}
{% block content_before %}
Viewing {{property_data.owner.name}}'s
{% if property_data.name %}
{{ property_data.name }}
{% else %}
{{ property_data.zone_id|get_zone_name }}
{% endif %}
{% endblock %}
{% block content %}
<form method=post>
{{ form.csrf_token }}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
<div class="card-body">
{{ helper.render_field(form.rejection_reason) }}
{{ helper.render_submit_field(form.submit) }}
</div>
</div>
</form>
{% endblock content %}

View File

@@ -0,0 +1,51 @@
{% extends 'base.html.j2' %}
{% block title %}
Currency on {{ date }}
{% endblock title %}
{% block content_before %}
Currency on {{ date }}
{% endblock content_before %}
{% block content %}
<div class='table-responsive'>
<table class="table table-dark table-striped table-bordered table-hover"
id="currency_by_date"
data-order='[[ 1, "desc" ]]'>
<thead>
<th scope="col">
Character
</th>
<th scope="col">
Currency
</th>
</thead>
<tbody>
{% for name, currency in data.items() %}
<tr>
<td>
{{ name }}
</td>
<td>
{{ currency }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block js %}
{{ super () }}
<script>
$(document).ready(function(){
let currency_by_date = $('#currency_by_date').DataTable({
"processing": false,
"serverSide": false,
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends 'base.html.j2' %}
{% block title %}
{{ name }} History for {{start_date}} to {{end_date}}
{% endblock title %}
{% block content_before %}
{{ name }} History for {{start_date}} to {{end_date}}
{% endblock content_before %}
{% block content %}
<div class ="row">
{% if data_type == "items" %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.items_graph', start=(start|int+1), end=(end|int+1))}}'>
Previous
</a>
</div>
{% if end|int > 0 %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.items_graph', start=(start|int-1), end=(end|int-1))}}'>
Next
</a>
</div>
{% endif %}
{% elif data_type == "currency" %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.currency_graph', start=(start|int+1), end=(end|int+1))}}'>
Previous
</a>
</div>
{% if end|int > 0 %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.currency_graph', start=(start|int-1), end=(end|int-1))}}'>
Next
</a>
</div>
{% endif %}
{% elif data_type == "uscore" %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.uscore_graph', start=(start|int+1), end=(end|int+1))}}'>
Previous
</a>
</div>
{% if end|int > 0 %}
<div class="col">
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.uscore_graph', start=(start|int-1), end=(end|int-1))}}'>
Next
</a>
</div>
{% endif %}
{% else %}
<h1> INVALID DATA TYPE </h1>
{% endif %}
</div>
<hr/>
<canvas id="item_chart"></canvas>
{% endblock %}
{% block js %}
{{ super () }}
<script type="text/javascript" src="{{ url_for('static', filename='chartjs/chart.min.js') }}"></script>
<script>
$(document).ready(function(){
let config = {
type: 'line',
data: {
labels: {{labels}},
datasets: {{datasets}},
},
options: {
plugins: { legend: { display: false }, },
scales: {
x: {
display: true,
title: {
display: true,
text: 'Date'
}
},
y: {
display: true,
title: {
display: true,
text: '{{ name }}'
}
}
}
}
};
let items_chart = new Chart(
document.getElementById('item_chart'),
config
);
}
);
</script>
{% endblock %}

View File

@@ -11,23 +11,43 @@
{% block content %}
<div class="row">
<div class="col">
Items: <br/>
{% for item in items %}
Items:
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.items_graph', start=1, end=0)}}'>
Graph
</a><br/>
{% for report in reports_items|reverse %}
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.items_by_date', date=item.date)}}'>
{{item.date}}
href='{{url_for('reports.items_by_date', date=report.date)}}'>
{{report.date}}
</a>
{% endfor %}
</div>
<div class="col">
Currency:
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.currency_graph', start=1, end=0)}}'>
Graph
</a><br/>
{% for report in reports_currency|reverse %}
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.currency_by_date', date=report.date)}}'>
{{report.date}}
</a>
{% endfor %}
</div>
<div class="col">
U-Score:
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.uscore_graph', start=1, end=0)}}'>
Graph
</a><br/>
{% for report in reports_uscore|reverse %}
<a role="button" class="btn btn-primary btn btn-block"
href='{{url_for('reports.uscore_by_date', date=report.date)}}'>
{{report.date}}
</a>
{% endfor %}
</div>
</div>
{% endblock %}
{% block js %}
{{ super () }}
{% endblock %}

View File

@@ -11,9 +11,8 @@
{% block content %}
<div class='table-responsive'>
<table class="table table-dark table-striped table-bordered table-hover"
id="two_weeks"
data-order='[[ 1, "asc" ]]'
data-page-length='25'>
id="items_by_date"
data-order='[[ 1, "desc" ]]'>
<thead>
<th scope="col">
Item
@@ -21,21 +20,37 @@
<th scope="col">
Count
</th>
<th scope="col">
Breakdown
</th>
<th scope="col">
Rarity
</th>
</thead>
<tbody>
{% for item in items %}
{% for lot, details in data.items() %}
<tr>
<td>
{{ item.item|get_lot_name }}
{{ lot|get_lot_name }}
</td>
<td>
{{ item.count }}
{% if details.chars %}
{{ details.item_count }}
{% else %}
{{ details }}
{% endif %}
</td>
<td>
{{ item.item|get_lot_rarity }}
{% 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 }}
</td>
</tr>
{% endfor %}
@@ -44,3 +59,14 @@
</div>
{% endblock %}
{% block js %}
{{ super () }}
<script>
$(document).ready(function(){
let items_by_date = $('#items_by_date').DataTable({
"processing": false,
"serverSide": false,
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends 'base.html.j2' %}
{% block title %}
U-Score on {{ date }}
{% endblock title %}
{% block content_before %}
U-Score on {{ date }}
{% endblock content_before %}
{% block content %}
<div class='table-responsive'>
<table class="table table-dark table-striped table-bordered table-hover"
id="uscore_by_date"
data-order='[[ 1, "desc" ]]'>
<thead>
<th scope="col">
Character
</th>
<th scope="col">
U-Score
</th>
</thead>
<tbody>
{% for name, uscore in data.items() %}
<tr>
<td>
{{ name }}
</td>
<td>
{{ uscore }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block js %}
{{ super () }}
<script>
$(document).ready(function(){
let uscore_by_date = $('#uscore_by_date').DataTable({
"processing": false,
"serverSide": false,
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'base.html.j2' %}
{% block title %}ERROR{% endblock %}
{% block content_before %}
ERROR - {{ config.APP_NAME }}
{% endblock %}
{% block content %}
{% if current_user.gm_level == 9 %}
<code>
{{ exception }}
</code>
{% else %}
<h2 class="text-center">An Error has Occurred!!!</h2>
<div>Please Report this to an Admin</div>
{% endif %}
{% endblock %}

View File

@@ -3,5 +3,11 @@
# 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
# update the DB
flask db upgrade
# RUNNNNNNNNNNNNN
gunicorn -b :8000 -w 4 wsgi:app

View File

@@ -0,0 +1,38 @@
"""fix nullables
Revision ID: 3132aaef7413
Revises: bd908969d8fe
Create Date: 2022-02-11 21:51:58.479066
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '3132aaef7413'
down_revision = 'bd908969d8fe'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('audit_logs', 'account_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.alter_column('audit_logs', 'action',
existing_type=mysql.TEXT(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('audit_logs', 'action',
existing_type=mysql.TEXT(),
nullable=True)
op.alter_column('audit_logs', 'account_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
# ### end Alembic commands ###

View File

@@ -0,0 +1,51 @@
"""reporter_id
Revision ID: 8e52b5c7568a
Revises: fa97b0d0c351
Create Date: 2022-04-02 17:35:54.814007
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy import engine_from_config
from sqlalchemy.engine import reflection
# revision identifiers, used by Alembic.
revision = '8e52b5c7568a'
down_revision = 'fa97b0d0c351'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
if not _table_has_column('bug_reports', 'reporter_id'):
op.add_column(
'bug_reports',
sa.Column(
'reporter_id',
mysql.INTEGER(),
server_default='0',
nullable=False
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('bug_reports', 'reporter_id')
# ### end Alembic commands ###
def _table_has_column(table, column):
config = op.get_context().config
engine = engine_from_config(
config.get_section(config.config_ini_section), prefix='sqlalchemy.')
insp = reflection.Inspector.from_engine(engine)
has_column = False
for col in insp.get_columns(table):
if column not in col['name']:
continue
has_column = True
return has_column

View File

@@ -0,0 +1,33 @@
"""reports
Revision ID: aee4c6c24811
Revises: 8a2966b9f7ee
Create Date: 2022-01-16 20:12:39.816567
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'aee4c6c24811'
down_revision = '8a2966b9f7ee'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('reports',
sa.Column('data', sa.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 ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('reports')
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""make pet owner not a forein key
Revision ID: b470795db8e1
Revises: e3e8e05f27ee
Create Date: 2022-02-12 20:51:12.318782
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b470795db8e1'
down_revision = 'e3e8e05f27ee'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('pet_names_ibfk_1', 'pet_names', type_='foreignkey')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key('pet_names_ibfk_1', 'pet_names', 'charinfo', ['owner_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###

View File

@@ -1,33 +0,0 @@
"""itemreport table
Revision ID: b89c21b5112f
Revises: 8a2966b9f7ee
Create Date: 2022-01-15 19:21:44.544653
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'b89c21b5112f'
down_revision = '8a2966b9f7ee'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('item_reports',
sa.Column('item', sa.Integer(), nullable=False),
sa.Column('count', sa.Integer(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.PrimaryKeyConstraint('item', 'date')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('item_reports')
# ### end Alembic commands ###

View File

@@ -0,0 +1,35 @@
"""Add audit_log table
Revision ID: bd908969d8fe
Revises: aee4c6c24811
Create Date: 2022-02-11 21:48:03.798474
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'bd908969d8fe'
down_revision = 'aee4c6c24811'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('audit_logs',
sa.Column('id', mysql.INTEGER(), nullable=False),
sa.Column('account_id', sa.Integer(), nullable=True),
sa.Column('action', mysql.TEXT(), nullable=True),
sa.Column('date', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('audit_logs')
# ### end Alembic commands ###

View File

@@ -0,0 +1,30 @@
"""pet owners
Revision ID: e3e8e05f27ee
Revises: 3132aaef7413
Create Date: 2022-02-11 23:18:20.978203
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'e3e8e05f27ee'
down_revision = '3132aaef7413'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('pet_names', sa.Column('owner_id', mysql.BIGINT(), nullable=True))
op.create_foreign_key(None, 'pet_names', 'charinfo', ['owner_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'pet_names', type_='foreignkey')
op.drop_column('pet_names', 'owner_id')
# ### end Alembic commands ###

View File

@@ -0,0 +1,56 @@
"""property performance index
Revision ID: fa97b0d0c351
Revises: b470795db8e1
Create Date: 2022-03-31 10:38:06.367277
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy import engine_from_config
from sqlalchemy.engine import reflection
# revision identifiers, used by Alembic.
revision = 'fa97b0d0c351'
down_revision = 'b470795db8e1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
if not _table_has_column('properties', 'performance_cost'):
op.add_column(
'properties',
sa.Column(
'performance_cost',
mysql.DOUBLE(
precision=20,
scale=15,
asdecimal=False
),
server_default='0.0',
nullable=True
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('properties', 'performance_cost')
# ### end Alembic commands ###
def _table_has_column(table, column):
config = op.get_context().config
engine = engine_from_config(
config.get_section(config.config_ini_section), prefix='sqlalchemy.')
insp = reflection.Inspector.from_engine(engine)
has_column = False
for col in insp.get_columns(table):
if column not in col['name']:
continue
has_column = True
return has_column

0
property_files/.gitkeep Normal file
View File

6
pylama.ini Normal file
View File

@@ -0,0 +1,6 @@
[pylama]
ignore = D203, D212, D213, D406, D407, D408, D409, D100, D104, D401, F722
max_line_length = 160
[pylama:mccabe]
max-complexity = 35

View File

@@ -49,6 +49,7 @@ six==1.16.0
snowballstemmer==2.2.0
SQLAlchemy==1.4.22
sqlalchemy-datatables==2.0.1
SQLAlchemy-Utils==0.38.2
toml==0.10.2
tzdata==2021.5
tzlocal==4.1

11
wsgi.py
View File

@@ -13,4 +13,13 @@ def make_shell_context():
if __name__ == '__main__':
with app.app_context():
app.run(host='0.0.0.0')
else:
import logging
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)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(gunicorn_logger.level)