Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""LICENSE
2Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
4This file is part of jerrycan.
6jerrycan is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
11jerrycan is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
16You should have received a copy of the GNU General Public License
17along with jerrycan. If not, see <http://www.gnu.org/licenses/>.
18LICENSE"""
20import os
21import logging
22import pkg_resources
23from typing import Type, Dict, Any, Callable, List, Optional
24from bokkichat.settings.impl.TelegramBotSettings import TelegramBotSettings
25from bokkichat.connection.impl.TelegramBotConnection import \
26 TelegramBotConnection
29class Config:
30 """
31 Class that keeps track of configuration data
32 The class attributes should only be called after running load_config
33 """
35 @classmethod
36 def load_config(cls, root_path: str, module_name: str, sentry_dsn: str):
37 """
38 Loads the configuration from environment variables
39 :param root_path: The root path of the application
40 :param module_name: The name of the project's module
41 :param sentry_dsn: The sentry DSN used for error logging
42 :return: None
43 """
44 cls.ensure_environment_variables_present()
46 Config.LOGGING_PATH = os.environ.get(
47 "LOGGING_PATH",
48 os.path.join("/tmp", f"{module_name}.log")
49 )
50 Config.DEBUG_LOGGING_PATH = os.environ.get(
51 "DEBUG_LOGGING_PATH",
52 os.path.join("/tmp", f"{module_name}-debug.log")
53 )
54 verbosity_name = os.environ.get("VERBOSITY", "debug").lower()
55 Config.VERBOSITY = {
56 "error": logging.ERROR,
57 "warning": logging.WARNING,
58 "info": logging.INFO,
59 "debug": logging.DEBUG
60 }.get(verbosity_name, logging.DEBUG)
62 Config.SENTRY_DSN = sentry_dsn
63 Config.VERSION = \
64 pkg_resources.get_distribution(module_name).version
65 Config.FLASK_SECRET = os.environ["FLASK_SECRET"]
66 Config.TESTING = os.environ.get("FLASK_TESTING") == "1"
67 Config.BEHIND_PROXY = os.environ.get("BEHIND_PROXY") == "1"
68 Config.HTTP_PORT = int(os.environ.get("HTTP_PORT", "80"))
69 Config.DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "localhost")
71 if Config.TESTING:
72 Config.DB_MODE = "sqlite"
73 else:
74 Config.DB_MODE = os.environ["DB_MODE"].lower()
75 if Config.DB_MODE == "sqlite":
76 sqlite_path = os.environ.get(
77 "SQLITE_PATH",
78 os.path.join("/tmp", f"{module_name}.db")
79 )
80 Config.DB_URI = "sqlite:///" + sqlite_path
81 else:
82 base = Config.DB_MODE.upper()
83 db_keyword = "DATABASE"
84 if base == "POSTGRESQL":
85 base = "POSTGRES"
86 db_keyword = "DB"
87 base += "_"
88 db_host = os.environ[base + "HOST"]
89 db_port = os.environ[base + "PORT"]
90 db_user = os.environ[base + "USER"]
91 db_password = os.environ[base + "PASSWORD"]
92 db_database = os.environ[base + db_keyword]
93 Config.DB_URI = f"{Config.DB_MODE}://{db_user}:{db_password}@"\
94 f"{db_host}:{db_port}/{db_database}"
96 Config.RECAPTCHA_SITE_KEY = os.environ["RECAPTCHA_SITE_KEY"]
97 Config.RECAPTCHA_SECRET_KEY = os.environ["RECAPTCHA_SECRET_KEY"]
99 Config.SMTP_HOST = os.environ["SMTP_HOST"]
100 Config.SMTP_PORT = int(os.environ["SMTP_PORT"])
101 Config.SMTP_ADDRESS = os.environ["SMTP_ADDRESS"]
102 Config.SMTP_PASSWORD = os.environ["SMTP_PASSWORD"]
103 Config.TELEGRAM_API_KEY = os.environ["TELEGRAM_API_KEY"]
104 Config.TELEGRAM_WHOAMI = os.environ.get("TELEGRAM_WHOAMI", "1") == "1"
106 cls._load_extras(Config)
108 for required_template in cls.REQUIRED_TEMPLATES.values():
109 path = os.path.join(root_path, "templates", required_template)
110 if not os.path.isfile(path):
111 print(f"Missing template file {path}")
112 exit(1)
114 @classmethod
115 def _load_extras(cls, parent: Type["Config"]):
116 """
117 This method can be used to add attributes in subclasses as well as
118 change attributes in the base Config class
119 :param parent: The base Config class, used to chage attributes
120 :return: None
121 """
122 pass
124 @classmethod
125 def environment_variables(cls) -> Dict[str, List[str]]:
126 """
127 Specifies required and optional environment variables
128 :return: The specified environment variables in two lists in
129 a dictionary, grouped by whether the variables are
130 required or optional
131 """
132 required = [
133 "FLASK_SECRET",
134 "DB_MODE",
135 "RECAPTCHA_SITE_KEY",
136 "RECAPTCHA_SECRET_KEY",
137 "SMTP_HOST",
138 "SMTP_PORT",
139 "SMTP_ADDRESS",
140 "SMTP_PASSWORD",
141 "TELEGRAM_API_KEY"
142 ]
143 optional = [
144 "LOGGING_PATH",
145 "DEBUG_LOGGING_PATH",
146 "FLASK_TESTING",
147 "DOMAIN_NAME",
148 "HTTP_PORT",
149 "BEHIND_PROXY",
150 "TELEGRAM_WHOAMI"
151 ]
153 db_mode = os.environ.get("DB_MODE")
154 if db_mode == "sqlite":
155 optional.append("SQLITE_PATH")
156 elif db_mode is not None:
157 db_mode = db_mode.upper()
158 db_keyword = "DATABASE"
159 if db_mode == "POSTGRESQL":
160 db_mode = "POSTGRES"
161 db_keyword = "DB"
163 required.append(f"{db_mode}_HOST")
164 required.append(f"{db_mode}_PORT")
165 required.append(f"{db_mode}_USER")
166 required.append(f"{db_mode}_PASSWORD")
167 required.append(f"{db_mode}_{db_keyword}")
169 return {
170 "required": required,
171 "optional": optional
172 }
174 @classmethod
175 def ensure_environment_variables_present(cls):
176 """
177 Makes sure that all required environment variables have been set.
178 If this is not the case, the app will exit.
179 :return: None
180 """
181 for env_name in cls.environment_variables()["required"]:
182 if env_name not in os.environ:
183 print(f"Missing environment variable: {env_name}")
184 exit(1)
186 @classmethod
187 def dump_env_variables(cls, path: Optional[str] = None):
188 """
189 Dumps all environment variables used by this application to a file
190 :param path: The path to the file to which to dump the content.
191 If this is None, the file contents will be printed.
192 :return: None
193 """
194 envs = ""
195 all_env_names = cls.environment_variables()["required"] + \
196 cls.environment_variables()["optional"]
197 for env_name in all_env_names:
198 value = os.environ.get(env_name)
199 if value is not None:
200 envs += f"{env_name}={value}\n"
202 if path is not None:
203 with open(path, "w") as f:
204 f.write(envs)
205 else:
206 print(envs)
208 @classmethod
209 def base_url(cls) -> str:
210 """
211 :return: The base URL of the website
212 """
213 if cls.BEHIND_PROXY:
214 return f"https://{cls.DOMAIN_NAME}"
215 else:
216 return f"http://{cls.DOMAIN_NAME}:{cls.HTTP_PORT}"
218 @classmethod
219 def initialize_telegram(cls):
220 """
221 Initializes the telegram bot connection
222 :return: None
223 """
224 Config.TELEGRAM_BOT_CONNECTION = TelegramBotConnection(
225 TelegramBotSettings(Config.TELEGRAM_API_KEY)
226 )
228 VERSION: str
229 """
230 The current version of the application
231 """
233 SENTRY_DSN: str
234 """
235 The sentry DSN used for error logging
236 """
238 VERBOSITY: int
239 """
240 The verbosity level of the logging when printing to the console
241 """
243 FLASK_SECRET: str
244 """
245 The flask secret key
246 """
248 TESTING: bool
249 """
250 Whether or not testing is enabled
251 """
253 LOGGING_PATH: str
254 """
255 The path to the logging file
256 """
258 DEBUG_LOGGING_PATH: str
259 """
260 The path to the debug logging path
261 """
263 WARNING_LOGGING_PATH: str
264 """
265 The path to the logging path for WARNING messages
266 """
268 HTTP_PORT: int
269 """
270 The port to use when serving the flask application
271 """
273 DOMAIN_NAME: str
274 """
275 The domain name of the website
276 """
278 DB_MODE: str
279 """
280 The database mode (for example 'sqlite' or 'mysql')
281 """
283 DB_URI: str
284 """
285 The database URI to use for database operations
286 """
288 RECAPTCHA_SITE_KEY: str
289 """
290 Google ReCaptcha site key for bot detection
291 """
293 RECAPTCHA_SECRET_KEY: str
294 """
295 Google ReCaptcha secret key for bot detection
296 """
298 SMTP_HOST: str
299 """
300 The SMPT Host used for sending emails
301 """
303 SMTP_PORT: int
304 """
305 The SMPT Port used for sending emails
306 """
308 SMTP_ADDRESS: str
309 """
310 The SMPT Address used for sending emails
311 """
312 SMTP_PASSWORD: str
313 """
314 The SMPT Password used for sending emails
315 """
317 TELEGRAM_API_KEY: str
318 """
319 API key for a telegram bot
320 """
322 TELEGRAM_BOT_CONNECTION: TelegramBotConnection
323 """
324 Telegram bot connection
325 """
327 TELEGRAM_WHOAMI: bool
328 """
329 Whether or not the telegram WHOAMI background thread will be started
330 """
332 BEHIND_PROXY: bool
333 """
334 Whether or not the site is being served by a reverse proxy like nginx.
335 """
337 MIN_USERNAME_LENGTH: int = 1
338 """
339 The Minimum length for usernames
340 """
342 MAX_USERNAME_LENGTH: int = 12
343 """
344 The maximum length of usernames
345 """
347 MAX_API_KEY_AGE: int = 2592000 # 30 days
348 """
349 The maximum age for API keys
350 """
352 SESSION_PROTECTION: str = "basic"
353 """
354 The flask_login session protection value
355 With 'basic', the remember me functionality will function,
356 with 'strong', a browser restart will log the user out.
357 """
359 API_VERSION: str = "1"
360 """
361 The API Version
362 """
364 REQUIRED_TEMPLATES: Dict[str, str] = {
365 "index": "static/index.html",
366 "about": "static/about.html",
367 "privacy": "static/privacy.html",
368 "error_page": "static/error_page.html",
369 "registration_email": "email/registration.html",
370 "forgot_password_email": "email/forgot_password.html",
371 "forgot": "user_management/forgot.html",
372 "login": "user_management/login.html",
373 "profile": "user_management/profile.html",
374 "register": "user_management/register.html"
375 }
376 """
377 Specifies required template files
378 """
380 STRINGS: Dict[str, str] = {
381 "401_message": "You are not logged in",
382 "500_message": "The server encountered an internal error and "
383 "was unable to complete your request. "
384 "Either the server is overloaded or there "
385 "is an error in the application.",
386 "user_does_not_exist": "User does not exist",
387 "user_already_logged_in": "User already logged in",
388 "user_already_confirmed": "User already confirmed",
389 "user_is_not_confirmed": "User is not confirmed",
390 "invalid_password": "Invalid Password",
391 "logged_in": "Logged in successfully",
392 "logged_out": "Logged out",
393 "username_length": "Username must be between {} and {} characters "
394 "long",
395 "passwords_do_not_match": "Passwords do not match",
396 "email_already_in_use": "Email already in use",
397 "username_already_exists": "Username already exists",
398 "recaptcha_incorrect": "ReCaptcha not solved correctly",
399 "registration_successful": "Registered Successfully. Check your email "
400 "inbox for confirmation",
401 "registration_email_title": "Registration",
402 "confirmation_key_invalid": "Confirmation key invalid",
403 "user_confirmed_successfully": "User confirmed successfully",
404 "password_reset_email_title": "Password Reset",
405 "password_was_reset": "Password was reset successfully",
406 "password_changed": "Password changed successfully",
407 "user_was_deleted": "User was deleted",
408 "telegram_chat_id_set": "Telegram Chat ID was set"
409 }
410 """
411 Dictionary that defines various strings used in the application.
412 Makes it easier to use custom phrases.
413 """
415 TEMPLATE_EXTRAS: Dict[str, Callable[[], Dict[str, Any]]] = {
416 "index": lambda: {},
417 "about": lambda: {},
418 "privacy": lambda: {},
419 "login": lambda: {},
420 "register": lambda: {},
421 "forgot": lambda: {},
422 "profile": lambda: {},
423 "registration_email": lambda: {},
424 "forgot_email": lambda: {}
425 }
426 """
427 This can be used to provide the template rendering engine additional
428 parameters, which may be necessary when adding UI elements.
429 This is done with functions that don't expect any input and
430 return a dictionary of keys and values to be passed to the template
431 rendering engine
432 """