aboutsummaryrefslogtreecommitdiff
path: root/database.py
blob: 5a0287fc536185d7d2605f117d7e470c49c63bb6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# SQL Tables

# This shits a mess but hey it's python...

import contextlib
import enum
import os

from sqlalchemy import Boolean, Column, create_engine, DateTime, ForeignKey, Integer, MetaData, String, Table, Text, Unicode, UnicodeText
from sqlalchemy.dialects.mysql import BIGINT, DOUBLE
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship, RelationshipProperty, sessionmaker, validates
from sqlalchemy.sql import func

from mapping import Direction
from myconfig import config, mappings

Base = declarative_base()

engine = create_engine(config["dbaddr"], pool_recycle = 3600)
DbSession = sessionmaker(bind = engine)

class ValidationError(Exception):
	pass

def validated(col, validator = None, strip = False, min_length = None, min_value = None, max_value = None):
	
	def validate_string(row, key, val, encoding = None):
		if not isinstance(val, str):
			raise ValidationError(f"{col.name} must be a string")
		if encoding is not None:
			try:
				val.encode(encoding)
			except UnicodeEncodeError: # this exc is also used for ASCII!
				raise ValidationError(f"{col.name} must be a valid {encoding} string")
		
		if strip:
			val = val.strip()
		if min_length is not None and len(val) < min_length:
			raise ValidationError(f"{col.name} must be at least {min_length} characters long")
		if col.type.length is not None and len(val) > col.type.length:
			raise ValidationError(f"{col.name} cannot be more than {col.type.length} characters long")
		return val
	
	def validate_number(row, key, val, min, max, t):
		if not isinstance(val, t):
			raise ValidationError(f"{val} must be {t.__name__}")
		if min is not None and val < min:
			raise ValidationError(f"{col.name} cannot be less than {min}")
		if max is not None and val > max:
			raise ValidationError(f"{col.name} cannot be greater than {max}")
		return val
	
	@validates(col.name)
	def validate(row, key, val):
		if hasattr(row, "dontvalidate"):
			return val
		if validator is not None:
			val = validator(row, key, val)
		if val is None:
			if col.nullable:
				return None
			else:
				raise ValidationError(f"{col.name} cannot be null")
		
		min = min_value
		max = max_value
		
		if isinstance(col.type, String) or isinstance(col.type, Text):
			val = validate_string(row, key, val, "ASCII")
		elif isinstance(col.type, Unicode) or isinstance(col.type, UnicodeText):
			val = validate_string(row, key, val, "UTF-8")
		
		elif isinstance(col.type, Boolean) and not isinstance(val, bool):
			raise ValidationError(f"{col.name} must be a boolean")


		elif isinstance(col.type, Integer):
			if min is None:
				min = -(1 << 31)
			if max is None:
				max = (1 << 31) - 1
			val = validate_number(row, key, val, min, max, int)
		elif isinstance(col.type, BIGINT):
			if min is None:
				min = 0 if col.type.unsigned else -(1 << 63)
			if max is None:
				max = 1 << (63 if col.type.unsigned else 64)
			val = validate_number(row, key, val, min, max, int)
		elif isinstance(col.type, DOUBLE):
			val = validate_number(row, key, val, min, max, float)
		return val
	return col, validate

def validate_email(row, key, val):
	if val.count("@") == 1 and ".." not in val.split("@")[-1]:
		return val
	else:
		raise ValidationError("Invalid email")

class User(Base):
	__tablename__ = "User"
	
	# This is a GUID
	ID = Column(String(36), nullable = False, primary_key = True)
	
	Email, ValidateEmail = validated(Column("Email", Unicode(254), nullable = False, unique = True),
		validate_email, strip = True)
	Password = Column(String(60))
	
	DisplayName, ValidateDisplayName = validated(
		Column("DisplayName", Unicode(255), unique = True, nullable = False),
		strip = True, min_length = 1)
	
	SysName, ValidateSysName = validated(
		Column("SysName", Unicode(255), unique = True, nullable = False),
		strip = True, min_length = 5)
	
	FullName, ValidateFullName = validated(
		Column("FullName", Unicode(255), nullable = False, default = ""),
		strip = True)
	
	Codepoints, ValidateCodepoints = validated(Column("Codepoints", BIGINT(unsigned = True), nullable = False, default = 0))
	PongLevel, ValidatePongLevel = validated(Column("PongLevel", Integer), min_value = 1, max_value = 42)
	PongCP, ValidatePongCP = validated(Column("PongCP", BIGINT(unsigned = True)))
	
	Sessions = relationship("AuthSession", back_populates = "User")

class AuthSession(Base):
	__tablename__ = "AuthSession"
	Token = Column(String(43), primary_key = True)
	UserID = Column(String(36), ForeignKey("User.ID"))
	User = relationship("User", back_populates = "Sessions")
	AppName, ValidateAppName = validated(Column("AppName", Unicode(255), nullable = False), strip = True)
	AppDesc, ValidateAppDesc = validated(Column("AppDesc", Unicode(255), nullable = False), strip = True)
	Version, ValidateVersion = validated(Column("Version", Unicode(255), nullable = False), strip = True)
	Created = Column(DateTime)
	LastUsed = Column(DateTime)
	LastIP = Column(String(39), nullable = False)

# Save is out to a separate table cause the player can delete it without
# deleting their whole account
class Save(Base):
	__tablename__ = "Save"
	
	ID = Column(Integer, primary_key = True)
	
	UserID = Column(String(36), ForeignKey("User.ID"))
	User = relationship("User", backref = backref("Save", uselist = False))
	
	MusicVolume, ValidateMusicVolume = validated(Column("MusicVolume", Integer, nullable = False, default = 0))
	SfxVolume, ValidateSfxVolume = validated(Column("SfxVolume", Integer, nullable = False, default = 0))
	
	Upgrades = relationship("Upgrade", backref = "Save")
	
	# I think if the StoryPosition is changed back to 0 after the Oobe then ShiftOS will softlock.
	# Keep an eye on this...
	StoryPosition, ValidateStoryPosition = validated(Column("StoryPosition", Integer, nullable = False, default = 0))
	
	Language, ValidateLanguage = validated(Column("Language", Unicode(255)))
	MyShop, ValidateMyShop = validated(Column("MyShop", Unicode(255)))
	
	MajorVersion, ValidateMajorVersion = validated(Column("MajorVersion", Integer, nullable = False, default = 0))
	MinorVersion, ValidateMinorVersion = validated(Column("MinorVersion", Integer, nullable = False, default = 0))
	Revision, ValidateRevision = validated(Column("Revision", Integer, nullable = False, default = 0))
	
	IsPatreon = Column(Boolean, nullable = False, default = False)
	Class, ValidateClass = validated(Column("Class", Integer, nullable = False, default = 0),
		min_value = 0, max_value = 8)
	RawReputation, ValidateRawReputation = validated(Column("RawReputation", DOUBLE, nullable = False, default = 0))
	
	Password, ValidatePassword = validated(Column("Password", Unicode(255)))
	PasswordHashed, ValidatePasswordHashed = validated(Column("PasswordHashed", Boolean, nullable = False, default = False))
	
	ShiftnetSubscription, ValidateShiftnetSubscription = validated(Column("ShiftnetSubscription", Integer, nullable = False, default = False),
		min_value = 0, max_value = 3)
	
	IsMUDAdmin = Column(Boolean, nullable = False, default = False)
	
	LastMonthPaid, ValidateLastMonthPaid = validated(Column("LastMonthPaid", Integer, nullable = False, default = 0))
	
	StoriesExperienced = relationship("StoryExperienced", backref = "Save")
	Users = relationship("ClientSave", backref = "Save")

class Upgrade(Base):
	__tablename__ = "Upgrade"
	ID = Column(Integer, primary_key = True)
	SaveID = Column(Integer, ForeignKey("Save.ID"))
	Name, ValidateName = validated(Column("Name", Unicode(255)))
	Installed, ValidateInstalled = validated(Column("Installed", Boolean, nullable = False))

class StoryExperienced(Base):
	__tablename__ = "StoryExperienced"
	ID = Column(Integer, primary_key = True)
	SaveID = Column(Integer, ForeignKey("Save.ID"))
	Name, ValidateName = validated(Column("Name", Unicode(255)))
	Name = Column(Unicode(255))

class ClientSave(Base):
	__tablename__ = "ClientSave"
	ID = Column(Integer, primary_key = True)
	SaveID = Column(Integer, ForeignKey("Save.ID"))
	Username, ValidateUsername = validated(Column("Username", Unicode(255)))
	Password, ValidatePassword = validated(Column("Password", Unicode(255)))
	Permissions, ValidatePermissions = validated(Column("Permissions", Integer, nullable = False, default = 0), min_value = 0, max_value = 3)