Something something like soundcloud but not like soundcloud.
Log in, upload records, done.
Simple, easy, KISS.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. # encoding: utf-8
  2. import logging
  3. import os
  4. import subprocess
  5. from logging.handlers import RotatingFileHandler
  6. from flask_babelex import gettext, Babel
  7. from flask import Flask, render_template, g, send_from_directory, jsonify, safe_join, request, flash, Response
  8. from flask_bootstrap import Bootstrap
  9. from flask_mail import Mail
  10. from flask_migrate import Migrate
  11. from flask_security import Security
  12. from flask_security.utils import encrypt_password
  13. from flask_security import signals as FlaskSecuritySignals
  14. from flask_security import confirmable as FSConfirmable
  15. from flask_uploads import configure_uploads, UploadSet, AUDIO, patch_request_class
  16. from forms import ExtendedRegisterForm
  17. from models import db, Config, user_datastore, Role, create_actor
  18. from utils import InvalidUsage, is_admin, duration_elapsed_human, duration_song_human, add_user_log
  19. import texttable
  20. from flask_debugtoolbar import DebugToolbarExtension
  21. from dbseed import make_db_seed
  22. from pprint import pprint as pp
  23. import click
  24. from little_boxes import activitypub as ap
  25. from activitypub.backend import Reel2BitsBackend
  26. from celery import Celery
  27. from version import VERSION
  28. __VERSION__ = VERSION
  29. AVAILABLE_LOCALES = ["fr", "fr_FR", "en", "en_US", "pl"]
  30. try:
  31. import sentry_sdk
  32. from sentry_sdk.integrations.flask import FlaskIntegration as SentryFlaskIntegration
  33. from sentry_sdk.integrations.celery import CeleryIntegration as SentryCeleryIntegration
  34. print(" * Sentry Flask/Celery support have been loaded")
  35. HAS_SENTRY = True
  36. except ImportError:
  37. print(" * No Sentry Flask/Celery support available")
  38. HAS_SENTRY = False
  39. mail = Mail()
  40. GIT_VERSION = ""
  41. gitpath = os.path.join(os.getcwd(), ".git")
  42. if os.path.isdir(gitpath):
  43. GIT_VERSION = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
  44. if GIT_VERSION:
  45. GIT_VERSION = GIT_VERSION.strip().decode("UTF-8")
  46. def make_celery(remoulade):
  47. celery = Celery(remoulade.import_name, broker=remoulade.config["CELERY_BROKER_URL"])
  48. celery.conf.update(remoulade.config)
  49. TaskBase = celery.Task
  50. class ContextTask(TaskBase):
  51. abstract = True
  52. def __call__(self, *args, **kwargs):
  53. with remoulade.app_context():
  54. return TaskBase.__call__(self, *args, **kwargs)
  55. celery.Task = ContextTask
  56. return celery # omnomnom
  57. def create_app(config_filename="config.py", app_name=None, register_blueprints=True):
  58. # App configuration
  59. app = Flask(app_name or __name__)
  60. app.config.from_pyfile(config_filename)
  61. Bootstrap(app)
  62. app.jinja_env.add_extension("jinja2.ext.with_")
  63. app.jinja_env.add_extension("jinja2.ext.do")
  64. app.jinja_env.globals.update(is_admin=is_admin)
  65. app.jinja_env.globals.update(duration_elapsed_human=duration_elapsed_human)
  66. app.jinja_env.globals.update(duration_song_human=duration_song_human)
  67. if HAS_SENTRY:
  68. sentry_sdk.init(
  69. app.config["SENTRY_DSN"],
  70. integrations=[SentryFlaskIntegration(), SentryCeleryIntegration()],
  71. release=f"{VERSION} ({GIT_VERSION})",
  72. )
  73. print(" * Sentry Flask/Celery support activated")
  74. print(" * Sentry DSN: %s" % app.config["SENTRY_DSN"])
  75. if app.config["DEBUG"] is True:
  76. app.jinja_env.auto_reload = True
  77. app.logger.setLevel(logging.DEBUG)
  78. # Logging
  79. if not app.debug:
  80. formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]")
  81. file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), "a", 1000000, 1)
  82. file_handler.setLevel(logging.DEBUG)
  83. file_handler.setFormatter(formatter)
  84. app.logger.addHandler(file_handler)
  85. mail.init_app(app)
  86. migrate = Migrate(app, db) # noqa: F841
  87. babel = Babel(app) # noqa: F841
  88. app.babel = babel
  89. toolbar = DebugToolbarExtension(app) # noqa: F841
  90. db.init_app(app)
  91. # ActivityPub backend
  92. back = Reel2BitsBackend()
  93. ap.use_backend(back)
  94. # Setup Flask-Security
  95. security = Security( # noqa: F841
  96. app, user_datastore, register_form=ExtendedRegisterForm, confirm_register_form=ExtendedRegisterForm
  97. )
  98. @FlaskSecuritySignals.password_reset.connect_via(app)
  99. @FlaskSecuritySignals.password_changed.connect_via(app)
  100. def log_password_reset(sender, user):
  101. if not user:
  102. return
  103. add_user_log(user.id, user.id, "user", "info", "Your password has been changed !")
  104. @FlaskSecuritySignals.reset_password_instructions_sent.connect_via(app)
  105. def log_reset_password_instr(sender, user, token):
  106. if not user:
  107. return
  108. add_user_log(user.id, user.id, "user", "info", "Password reset instructions sent.")
  109. @FlaskSecuritySignals.user_registered.connect_via(app)
  110. def create_actor_for_registered_user(app, user, confirm_token):
  111. if not user:
  112. return
  113. actor = create_actor(user)
  114. actor.user = user
  115. actor.user_id = user.id
  116. db.session.add(actor)
  117. db.session.commit()
  118. @babel.localeselector
  119. def get_locale():
  120. # if a user is logged in, use the locale from the user settings
  121. identity = getattr(g, "identity", None)
  122. if identity is not None and identity.id:
  123. return identity.user.locale
  124. # otherwise try to guess the language from the user accept
  125. # header the browser transmits. We support fr/en in this
  126. # example. The best match wins.
  127. return request.accept_languages.best_match(AVAILABLE_LOCALES)
  128. @babel.timezoneselector
  129. def get_timezone():
  130. identity = getattr(g, "identity", None)
  131. if identity is not None and identity.id:
  132. return identity.user.timezone
  133. @app.before_request
  134. def before_request():
  135. _config = Config.query.first()
  136. if not _config:
  137. flash(gettext("Config not found"), "error")
  138. cfg = {
  139. "REEL2BITS_VERSION_VER": VERSION,
  140. "REEL2BITS_VERSION_GIT": GIT_VERSION,
  141. "REEL2BITS_VERSION": "{0} ({1})".format(VERSION, GIT_VERSION),
  142. "app_name": _config.app_name,
  143. "app_description": _config.app_description,
  144. }
  145. g.cfg = cfg
  146. @app.errorhandler(InvalidUsage)
  147. def handle_invalid_usage(error):
  148. response = jsonify(error.to_dict())
  149. response.status_code = error.status_code
  150. return response
  151. sounds = UploadSet("sounds", AUDIO)
  152. configure_uploads(app, sounds)
  153. patch_request_class(app, 500 * 1024 * 1024) # 500m limit
  154. if register_blueprints:
  155. from controllers.main import bp_main
  156. app.register_blueprint(bp_main)
  157. from controllers.users import bp_users
  158. app.register_blueprint(bp_users)
  159. from controllers.admin import bp_admin
  160. app.register_blueprint(bp_admin)
  161. from controllers.sound import bp_sound
  162. app.register_blueprint(bp_sound)
  163. from controllers.albums import bp_albums
  164. app.register_blueprint(bp_albums)
  165. from controllers.search import bp_search
  166. app.register_blueprint(bp_search)
  167. from controllers.api.v1.well_known import bp_wellknown
  168. app.register_blueprint(bp_wellknown)
  169. from controllers.api.v1.nodeinfo import bp_nodeinfo
  170. app.register_blueprint(bp_nodeinfo)
  171. from controllers.api.v1.activitypub import bp_ap
  172. app.register_blueprint(bp_ap)
  173. @app.route("/uploads/<string:thing>/<path:stuff>", methods=["GET"])
  174. def get_uploads_stuff(thing, stuff):
  175. if app.testing:
  176. directory = safe_join(app.config["UPLOADS_DEFAULT_DEST"], thing)
  177. app.logger.debug(f"serving {stuff} from {directory}")
  178. return send_from_directory(directory, stuff, as_attachment=True)
  179. else:
  180. app.logger.debug(f"X-Accel-Redirect serving {stuff}")
  181. resp = Response("")
  182. resp.headers["Content-Disposition"] = f"attachment; filename={stuff}"
  183. resp.headers["X-Accel-Redirect"] = f"/_protected/media/{thing}/{stuff}"
  184. resp.headers["Content-Type"] = "" # empty it so Nginx will guess it correctly
  185. return resp
  186. @app.errorhandler(404)
  187. def page_not_found(msg):
  188. pcfg = {
  189. "title": gettext("Whoops, something failed."),
  190. "error": 404,
  191. "message": gettext("Page not found"),
  192. "e": msg,
  193. }
  194. return render_template("error_page.jinja2", pcfg=pcfg), 404
  195. @app.errorhandler(403)
  196. def err_forbidden(msg):
  197. pcfg = {
  198. "title": gettext("Whoops, something failed."),
  199. "error": 403,
  200. "message": gettext("Access forbidden"),
  201. "e": msg,
  202. }
  203. return render_template("error_page.jinja2", pcfg=pcfg), 403
  204. @app.errorhandler(410)
  205. def err_gone(msg):
  206. pcfg = {"title": gettext("Whoops, something failed."), "error": 410, "message": gettext("Gone"), "e": msg}
  207. return render_template("error_page.jinja2", pcfg=pcfg), 410
  208. if not app.debug:
  209. @app.errorhandler(500)
  210. def err_failed(msg):
  211. pcfg = {
  212. "title": gettext("Whoops, something failed."),
  213. "error": 500,
  214. "message": gettext("Something is broken"),
  215. "e": msg,
  216. }
  217. return render_template("error_page.jinja2", pcfg=pcfg), 500
  218. @app.after_request
  219. def set_x_powered_by(response):
  220. response.headers["X-Powered-By"] = "reel2bits"
  221. return response
  222. # Other commands
  223. @app.cli.command()
  224. def routes():
  225. """Dump all routes of defined app"""
  226. table = texttable.Texttable()
  227. table.set_deco(texttable.Texttable().HEADER)
  228. table.set_cols_dtype(["t", "t", "t"])
  229. table.set_cols_align(["l", "l", "l"])
  230. table.set_cols_width([50, 30, 80])
  231. table.add_rows([["Prefix", "Verb", "URI Pattern"]])
  232. for rule in sorted(app.url_map.iter_rules(), key=lambda x: str(x)):
  233. methods = ",".join(rule.methods)
  234. table.add_row([rule.endpoint, methods, rule])
  235. print(table.draw())
  236. @app.cli.command()
  237. def config():
  238. """Dump config"""
  239. pp(app.config)
  240. @app.cli.command()
  241. def seed():
  242. """Seed database with default content"""
  243. make_db_seed(db)
  244. @app.cli.command()
  245. def createuser():
  246. """Create an user"""
  247. username = click.prompt("Username", type=str)
  248. email = click.prompt("Email", type=str)
  249. password = click.prompt("Password", type=str, hide_input=True, confirmation_prompt=True)
  250. while True:
  251. role = click.prompt("Role [admin/user]", type=str)
  252. if role == "admin" or role == "user":
  253. break
  254. if click.confirm("Do you want to continue ?"):
  255. role = Role.query.filter(Role.name == role).first()
  256. if not role:
  257. raise click.UsageError("Roles not present in database")
  258. u = user_datastore.create_user(
  259. name=username, email=email, password=encrypt_password(password), roles=[role]
  260. )
  261. actor = create_actor(u)
  262. actor.user = u
  263. actor.user_id = u.id
  264. db.session.add(actor)
  265. db.session.commit()
  266. if FSConfirmable.requires_confirmation(u):
  267. FSConfirmable.send_confirmation_instructions(u)
  268. print("Look at your emails for validation instructions.")
  269. return app