Hide keyboard shortcuts

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> 

3 

4This file is part of jerrycan. 

5 

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. 

10 

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. 

15 

16You should have received a copy of the GNU General Public License 

17along with jerrycan. If not, see <http://www.gnu.org/licenses/>. 

18LICENSE""" 

19 

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 

27 

28 

29class Config: 

30 """ 

31 Class that keeps track of configuration data 

32 The class attributes should only be called after running load_config 

33 """ 

34 

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() 

45 

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) 

61 

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") 

70 

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}" 

95 

96 Config.RECAPTCHA_SITE_KEY = os.environ["RECAPTCHA_SITE_KEY"] 

97 Config.RECAPTCHA_SECRET_KEY = os.environ["RECAPTCHA_SECRET_KEY"] 

98 

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" 

105 

106 cls._load_extras(Config) 

107 

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) 

113 

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 

123 

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 ] 

152 

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" 

162 

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}") 

168 

169 return { 

170 "required": required, 

171 "optional": optional 

172 } 

173 

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) 

185 

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" 

201 

202 if path is not None: 

203 with open(path, "w") as f: 

204 f.write(envs) 

205 else: 

206 print(envs) 

207 

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}" 

217 

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 ) 

227 

228 VERSION: str 

229 """ 

230 The current version of the application 

231 """ 

232 

233 SENTRY_DSN: str 

234 """ 

235 The sentry DSN used for error logging 

236 """ 

237 

238 VERBOSITY: int 

239 """ 

240 The verbosity level of the logging when printing to the console 

241 """ 

242 

243 FLASK_SECRET: str 

244 """ 

245 The flask secret key 

246 """ 

247 

248 TESTING: bool 

249 """ 

250 Whether or not testing is enabled 

251 """ 

252 

253 LOGGING_PATH: str 

254 """ 

255 The path to the logging file 

256 """ 

257 

258 DEBUG_LOGGING_PATH: str 

259 """ 

260 The path to the debug logging path 

261 """ 

262 

263 WARNING_LOGGING_PATH: str 

264 """ 

265 The path to the logging path for WARNING messages 

266 """ 

267 

268 HTTP_PORT: int 

269 """ 

270 The port to use when serving the flask application 

271 """ 

272 

273 DOMAIN_NAME: str 

274 """ 

275 The domain name of the website 

276 """ 

277 

278 DB_MODE: str 

279 """ 

280 The database mode (for example 'sqlite' or 'mysql') 

281 """ 

282 

283 DB_URI: str 

284 """ 

285 The database URI to use for database operations 

286 """ 

287 

288 RECAPTCHA_SITE_KEY: str 

289 """ 

290 Google ReCaptcha site key for bot detection 

291 """ 

292 

293 RECAPTCHA_SECRET_KEY: str 

294 """ 

295 Google ReCaptcha secret key for bot detection 

296 """ 

297 

298 SMTP_HOST: str 

299 """ 

300 The SMPT Host used for sending emails 

301 """ 

302 

303 SMTP_PORT: int 

304 """ 

305 The SMPT Port used for sending emails 

306 """ 

307 

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

316 

317 TELEGRAM_API_KEY: str 

318 """ 

319 API key for a telegram bot 

320 """ 

321 

322 TELEGRAM_BOT_CONNECTION: TelegramBotConnection 

323 """ 

324 Telegram bot connection 

325 """ 

326 

327 TELEGRAM_WHOAMI: bool 

328 """ 

329 Whether or not the telegram WHOAMI background thread will be started 

330 """ 

331 

332 BEHIND_PROXY: bool 

333 """ 

334 Whether or not the site is being served by a reverse proxy like nginx. 

335 """ 

336 

337 MIN_USERNAME_LENGTH: int = 1 

338 """ 

339 The Minimum length for usernames 

340 """ 

341 

342 MAX_USERNAME_LENGTH: int = 12 

343 """ 

344 The maximum length of usernames 

345 """ 

346 

347 MAX_API_KEY_AGE: int = 2592000 # 30 days 

348 """ 

349 The maximum age for API keys 

350 """ 

351 

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

358 

359 API_VERSION: str = "1" 

360 """ 

361 The API Version 

362 """ 

363 

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

379 

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

414 

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