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

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