diff options
author | 2018-08-07 15:09:08 +0100 | |
---|---|---|
committer | 2018-08-07 15:09:16 +0100 | |
commit | af54db6c136138c66cf5ca72419989525a0baa5c (patch) | |
tree | 8519aeab8d45277c51797c7dc23aacf3b56ed1bb /pysite | |
parent | A wizard is never late, nor is he early. (diff) |
Initial project layout for django
Diffstat (limited to 'pysite')
164 files changed, 0 insertions, 16310 deletions
diff --git a/pysite/__init__.py b/pysite/__init__.py deleted file mode 100644 index c02afd0d..00000000 --- a/pysite/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging -import os -import sys -from logging import Logger, StreamHandler, handlers - -from logmatic import JsonFormatter - -from pysite.constants import DEBUG_MODE - -# region Logging -# Get the log level from environment - -logging.TRACE = 5 -logging.addLevelName(logging.TRACE, "TRACE") - - -def monkeypatch_trace(self, msg, *args, **kwargs): - """ - Log 'msg % args' with severity 'TRACE'. - - To pass exception information, use the keyword argument exc_info with - a true value, e.g. - - logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) - """ - if self.isEnabledFor(logging.TRACE): - self._log(logging.TRACE, msg, args, **kwargs) - - -Logger.trace = monkeypatch_trace -log_level = logging.TRACE if DEBUG_MODE else logging.INFO -logging_handlers = [] - -if DEBUG_MODE: - logging_handlers.append(StreamHandler(stream=sys.stdout)) - - json_handler = logging.FileHandler(filename="log.json", mode="w") - json_handler.formatter = JsonFormatter() - logging_handlers.append(json_handler) -else: - logdir = "log" - logfile = logdir+os.sep+"site.log" - megabyte = 1048576 - - if not os.path.exists(logdir): - os.makedirs(logdir) - - filehandler = handlers.RotatingFileHandler(logfile, maxBytes=(megabyte*5), backupCount=7) - logging_handlers.append(filehandler) - - json_handler = logging.StreamHandler(stream=sys.stdout) - json_handler.formatter = JsonFormatter() - logging_handlers.append(json_handler) - -logging.basicConfig( - format="%(asctime)s pd.beardfist.com Site: | %(name)35s | %(levelname)8s | %(message)s", - datefmt="%b %d %H:%M:%S", - level=log_level, - handlers=logging_handlers -) -# endregion diff --git a/pysite/base_route.py b/pysite/base_route.py deleted file mode 100644 index 8178b142..00000000 --- a/pysite/base_route.py +++ /dev/null @@ -1,328 +0,0 @@ -from collections import Iterable -from datetime import datetime -from typing import Any - -from flask import Blueprint, Response, jsonify, redirect, render_template, session, url_for -from flask.views import MethodView -from werkzeug.exceptions import default_exceptions - -from pysite.constants import ALL_STAFF_ROLES, DEBUG_MODE, ErrorCodes -from pysite.mixins import OAuthMixin - - -class BaseView(MethodView, OAuthMixin): - """ - Base view class with functions and attributes that should be common to all view classes. - - This class should be subclassed, and is not intended to be used directly. - """ - - name = None # type: str - blueprint = None # type: str - - def render(self, *template_names: str, **context: Any) -> str: - """ - Render some templates and get them back in a form that you can simply return from your view function. - - Here's what's inserted: - * "current_page" - the "name" attribute from the view class - * "view" - the view class instance - * "logged_in" - a boolean, True if the user is logged in - * "static_file(filename)", a function used to get the URL for a given static file - * "csrf_token()", a function returning the CSRF token stored in the current session - - For XSS protection, a CSRF token must be used. The "csrf_token()" function returns the correct token - to be used in the current rendering context - if your view methods are to be protected from XSS - exploits, the following steps must be taken: - - 1. Apply the "csrf" decorator to the view method - 2. For forms, a hidden input must be declared in the template, with the name "csrf_token", and the value set to - the CSRF token. - 3. For any AJAX work, the CSRF token should be stored in a variable, and sent as part of the request headers. - You can set the "X-CSRFToken" header to the CSRF token for this. - - Any API call or form submission not protected by an API key must not be vulnerable to XSS, unless the API - call is intended to be a completely public feature. Public API methods must not be account-bound, and they - must never return information on a current user or perform any action. Only data retrieval is permissible. - - :param template_names: Names of the templates to render - :param context: Extra data to pass into the template - :return: String representing the rendered templates - """ - - context["current_page"] = self.name - context["view"] = self - context["logged_in"] = self.logged_in - context["user"] = self.user_data - context["static_file"] = self._static_file - context["debug"] = DEBUG_MODE - context["format_datetime"] = lambda dt: dt.strftime("%b %d %Y, %H:%M") if isinstance(dt, datetime) else dt - context["blueprint"] = self.blueprint - - def is_staff(): - if DEBUG_MODE: - return True - - if not self.logged_in: - return False - - for role in ALL_STAFF_ROLES: - if role in self.user_data.get("roles", []): - return True - - return False - - context["is_staff"] = is_staff - - return render_template(template_names, **context) - - def _static_file(self, filename): - return url_for("static", filename=filename) - - -class RouteView(BaseView): - """ - Standard route-based page view. For a standard page, this is what you want. - - This class is intended to be subclassed - use it as a base class for your own views, and set the class-level - attributes as appropriate. For example: - - >>> class MyView(RouteView): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... - ... def get(self): # Name your function after the relevant HTTP method - ... return self.render("index.html") - - For more complicated routing, see http://exploreflask.com/en/latest/views.html#built-in-converters - """ - - path = None # type: str - - @classmethod - def setup(cls: "RouteView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view by adding it to the blueprint passed in - this will also deal with multiple inheritance by - calling `super().setup()` as appropriate. - - This is for a standard route view. Nothing special here. - - :param manager: Instance of the current RouteManager - :param blueprint: Current Flask blueprint to register this route to - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) - - if not cls.path or not cls.name: - raise RuntimeError("Route views must have both `path` and `name` defined") - - blueprint.add_url_rule(cls.path, view_func=cls.as_view(cls.name)) - - cls.blueprint = blueprint.name - cls.name = f"{blueprint.name}.{cls.name}" # Add blueprint to page name - - def redirect_login(self, **kwargs): - session["redirect_target"] = { - "url": self.name, - "kwargs": kwargs - } - - response = redirect(url_for("discord.login")) - response.headers.add("X-Robots-Tag", "noindex") - - return response - - -class APIView(RouteView): - """ - API route view, with extra methods to help you add routes to the JSON API with ease. - - This class is intended to be subclassed - use it as a base class for your own views, and set the class-level - attributes as appropriate. For example: - - >>> class MyView(APIView): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... - ... def get(self): # Name your function after the relevant HTTP method - ... return self.error(ErrorCodes.unknown_route) - """ - - def error(self, error_code: ErrorCodes, error_info: str = "") -> Response: - """ - Generate a JSON response for you to return from your handler, for a specific type of API error - - :param error_code: The type of error to generate a response for - see `constants.ErrorCodes` for more - :param error_info: An optional message with more information about the error. - :return: A Flask Response object that you can return from your handler - """ - - data = { - "error_code": error_code.value, - "error_message": error_info or "Unknown error" - } - - http_code = 200 - - if error_code is ErrorCodes.unknown_route: - data["error_message"] = error_info or "Unknown API route" - http_code = 404 - elif error_code is ErrorCodes.unauthorized: - data["error_message"] = error_info or "Unauthorized" - http_code = 401 - elif error_code is ErrorCodes.invalid_api_key: - data["error_message"] = error_info or "Invalid API-key" - http_code = 401 - elif error_code is ErrorCodes.bad_data_format: - data["error_message"] = error_info or "Input data in incorrect format" - http_code = 400 - elif error_code is ErrorCodes.incorrect_parameters: - data["error_message"] = error_info or "Incorrect parameters provided" - http_code = 400 - - response = jsonify(data) - response.status_code = http_code - return response - - -class ErrorView(BaseView): - """ - Error view, shown for a specific HTTP status code, as defined in the class attributes. - - This class is intended to be subclassed - use it as a base class for your own views, and set the class-level - attributes as appropriate. For example: - - >>> class MyView(ErrorView): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... error_code = 404 # Error code - ... - ... def get(self, error: HTTPException): # Name your function after the relevant HTTP method - ... return "Replace me with a template, 404 not found", 404 - - If you'd like to catch multiple HTTP error codes, feel free to supply an iterable for `error_code`. For example... - - >>> error_code = [401, 403] # Handle two specific errors - >>> error_code = range(500, 600) # Handle all 5xx errors - """ - - error_code = None # type: Union[int, Iterable] - register_on_app = True - blueprint = "error" # Because it doesn't truly have its own - - @classmethod - def setup(cls: "ErrorView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view by registering it as the error handler for the HTTP status codes specified in the class - attributes - this will also deal with multiple inheritance by calling `super().setup()` as appropriate. - - :param manager: Instance of the current RouteManager - :param blueprint: Current Flask blueprint to register the error handler for - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - if not cls.name or not cls.error_code: - raise RuntimeError("Error views must have both `name` and `error_code` defined") - - if isinstance(cls.error_code, int): - cls.error_code = [cls.error_code] - - if isinstance(cls.error_code, Iterable): - for code in cls.error_code: - if isinstance(code, int) and code not in default_exceptions: - continue # Otherwise we'll possibly get an exception thrown during blueprint registration - - if cls.register_on_app: - manager.app.errorhandler(code)(cls.as_view(cls.name)) - else: - blueprint.errorhandler(code)(cls.as_view(cls.name)) - else: - raise RuntimeError( - "Error views must have an `error_code` that is either an `int` or an iterable") # pragma: no cover # noqa: E501 - - -class TemplateView(RouteView): - """ - An easy view for routes that simply render a template with no extra information. - - This class is intended to be subclassed - use it as a base class for your own views, and set the class-level - attributes as appropriate. For example: - - >>> class MyView(TemplateView): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... template = "my_view.html" # Template to use - - Note that this view only handles GET requests. If you need any other verbs, you can implement them yourself - or just use one of the more customizable base view classes. - """ - - template = None # type: str - - @classmethod - def setup(cls: "TemplateView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view, deferring most setup to the superclasses but checking for the template attribute. - - :param manager: Instance of the current RouteManager - :param blueprint: Current Flask blueprint to register the error handler for - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - if not cls.template: - raise RuntimeError("Template views must have `template` defined") - - def get(self, *_): - return self.render(self.template) - - -class RedirectView(RouteView): - """ - An easy view for routes that simply redirect to another page or view. - - This class is intended to be subclassed - use it as a base class for your own views, and set the class-level - attributes as appropriate. For example: - - >>> class MyView(RedirectView): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... code = 303 # HTTP status code to use for the redirect; 303 by default - ... page = "staff.index" # Page to redirect to - ... kwargs = {} # Any extra keyword args to pass to the url_for call, if redirecting to another view - - You can specify a full URL, including the protocol, eg "http://google.com" or a Flask internal route name, - eg "main.index". Nothing else is supported. - - Note that this view only handles GET requests. If you need any other verbs, you can implement them yourself - or just use one of the more customizable base view classes. - """ - - code = 303 # type: int - page = None # type: str - kwargs = {} # type: Optional[dict] - - @classmethod - def setup(cls: "RedirectView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view, deferring most setup to the superclasses but checking for the template attribute. - - :param manager: Instance of the current RouteManager - :param blueprint: Current Flask blueprint to register the error handler for - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - if not cls.page or not cls.code: - raise RuntimeError("Redirect views must have both `code` and `page` defined") - - def get(self, *_): - if "://" in self.page: - return redirect(self.page, code=self.code) - - return redirect(url_for(self.page, **self.kwargs), code=self.code) diff --git a/pysite/constants.py b/pysite/constants.py deleted file mode 100644 index 7d8dbf6e..00000000 --- a/pysite/constants.py +++ /dev/null @@ -1,151 +0,0 @@ -from enum import Enum, IntEnum -from os import environ - -from flask_wtf import CSRFProtect - - -class ErrorCodes(IntEnum): - unknown_route = 0 - unauthorized = 1 - invalid_api_key = 2 - incorrect_parameters = 3 - bad_data_format = 4 - - -class ValidationTypes(Enum): - json = "json" - none = "none" - params = "params" - - -class BotEventTypes(Enum): - mod_log = "mod_log" - - send_message = "send_message" - send_embed = "send_embed" - - add_role = "add_role" - remove_role = "remove_role" - - -DEBUG_MODE = "FLASK_DEBUG" in environ - -# All snowflakes should be strings as RethinkDB rounds them as ints -ADMIN_BOTS_ROLE = "270988689419665409" -ADMINS_ROLE = "267628507062992896" -ANNOUNCEMENTS_ROLE = "463658397560995840" -BOTS_ROLE = "277546923144249364" -CODE_JAM_CHAMPIONS_ROLE = "430492892331769857" -CONTRIBS_ROLE = "295488872404484098" -DEVOPS_ROLE = "409416496733880320" -DEVELOPERS_ROLE = "352427296948486144" -HELPERS_ROLE = "267630620367257601" -JAMMERS_ROLE = "423054537079783434" -MODERATORS_ROLE = "267629731250176001" -MUTED_ROLE = "277914926603829249" -OWNERS_ROLE = "267627879762755584" -PARTNERS_ROLE = "323426753857191936" -PYTHON_ROLE = "458226699344019457" -STREAMERS_ROLE = "462650825978806274" -SUBREDDIT_MOD_ROLE = "458226413825294336" - -ALL_STAFF_ROLES = (OWNERS_ROLE, ADMINS_ROLE, MODERATORS_ROLE, DEVOPS_ROLE) -TABLE_MANAGER_ROLES = (OWNERS_ROLE, ADMINS_ROLE, DEVOPS_ROLE) -EDITOR_ROLES = ALL_STAFF_ROLES + (HELPERS_ROLE, CONTRIBS_ROLE) - -SERVER_ID = 267624335836053506 - -DISCORD_API_ENDPOINT = "https://discordapp.com/api" - -DISCORD_OAUTH_REDIRECT = "/auth/discord" -DISCORD_OAUTH_AUTHORIZED = "/auth/discord/authorized" -DISCORD_OAUTH_ID = environ.get('DISCORD_OAUTH_ID', '') -DISCORD_OAUTH_SECRET = environ.get('DISCORD_OAUTH_SECRET', '') -DISCORD_OAUTH_SCOPE = 'identify' -OAUTH_DATABASE = "oauth_data" - -GITLAB_ACCESS_TOKEN = environ.get("GITLAB_ACCESS_TOKEN", '') - -PREFERRED_URL_SCHEME = environ.get("PREFERRED_URL_SCHEME", "http") - -ERROR_DESCRIPTIONS = { - # 5XX - 500: "The server encountered an unexpected error ._.", - 501: "Woah! You seem to have found something we haven't even implemented yet!", - 502: "This is weird, one of our upstream servers seems to have experienced an error.", - 503: "Looks like one of our services is down for maintenance and couldn't respond to your request.", - 504: "Looks like an upstream server experienced a timeout while we tried to talk to it!", - 505: "You're using an old HTTP version. It might be time to upgrade your browser.", - # 4XX - 400: "You sent us a request that we don't know what to do with.", - 401: "Nope! You'll need to authenticate before we let you do that.", - 403: "No way! You're not allowed to do that.", - 404: "We looked, but we couldn't seem to find that page.", - 405: "That's a real page, but you can't use that method.", - 408: "We waited a really long time, but never got your request.", - 410: "This used to be here, but it's gone now.", - 411: "You forgot to tell us the length of the content.", - 413: "No way! That payload is, like, way too big!", - 415: "The thing you sent has the wrong format.", - 418: "I'm a teapot, I can't make coffee. (._.)", - 429: "Please don't send us that many requests." -} - -JAM_STATES = [ - "planning", - "announced", - "preparing", - "running", - "judging", - "finished" -] - -JAM_QUESTION_TYPES = [ - "checkbox", - "email", - "number", - "radio", - "range", - "text", - "textarea", - "slider" -] - -# Server role colors -ROLE_COLORS = { - ADMIN_BOTS_ROLE: "#6f9fed", - ADMINS_ROLE: "#e76e6c", - BOTS_ROLE: "#6f9fed", - CODE_JAM_CHAMPIONS_ROLE: "#b108b4", - CONTRIBS_ROLE: "#55cc6c", - DEVOPS_ROLE: "#a1d1ff", - DEVELOPERS_ROLE: "#fcfcfc", - HELPERS_ROLE: "#e0b000", - JAMMERS_ROLE: "#258639", - MODERATORS_ROLE: "#ce3c42", - MUTED_ROLE: "#fcfcfc", - OWNERS_ROLE: "#ffa3a1", - PARTNERS_ROLE: "#b66fed", - PYTHON_ROLE: "#6f9fed", - STREAMERS_ROLE: "#833cba", - SUBREDDIT_MOD_ROLE: "#d897ed", -} - -# CSRF -CSRF = CSRFProtect() - -# Bot key -BOT_API_KEY = environ.get("BOT_API_KEY") - -# RabbitMQ settings -BOT_EVENT_QUEUE = "bot_events" - -RMQ_USERNAME = environ.get("RABBITMQ_DEFAULT_USER") or "guest" -RMQ_PASSWORD = environ.get("RABBITMQ_DEFAULT_PASS") or "guest" -RMQ_HOST = "localhost" if DEBUG_MODE else environ.get("RABBITMQ_HOST") or "pdrmq" -RMQ_PORT = 5672 - -# Channels -CHANNEL_MOD_LOG = 282638479504965634 -CHANNEL_DEV_LOGS = 409308876241108992 -CHANNEL_JAM_LOGS = 452486310121439262 diff --git a/pysite/database.py b/pysite/database.py deleted file mode 100644 index ddf79a31..00000000 --- a/pysite/database.py +++ /dev/null @@ -1,562 +0,0 @@ -import logging -import os -from typing import Any, Callable, Dict, Iterator, List, Optional, Union -import re - -import rethinkdb -from rethinkdb.ast import RqlMethodQuery, Table, UserError -from rethinkdb.net import DefaultConnection -from werkzeug.exceptions import ServiceUnavailable - -from pysite.tables import TABLES - -STRIP_REGEX = re.compile(r"<[^<]+?>") -WIKI_TABLE = "wiki" - - -class RethinkDB: - - def __init__(self, loop_type: Optional[str] = "gevent"): - self.host = os.environ.get("RETHINKDB_HOST", "127.0.0.1") - self.port = os.environ.get("RETHINKDB_PORT", "28015") - self.database = os.environ.get("RETHINKDB_DATABASE", "pythondiscord") - self.log = logging.getLogger(__name__) - self.conn = None - - if loop_type: - rethinkdb.set_loop_type(loop_type) - - with self.get_connection() as self.conn: - try: - rethinkdb.db_create(self.database).run(self.conn) - self.log.debug(f"Database created: '{self.database}'") - except rethinkdb.RqlRuntimeError: - self.log.debug(f"Database found: '{self.database}'") - - def create_tables(self) -> List[str]: - """ - Creates whichever tables exist in the TABLES - constant if they don't already exist in the database. - - :return: a list of the tables that were created. - """ - created = [] - - for table, obj in TABLES.items(): - if self.create_table(table, obj.primary_key): - created.append(table) - - return created - - def get_connection(self, connect_database: bool = True) -> DefaultConnection: - """ - Grab a connection to the RethinkDB server, optionally without selecting a database - - :param connect_database: Whether to immediately connect to the database or not - """ - - if connect_database: - return rethinkdb.connect(host=self.host, port=self.port, db=self.database) - else: - return rethinkdb.connect(host=self.host, port=self.port) - - def before_request(self): - """ - Flask pre-request callback to set up a connection for the duration of the request - """ - - try: - self.conn = self.get_connection() - except rethinkdb.RqlDriverError: - raise ServiceUnavailable("Database connection could not be established.") - - def teardown_request(self, _): - """ - Flask post-request callback to close a previously set-up connection - - :param _: Exception object, not used here - """ - - try: - self.conn.close() - except AttributeError: - pass - - # region: Convenience wrappers - - def create_table(self, table_name: str, primary_key: str = "id", durability: str = "hard", shards: int = 1, - replicas: Union[int, Dict[str, int]] = 1, primary_replica_tag: Optional[str] = None) -> bool: - """ - Attempt to create a new table on the current database - - :param table_name: The name of the table to create - :param primary_key: The name of the primary key - defaults to "id" - :param durability: "hard" (the default) to write the change immediately, "soft" otherwise - :param shards: The number of shards to span the table over - defaults to 1 - :param replicas: See the RethinkDB documentation relating to replicas - :param primary_replica_tag: See the RethinkDB documentation relating to replicas - - :return: True if the table was created, False if it already exists - """ - - with self.get_connection() as conn: - all_tables = rethinkdb.db(self.database).table_list().run(conn) - self.log.debug(f"Call to table_list returned the following list of tables: {all_tables}") - - if table_name in all_tables: - self.log.debug(f"Table found: '{table_name}' ({len(all_tables)} tables in total)") - return False - - # Use a kwargs dict because the driver doesn't check the value - # of `primary_replica_tag` properly; None is not handled - kwargs = { - "primary_key": primary_key, - "durability": durability, - "shards": shards, - "replicas": replicas - } - - if primary_replica_tag is not None: - kwargs["primary_replica_tag"] = primary_replica_tag - - rethinkdb.db(self.database).table_create(table_name, **kwargs).run(conn) - - self.log.debug(f"Table created: '{table_name}'") - return True - - def delete(self, - table_name: str, - primary_key: Union[str, None] = None, - durability: str = "hard", - return_changes: Union[bool, str] = False) -> dict: - """ - Delete one or all documents from a table. This can only delete - either the contents of an entire table, or a single document. - For more complex delete operations, please use self.query. - - :param table_name: The name of the table to delete from. This must be provided. - :param primary_key: The primary_key to delete from that table. This is optional. - :param durability: "hard" (the default) to write the change immediately, "soft" otherwise - :param return_changes: Whether to return a list of changed values or not - defaults to False - :return: if return_changes is True, returns a dict containing all changes. Else, returns None. - """ - - if primary_key: - query = self.query(table_name).get(primary_key).delete( - durability=durability, return_changes=return_changes - ) - else: - query = self.query(table_name).delete( - durability=durability, return_changes=return_changes - ) - - if return_changes: - return self.run(query, coerce=dict) - self.run(query) - - def drop_table(self, table_name: str): - """ - Attempt to drop a table from the database, along with its data - - :param table_name: The name of the table to drop - :return: True if the table was dropped, False if the table doesn't exist - """ - - with self.get_connection() as conn: - all_tables = rethinkdb.db(self.database).table_list().run(conn) - - if table_name not in all_tables: - return False - - rethinkdb.db(self.database).table_drop(table_name).run(conn) - return True - - def query(self, table_name: str) -> Table: - """ - Get a RethinkDB table object that you can run queries against - - >>> db = RethinkDB() - >>> query = db.query("my_table") - >>> db.run(query.insert({"key": "value"}), coerce=dict) - { - "deleted": 0, - "errors": 0, - "inserted": 1, - "replaced": 0, - "skipped": 0, - "unchanged": 0 - } - - :param table_name: Name of the table to query against - :return: The RethinkDB table object for the table - """ - - if table_name not in TABLES: - self.log.warning(f"Table not declared in tables.py: {table_name}") - - return rethinkdb.table(table_name) - - def run(self, query: Union[RqlMethodQuery, Table], *, new_connection: bool = False, - connect_database: bool = True, coerce: type = None) -> Union[rethinkdb.Cursor, List, Dict, object]: - """ - Run a query using a table object obtained from a call to `query()` - - >>> db = RethinkDB() - >>> query = db.query("my_table") - >>> db.run(query.insert({"key": "value"}), coerce=dict) - { - "deleted": 0, - "errors": 0, - "inserted": 1, - "replaced": 0, - "skipped": 0, - "unchanged": 0 - } - - Note that result coercion is very basic, and doesn't really do any magic. If you want to be able to work - directly with the result of your query, then don't specify the `coerce` argument - the object that you'd - usually get from the RethinkDB API will be returned instead. - - :param query: The full query to run - :param new_connection: Whether to create a new connection or use the current request-bound one - :param connect_database: If creating a new connection, whether to connect to the database immediately - :param coerce: Optionally, an object type to attempt to coerce the result to - - :return: The result of the operation - """ - - if not new_connection: - try: - result = query.run(self.conn) - except rethinkdb.ReqlDriverError as e: - if e.message == "Connection is closed.": - self.log.warning("Connection was closed, attempting with a new connection...") - result = query.run(self.get_connection(connect_database)) - else: - raise - else: - result = query.run(self.get_connection(connect_database)) - - if coerce: - return coerce(result) if result else coerce() - return result - - # endregion - - # region: RethinkDB wrapper functions - - def between(self, table_name: str, *, lower: Any = rethinkdb.minval, upper: Any = rethinkdb.maxval, - index: Optional[str] = None, left_bound: str = "closed", right_bound: str = "open") -> List[ - Dict[str, Any]]: - """ - Get all documents between two keys - - >>> db = RethinkDB() - >>> db.between("users", upper=10, index="conquests") - [ - {"username": "gdude", "conquests": 2}, - {"username": "joseph", "conquests": 5} - ] - >>> db.between("users", lower=10, index="conquests") - [ - {"username": "lemon", "conquests": 15} - ] - >>> db.between("users", lower=2, upper=10, index="conquests" left_bound="open") - [ - {"username": "gdude", "conquests": 2}, - {"username": "joseph", "conquests": 5} - ] - - :param table_name: The table to get documents from - :param lower: The lower-bounded value, leave blank to ignore - :param upper: The upper-bounded value, leave blank to ignore - :param index: The key or index to check on each document - :param left_bound: "open" to include documents that exactly match the lower bound, "closed" otherwise - :param right_bound: "open" to include documents that exactly match the upper bound, "closed" otherwise - - :return: A list of matched documents; may be empty - """ - return self.run( # pragma: no cover - self.query(table_name).between(lower, upper, index=index, left_bound=left_bound, right_bound=right_bound), - coerce=list - ) - - def changes(self, table_name: str, squash: Union[bool, int] = False, changefeed_queue_size: int = 100_000, - include_initial: Optional[bool] = None, include_states: bool = False, - include_types: bool = False) -> Iterator[Dict[str, Any]]: - """ - A complicated function allowing you to follow a changefeed for a specific table - - This function will not allow you to specify a set of conditions for your changefeed, so you'll - have to write your own query and run it with `run()` if you need that. If not, you'll just get every - change for the specified table. - - >>> db = RethinkDB() - >>> for document in db.changes("my_table", squash=True): - ... print(document.get("new_val", {})) - - Documents take the form of a dict with `old_val` and `new_val` fields by default. These are set to a copy of - the document before and after the change being represented was made, respectively. The format of these dicts - can change depending on the arguments you pass to the function, however. - - If a changefeed must be aborted (for example, if the table was deleted), a ReqlRuntimeError will be - raised. - - Note: This function always creates a new connection. This is to prevent you from losing your changefeed - when the connection used for a request context is closed. - - :param table_name: The name of the table to watch for changes on - - :param squash: How to deal with batches of changes to a single document - False (the default) to send changes - as they happen, True to squash changes for single objects together and send them as a single change, - or an int to specify how many seconds to wait for an object to change before batching it - - :param changefeed_queue_size: The number of changes the server will buffer between client reads before it - starts to drop changes and issues errors - defaults to 100,000 - - :param include_initial: If True, the changefeed will start with the initial values of all the documents in - the table; the results will have `new_val` fields ONLY to start with if this is the case. Note that - the old values may be intermixed with new changes if you're still iterating through the old values, but - only as long as the old value for that field has already been sent. If the order of a document you've - already seen moves it to a part of the group you haven't yet seen, an "unitial" notification is sent, which - is simply a dict with an `old_val` field set, and not a `new_val` field set. This option defaults to - False. - - :param include_states: Whether to send special state documents to the changefeed as its state changes. This - comprises of special documents with only a `state` field, set to a string - the state of the feed. There - are currently two states - "initializing" and "ready". This option defaults to False. - - :param include_types: If True, each document generated will include a `type` field which states what type - of change the document represents. This may be "add", "remove", "change", "initial", "uninitial" or - "state". This option defaults to False. - - :return: A special iterator that will iterate over documents in the changefeed as they're sent. If there is - no document waiting, this will block the function until there is. - """ - return self.run( # pragma: no cover - self.query(table_name).changes( - squash=squash, changefeed_queue_size=changefeed_queue_size, include_initial=include_initial, - include_states=include_states, include_offsets=False, include_types=include_types - ), - new_connection=True - ) - - def filter(self, table_name: str, predicate: Callable[[Dict[str, Any]], bool], - default: Union[bool, UserError] = False) -> List[Dict[str, Any]]: - """ - Return all documents in a table for which `predicate` returns true. - - The `predicate` argument should be a function that takes a single argument - a single document to check - and - it should return True or False depending on whether the document should be included. - - >>> def many_conquests(doc): - ... '''Return documents with at least 10 conquests''' - ... return doc["conquests"] >= 10 - ... - >>> db = RethinkDB() - >>> db.filter("users", many_conquests) - [ - {"username": "lemon", "conquests": 15} - ] - - :param table_name: The name of the table to get documents for - :param predicate: The callable to use to filter the documents - :param default: What to do if a document is missing fields; True to include them, `rethink.error()` to raise - aa ReqlRuntimeError, or False to skip over the document (the default) - :return: A list of documents that match the predicate; may be empty - """ - - return self.run( # pragma: no cover - self.query(table_name).filter(predicate, default=default), - coerce=list - ) - - def get(self, table_name: str, key: Any) -> Optional[Dict[str, Any]]: - """ - Get a single document from a table by primary key - - :param table_name: The name of the table to get the document from - :param key: The value of the primary key belonging to the document you want - - :return: The document, or None if it wasn't found - """ - - result = self.run( # pragma: no cover - self.query(table_name).get(key) - ) - - return dict(result) if result else None # pragma: no cover - - def get_all(self, table_name: str, *keys: str, index: str = "id") -> List[Any]: - """ - Get a list of documents matching a set of keys, on a specific index - - :param table_name: The name of the table to get documents from - :param keys: The key values to match against - :param index: The name of the key or index to match on - - :return: A list of matching documents; may be empty if no matches were made - """ - - if keys: - return self.run( # pragma: no cover - self.query(table_name).get_all(*keys, index=index), - coerce=list - ) - else: - return self.run( - self.query(table_name), - coerce=list - ) - - def insert(self, table_name: str, *objects: Dict[str, Any], - durability: str = "hard", - return_changes: Union[bool, str] = False, - conflict: Union[ # Any of... - str, Callable[ # ...str, or a callable that... - [Dict[str, Any], Dict[str, Any]], # ...takes two dicts with string keys and any values... - Dict[str, Any] # ...and returns a dict with string keys and any values - ] - ] = "error") -> Dict[str, Any]: # flake8: noqa - """ - Insert an object or a set of objects into a table - - :param table_name: The name of the table to insert into - :param objects: The objects to be inserted into the table - :param durability: "hard" (the default) to write the change immediately, "soft" otherwise - :param return_changes: Whether to return a list of changed values or not - defaults to False - :param conflict: What to do in the event of a conflict - "error", "replace" and "update" are included, but - you can also provide your own function in order to handle conflicts yourself. If you do this, the function - should take two arguments (the old document and the new one), and return a single document to replace both. - - :return: A dict detailing the operations run - """ - - query = self.query(table_name).insert( - objects, durability=durability, return_changes=return_changes, conflict=conflict - ) - - return self.run(query, coerce=dict) - - def map(self, table_name: str, func: Callable): - """ - Map a function over every document in a table, with the possibility of modifying it - - As an example, you could do the following to rename the "id" field to "user_id" for all documents - in the "users" table. - - >>> db = RethinkDB() - >>> db.map( - ... "users", - ... lambda doc: doc.merge({"user_id": doc["id"]}).without("id") - ... ) - - :param table_name: The name of the table to map the function over - :param func: A callable that takes a single argument - - :return: Unknown, needs more testing - """ - - return self.run( # pragma: no cover - self.query(table_name).map(func), - coerce=list - ) - - def pluck(self, table_name: str, *selectors: Union[str, Dict[str, Union[List, Dict]]]) -> List[Dict[str, Any]]: - """ - Get a list of values for a specific set of keys for every document in the table; this can include - nested values - - >>> db = RethinkDB() - >>> db.pluck("users", "username", "password") # Select a flat document - [ - {"username": "lemon", "password": "hunter2"} - ] - >>> db.pluck("users", {"posts": ["title"]}) # Select from nested documents - [ - { - "posts": [ - {"title": "New website!"} - ] - } - ] - - :param table_name: The table to get values from - :param selectors: The set of keys to get values for - :return: A list containing the requested documents, with only the keys requested - """ - - return self.run( # pragma: no cover - self.query(table_name).pluck(*selectors), - coerce=list - ) - - def sample(self, table_name: str, sample_size: int) -> List[Dict[str, Any]]: - """ - Select a given number of elements from a table at random. - - :param table_name: The name of the table to select from. - :param sample_size: The number of elements to select. - If this number is higher than the total amount of items in - the table, this will return the entire table in random order. - - :return: A list of items from the table. - """ - return self.run( # pragma: no cover - self.query(table_name).sample(sample_size), - coerce=list - ) - - def sync(self, table_name: str) -> bool: - """ - Following a set of edits with durability set to "soft", this must be called to save those edits - - :param table_name: The name of the table to sync - - :return: True if the sync was successful; False otherwise - """ - result = self.run( # pragma: no cover - self.query(table_name).sync(), - coerce=dict - ) - - return result.get("synced", 0) > 0 # pragma: no cover - - def wait(self, table_name: str, wait_for: str = "all_replicas_ready", timeout: int = 0) -> bool: - """ - Wait until an operation has happened on a specific table; will block the current function - - :param table_name: The name of the table to wait against - :param wait_for: The operation to wait for; may be "ready_for_outdated_reads", - "ready_for_reads", "ready_for_writes" or "all_replicas_ready", which is the default - :param timeout: How long to wait before returning; defaults to 0 (forever) - - :return: True; but may return False if the timeout was reached - """ - - result = self.run( # pragma: no cover - self.query(table_name).wait(wait_for=wait_for, timeout=timeout), - coerce=dict - ) - - return result.get("ready", 0) > 0 - - def without(self, table_name: str, *selectors: Union[str, Dict[str, Union[List, Dict]]]): - """ - The functional opposite of `pluck()`, returning full documents without the specified selectors - - >>> db = RethinkDB() - >>> db.without("users", "posts") - [ - {"username": "lemon", "password": "hunter2"} - ] - - :param table_name: The table to get values from - :param selectors: The set of keys to exclude - :return: A list containing the requested documents, without the keys requested - """ - - return self.run( # pragma: no cover - self.query(table_name).without(*selectors) - ) - # endregion diff --git a/pysite/decorators.py b/pysite/decorators.py deleted file mode 100644 index fbfb90f8..00000000 --- a/pysite/decorators.py +++ /dev/null @@ -1,151 +0,0 @@ -from functools import wraps -from json import JSONDecodeError - -from flask import request -from schema import Schema, SchemaError -from werkzeug.exceptions import BadRequest, Forbidden - -from pysite.base_route import APIView, RouteView -from pysite.constants import BOT_API_KEY, CSRF, DEBUG_MODE, ErrorCodes, ValidationTypes - - -def csrf(f): - """ - Apply CSRF protection to a specific view function. - """ - - @wraps(f) - def inner_decorator(*args, **kwargs): - CSRF.protect() - - return f(*args, **kwargs) - - return inner_decorator - - -def require_roles(*roles: int): - def inner_decorator(f): - - @wraps(f) - def inner(self: RouteView, *args, **kwargs): - data = self.user_data - - if DEBUG_MODE: - return f(self, *args, **kwargs) - elif data: - for role in roles: - if role in data.get("roles", []): - return f(self, *args, **kwargs) - - if isinstance(self, APIView): - return self.error(ErrorCodes.unauthorized) - - raise Forbidden() - return self.redirect_login(**kwargs) - - return inner - - return inner_decorator - - -def api_key(f): - """ - Decorator to check if X-API-Key is valid. - - Should only be applied to functions on APIView routes. - """ - - @wraps(f) - def inner_decorator(self: APIView, *args, **kwargs): - if not request.headers.get("X-API-Key") == BOT_API_KEY: - return self.error(ErrorCodes.invalid_api_key) - return f(self, *args, **kwargs) - - return inner_decorator - - -def api_params( - schema: Schema = None, - validation_type: ValidationTypes = ValidationTypes.json, - allow_duplicate_params: bool = False): - """ - Validate parameters of data passed to the decorated view. - - Should only be applied to functions on APIView routes. - - This will pass the validated data in as the first parameter to the decorated function. - This data will always be a list, and view functions are expected to be able to handle that - in the case of multiple sets of data being provided by the api. - - If `allow_duplicate_params` is set to False (only effects dictionary schemata - and parameter validation), then the view will return a 400 Bad Request - response if the client submits multiple parameters with the same name. - """ - - def inner_decorator(f): - - @wraps(f) - def inner(self: APIView, *args, **kwargs): - if validation_type == ValidationTypes.json: - try: - if not request.is_json: - return self.error(ErrorCodes.bad_data_format) - - data = request.get_json() - - if not isinstance(data, list) and isinstance(schema._schema, list): - data = [data] - - except JSONDecodeError: - return self.error(ErrorCodes.bad_data_format) # pragma: no cover - - elif validation_type == ValidationTypes.params and isinstance(schema._schema, list): - # I really don't like this section here, but I can't think of a better way to do it - multi = request.args # This is a MultiDict, which should be flattened to a list of dicts - - # We'll assume that there's always an equal number of values for each param - # Anything else doesn't really make sense anyway - data = [] - longest = None - - for _key, items in multi.lists(): - # Make sure every key has the same number of values - if longest is None: - # First iteration, store it - longest = len(items) - - elif len(items) != longest: # pragma: no cover - # At least one key has a different number of values - return self.error(ErrorCodes.bad_data_format) # pragma: no cover - - if longest is not None: - for i in range(longest): # Now we know all keys have the same number of values... - obj = {} # New dict to store this set of values - - for key, items in multi.lists(): - obj[key] = items[i] # Store the item at that specific index - - data.append(obj) - - elif validation_type == ValidationTypes.params and isinstance(schema._schema, dict): - if not allow_duplicate_params: - for _arg, value in request.args.to_dict(flat=False).items(): - if len(value) > 1: - raise BadRequest("This view does not allow duplicate query arguments") - data = request.args.to_dict() - elif validation_type == ValidationTypes.none: - return f(self, None, *args, **kwargs) - - else: - raise ValueError(f"Unknown validation type: {validation_type}") # pragma: no cover - - try: - schema.validate(data) - except SchemaError as e: - return self.error(ErrorCodes.incorrect_parameters, str(e)) - - return f(self, data, *args, **kwargs) - - return inner - - return inner_decorator diff --git a/pysite/migrations/__init__.py b/pysite/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/__init__.py +++ /dev/null diff --git a/pysite/migrations/runner.py b/pysite/migrations/runner.py deleted file mode 100644 index d498832f..00000000 --- a/pysite/migrations/runner.py +++ /dev/null @@ -1,95 +0,0 @@ -import importlib -import json -import os -from typing import Callable - -from pysite.database import RethinkDB -from pysite.tables import TABLES - -TABLES_DIR = os.path.abspath("./pysite/migrations/tables") -VERSIONS_TABLE = "_versions" - - -def get_migrations(table_path, table): - """ - Take a table name and the path to its migration files, and return a dict containing versions and modules - corresponding with each migration. - - And, yes, migrations start at 1. - """ - migrations = {} - final_version = 0 - - for filename in sorted(os.listdir(table_path)): - if filename.startswith("v") and filename.endswith(".py"): - final_version = int(filename[1:-3]) - migrations[final_version] = f"pysite.migrations.tables.{table}.v{final_version}" - - return migrations, final_version - - -def run_migrations(db: RethinkDB, output: Callable[[str], None]=None): - for table, obj in TABLES.items(): # All _defined_ tables - table_path = os.path.join(TABLES_DIR, table) - - if not os.path.exists(table_path): # Check whether we actually have any migration data for this table at all - output(f"No migration data found for table: {table}") - continue - - with db.get_connection() as conn: # Make sure we have an active connection - try: - if not db.query(table).count().run(conn): # If there are no documents in the table... - # Table's empty, so we'll have to run migrations again anyway - db.delete(VERSIONS_TABLE, table) - - json_path = os.path.join(table_path, "initial_data.json") - - if os.path.exists(json_path): # We have initial data to insert, so let's do that - with open(json_path, "r", encoding="utf-8") as json_file: - data = json.load(json_file) - db.insert(table, *data) # Table's empty, so... just do the thing - - output(f"Inserted initial data for table: {table}") - else: # There's no json data file for this table - output(f"No initial_data.json file for table: {table}") - output(json_path) - - # Translate migration files into modules and versions - migrations, final_version = get_migrations(table_path, table) - - if not migrations: # No migration files found - output(f"No structural migrations for table: {table}") - continue - - current_version = 0 - doc = db.get(VERSIONS_TABLE, table) - - if doc: # We've done a migration before, so continue from where we left off - current_version = doc["version"] - - if current_version == final_version: # Nothing to do, we're up to date - output(f"Table is already up to date: {table}") - continue - output(f"Table has never been migrated: {table}") - - while current_version < final_version: - current_version += 1 - - module = importlib.import_module(migrations[current_version]) - module.run(db, table, obj) - output(f"Table upgraded to version {current_version}/{final_version}: {table}") - - # Make sure the versions table is kept up to date, so we don't ever migrate twice - # We do this in the loop to save our progress, in case we fail during a migration - - db.insert( - VERSIONS_TABLE, - {"table": table, "version": current_version}, - conflict="replace", - durability="soft" - ) - except Exception: - output(f"Failed to migrate table: {table}") - raise - finally: - db.sync(VERSIONS_TABLE) diff --git a/pysite/migrations/tables/__init__.py b/pysite/migrations/tables/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/code_jam_participants/__init__.py b/pysite/migrations/tables/code_jam_participants/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/code_jam_participants/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/code_jam_participants/v1.py b/pysite/migrations/tables/code_jam_participants/v1.py deleted file mode 100644 index c6e7bff6..00000000 --- a/pysite/migrations/tables/code_jam_participants/v1.py +++ /dev/null @@ -1,11 +0,0 @@ -def run(db, table, table_obj): - """ - Remove stored dates of birth from code jam participants - """ - - for document in db.get_all(table): - if "dob" in document: - del document["dob"] - - db.insert(table, document, conflict="replace", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/code_jam_participants/v2.py b/pysite/migrations/tables/code_jam_participants/v2.py deleted file mode 100644 index 858da279..00000000 --- a/pysite/migrations/tables/code_jam_participants/v2.py +++ /dev/null @@ -1,12 +0,0 @@ -def run(db, table, table_obj): - """ - GitHub usernames -> Store as GitLab username, this will be correct for most jammers - """ - - for document in db.get_all(table): - if "github_username" in document: - document["gitlab_username"] = document["github_username"] - del document["github_username"] - - db.insert(table, document, conflict="replace", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/code_jam_teams/__init__.py b/pysite/migrations/tables/code_jam_teams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/code_jam_teams/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/code_jam_teams/v1.py b/pysite/migrations/tables/code_jam_teams/v1.py deleted file mode 100644 index 165d3100..00000000 --- a/pysite/migrations/tables/code_jam_teams/v1.py +++ /dev/null @@ -1,13 +0,0 @@ -def run(db, table, table_obj): - """ - Associate the ID of each team's code jam (team -> jam) - """ - - for document in db.get_all(table): - if "jam" not in document: - # find the code jam containing this team - for jam in db.get_all("code_jams"): - if document["id"] in jam["teams"]: - document["jam"] = jam["number"] - db.insert(table, document, conflict="update", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/code_jam_teams/v2.py b/pysite/migrations/tables/code_jam_teams/v2.py deleted file mode 100644 index c6d7c972..00000000 --- a/pysite/migrations/tables/code_jam_teams/v2.py +++ /dev/null @@ -1,13 +0,0 @@ -def run(db, table, table_obj): - """ - Associate the ID of each team's code jam (team -> jam) - again - """ - - for document in db.get_all(table): - if "jam" not in document: - # find the code jam containing this team - for jam in db.get_all("code_jams"): - if document["id"] in jam["teams"]: - document["jam"] = jam["number"] - db.insert(table, document, conflict="update", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/code_jams/__init__.py b/pysite/migrations/tables/code_jams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/code_jams/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/code_jams/v1.py b/pysite/migrations/tables/code_jams/v1.py deleted file mode 100644 index cce3b112..00000000 --- a/pysite/migrations/tables/code_jams/v1.py +++ /dev/null @@ -1,11 +0,0 @@ -def run(db, table, table_obj): - """ - Add "teams" list to jams without it - """ - - for document in db.get_all(table): - if "teams" not in document: - document["teams"] = [] - - db.insert(table, document, conflict="replace", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/code_jams/v2.py b/pysite/migrations/tables/code_jams/v2.py deleted file mode 100644 index df4752c8..00000000 --- a/pysite/migrations/tables/code_jams/v2.py +++ /dev/null @@ -1,10 +0,0 @@ -def run(db, table, table_obj): - """ - Clean list of teams from teams that do not exist anymore. - """ - for document in db.get_all(table): - for team_id in document["teams"]: - if db.get("code_jam_teams", team_id) is None: - document["teams"].remove(team_id) - db.insert(table, document, conflict="update", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/hiphopify_namelist/__init__.py b/pysite/migrations/tables/hiphopify_namelist/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/hiphopify_namelist/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/hiphopify_namelist/initial_data.json b/pysite/migrations/tables/hiphopify_namelist/initial_data.json deleted file mode 100644 index f0b15f63..00000000 --- a/pysite/migrations/tables/hiphopify_namelist/initial_data.json +++ /dev/null @@ -1,5198 +0,0 @@ -[ - { - "name": "100 Kila", - "image_url": "http://hotnews.bg/uploads/tinymce/w09/100-%D0%BA%D0%B8%D0%BB%D0%B0.jpg" - }, - { - "name": "100s", - "image_url": "http://images.complex.com/complex/image/upload/t_article_image/nzwte8lxj3p1orqf5yrm.jpg" - }, - { - "name": "12 Gauge", - "image_url": "http://cps-static.rovicorp.com/3/JPG_250/MI0001/358/MI0001358517.jpg" - }, - { - "name": "2 Chainz", - "image_url": "http://thesource.com/wp-content/uploads/2016/01/2-Chainz-rapper.jpg" - }, - { - "name": "2 Pistols", - "image_url": "http://images3.wikia.nocookie.net/__cb20130521104737/rap/images/0/0c/2p.jpg" - }, - { - "name": "2$ Fabo", - "image_url": "http://assets.audiomack.com/2-fabo/355f3587905bb4e2bc936f76cd64cef3.jpeg" - }, - { - "name": "21 Savage", - "image_url": "https://nyppagesix.files.wordpress.com/2018/04/21-savage-amber-rose.jpg" - }, - { - "name": "2Mex", - "image_url": "https://www.ballerstatus.com/wp-content/uploads/2016/05/2mex.jpg" - }, - { - "name": "360", - "image_url": "http://resources1.news.com.au/images/2012/07/19/1226428/465937-rapper-360-hit.jpg" - }, - { - "name": "40 Glocc", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2014/06/40glocc.jpg" - }, - { - "name": "50 Cent", - "image_url": "http://www.michellehenry.fr/rapper_50_cent.jpg" - }, - { - "name": "6lack", - "image_url": "http://electriccircus.co/home/wp-content/uploads/2017/01/black.jpg" - }, - { - "name": "6ix9ine", - "image_url": "http://thesource.com/wp-content/uploads/2018/04/Screen-Shot-2018-04-11-at-8.51.56-AM.png" - }, - { - "name": "The 6th Letter", - "image_url": "http://hw-img.datpiff.com/m5dadf91/The_6th_Letter_What_The_F_the_Mixtape-front-large.jpg" - }, - { - "name": "9th Wonder", - "image_url": "http://www.hiphopvideoworld.com/wp-content/uploads/2016/06/Talib-Kweli-ft.-9th-Wonder-Rapsody-Life-Ahead-Of-Me.jpg" - }, - { - "name": "Andre 3000", - "image_url": "http://1.bp.blogspot.com/-1TAyMhARS8s/UaWDLXzZzdI/AAAAAAAACDg/NOToaM0g_as/s1600/80999804-e1369795094594.jpg" - }, - { - "name": "Big Boi", - "image_url": "http://redalertpolitics.com/files/2013/01/BigBoi.jpg" - }, - { - "name": "A.CHAL", - "image_url": "http://images.thissongissick.com/c_fill-f_auto-g_faces-h_630-w_1200-v1496443961-this-song-is-sick-media-image-a-chal-press-shot-1496443960836-png.jpg" - }, - { - "name": "A+", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/7/72/Guru_(rapper).jpg" - }, - { - "name": "A-Q", - "image_url": "http://www.eurweb.com/wp-content/uploads/2011/05/qtip.jpg" - }, - { - "name": "Arabian Prince", - "image_url": "https://68.media.tumblr.com/3938a8b6b11d462ba95807666e47f778/tumblr_mvjf8kRjCx1qzx6s2o1_500.jpg" - }, - { - "name": "Ab-Soul", - "image_url": "http://16762-presscdn-0-89.pagely.netdna-cdn.com/wp-content/uploads/2014/05/ab-soul.jpg" - }, - { - "name": "A Boogie wit da Hoodie", - "image_url": "https://vergecampus.com/wp-content/uploads/2018/04/Screen-Shot-2018-04-09-at-9.47.46-PM-1024x594.png" - }, - { - "name": "Abstract Rude", - "image_url": "http://media1.fdncms.com/orlando/imager/u/original/2554165/20170112_abstract_rude_5.jpg" - }, - { - "name": "Ace Hood", - "image_url": "http://www.aceshowbiz.com/images/news/00024363.jpg" - }, - { - "name": "Aceyalone", - "image_url": "http://images.rapgenius.com/4f38e699f6f80a3255420adf5e98a0a8.600x600x1.jpg" - }, - { - "name": "Action Bronson", - "image_url": "http://alloveralbany.com/images/rapper_Action_Bronson.jpg" - }, - { - "name": "Adam Saleh", - "image_url": "http://naibuzz.com/wp-content/uploads/2016/05/adam-saleh.jpg" - }, - { - "name": "Aesop Rock", - "image_url": "https://static-secure.guim.co.uk/sys-images/Guardian/Pix/pictures/2014/5/5/1399294322119/Rapper-Aesop-Rock-014.jpg" - }, - { - "name": "Afrika Bambaataa", - "image_url": "http://imageslogotv-a.akamaihd.net/uri/mgid:uma:image:logotv.com:11564172" - }, - { - "name": "Afroman", - "image_url": "http://www.irishnews.com/picturesarchive/irishnews/irishnews/2017/01/10/130012018-8220bff2-983c-48f3-ae57-276cab82f911.png" - }, - { - "name": "Afu-Ra", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/20120504_-_Afu-Ra.jpg/220px-20120504_-_Afu-Ra.jpg" - }, - { - "name": "Agallah", - "image_url": "http://1.bp.blogspot.com/-jJBR6f4cBwI/ThQ-NxjTffI/AAAAAAAABF4/Oe2n71hDmtM/s320/8%2Boff%2Bagallah.jpg" - }, - { - "name": "Ahmad", - "image_url": "http://images.rapgenius.com/38af31ae445f8c6ac6ce2a3b21a4605b.1000x684x1.jpg" - }, - { - "name": "Ajs Nigrutin", - "image_url": "http://vignette2.wikia.nocookie.net/rap/images/0/0f/Medium_ajs-nigrutin.jpg/revision/latest" - }, - { - "name": "Akala", - "image_url": "http://assets.londonist.com/uploads/2015/11/akala.jpg" - }, - { - "name": "Akinyele", - "image_url": "http://www.hiphopfind.com/upload/bukmtqhhlj.jpg" - }, - { - "name": "Akir", - "image_url": "http://www.iamhiphopmagazine.com/wp-content/uploads/2013/05/Akir.jpg" - }, - { - "name": "Akon", - "image_url": "http://img.mi9.com/male-celebrities/5077/akon-rapper-2012_1920x1200_96228.jpg" - }, - { - "name": "The Alchemist", - "image_url": "http://4.bp.blogspot.com/_nF-S6ZVuhd4/SuESHfU70_I/AAAAAAAAABs/s0txyMZP-Ps/s400/alc.jpg" - }, - { - "name": "Ali Vegas", - "image_url": "https://s3-us-west-2.amazonaws.com/maven-user-photos/f0f13b1e-f33a-4136-bc5a-6a0f09dd591f" - }, - { - "name": "Alpha", - "image_url": "https://images.rapgenius.com/9f2ba7bd713179faa8fe8968969f82eb.1000x667x1.jpg" - }, - { - "name": "AMG", - "image_url": "http://en.academic.ru/pictures/enwiki/65/Amg_rapper.jpg" - }, - { - "name": "Amil", - "image_url": "http://3.bp.blogspot.com/_RaOrchOImw8/SxSVt5z1auI/AAAAAAAAbeQ/sZ7xLZjOmco/s1600/Amil.jpg" - }, - { - "name": "Aminé", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Amine_performing_on_Jimmy_Fallon_in_2017_%28crop%29.png/1200px-Amine_performing_on_Jimmy_Fallon_in_2017_%28crop%29.png" - }, - { - "name": "Amir Obè", - "image_url": "http://www.brooklynvegan.com/files/2016/03/Amir-Obe-Press-2015-billboard-650-e1457450170547.jpg" - }, - { - "name": "Ampichino", - "image_url": "http://ecx.images-amazon.com/images/I/51rPUuz7FTL._SL500_AA280_.jpg" - }, - { - "name": "Anderson .Paak", - "image_url": "http://okp-cdn.okayplayer.com/wp-content/uploads/2017/01/Chance-The-Rapper-Anderson-.Paak_.jpg" - }, - { - "name": "André 3000", - "image_url": "http://1.bp.blogspot.com/-1TAyMhARS8s/UaWDLXzZzdI/AAAAAAAACDg/NOToaM0g_as/s1600/80999804-e1369795094594.jpg" - }, - { - "name": "Andre Nickatina", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-andre-nickatina-singer-man-star.jpg" - }, - { - "name": "Andy Mineo", - "image_url": "http://media-cache-ec0.pinimg.com/736x/8c/fa/7a/8cfa7aebe58f9bc08a140cc571d62723.jpg" - }, - { - "name": "Angel Haze", - "image_url": "http://www.trbimg.com/img-508ac7c2/turbine/la-et-ms-fall-in-love-with-angel-haze-20121025-003/600" - }, - { - "name": "Angie Martinez", - "image_url": "http://www.missinfo.tv/wp-content/uploads/2014/03/yg-angie-martinez.png" - }, - { - "name": "Ant", - "image_url": "http://static.vibe.com/files/article_images/yg-addie.jpg" - }, - { - "name": "Ant Banks", - "image_url": "http://www.rapmusicguide.com/amass/images/inventory/2415/Ant%20Banks%20-%20Big%20Thangs.jpg" - }, - { - "name": "Antoinette", - "image_url": "http://static.squarespace.com/static/520ed800e4b0229123208764/521febeae4b011f034449849/521febebe4b0b42980e39d2d/1377823723826/whostheboss.jpg" - }, - { - "name": "Anybody Killa", - "image_url": "http://cdn.ticketfly.com/i/00/01/38/13/01-atxl1.png" - }, - { - "name": "Apache", - "image_url": "http://mtv.mtvnimages.com/uri/mgid:uma:image:mtv.com:4554653" - }, - { - "name": "Apathy", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/1/1f/Apathy_rapper.jpg" - }, - { - "name": "Arin Hanson", - "image_url": "https://i.redditmedia.com/WE_gR0aOODE9WLF-83188AYG9Rd2QknnezyMyKVv_q0.jpg" - }, - { - "name": "A$AP Ferg", - "image_url": "http://media.gettyimages.com/photos/rapper-aap-ferg-performs-in-concert-at-austin-music-hall-on-february-picture-id508118894" - }, - { - "name": "A$AP Nast", - "image_url": "https://i1.wp.com/hypebeast.com/image/2017/02/asap-nast-calabasas-collection-video-0-1.jpg" - }, - { - "name": "A$AP Rocky", - "image_url": "http://3.bp.blogspot.com/-4ks0l9W_v9w/T_H3jtPbI-I/AAAAAAACePQ/9fbnSjU60zE/s1600/Rapper+A$AP+Rocky.jpg" - }, - { - "name": "A$AP Yams", - "image_url": "http://cdn.chartattack.com/wp-content/uploads/2015/01/asap-yams.jpg" - }, - { - "name": "A$ton Matthews", - "image_url": "http://hivesociety.com/wp-content/uploads/2014/07/Aston004.jpg" - }, - { - "name": "Asher Roth", - "image_url": "http://www.streetgangs.com/wp-content/uploads/2010/08/asher-roth.jpg" - }, - { - "name": "Astronautalis", - "image_url": "http://jacksonville.com/sites/default/files/imagecache/superphoto/photos/blogs/141/13358_203735570707_32280295707_4512309_5261114_n.jpg" - }, - { - "name": "Awol One", - "image_url": "http://images.complex.com/complex/image/upload/c_limit,w_680/fl_lossy,pg_1,q_auto/lqiqtknfvukvwx4ki4te.jpg" - }, - { - "name": "Awkwafina", - "image_url": "http://media3.s-nbcnews.com/i/newscms/2014_35/635111/awkwafina_06_00cd08f4bbc01bf20145954bf5e97fc0.jpg" - }, - { - "name": "AZ", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/7/76/Az-03.jpg" - }, - { - "name": "Azealia Banks", - "image_url": "http://i.huffpost.com/gen/920953/images/o-RAPPER-AZEALIA-BANKS-facebook.jpg" - }, - { - "name": "Big Daddy Kane", - "image_url": "http://images.rapgenius.com/30a5d7f5135fdf9124beff6834884ff8.640x773x1.jpg" - }, - { - "name": "Busta Rhymes", - "image_url": "http://newsbite.it/public/images/articles/busta-drake.jpg" - }, - { - "name": "B-Legit", - "image_url": "http://cps-static.rovicorp.com/3/JPG_500/MI0003/729/MI0003729154.jpg" - }, - { - "name": "B-Real", - "image_url": "http://www3.pictures.zimbio.com/pc/Rapper+B+Real+Cypress+Hill+outside+Beso+Hollywood+sklqv7IPKMfl.jpg" - }, - { - "name": "B.G.", - "image_url": "http://1.bp.blogspot.com/-L8lASsCAFzo/Ve46XOQJynI/AAAAAAAAJOw/zJ6vIxtAbrs/s400/BG-In-Prison-2015.jpg" - }, - { - "name": "B.G. Knocc Out", - "image_url": "https://escobar300.files.wordpress.com/2013/08/b-g-knocc-out.png" - }, - { - "name": "B.o.B", - "image_url": "http://www4.pictures.zimbio.com/gi/B+o+B+rapper+BET+Hip+Hop+Awards+2010+Arrivals+bQ93QLw6t05l.jpg" - }, - { - "name": "Baby Bash", - "image_url": "http://ww2.hdnux.com/photos/24/26/14/5333761/5/960x540.jpg" - }, - { - "name": "Baby Boy da Prince", - "image_url": "http://www.rapartists.com/_files/pictures/full/1924_baby_boy_da_prince_u03.jpg" - }, - { - "name": "Baby D", - "image_url": "http://cdn.straightfromthea.com/wp-content/uploads/2008/03/babyd2.jpg" - }, - { - "name": "Bad Azz", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2015/10/Bad-Azz_10-08-2015.jpg" - }, - { - "name": "Badshah", - "image_url": "http://freehdwallpapersz.com/wp-content/uploads/2016/06/badshah-rapper-wallpaper.jpg" - }, - { - "name": "Baeza", - "image_url": "https://40.media.tumblr.com/bcb78b58c6c7a906342772115424c983/tumblr_mhpekhIOKd1rily04o1_500.jpg" - }, - { - "name": "Bahamadia", - "image_url": "http://hiphopgoldenage.com/wp-content/uploads/2015/08/CS1700455-02A-BIG.jpg" - }, - { - "name": "Baka Not Nice", - "image_url": "http://rapradar.com/wp-content/uploads/2015/05/baka.jpg" - }, - { - "name": "Bang Yong-guk", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-bang-yong-guk-dance-song-hip-hop.jpg" - }, - { - "name": "Bangladesh", - "image_url": "http://www.nodfactor.com/wp-content/uploads/2010/04/bangladesh_0.jpg" - }, - { - "name": "Bas", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0004/032/MI0004032446.jpg" - }, - { - "name": "Battlecat", - "image_url": "https://mediaanarchist.files.wordpress.com/2013/07/7995104660_aeda73b31f_z.jpg" - }, - { - "name": "Beanie Sigel", - "image_url": "http://hustlebunny.com/content/2012/07/beanie-sigel-rapper.jpg" - }, - { - "name": "Becky G", - "image_url": "https://40.media.tumblr.com/da84f75189bb52c8d66f64dc5ea20bf1/tumblr_mo8inbcBAK1qfyjoto1_500.jpg" - }, - { - "name": "Benny Blanco", - "image_url": "http://cache2.asset-cache.net/gc/473115600-rapper-benny-blanco-attends-the-63rd-annual-gettyimages.jpg" - }, - { - "name": "Beenzino", - "image_url": "http://www.kpopmusic.com/wp-content/uploads/2015/10/beenzino-3.jpg" - }, - { - "name": "Benzino", - "image_url": "http://missxpose.com/wp-content/uploads/2014/03/benzino.png" - }, - { - "name": "Big Boi", - "image_url": "http://redalertpolitics.com/files/2013/01/BigBoi.jpg" - }, - { - "name": "Big Daddy Kane", - "image_url": "http://images.rapgenius.com/30a5d7f5135fdf9124beff6834884ff8.640x773x1.jpg" - }, - { - "name": "Big Ed", - "image_url": "http://upload.wikimedia.org/wikipedia/en/8/8d/Big_Ed.jpg" - }, - { - "name": "Big Gipp", - "image_url": "http://www.stacksmag.net/wp-content/uploads/2014/09/stacksmag007atllive.png" - }, - { - "name": "Big Hawk", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-big-hawk-celebrity-star-style.jpg" - }, - { - "name": "Big K.R.I.T.", - "image_url": "http://www.thissongslaps.com/wp-content/uploads/2014/09/big_krit-news-article921121.png" - }, - { - "name": "Big Kuntry King", - "image_url": "http://s4.evcdn.com/images/block250/I0-001/000/989/283-8.jpeg_/big-kuntry-king-83.jpeg" - }, - { - "name": "Big L", - "image_url": "http://outlookaub.com/wp-content/uploads/2015/03/big-l.jpg" - }, - { - "name": "Big Lurch", - "image_url": "https://thoughtcatalog.files.wordpress.com/2015/09/big_lurch.jpg" - }, - { - "name": "Big Mello", - "image_url": "http://hivesociety.com/wp-content/uploads/2015/10/big-mello.jpg" - }, - { - "name": "Big Mike", - "image_url": "http://fakehustle.files.wordpress.com/2009/02/big-mike.jpg" - }, - { - "name": "Big Moe", - "image_url": "http://s3.amazonaws.com/rapgenius/big-moe_480x480.jpg" - }, - { - "name": "Big Noyd", - "image_url": "http://singersroom.com/upload/2013/03/Big-Noyd-Light-Up-The-Night.jpg" - }, - { - "name": "Big Pokey", - "image_url": "http://purple-drank.com/wp-content/uploads/2013/06/Big-Pokey.png" - }, - { - "name": "Big Pooh", - "image_url": "http://ambrosiaforheads.com/wp-content/uploads/2012/07/Rapper+Big+Pooh+Big+p00h.jpg" - }, - { - "name": "Big Pun", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.1620420.1392857208!/img/httpImage/image.jpg_gen/derivatives/article_750/big-pun-legacy.jpg" - }, - { - "name": "Big Reese", - "image_url": "https://dpjsgvx0fhwmn.cloudfront.net/albums/34235/large/bb2c7032b95187e95b92.jpg" - }, - { - "name": "Big Scoob", - "image_url": "http://www.funmissouri.com/uploads/files/2013/06/big-scoob-rapper-missouri.png" - }, - { - "name": "Big Smo", - "image_url": "https://i.ytimg.com/vi/igZweLr604Y/maxresdefault.jpg" - }, - { - "name": "Big Sean", - "image_url": "http://images5.fanpop.com/image/photos/27200000/Big-Sean-big-sean-rapper-27232010-500-610.jpg" - }, - { - "name": "Big Shaq", - "image_url": "https://hypebeast.imgix.net/http%3A%2F%2Fhypebeast.com%2Fimage%2F2017%2F10%2Frapper-halloween-costumes-lil-pump-roadman-shaq-lil-peep-lil-b-lil-uzi-vert-00.jpg" - }, - { - "name": "Big Shug", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/d1/2a/db/d12adb999028c2db003aa5e736e585a2.jpg" - }, - { - "name": "Big Syke", - "image_url": "https://pmchollywoodlife.files.wordpress.com/2016/12/big-syke-rapper-died-ftr.jpg" - }, - { - "name": "Bigg D", - "image_url": "http://www.4umf.com/wp-content/uploads/2014/11/Rapper-Big-Paybacc-Killed.jpg" - }, - { - "name": "Billy Woods", - "image_url": "http://backwoodzstudioz.com/wp-content/uploads/2013/06/BillyWoods_AlexanderRichter_WebReady_Press_DourCandy_2C.jpg" - }, - { - "name": "Birdman", - "image_url": "http://www.ablogtowatch.com/wp-content/uploads/2010/12/clip_image008.jpg" - }, - { - "name": "Bishop Nehru", - "image_url": "http://cdn2.pitchfork.com/news/53665/e5bac495.jpg" - }, - { - "name": "Biz Markie", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.1451937.1378902764!/img/httpImage/image.jpg_gen/derivatives/article_1200/biz-markie.jpg" - }, - { - "name": "Bizarre", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Bizarre.jpg/1200px-Bizarre.jpg" - }, - { - "name": "Bugz", - "image_url": "http://1.bp.blogspot.com/-zqDNlEcJS9Q/U3JTKpcl7hI/AAAAAAAAAcw/sQJSzSDVIq4/s1600/bugz+image.jpg" - }, - { - "name": "Bizzy", - "image_url": "http://wprapradar.s3.amazonaws.com/wp-content/uploads/2010/05/bizzy-bone1.jpg" - }, - { - "name": "Bizzy Bone", - "image_url": "https://nowtoronto.com/downloads/68186/download/BizzyBone.jpg" - }, - { - "name": "BJ the Chicago Kid", - "image_url": "https://consequenceofsound.files.wordpress.com/2015/08/chance-the-rapper-bj-the-chicago-kid.jpg" - }, - { - "name": "Black Milk", - "image_url": "http://hiphop-n-more.com/wp-content/uploads/2010/09/black-milk-9.jpg" - }, - { - "name": "Black Rob", - "image_url": "http://mtv.mtvnimages.com/uri/mgid:uma:image:mtv.com:1676761" - }, - { - "name": "Black Thought", - "image_url": "http://vibesource.files.wordpress.com/2008/06/black-thought-mcsforlifevibesourcemag.jpg" - }, - { - "name": "Blade Icewood", - "image_url": "http://www.officialpsds.com/images/thumbs/Blade-Icewood-psd61214.png" - }, - { - "name": "Blaq Poet", - "image_url": "http://www.audibletreats.com/wp-content/gallery/blaqpoet/blaq_poet-street.jpg" - }, - { - "name": "Blaze Ya Dead Homie", - "image_url": "http://img3.wikia.nocookie.net/__cb20130520210459/rap/images/b/bb/Blaze_ya_dead_homie.jpg" - }, - { - "name": "BlocBoy JB", - "image_url": "https://rapdose.com/wp-content/uploads/fly-images/146583/asap-forever-378x250-c.jpg" - }, - { - "name": "Blood Raw", - "image_url": "http://www.rapartists.com/_files/pictures/full/2313_blood_raw.jpg" - }, - { - "name": "Blu", - "image_url": "http://thecomeupshow.com/wp-content/uploads/2012/08/blu-rapper.jpg" - }, - { - "name": "Bob Doe", - "image_url": "http://media-cache-ak0.pinimg.com/736x/84/e0/35/84e0355d2eaac3e990892bad26267ff2.jpg" - }, - { - "name": "Bobby Brackins", - "image_url": "http://www1.pictures.zimbio.com/gi/Bobby+Brackins+Puma+Presents+Riddim+Run+Benefiting+1v-Ki1nD2Owl.jpg" - }, - { - "name": "Bobby Creekwater", - "image_url": "http://static.djbooth.net/pics-artist/bobbycreekwater2.jpg" - }, - { - "name": "Bobby Shmurda", - "image_url": "https://static.pulse.ng/img/incoming/origs3920830/3240485024-w980-h640/bobby-shmurda-and-rihanna.jpg" - }, - { - "name": "Bohemia", - "image_url": "http://1.bp.blogspot.com/-vnAVLnSXj1k/UskN6icYkBI/AAAAAAAAAUM/6jbrIjj-GS0/s1600/From+the+set+of+Dada+in+Bahrain.jpg" - }, - { - "name": "Boi-1da", - "image_url": "http://www.beatmakingvideos.com/sites/default/files/producer_foto/boi_1da.jpg" - }, - { - "name": "Boldy James", - "image_url": "http://thoughtontracks.files.wordpress.com/2012/06/boldy-james.jpg" - }, - { - "name": "Bone Crusher", - "image_url": "http://images2.mtv.com/uri/mgid:uma:artist:mtv.com:1235147" - }, - { - "name": "Bones", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/e7/3a/2e/e73a2e853bd074c32d1abe08900ba2a7.jpg" - }, - { - "name": "Booba", - "image_url": "https://underneathestarz.files.wordpress.com/2015/06/boo23.jpg" - }, - { - "name": "Boondox", - "image_url": "http://static.tvtropes.org/pmwiki/pub/images/boondox01_7971.jpg" - }, - { - "name": "Boosie Badazz", - "image_url": "http://www.rap-up.com/app/uploads/2015/11/boosie-hat.jpg" - }, - { - "name": "Boss", - "image_url": "http://3.bp.blogspot.com/-2Tn1dMXQIzE/TtW53Kcvo4I/AAAAAAAADcQ/LNWNkwDCwIY/s1600/Boss.jpg" - }, - { - "name": "Bow Wow", - "image_url": "https://thedrinksbusiness.com/wordpress/wp-content/uploads/2015/03/lil-bow-wow-net-worth-424x640.jpg" - }, - { - "name": "Braille", - "image_url": "http://media1.fdncms.com/bend/imager/braille-read-him-with-your-ears/u/original/2148215/braille.jpg" - }, - { - "name": "Brandun DeShay", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/4b/28/bb/4b28bbbd4f6d7223e07d312b7cacc2ac--rapper-artists.jpg" - }, - { - "name": "Brianna Perry", - "image_url": "http://4.bp.blogspot.com/-IXYep2nCIwI/T0F0AX6zRZI/AAAAAAAACRg/4agQBS5a50Y/s1600/rapper-brianna.jpg" - }, - { - "name": "Brisco", - "image_url": "http://www.yorapper.com/Photos/brisco-rap.jpg" - }, - { - "name": "Brotha Lynch Hung", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-brotha-lynch-hung-song-recitation-fame.jpg" - }, - { - "name": "Bruno Mars", - "image_url": "http://www2.pictures.zimbio.com/gi/Bruno+Mars+B+o+B+rapper+2010+MTV+Video+Music+TeX5_EMP-jDl.jpg" - }, - { - "name": "Brother Ali", - "image_url": "http://www.killerhiphop.com/wp-content/uploads/2012/02/Brother-Ali.jpg" - }, - { - "name": "Bryson Tiller", - "image_url": "https://i.pinimg.com/736x/00/2d/f7/002df7c879422acb3a18c00c224300f3--bryson-tiller-rapper.jpg" - }, - { - "name": "Bubba Sparxxx", - "image_url": "http://www.contactmusic.com/pics/l/ludacris_benefit_250408/rapper_bubba_sparxxx_5124692.jpg" - }, - { - "name": "Buckshot", - "image_url": "https://i.ytimg.com/vi/JU81txDlbx8/maxresdefault.jpg" - }, - { - "name": "Buckwild", - "image_url": "http://www.blackouthiphop.com/blog/wp-content/uploads/2011/04/buckwild-producer1.jpg" - }, - { - "name": "Bumpy Knuckles", - "image_url": "http://streetknowledge.files.wordpress.com/2008/07/bumpy_knuckles.jpg" - }, - { - "name": "Bun B", - "image_url": "http://ww4.hdnux.com/photos/23/53/75/5160803/3/rawImage.jpg" - }, - { - "name": "Busdriver", - "image_url": "http://images2.laweekly.com/imager/busdriver/u/original/5177944/busdriver_photo-l_dianadalsasso2014.jpg" - }, - { - "name": "Bushwick Bill", - "image_url": "http://www.collegebaseballtoday.com/files/2013/05/BushwickBill_Stitches1.jpg" - }, - { - "name": "Busta Rhymes", - "image_url": "http://newsbite.it/public/images/articles/busta-drake.jpg" - }, - { - "name": "Busy Bee Starski", - "image_url": "https://s3.amazonaws.com/battlerap-production/2014/09/Busy-Bee610.jpg" - }, - { - "name": "Butch Cassidy", - "image_url": "https://pbs.twimg.com/profile_images/378800000221503061/5d6802b25f28f80c390d48fd1aca20d1.jpeg" - }, - { - "name": "Common", - "image_url": "http://i.huffpost.com/gen/1352726/thumbs/o-RAPPER-COMMON-facebook.jpg" - }, - { - "name": "C-Bo", - "image_url": "http://unitedgangs.files.wordpress.com/2010/04/c-bo.jpg" - }, - { - "name": "C-Murder", - "image_url": "http://media.nola.com/crime_impact/photo/cmurder-horizontal-cropjpg-77be374eafb600e4.jpg" - }, - { - "name": "C-Note", - "image_url": "https://i.ytimg.com/vi/F40M4icrNMQ/maxresdefault.jpg" - }, - { - "name": "C-Rayz Walz", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2017/04/C-Rayz-WalzInstagram-e1491328882399-827x620.png" - }, - { - "name": "Cage", - "image_url": "http://1.bp.blogspot.com/_4ZiWxZWnbZE/TVEuUjybxkI/AAAAAAAAAWQ/SjyC6aacpXc/s1600/cagecrazy.jpg" - }, - { - "name": "Cam'ron", - "image_url": "http://www.yorapper.com/Photos/camron-ringtones.jpg" - }, - { - "name": "Canibus", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/d/d9/Canibus_at_Amager_Bio_4.jpg" - }, - { - "name": "Capital Steez", - "image_url": "http://images.complex.com/complex/image/upload/c_fill,g_center,h_640,w_640/fl_lossy,pg_1,q_auto/gjpwujnfpj1afsu4uawy.jpg" - }, - { - "name": "Capone", - "image_url": "https://consequenceofsound.files.wordpress.com/2018/04/the-meadows-2017-ben-kaye-run-the-jewels-7.jpg" - }, - { - "name": "Cappadonna", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-cappadonna-star-celebrity-best-photo.jpg" - }, - { - "name": "Cardi B", - "image_url": "https://rapdose.com/wp-content/uploads/2018/03/cardi-b-be-careful.jpg" - }, - { - "name": "Casey Veggies", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/theboombox.com/files/2015/02/caseyveggies-630x420.jpg" - }, - { - "name": "Cash Out", - "image_url": "http://jojocrews.com/wp-content/uploads/2015/01/cash1.jpg" - }, - { - "name": "Cashis", - "image_url": "http://mediastarr.files.wordpress.com/2008/10/cashismusiccom1.jpg" - }, - { - "name": "Caskey", - "image_url": "http://social.rollins.edu/wpsites/mousetrap/files/2016/06/Caskey.jpg" - }, - { - "name": "Casper Nyovest", - "image_url": "http://www.destinyman.com/wp-content/uploads/2015/04/Casper-Nyovest--690x450.jpg" - }, - { - "name": "Cassidy", - "image_url": "http://www.missxpose.com/wp-content/uploads/2010/03/BET+Rip+Runway+2010+Arrivals+MPN9FSqiQxql2.jpg" - }, - { - "name": "Cazwell", - "image_url": "http://1.bp.blogspot.com/-yxVYPS6GT4Q/TdLjbiV2bYI/AAAAAAAAESs/ecXi9lrdkKM/s1600/cazwell.jpg" - }, - { - "name": "CeeLo Green", - "image_url": "https://www.festivalsherpa.com/wp-content/uploads/2014/09/ceelo.jpg" - }, - { - "name": "Cellski", - "image_url": "http://images.rapgenius.com/56c23a073295a1baa7c88f22411a8a9a.500x500x1.jpg" - }, - { - "name": "Celly Cel", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/729/MI0003729697.jpg" - }, - { - "name": "Celph Titled", - "image_url": "http://s3.amazonaws.com/rapgenius/1362723298_l.jpg" - }, - { - "name": "Cesar Comanche", - "image_url": "http://d2jos65913uaef.cloudfront.net/wp-content/uploads/2013/07/Cesar_comanche_interview-200x130.jpg" - }, - { - "name": "Ceza", - "image_url": "https://musicinculture.files.wordpress.com/2011/04/ceza40ke9_1_.jpg" - }, - { - "name": "Chamillionaire", - "image_url": "http://i.dailymail.co.uk/i/pix/2016/02/19/09/01BC5195000004B0-3454235-image-m-26_1455874575624.jpg" - }, - { - "name": "Chance the Rapper", - "image_url": "http://rukkus.com/blog/wp-content/uploads/2014/01/chance-the-rapper.png" - }, - { - "name": "Chanel West Coast", - "image_url": "http://images1.laweekly.com/imager/rapper-reality-tv-star-chanel-west-coast/u/original/5002632/law_chanel_west_coast-3280-edit.jpg" - }, - { - "name": "Channel 7", - "image_url": "http://media.gettyimages.com/photos/south-korean-rapper-psy-performs-live-on-channel-7s-sunrise-at-martin-picture-id154251648" - }, - { - "name": "Charizma", - "image_url": "http://4ca03fhcpiv4bsn7vbg2ef11td.wpengine.netdna-cdn.com/wp-content/uploads/2014/09/1b820b3864413e7b3fba3958b433d733-1024x690.jpg" - }, - { - "name": "Charles Hamilton", - "image_url": "http://www.billboard.com/files/styles/promo_650/public/media/charles-hamilton-brigitte-sire-bb7-2015-billboard-650.jpg" - }, - { - "name": "Charli Baltimore", - "image_url": "http://djpinkietuscadero.files.wordpress.com/2013/08/charli-baltimore.jpg" - }, - { - "name": "Chevy Woods", - "image_url": "http://www.post-gazette.com/image/2015/08/04/ca11,5,1918,1913/chevy0806b0553-1.jpg" - }, - { - "name": "Chi Ali", - "image_url": "http://www.vladtv.com/images/size_fs/video_image-133965.jpg" - }, - { - "name": "Chali 2na", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/2/2a/Chali2na.jpg" - }, - { - "name": "Chiddy Bang", - "image_url": "http://campuseventsblog.com/wp-content/uploads/2012/09/CHIDDYPHOTO1.jpg" - }, - { - "name": "Chief Keef", - "image_url": "http://wallpapersqq.net/wp-content/uploads/2016/01/Chief-Keef-6.jpg" - }, - { - "name": "Childish Gambino", - "image_url": "http://diymag.com/media/img/Artists/C/Childish-Gambino/_1500x1000_crop_center-center_75/Childish-Gambino-Press-Photo-copy.jpg" - }, - { - "name": "Chill Rob G", - "image_url": "https://img.youtube.com/vi/Hd0aNVDOo3U/0.jpg" - }, - { - "name": "Chingy", - "image_url": "http://tattletailzz.com/wp-content/uploads/2013/03/SGG-008258.jpg" - }, - { - "name": "Chingo Bling", - "image_url": "https://atlantalatinos.com/wp-content/uploads/2018/03/chingo-bling-atlanta-georgia-atlantalatinos-2-1068x1067.jpg" - }, - { - "name": "Chino XL", - "image_url": "http://iv1.lisimg.com/image/436161/600full-chino-xl.jpg" - }, - { - "name": "Chinx", - "image_url": "http://images.complex.com/complex/image/upload/t_article_image/chinx-rapper-2_jyerea.jpg" - }, - { - "name": "Chip", - "image_url": "http://www.voice-online.co.uk/sites/default/files/imagecache/455/chip-rapper-RIP.jpg" - }, - { - "name": "Choice", - "image_url": "http://2.bp.blogspot.com/-TjL6ZSCN8kA/UTimdEyVUSI/AAAAAAAABYI/fUlPXSKfg7o/s320/choiceback.jpg" - }, - { - "name": "Choppa", - "image_url": "http://fanpagepress.net/m/C/choppa-rapper-1.jpg" - }, - { - "name": "Chris Brown", - "image_url": "http://www.hdwallpaper4u.com/wp-content/uploads/2015/07/chris-brown_rapper_look.jpg" - }, - { - "name": "Chris Webby", - "image_url": "http://thissongissick.com/blog/wp-content/uploads/2011/11/Chris-Webby-Rapper.jpg" - }, - { - "name": "Christopher Martin", - "image_url": "http://www2.pictures.zimbio.com/gi/Heavy+D+Funeral+Service+Qy4IEAGabbUx.jpg" - }, - { - "name": "Christopher Reid", - "image_url": "http://static.atlantablackstar.com/wp-content/uploads/2014/09/Christopher-Kid-Reid.jpg" - }, - { - "name": "Chubb Rock", - "image_url": "http://www.largeup.com/wp-content/uploads/2012/07/chubb-rock.jpg" - }, - { - "name": "CJ Fly", - "image_url": "https://images.rapgenius.com/02df360b384c97322368321affc6603c.600x338x73.gif" - }, - { - "name": "CL", - "image_url": "http://koogle.tv/static/media/uploads/news/010915_2ne1-cl_01.jpg" - }, - { - "name": "CL Smooth", - "image_url": "http://i2.cdn.turner.com/cnnnext/dam/assets/130502100438-08-90s-rappers-horizontal-large-gallery.jpg" - }, - { - "name": "Classified", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/c/c5/2011_MuchMusic_Video_Awards_-_Classified.jpg" - }, - { - "name": "Clinton Sparks", - "image_url": "http://www.themusicgroupagency.com/Artist%20Bio_Pixs/DJ%20CLINTON%20SPARKS.jpg" - }, - { - "name": "Clyde Carson", - "image_url": "http://www2.pictures.zimbio.com/gi/Clyde+Carson+2012+BET+Awards+Celebrity+Gifting+h3makmUgLDQl.jpg" - }, - { - "name": "Cold 187um", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/471/MI0003471422.jpg" - }, - { - "name": "Common", - "image_url": "http://i.huffpost.com/gen/1352726/thumbs/o-RAPPER-COMMON-facebook.jpg" - }, - { - "name": "Consequence", - "image_url": "http://cdn.cnwimg.com/wp-content/uploads/2013/01/consequence.jpg" - }, - { - "name": "Cool Breeze", - "image_url": "http://www4.pictures.zimbio.com/gi/2010+Vh1+Hip+Hop+Honors+Arrivals+6Kw4hn4nNHJl.jpg" - }, - { - "name": "Cool C", - "image_url": "http://bloximages.chicago2.vip.townnews.com/phillytrib.com/content/tncms/assets/v3/editorial/e/a3/ea3f0f99-c7b3-5a16-a498-43e00ef30007/5473f146bc971.image.jpg" - }, - { - "name": "Coolio", - "image_url": "http://1.bp.blogspot.com/_B1LlYh6iKqs/TEpHopMKndI/AAAAAAAACVQ/w5typW_-Cqo/s1600/coolio.jpg" - }, - { - "name": "Copywrite", - "image_url": "http://ifelicious.com/wp-content/uploads/2010/06/RapperCopywrite_614cap.jpg" - }, - { - "name": "Cormega", - "image_url": "http://www.iamhiphopmagazine.com/wp-content/uploads/2013/02/cormega_by_G_M_D_THREE_02.jpg" - }, - { - "name": "Cory Gunz", - "image_url": "http://www.aceshowbiz.com/images/wennpic/cory-gunz-set-music-video-fred-the-godson-01.jpg" - }, - { - "name": "Cordaro Stewart", - "image_url": "http://www.famousbirthdays.com/thumbnails/stewart-cordaro-large.jpg" - }, - { - "name": "Count Bass D", - "image_url": "https://massappeal.com/wp-content/uploads/2012/10/count-bass-d.jpg" - }, - { - "name": "The Coup", - "image_url": "http://media.philly.com/images/600*450/20121207_inq_wkpfea07-a.JPG" - }, - { - "name": "Craig Mack", - "image_url": "https://escobar300.files.wordpress.com/2011/08/craig-mack.jpg" - }, - { - "name": "Crime Boss", - "image_url": "http://www.angelfire.com/ok/midsouthhiphop/images/CRIME.jpg" - }, - { - "name": "Criminal Manne", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2013/02/Criminal-Manne.jpg" - }, - { - "name": "Crooked I", - "image_url": "http://zmldajoker.com/wp-content/uploads/2012/08/Crooked-I.jpg" - }, - { - "name": "Crucial Star", - "image_url": "http://www.allkpop.com/upload/2017/02/af_org/crucial-star_1486480774_af_org.jpg" - }, - { - "name": "Cupcakke", - "image_url": "https://lastfm-img2.akamaized.net/i/u/770x0/fd2ee4b462b471a02f1d47c6908aa414.jpg#fd2ee4b462b471a02f1d47c6908aa414" - }, - { - "name": "Currensy", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2013/09/currensy.jpg" - }, - { - "name": "Curtiss King", - "image_url": "http://images.genius.com/d117bf90749090895c48b26d53585f3f.640x640x1.jpg" - }, - { - "name": "Cyhi the Prynce", - "image_url": "http://hiphop-n-more.com/wp-content/uploads/2016/02/cyhi-the-prynce-montreality-680x462.jpg" - }, - { - "name": "Del Tha Funkee Homosapien", - "image_url": "http://www.fagostore.com/shared/d/de988c79be500dce37c77c306ca4c1c7_hw600_width.jpg" - }, - { - "name": "Dr. Dre", - "image_url": "https://pennylibertygbow.files.wordpress.com/2012/02/drdre.gif" - }, - { - "name": "D'Angelo", - "image_url": "http://1.bp.blogspot.com/-eWbI_zHem7w/T05dilWoEqI/AAAAAAADZjw/w-bTjY7Ro1Q/s400/dangelo-1.jpg" - }, - { - "name": "D'banj", - "image_url": "http://cache2.asset-cache.net/gc/150994476-rapper-d-banj-is-photographed-for-the-gettyimages.jpg" - }, - { - "name": "D-Loc", - "image_url": "http://s3.amazonaws.com/rapgenius/YJsUar79REWtkxvOpCVw_d-loc.jpg" - }, - { - "name": "D-Nice", - "image_url": "http://img.wax.fm/releases/2386762/d-nice-call-me-d-nice-1523902.jpeg" - }, - { - "name": "D-Pryde", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/b/ba/Plan_A_Merchandise_Promotion.png" - }, - { - "name": "The D.O.C.", - "image_url": "http://www.blackouthiphop.com/blog/wp-content/uploads/2011/01/The+DOC.jpg" - }, - { - "name": "Da Brat", - "image_url": "http://3.bp.blogspot.com/-2ptC5bvDzrc/Ti2nZQfRhKI/AAAAAAAARhA/Zq7JHQZPftU/s1600/Da-Brat-vibe1.jpg" - }, - { - "name": "Da$h", - "image_url": "http://s3.amazonaws.com/rapgenius/1362086799_Dah%20dashtweet.jpg" - }, - { - "name": "Da'unda'dogg", - "image_url": "https://images-na.ssl-images-amazon.com/images/I/61Scu%2BIkLYL._SL500_AA280_.jpg" - }, - { - "name": "Daddy-O", - "image_url": "http://m.i.uol.com.br/musica/2010/02/26/o-rapper-puffy-daddy-em-evento-em-nova-york-23012010-1267194075418_956x500.jpg" - }, - { - "name": "Dae Dae", - "image_url": "http://cdn.baeblemusic.com/bandcontent/dae_dae/dae_dae_bio-498.jpg" - }, - { - "name": "Damu the Fudgemunk", - "image_url": "http://berlin030.de/wp-content/uploads/2016/07/Damu-The-Fudgemunk.jpg" - }, - { - "name": "Dan Bull", - "image_url": "http://www.tubefilter.com/wp-content/uploads/2016/03/dan-bull.jpg" - }, - { - "name": "Dana Dane", - "image_url": "http://media-cache-ak0.pinimg.com/736x/14/57/5c/14575c78fb7f3e2e844ec15621acea74.jpg" - }, - { - "name": "Danny Boy", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/House_of_Pain-IMG_6536.jpg/1200px-House_of_Pain-IMG_6536.jpg" - }, - { - "name": "Danny Brown", - "image_url": "http://media.npr.org/assets/img/2012/08/27/rap-stream-db2_wide-2f0e39013c78f49f93129e2eef4c93e72b9c0eb1-s6-c30.jpg" - }, - { - "name": "Dappy", - "image_url": "http://madnews.files.wordpress.com/2010/05/dappy.jpg" - }, - { - "name": "Dave East", - "image_url": "http://www.rehabonlinemag.com/wp-content/uploads/2014/09/dave-east-cropped.jpg" - }, - { - "name": "Daveed Diggs", - "image_url": "http://theawesomer.com/photos/2016/05/Daveed-Diggs_fastest_rapper_on_broadway_t.jpg" - }, - { - "name": "David Banner", - "image_url": "http://www3.pictures.zimbio.com/gi/David+Banner+33rd+Annual+UNCF+Evening+Stars+ZUzQpuO0gb0l.jpg" - }, - { - "name": "David Dallas", - "image_url": "http://static.djbooth.net/pics-artist/daviddallas2.jpg" - }, - { - "name": "David Rush", - "image_url": "http://www.vegasnews.com/wp-content/uploads/davidrush_redcarpet-588.jpg" - }, - { - "name": "David Stones", - "image_url": "https://media.licdn.com/mpr/mpr/AAEAAQAAAAAAAAfNAAAAJGY2M2Q3MDY5LWE4MTYtNDgyMS1hMWZmLWEzOTMzOTFmYTQ4Mw.jpg" - }, - { - "name": "Daz Dillinger", - "image_url": "https://s3.amazonaws.com/rapgenius/filepicker%2FXvubZbsJQue813tNktYE_Daz_Dillinger.jpg" - }, - { - "name": "Dazzie Dee", - "image_url": "https://lh6.googleusercontent.com/-2BfKUpLN9OU/TX9xEi7kAUI/AAAAAAAACDY/tcA_AynfDM8/s400/1.jpg" - }, - { - "name": "Dee Barnes", - "image_url": "http://i.dailymail.co.uk/i/pix/2015/08/19/01/2B7A5D7E00000578-0-image-a-2_1439945563429.jpg" - }, - { - "name": "Dee Dee King", - "image_url": "http://4.bp.blogspot.com/_svH18z9S5bU/SnSk-Vq8-SI/AAAAAAAAAsc/dr_YK0SSeV0/s320/FRONT.jpg" - }, - { - "name": "Dej Loaf", - "image_url": "http://i1.wp.com/inyaearhiphop.com/wp-content/uploads/2016/03/rapper-says-dej-loaf-is-lying-about-her-sexuality.png" - }, - { - "name": "Delyric Oracle", - "image_url": "http://thebaybridged.com/wp-content/uploads/2017/04/Chance-The-Rapper-at-Oracle-Arena-by-Joshua-Huver-14.jpg" - }, - { - "name": "Del the Funky Homosapien", - "image_url": "http://s3.amazonaws.com/rapgenius/del_the_funky_homosapien-130.jpg" - }, - { - "name": "Demrick", - "image_url": "http://theindustrywest.com/wp-content/uploads/2013/03/demrick.jpg" - }, - { - "name": "Deniro Farrar", - "image_url": "https://assets.audiomack.com/deniro-farrar/5a30d8d7a0f778a2e84ec2ebb5992768.jpeg" - }, - { - "name": "Denzel Curry", - "image_url": "https://media2.fdncms.com/orlando/imager/u/blog/2488240/denzel_-_photo-jmp_web.jpg" - }, - { - "name": "Derek Minor", - "image_url": "http://thegospelguru.com/wp-content/uploads/2014/10/derek-minor-03.jpg" - }, - { - "name": "Desiigner", - "image_url": "https://i.ytimg.com/vi/nmCYm7NOWME/maxresdefault.jpg" - }, - { - "name": "Detail", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2011/03/detail_03-21-11-300x300.jpg" - }, - { - "name": "Deuce", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Deuce.jpg" - }, - { - "name": "Dev", - "image_url": "http://4.bp.blogspot.com/-DTmssduDnJk/TWfNLurcS1I/AAAAAAAAAjY/yNyTtcEEuM0/s1600/Dev-Bass_Down_Low-single_cover.jpg" - }, - { - "name": "Devin the Dude", - "image_url": "http://lahiphopevents.com/wp-content/uploads/2015/11/DEVIN-THE-DUDE.jpg" - }, - { - "name": "Devlin", - "image_url": "http://musiqexpo.files.wordpress.com/2012/08/devlin-rapper-freestyle-2012-e1343833573673.jpg" - }, - { - "name": "Diabolic", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/a/a3/DIABOLIC.jpg" - }, - { - "name": "Diamond", - "image_url": "http://2.bp.blogspot.com/-Zk504yF-lDo/Tbo7Vy75dfI/AAAAAAAAuEQ/FtRTLC65dJ8/s1600/diamond-rapper-3.jpg" - }, - { - "name": "Diamond D", - "image_url": "http://nahright.com/wp-content/uploads/2013/12/DIAMOND-450x304.jpg" - }, - { - "name": "Diggy Simmons", - "image_url": "http://www4.pictures.zimbio.com/gi/Diggy+Simmons+2012+BET+Awards+Celebrity+Gifting+f_6FuVhNcnSl.jpg" - }, - { - "name": "Dillon Cooper", - "image_url": "http://s3.amazonaws.com/rapgenius/1352155921_Dillon-Cooper-1.jpg" - }, - { - "name": "Disco D", - "image_url": "https://i.ytimg.com/vi/-pzjJ0Gnwg0/maxresdefault.jpg" - }, - { - "name": "Disco King Mario", - "image_url": "http://hiphopandpolitics.files.wordpress.com/2013/01/disco-king-mario.jpg" - }, - { - "name": "Dizzee Rascal", - "image_url": "https://juelzone.files.wordpress.com/2012/01/dizzee_rascal_2.jpg" - }, - { - "name": "Dizzy Wright", - "image_url": "http://thedailyloud.com/wp-content/uploads/2013/07/dizzy-wright.jpeg" - }, - { - "name": "DJ Cash Money", - "image_url": "https://i.ytimg.com/vi/YjQi0oLbfT4/maxresdefault.jpg" - }, - { - "name": "DJ Casper", - "image_url": "http://assets.libsyn.com/content/1652870" - }, - { - "name": "DJ Clay", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/162/MI0003162471.jpg" - }, - { - "name": "DJ Clue?", - "image_url": "https://hhvibe.files.wordpress.com/2010/01/dj-clue.jpg" - }, - { - "name": "DJ Drama", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/thumb/2/28/DJ_Drama.jpg/1280px-DJ_Drama.jpg" - }, - { - "name": "DJ Felli Fel", - "image_url": "http://images.complex.com/complex/image/upload/c_limit,w_680/f_auto,fl_lossy,pg_1,q_auto/vdonhefsu1vbqzkokfvl.jpg" - }, - { - "name": "DJ Fuze", - "image_url": "http://api.ning.com/files/twryudDDeJeBWWFC5lWwmMNonB3Ue79CfQIrNYzlFuV2rTlpK65jKYOrbTFCSI3uKyKLXfsXo-QtftbzfM98VkeRbvf10eQZ/fuze.jpg" - }, - { - "name": "DJ Green Lantern", - "image_url": "http://hw-img.datpiff.com/m76a31fd/Various_Artists_Green_Lantern_Instrumentals-front-large.jpg" - }, - { - "name": "DJ Head", - "image_url": "http://www.eminem.pro/wp-content/uploads/2013/09/DJ-Head.jpg" - }, - { - "name": "DJ Hurricane", - "image_url": "http://www.vanndigital.com/wp-content/uploads/djhurricanecominoffcentralcoastvibemusicvideoclip.jpg" - }, - { - "name": "DJ Kay Slay", - "image_url": "http://assets.audiomack.com/paperchaserdotcom/1d35c76ce398acf2bba0bc11508f0fba.jpeg" - }, - { - "name": "DJ Khaled", - "image_url": "https://www.bestvideorap.com/wp-content/uploads/bscap0008(3).jpg" - }, - { - "name": "DJ Krush", - "image_url": "https://image.redbull.com/rbcom/010/2016-11-16/1331829659822_2/0010/1/1600/1067/1/dj-krush.jpg" - }, - { - "name": "DJ Mustard", - "image_url": "http://the5thelementmag.files.wordpress.com/2014/08/rapper-yg-and-dj-mustard.jpg" - }, - { - "name": "DJ Paul", - "image_url": "http://hiphoprapscene.com/wp-content/uploads/2016/08/dj-paul.jpg" - }, - { - "name": "DJ Pooh", - "image_url": "https://img.discogs.com/KHTPZVL6Pa1dtmD3PhfQvKz-eQg=/fit-in/300x300/filters:strip_icc():format(jpeg):mode_rgb():quality(40)/discogs-images/A-131630-1487348732-7573.jpeg.jpg" - }, - { - "name": "DJ Premier", - "image_url": "http://hiphopgoldenage.com/wp-content/uploads/2016/05/dj-premier-producer.jpg" - }, - { - "name": "DJ Quik", - "image_url": "http://dieenormousla.files.wordpress.com/2013/05/dj-quik.jpg" - }, - { - "name": "DJ Run", - "image_url": "https://c2.staticflickr.com/6/5022/5615202725_2e4e019896_b.jpg" - }, - { - "name": "DJ Screw", - "image_url": "http://screweduprecords.com/wp-content/uploads/2010/11/djSCREW5.jpg" - }, - { - "name": "DJ Shadow", - "image_url": "http://static.djbooth.net/pics-artist/dj-shadow.jpg" - }, - { - "name": "DJ Yella", - "image_url": "https://i.ytimg.com/vi/qaRqdspVNdo/maxresdefault.jpg" - }, - { - "name": "DMC", - "image_url": "http://i.huffpost.com/gen/1235615/images/o-RAPPER-DMC-facebook.jpg" - }, - { - "name": "DMX", - "image_url": "http://4hdwallpapers.com/wp-content/uploads/2013/04/Dmx-Rapper.jpg" - }, - { - "name": "Doap Nixon", - "image_url": "http://imagecache.blastro.com/timthumb.php/src=http%3A%2F%2Fimages.blastro.com%2Fimages%2Fartist_images%2Ffull%2Ffull_vinniepazvinniepazondoapnixon.jpg&w=610&h=457&zc=2&a=T" - }, - { - "name": "Doe B", - "image_url": "http://assets.noisey.com/content-images/contentimage/22592/Doe%20B%20featured%20image.jpg" - }, - { - "name": "Dok2", - "image_url": "http://static.askkpop.com/images/upload/18/ifrit1112/2016/05/12/Dok2-says-female-rappers-dontwrite-their-own-lyrics.jpg" - }, - { - "name": "Dolla", - "image_url": "http://www.streetgangs.com/wp-content/uploads/2009/05/20090529-dolla3.jpg" - }, - { - "name": "Dom Kennedy", - "image_url": "http://cdn.ambrosiaforheads.com/wp-content/uploads/2015/05/Rapper-Dom-Kennedy-Reveals-Album-Release-Date-MusicSnake-1024x576.jpg" - }, - { - "name": "Dominique Young Unique", - "image_url": "http://images.dailystar.co.uk/dynamic/45/photos/984000/620x/5327940d40cfe_18f12domm.jpg" - }, - { - "name": "Domino", - "image_url": "https://unitedgangs.files.wordpress.com/2013/09/domino.png" - }, - { - "name": "Domo Genesis", - "image_url": "http://media.gettyimages.com/photos/rapper-domo-genesis-of-mellowhigh-and-the-odd-future-collective-at-picture-id538084320" - }, - { - "name": "Don Cannon", - "image_url": "http://freddyo.com/wp-content/uploads/2013/07/don-cannon.jpg" - }, - { - "name": "Donnis", - "image_url": "http://www3.pictures.zimbio.com/gi/Donnis+Nokia+Lumia+900+Launches+Times+Square+ZmfXDpow5Jkl.jpg" - }, - { - "name": "Dorrough", - "image_url": "http://theboombox.com/files/2011/02/dorrough-200-020811.jpg" - }, - { - "name": "Doseone", - "image_url": "http://l7.alamy.com/zooms/d51db8efa60f4be5890e618183a22976/doseone-rapper-producer-poet-and-artist-performing-at-all-tomorrows-d7wtt5.jpg" - }, - { - "name": "Doug E. Fresh", - "image_url": "https://s-media-cache-ak0.pinimg.com/564x/d3/e9/47/d3e94702d1d9ad80b155eca525e91ce9.jpg" - }, - { - "name": "Doughbeezy", - "image_url": "http://www.brooklynvegan.com/img/indie/doughbeezy.jpg" - }, - { - "name": "Dr. Dre", - "image_url": "https://pennylibertygbow.files.wordpress.com/2012/02/drdre.gif" - }, - { - "name": "Drag-On", - "image_url": "https://ioneglobalgrind.files.wordpress.com/2016/03/14573801941016.png" - }, - { - "name": "Drake", - "image_url": "http://2.bp.blogspot.com/-PF-iHgXDePo/TrjTRk8SsfI/AAAAAAAAAvk/8j_OnnCRbLc/s1600/Drake_ThankMe_Publici_5000DPI300RGB550255.jpg" - }, - { - "name": "Dres", - "image_url": "http://images.complex.com/complex/image/upload/c_fill,g_center,w_1200/fl_lossy,pg_1,q_auto/cxinsw78yglijdcx7xfh.jpg" - }, - { - "name": "Dresta", - "image_url": "http://www.prlog.org/11403708-dresta.jpg" - }, - { - "name": "Drew Deezy", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Photo-of-Drew-Deezy.jpg/220px-Photo-of-Drew-Deezy.jpg" - }, - { - "name": "Driicky Graham", - "image_url": "http://www3.pictures.zimbio.com/gi/2012+BET+Awards+Celebrity+Gifting+Suite+Day+s6iambQit8Kx.jpg" - }, - { - "name": "Droop-E", - "image_url": "http://images.complex.com/complex/image/upload/c_limit,w_680/f_auto,fl_lossy,pg_1,q_auto/yumgqwihf7wyqgrhbsjh.jpg" - }, - { - "name": "Dru Down", - "image_url": "http://s3.amazonaws.com/rapgenius/252244_106167516141612_7339951_n.jpg" - }, - { - "name": "Drumma Boy", - "image_url": "http://www.azquotes.com/public/pictures/authors/b2/29/b229ecbe96e3bf6858a880ad34c9dc21/55ee91fea5d8b_drumma_boy.jpg" - }, - { - "name": "Dumbfoundead", - "image_url": "http://onwardstate.com/wp-content/uploads/2013/12/DFD_15.jpg" - }, - { - "name": "Duncan Mighty", - "image_url": "http://howng.com/wp-content/uploads/2016/09/Duncan-Mighty.png" - }, - { - "name": "Eve", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/5/53/Eve_2011_cropped.jpg" - }, - { - "name": "E-40", - "image_url": "http://www.diablomag.com/March-2016/Rapper-E-40-Malt-Liquor-Sluricane-Hurricane/DM1603_116_DIG800.jpg" - }, - { - "name": "E.D.I. Mean", - "image_url": "https://www.ballerstatus.com/wp-content/uploads/2015/07/edi.jpg" - }, - { - "name": "E-Sens", - "image_url": "https://images.rapgenius.com/9b9cf127a072d14eda839946d33e8b06.500x500x1.jpg" - }, - { - "name": "E.S.G.", - "image_url": "https://vignette4.wikia.nocookie.net/hip-hop-music/images/9/90/E.S.G..jpg/revision/latest" - }, - { - "name": "Earl Sweatshirt", - "image_url": "https://s3.amazonaws.com/rapgenius/1348551787_Earl-Sweatshirt.jpg" - }, - { - "name": "Easy Mo Bee", - "image_url": "https://bomboclap.files.wordpress.com/2012/01/easy-mo-bee2.jpg" - }, - { - "name": "Eazy-E", - "image_url": "http://3.bp.blogspot.com/-OCsagfqI7hc/Ui9BHy4S6xI/AAAAAAAAATk/H1DL94luEHg/s1600/Eazy+E+rapper.jpg" - }, - { - "name": "Ed O.G.", - "image_url": "http://2.bp.blogspot.com/_Wm5H75m6zhE/TL4201QyKgI/AAAAAAAAAm0/UnMY54i171c/s1600/ed.jpg" - }, - { - "name": "Edo Maajka", - "image_url": "http://www.ravnododna.com/wp-content/uploads/2013/06/edo-maajka01.jpg" - }, - { - "name": "El Da Sensei", - "image_url": "http://cdn.ticketfly.com/i/00/02/15/04/45-exl.jpg" - }, - { - "name": "El-P", - "image_url": "https://consequenceofsound.files.wordpress.com/2018/04/the-meadows-2017-ben-kaye-run-the-jewels-7.jpg" - }, - { - "name": "Elephant Man", - "image_url": "http://www.farfrommoscow.com/wp-content/uploads/2009/02/elephant-man.jpg" - }, - { - "name": "Elzhi", - "image_url": "https://images.rapgenius.com/727e2b38afd6daf878861a026a9b748f.1000x667x1.jpg" - }, - { - "name": "Emcee N.I.C.E.", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/9/94/Emcee_N.I.C.E..JPG" - }, - { - "name": "Eminem", - "image_url": "http://www.dopeshxtdaily.com/wp-content/uploads/2018/04/Eminem-Framed-3.jpg" - }, - { - "name": "Eric Biddines", - "image_url": "http://images1.miaminewtimes.com/imager/u/745xauto/9106282/eric-biddines-elliot-liss.jpg" - }, - { - "name": "Erick Arc Elliott", - "image_url": "http://www4.pictures.zimbio.com/gi/Erick+Elliott+Coachella+Valley+Music+Arts+DCw4bFEm-O4l.jpg" - }, - { - "name": "Erick Sermon", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/5a/12/e1/5a12e16dbad591b63da7eef19e59a11d.jpg" - }, - { - "name": "Eric Stanley", - "image_url": "https://i.scdn.co/image/0b3f8ab808d49b6c4b8a7d70a5bedf399a105377" - }, - { - "name": "Esham", - "image_url": "https://media2.fdncms.com/metrotimes/imager/mayor-esham-what/u/slideshow/2229545/esham_press3jpg" - }, - { - "name": "Esoteric", - "image_url": "https://i.ytimg.com/vi/Ekzu8upcHo0/maxresdefault.jpg" - }, - { - "name": "Eve", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/5/53/Eve_2011_cropped.jpg" - }, - { - "name": "Everlast", - "image_url": "http://www.latimes.com/resizer/ON_hgfI8nrImP_htAnzUnEpLUBE=/1400x0/arc-anglerfish-arc2-prod-tronc.s3.amazonaws.com/public/OUCVGT32CRHGVA3VJGMJB2PSC4.jpg" - }, - { - "name": "Evidence", - "image_url": "http://www.brutalmusic.org/wp-content/uploads/2012/02/evidence-rapper.jpg" - }, - { - "name": "Eyedea", - "image_url": "http://www.rapgrid.com/sites/default/files/rapper-photo/eyedea.jpg" - }, - { - "name": "Ghostface Killah", - "image_url": "https://fanart.tv/fanart/music/3b39abeb-0064-4eed-9ddd-ee47a45c54cb/artistbackground/ghostface-killah-5053b6c4a440f.jpg" - }, - { - "name": "Fabolous", - "image_url": "http://www.maybachmedia.com/wp-content/uploads/2018/03/emily-b-and-fabolous.jpg" - }, - { - "name": "Fabri Fibra", - "image_url": "http://static.nanopress.it/nanopress/fotogallery/843X0/80541/fabri-fibra-tatuaggi.jpg" - }, - { - "name": "Fam-Lay", - "image_url": "http://s3.amazonaws.com/rapgenius/1369303498_FamLay.jpg" - }, - { - "name": "Famous Dex", - "image_url": "http://16762-presscdn-0-89.pagely.netdna-cdn.com/wp-content/uploads/2016/09/IMG_6195.jpg" - }, - { - "name": "Fashawn", - "image_url": "http://portlandmetrolive.com/wp-content/uploads/2015/02/Rapper-Fashawn-comes-to-Peter%E2%80%99s-Room.jpg" - }, - { - "name": "Fat Joe", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.1437632.1377553895!/img/httpImage/image.jpg_gen/derivatives/article_970/83094393.jpg" - }, - { - "name": "Fat Pat", - "image_url": "http://s3.amazonaws.com/rapgenius/1363587723_Fat%20Pat%20fatpat.jpg" - }, - { - "name": "Fat Trel", - "image_url": "https://ioneglobalgrind.files.wordpress.com/2013/11/screen-shot-2013-11-07-at-3-48-54-pm.png" - }, - { - "name": "Fatboi", - "image_url": "https://www.sohh.com/wp-content/uploads/Fatboy-SSE.jpg" - }, - { - "name": "Father MC", - "image_url": "http://www.rapartists.com/_files/pictures/full/618_fathermc.jpg" - }, - { - "name": "Fatman Scoop", - "image_url": "http://d.ibtimes.co.uk/en/full/1577170/fatman-scoop.jpg" - }, - { - "name": "Fergie", - "image_url": "http://4everstatic.com/pictures/674xX/people/musicians/fergie,-singer,-music-134929.jpg" - }, - { - "name": "Fetty Wap", - "image_url": "http://image1.redbull.com/rbcom/010/2015-05-07/1331721730967_2/0010/1/1500/1000/2/fetty-wap.jpg" - }, - { - "name": "Fiend", - "image_url": "http://i1169.photobucket.com/albums/r502/ThaFixxDotCom/fiend.jpg" - }, - { - "name": "FLAME", - "image_url": "http://thefrontrowreport.com/wp-content/uploads/2012/08/flame2.jpg" - }, - { - "name": "Flavor Flav", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.1185779.1350499171!/img/httpImage/image.jpg_gen/derivatives/landscape_635/flavor-flav.jpg" - }, - { - "name": "Flavour N'abania", - "image_url": "http://www.naijaolofofo.com/wp-content/uploads/2016/05/flavour.jpg" - }, - { - "name": "Flo Rida", - "image_url": "https://bossip.files.wordpress.com/2014/07/flo-rida.jpg" - }, - { - "name": "Flying Lotus", - "image_url": "http://exclaim.ca/images/flylo10.jpg" - }, - { - "name": "Focus...", - "image_url": "http://cdn5.hiphoplead.com/static/2011/07/focus.jpg" - }, - { - "name": "Fonzworth Bentley", - "image_url": "https://i.ytimg.com/vi/PKT8_mXk1-g/maxresdefault.jpg" - }, - { - "name": "Fort Minor", - "image_url": "http://4.bp.blogspot.com/_VVVnguvXP6s/S8tfaGjCefI/AAAAAAAAA0c/SfnVfFFEgjs/s1600/Fort%2BMinor.jpg" - }, - { - "name": "Foxx", - "image_url": "https://hiphollywood.com/wp-content/uploads/2018/04/946479462.jpg" - }, - { - "name": "Foxy Brown", - "image_url": "http://static.vibe.com/files/2017/03/foxy-brown-endorses-donald-trump-640x476-1488571302-640x476.jpg" - }, - { - "name": "Frank Ocean", - "image_url": "http://static.vibe.com/files/2017/03/frank-ocean-rapper-vibe-1489602237.jpg" - }, - { - "name": "Frankie J", - "image_url": "http://static.djbooth.net/pics-artist/frankiej.jpg" - }, - { - "name": "Frayser Boy", - "image_url": "http://trapsntrunks.com/wp-content/uploads/2017/07/motives-672x672.jpg" - }, - { - "name": "Freak Nasty", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/827/MI0003827380.jpg" - }, - { - "name": "Freaky Tah", - "image_url": "http://upload.wikimedia.org/wikipedia/en/9/9b/Freaky_Tah.jpg" - }, - { - "name": "Fred Durst", - "image_url": "http://trendliest.files.wordpress.com/2008/07/durst-fred-photo-xl-fred-durst-6209268.jpg" - }, - { - "name": "Freddie Foxxx", - "image_url": "http://grandgood.com/wordpress/wp-content/uploads/2008/06/freddie-foxxx.jpg" - }, - { - "name": "Freddie Gibbs", - "image_url": "http://www.networth2013.com/wp-content/uploads/2013/07/Frddie+Gibbs+VIP+Area+Governors+Ball+Day+3+p6VBOlpeThel.jpg" - }, - { - "name": "Fredo Santana", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2014/11/fredosantana.jpg" - }, - { - "name": "Fredro Starr", - "image_url": "http://ambrosiaforheads.com/wp-content/uploads/2014/02/Fredro-Starr-Sticky-Fingaz-610x400.jpg" - }, - { - "name": "Fredwreck", - "image_url": "http://www.beatmakingvideos.com/sites/default/files/producer_foto/fredwreck.jpg" - }, - { - "name": "Free", - "image_url": "http://www.whosampled.com/static/artist_images_200/lr2929_2010525_114148297125.jpg" - }, - { - "name": "Freekey Zekey", - "image_url": "https://1.bp.blogspot.com/-TSFhXxYHSZI/V8kVNFnN7TI/AAAAAAAAmJ8/GiIVH63gXwsxVIbWGZlYeVep9JhsPYgHQCK4B/w1200-h630-p-k-nu/freekey-zekey-net-worth.jpg" - }, - { - "name": "Freeway", - "image_url": "http://www.5pillarz.com/wp-content/uploads/2014/10/freeway1.jpg" - }, - { - "name": "French Montana", - "image_url": "https://lasentinel.net/wp-content/uploads/sites/5/2016/06/ENT-its-a-rap-french-montana2.jpg" - }, - { - "name": "Frenkie", - "image_url": "http://cps-static.rovicorp.com/3/JPG_500/MI0003/596/MI0003596298.jpg" - }, - { - "name": "Fresh Kid Ice", - "image_url": "https://zayzay.com/wp-content/uploads/2017/07/Rapper-Fresh-Kid-Ice-Of-%E2%80%982-Live-Crew%E2%80%99-Dead-At-53.jpg" - }, - { - "name": "Froggy Fresh", - "image_url": "http://i.ytimg.com/vi/4feIwig2AtA/0.jpg" - }, - { - "name": "Frost", - "image_url": "http://askkissy.com/wp-content/uploads/2015/09/EAZY-E-RAPPER-FROST.jpeg" - }, - { - "name": "Full Blooded", - "image_url": "https://i.skyrock.net/5301/5285301/pics/235349117_small.jpg" - }, - { - "name": "Funkmaster Flex", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2014/04/6a00d8341c4fe353ef01156ff9e8f1970c-800wi.jpg" - }, - { - "name": "Future", - "image_url": "http://www.thefamouspeople.com/profiles/images/future-1.jpg" - }, - { - "name": "Grandmaster Caz", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Grandmastercaz.jpg/1200px-Grandmastercaz.jpg" - }, - { - "name": "Grandmaster Flash", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/34/60/41/346041acd20fbe40c44b97a91c9a2a80.jpg" - }, - { - "name": "G-Dragon", - "image_url": "http://www.soompi.com/wp-content/uploads/a/t/7m/356158/356158.jpg" - }, - { - "name": "G-Eazy", - "image_url": "http://i.dailymail.co.uk/i/newpix/2018/04/14/16/4B18913E00000578-5615765-Cute_couple_The_New_Jersey_native_is_dating_rapper_G_Eazy_with_t-a-5_1523718500475.jpg" - }, - { - "name": "G. Dep", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.473448.1314633586!/img/httpImage/image.jpg_gen/derivatives/article_970/alg-g-dep-portrait-jpg.jpg" - }, - { - "name": "G Herbo", - "image_url": "http://www.datwav.com/wp-content/uploads/2017/04/G_Herbo_In_Studio-1024x683.jpg" - }, - { - "name": "Gaeko", - "image_url": "http://www.weekendnotes.co.uk/im/002/07/geko-rapper-tour-birmingham-academy-21.jpg" - }, - { - "name": "The Game", - "image_url": "http://images2.fanpop.com/images/photos/3600000/The-Game-the-game-rapper-3618562-1024-768.jpg" - }, - { - "name": "Gang Starr", - "image_url": "http://www.billboard.com/files/styles/promo_650/public/stylus/106631-Gangstarr-guru-617_409.jpg" - }, - { - "name": "Gangsta Blac", - "image_url": "http://purple-drank.com/wp-content/uploads/2011/06/Gangsta-Blac-Return-Of-The-Gangsta.jpg" - }, - { - "name": "Gangsta Boo", - "image_url": "http://live.drjays.com/wp-content/uploads/2009/12/43439_lg.jpg" - }, - { - "name": "Ganksta N-I-P", - "image_url": "http://www.ugs4life.com/wp-content/uploads/2014/06/ganxsta-nip.png" - }, - { - "name": "Gary", - "image_url": "http://media-cache-ec0.pinimg.com/736x/1e/52/6c/1e526ce56e4e7a0d8ce5b588faa49102.jpg" - }, - { - "name": "Gee Money", - "image_url": "https://amonpointtv.com/wp-content/uploads/2017/09/ABCD1505228816.jpg" - }, - { - "name": "General Woo", - "image_url": "https://natasavajagic.files.wordpress.com/2008/11/dsc044672.jpg" - }, - { - "name": "Ghostface Killah", - "image_url": "https://fanart.tv/fanart/music/3b39abeb-0064-4eed-9ddd-ee47a45c54cb/artistbackground/ghostface-killah-5053b6c4a440f.jpg" - }, - { - "name": "Giggs", - "image_url": "http://4.bp.blogspot.com/-NLMOcLtTIgs/Tbidhpi_LpI/AAAAAAAAANY/4gL5Wzj4f1s/s1600/mytypeofhype_giggs_rapper-thumb.jpg" - }, - { - "name": "Gilbere Forte", - "image_url": "http://famousfamilybirthdaysbiofacts.com/BirthDayPersonality/Gilbere-Forte-Rapper-birhday-image.jpg" - }, - { - "name": "Glasses Malone", - "image_url": "http://siccness.net/wp/wp-content/uploads/2012/11/glasses-malone.jpg" - }, - { - "name": "GLC", - "image_url": "https://d.ibtimes.co.uk/en/full/1396199/mike-glc.jpg" - }, - { - "name": "Goldie Loc", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/88/03/67/8803675d722cc75dd4eb071ea1b9809d.jpg" - }, - { - "name": "GoldLink", - "image_url": "http://unbiasedwriter.com/wp-content/uploads/2014/10/GoldLink-rapper.jpg" - }, - { - "name": "Gorilla Zoe", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Gorilla_zoe_picture.jpg/1200px-Gorilla_zoe_picture.jpg" - }, - { - "name": "Grafh", - "image_url": "http://static.djbooth.net/pics-artist/grafh.jpg" - }, - { - "name": "Grand Puba", - "image_url": "http://images.complex.com/complex/image/upload/t_article_image/akb1hkm8uzgscqwucuan.jpg" - }, - { - "name": "Grandmaster Caz", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Grandmastercaz.jpg/1200px-Grandmastercaz.jpg" - }, - { - "name": "Grandmaster Flash", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/34/60/41/346041acd20fbe40c44b97a91c9a2a80.jpg" - }, - { - "name": "Greydon Square", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/2/2e/TAM_6_-_Greydon_Square.jpg" - }, - { - "name": "Grieves", - "image_url": "http://thissongissick.com/blog/wp-content/uploads/2011/05/Grieves-Rapper-Bloody-Poetry.jpg" - }, - { - "name": "Gucci Mane", - "image_url": "https://www.amlu.com/wp-content/uploads/2018/04/rapper-gucci-mane-skips-three-year-waiting-list-and-gets-the-first-ferrari-812-superfast1.jpg" - }, - { - "name": "Gudda Gudda", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-gudda-gudda-singer-celebrity-rap.jpg" - }, - { - "name": "Guerilla Black", - "image_url": "http://i1-news.softpedia-static.com/images/news2/Rapper-Guerilla-Black-Arrested-for-Buying-and-Using-Stolen-Payment-Card-Details-2.jpg" - }, - { - "name": "Guilty Simpson", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-guilty-simpson-star-celebrity-rap.jpg" - }, - { - "name": "Gunplay", - "image_url": "http://www.passionweiss.com/wp-content/uploads/2015/07/gunplay-press2.jpg" - }, - { - "name": "Guru", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/7/72/Guru_%28rapper%29.jpg" - }, - { - "name": "GZA", - "image_url": "http://api.ning.com/files/Cr*mNN-mhgZVTnMO1Ax5ew2lFizHAFllT8mDVu1iYmXBixU5MNOvS6DzmIs82Yuhin8A2u0Hpk48uLC2goXSr74xqMAwQK5C/GZAMetro.jpg" - }, - { - "name": "Half a Mill", - "image_url": "http://www.kingsizemagazine.se/wp-content/uploads/2013/10/half-a-mill-S.jpg" - }, - { - "name": "Hard Kaur", - "image_url": "http://www.prokerala.com/news/photos/imgs/800/singer-rapper-hard-kaur-during-an-interview-at-440863.jpg" - }, - { - "name": "Hasan Salaam", - "image_url": "http://api.ning.com/files/2VGrzzXedu3*LX5VRoIFBZeWe8qqGJjpAxzb0ZR9giaHZEvLo8d8B7mpIcLrLmH5gmcJt8aUpyPHr2aVLVFxrlylSJByg*eO/HasanSalaam.jpg" - }, - { - "name": "Havoc", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2012/01/Prodigy.jpg" - }, - { - "name": "Heavy D", - "image_url": "http://www.guttaworld.com/wp-content/uploads/2011/12/obit-heavy-d.jpg" - }, - { - "name": "Hefe Heetroc", - "image_url": "https://1.bp.blogspot.com/-nmJ_Z8xC9Wg/V_GjdMvY_fI/AAAAAAAAAyQ/9hfGLMDGemgyOsjY5mM3XW9ul-3WCpGnQCLcB/s640/20160223_1125571.jpg" - }, - { - "name": "Heize", - "image_url": "http://cdn.koreaboo.com/wp-content/uploads/2016/12/heize.jpg" - }, - { - "name": "Hemlock Ernst", - "image_url": "http://static.stereogum.com/uploads/2015/09/rappingfutureislands1.png" - }, - { - "name": "Hi-C", - "image_url": "http://unitedgangs.files.wordpress.com/2013/11/hic_feb2005a.jpg" - }, - { - "name": "Hi-Tek", - "image_url": "http://thamidwest.com/wp-content/uploads/Hi-Tek.jpg" - }, - { - "name": "Hit-Boy", - "image_url": "http://3.bp.blogspot.com/-pzifRQd37EQ/TwaMSQmoOfI/AAAAAAAAA7s/YUaDMqmATM4/s1600/whiterapper.jpg" - }, - { - "name": "Hittman", - "image_url": "http://s3.amazonaws.com/hiphopdx-production/2014/11/Hittman_11-13-2014.jpg" - }, - { - "name": "Hodgy Beats", - "image_url": "http://www.rapburger.com/wp-content/uploads/2013/07/hodgy-beats-Godsss-free-download-1024x682.jpg" - }, - { - "name": "Honey Cocaine", - "image_url": "http://swaggarightentertainment.com/wp-content/uploads/2014/10/rapper-honey-cocaine.jpeg" - }, - { - "name": "Hoodie Allen", - "image_url": "http://conversationsabouther.net/wp-content/uploads/2014/10/Hoodie-Allen.jpg" - }, - { - "name": "Hopsin", - "image_url": "http://www.stasheverything.com/wp-content/uploads/2012/10/Hopsin-banner.jpg" - }, - { - "name": "Hot Dollar", - "image_url": "http://www.aceshowbiz.com/images/news/00010456.jpg" - }, - { - "name": "Huey", - "image_url": "http://images5.fanpop.com/image/photos/30200000/Huey-huey-rapper-30242374-1024-768.jpg" - }, - { - "name": "Hurricane Chris", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/c/c8/Hurricane_Chris.jpg" - }, - { - "name": "Hurricane G", - "image_url": "https://i.ytimg.com/vi/3pTO0lsjzco/maxresdefault.jpg" - }, - { - "name": "Hush", - "image_url": "https://www.gannett-cdn.com/-mm-/a691de1d2241d7baf1c60d5d31346451d4cd3669/c=0-236-1360-1004&r=x633&c=1200x630/local/-/media/2015/06/22/DetroitFreePress/DetroitFreePress/635705966302572023-Hush.jpg" - }, - { - "name": "Hussein Fatal", - "image_url": "https://pmchollywoodlife.files.wordpress.com/2015/07/hussein-fatal-rapper-dies-at-38-car-accident-lead.jpg" - }, - { - "name": "Ice Cube", - "image_url": "http://www.ultimatemovierankings.com/wp-content/uploads/2016/04/ice-cube-11111.jpg" - }, - { - "name": "I-20", - "image_url": "http://1.bp.blogspot.com/_IXe2z8hItAg/TBxH_T19eQI/AAAAAAAABX0/dFXw4HYqdqI/s1600/i20_self.jpg" - }, - { - "name": "Iamsu!", - "image_url": "http://media.gettyimages.com/photos/rapper-iamsu-arrives-at-ditch-fridays-at-palms-pool-dayclub-on-may-13-picture-id531300532" - }, - { - "name": "Ice Cube", - "image_url": "http://www.ultimatemovierankings.com/wp-content/uploads/2016/04/ice-cube-11111.jpg" - }, - { - "name": "Ice-T", - "image_url": "http://www.ireport.cz/images/ireport/clanky/Ice_T/ice-t.jpg" - }, - { - "name": "IDK", - "image_url": "https://static.highsnobiety.com/wp-content/uploads/2017/07/27163702/jay-idk-idk-interview-01-480x320.jpg" - }, - { - "name": "Iggy Azalea", - "image_url": "http://www.maybachmedia.com/wp-content/uploads/2018/04/Iggy-Azalea-Tyga.jpg" - }, - { - "name": "IHeartMemphis", - "image_url": "https://memphisrap.com/mr-uploads/2015/12/iLoveMemphis-rapper.jpg" - }, - { - "name": "Ill Bill", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Ill_Bill.jpg/1200px-Ill_Bill.jpg" - }, - { - "name": "Illmind", - "image_url": "http://media.charged.fm/media/file_5429de95c0e71.jpg" - }, - { - "name": "ILoveMakonnen", - "image_url": "http://s3-ak.buzzfeed.com/static/2014-08/13/14/enhanced/webdr11/enhanced-8028-1407955933-1.jpg" - }, - { - "name": "Immortal Technique", - "image_url": "http://www.digitaljournal.com/img/7/5/0/1/1/0/i/1/5/5/o/3534639765_39c888714b_b.jpg" - }, - { - "name": "Imran Khan", - "image_url": "http://bollyspice.com/wp-content/uploads/2014/12/14dec_Imran-Khan-singer.jpg" - }, - { - "name": "Indo G", - "image_url": "http://purple-drank.com/wp-content/uploads/2013/03/Indo-G-New.jpg" - }, - { - "name": "Inspectah Deck", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-inspectah-deck-celebrity-jacket-photo.jpg" - }, - { - "name": "Isaiah Rashad", - "image_url": "http://okp-cdn.okayplayer.com/wp-content/uploads/2014/06/xxl-freshman-2014-cypher-drama-cannon-lead.jpg" - }, - { - "name": "Iyanya", - "image_url": "https://4.bp.blogspot.com/-GeL-JX7MH2o/V4NryaxgfEI/AAAAAAAAIuw/KrtCr4Tp3cskgDG7QHHJ6M-Gvaz7a5AogCLcB/w1200-h630-p-k-no-nu/Iyanya.JPG" - }, - { - "name": "Iyaz", - "image_url": "http://muzicjunkies.com/wp-content/uploads/2014/10/slim2.jpg" - }, - { - "name": "Jay-Z", - "image_url": "http://th07.deviantart.net/fs70/PRE/f/2011/042/6/d/rapper___jay_z_by_rwpike-d39aevp.jpg" - }, - { - "name": "Kanye West", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.3936461.1523887222!/img/httpImage/image.jpg_gen/derivatives/article_750/604289829cc00020-kanye-west.jpg" - }, - { - "name": "Kendrick Lamar", - "image_url": "https://stupiddope.com/wp-content/uploads/2018/04/kendrick-lamar-damn-2018-pulitzer-prize-first-rapper-music.jpg" - }, - { - "name": "KRS-One", - "image_url": "http://ifihavent.files.wordpress.com/2007/07/krs_blaze02991.jpg" - }, - { - "name": "J Dilla", - "image_url": "http://www.stonesthrow.com/images/2012/DILLA_2.jpg" - }, - { - "name": "J-Diggs", - "image_url": "http://www.sierrasun.com/wp-content/uploads/2016/09/JDiggs-SSU-011415-1-244x325.jpg" - }, - { - "name": "J-Kwon", - "image_url": "http://hiphop-n-more.com/wp-content/uploads/2010/03/j-kwon-s02.jpg" - }, - { - "name": "J-Son", - "image_url": "https://i.ytimg.com/vi/SjV2lPTUB2o/maxresdefault.jpg" - }, - { - "name": "J. Cole", - "image_url": "http://www.rap-up.com/app/uploads/2018/04/j-cole-kod-cover.jpg" - }, - { - "name": "J. Stalin", - "image_url": "http://siccness.net/wp/wp-content/uploads/2015/10/stalin.jpg" - }, - { - "name": "J. Valentine", - "image_url": "http://cache2.asset-cache.net/gc/56850135-rapper-ll-cool-j-delivers-valentines-day-gettyimages.jpg" - }, - { - "name": "J.I.D", - "image_url": "http://www.musicfesttv.com/wp-content/uploads/2017/02/J.-Cole-Sign039s-Atlanta-Rapper-J.I.D.-To-Dreamville-1200x600.png" - }, - { - "name": "J.R. Rotem", - "image_url": "http://m2.paperblog.com/i/56/567622/j-r-rotem-beluga-heights-artist-of-the-week-L-9apUCg.jpeg" - }, - { - "name": "J.R. Writer", - "image_url": "http://static.djbooth.net/pics-artist/jrwriter.jpg" - }, - { - "name": "Ja Rule", - "image_url": "http://fanart.tv/fanart/music/b504f625-4ef6-4a5a-81e8-870a61e8dc9c/artistbackground/ja-rule-503dd1b16fcfa.jpg" - }, - { - "name": "Jack Parow", - "image_url": "http://sunelia89.files.wordpress.com/2012/11/parow_duck-manfred-werner-hr.jpg" - }, - { - "name": "The Jacka", - "image_url": "http://static.stereogum.com/uploads/2015/02/The-Jacka.jpg" - }, - { - "name": "Jackie Hill-Perry", - "image_url": "http://media.washtimes.com.s3.amazonaws.com/media/image/2014/10/27/10272014_jackie-3-color8201.jpg" - }, - { - "name": "Jadakiss", - "image_url": "https://hhvibe.files.wordpress.com/2010/02/jadakiss.jpg" - }, - { - "name": "Jaden Smith", - "image_url": "http://i.dailymail.co.uk/i/pix/2013/02/28/article-2285761-1856EF78000005DC-756_634x493.jpg" - }, - { - "name": "Jae Millz", - "image_url": "http://theboombox.com/files/2010/05/jae-millz-200ak051910.jpg" - }, - { - "name": "Jahlil Beats", - "image_url": "http://www3.pictures.zimbio.com/gi/Jahlil+Beats+BET+Hip+Hop+Awards+2012+Red+Carpet+8GcDsHs2IfRl.jpg" - }, - { - "name": "Jamie Madrox", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Jamie_Madrox_at_the_Abominationz_Tour.JPG/1200px-Jamie_Madrox_at_the_Abominationz_Tour.JPG" - }, - { - "name": "Jahred", - "image_url": "http://www.sportsgraphs.com/1314rampage4.jpg" - }, - { - "name": "Jake Miller", - "image_url": "http://thetriangle.org/wp-content/uploads/2013/11/Jake-Miller_Edgar-Estevez_WEB.jpg" - }, - { - "name": "Jake One", - "image_url": "http://www.nodfactor.com/wp-content/uploads/2013/11/Jake-One-Acid-Rain.png" - }, - { - "name": "Jam Master Jay", - "image_url": "http://ww3.hdnux.com/photos/10/27/41/2193534/5/920x920.jpg" - }, - { - "name": "Jamal", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.421164.1314528333!/img/httpImage/image.jpg_gen/derivatives/article_970/amd-jamal-woolard-jpg.jpg" - }, - { - "name": "Jamal Woolard", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.421164.1314528333!/img/httpImage/image.jpg_gen/derivatives/article_970/amd-jamal-woolard-jpg.jpg" - }, - { - "name": "Jamie Foxx", - "image_url": "https://hiphollywood.com/wp-content/uploads/2018/04/946479462.jpg" - }, - { - "name": "Jarren Benton", - "image_url": "https://ioneglobalgrind.files.wordpress.com/2015/02/photo-credit-funk-volume-extralarge_1408660385558.jpg" - }, - { - "name": "Jay Burna", - "image_url": "http://jamsphere.com/wp-content/uploads/2014/12/jay-burna-300.jpg" - }, - { - "name": "Jay Critch", - "image_url": "https://cdn.spinrilla.com/users/11645663/original/46d961baf0.jpg" - }, - { - "name": "Jay Electronica", - "image_url": "http://i.dailymail.co.uk/i/pix/2012/06/09/article-2156691-13845E1F000005DC-865_634x809.jpg" - }, - { - "name": "Jay Park", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/76/2a/65/762a65ec5adc664e7e7edc3c7f3ce526.jpg" - }, - { - "name": "Jay Rock", - "image_url": "http://rapsandhustles.com/wp-content/uploads/2012/04/jayrock.jpeg" - }, - { - "name": "Jay Z", - "image_url": "http://www.streetgangs.com/wp-content/uploads/2010/06/jay-z.jpg" - }, - { - "name": "Jayo Felony", - "image_url": "http://unitedgangs.files.wordpress.com/2013/12/jayo_felony.jpg" - }, - { - "name": "Jaz-O", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/1/1c/Jaz-O--mika.jpg" - }, - { - "name": "Jazz Cartier", - "image_url": "http://respect-mag.com/wp-content/uploads/2018/04/1C6378F4-D2A9-4F48-93E8-AA47A154E54E.jpeg" - }, - { - "name": "Jazze Pha", - "image_url": "http://media.gettyimages.com/photos/music-producer-jazze-pha-rapper-heavy-d-and-dj-toomp-attend-tis-akoo-picture-id127898169" - }, - { - "name": "Jean Grae", - "image_url": "http://www.jayforce.com/wp-content/uploads/2011/03/jeangreen.jpg" - }, - { - "name": "Jeremiah Jae", - "image_url": "http://www.brooklynvegan.com/img/as/jeremiah-jae.jpg" - }, - { - "name": "Jeremih", - "image_url": "http://i2.wp.com/therighthairstyles.com/wp-content/uploads/2013/12/jeremih.jpg" - }, - { - "name": "Jermaine Dupri", - "image_url": "http://media.gettyimages.com/photos/rapper-jermaine-dupri-poses-for-photos-at-the-swissotel-in-chicago-picture-id145014316" - }, - { - "name": "Jeru the Damaja", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c9/Out4Fame-Festival_2016_-_Jeru_the_Damaja.JPG/1200px-Out4Fame-Festival_2016_-_Jeru_the_Damaja.JPG" - }, - { - "name": "Jewell", - "image_url": "http://www.celebpronto.com/wp-content/uploads/2010/08/jewel11.jpg" - }, - { - "name": "Jibbs", - "image_url": "http://rapdirt.com/images/misc/Jibbs_0306f.jpg" - }, - { - "name": "Jim Jones", - "image_url": "http://www.networth2013.com/wp-content/uploads/2013/08/Jim-Jones-Rapper.jpg" - }, - { - "name": "Jim Jonsin", - "image_url": "http://www.networth2013.com/wp-content/uploads/2013/08/Jim-Jones-Rapper.jpg" - }, - { - "name": "Jipsta", - "image_url": "http://getoutmag.com/wp-content/uploads/2011/10/jipsta.jpg" - }, - { - "name": "Jme", - "image_url": "http://conversationsabouther.net/wp-content/uploads/2015/09/Jme.jpg" - }, - { - "name": "Joe Budden", - "image_url": "http://dailyentertainmentnews.com/wpgo/wp-content/uploads/2014/08/rapper-joe-budden-girlfriend-Audely-Robles.jpg" - }, - { - "name": "Joell Ortiz", - "image_url": "https://movesmusic.files.wordpress.com/2015/01/joell.jpg" - }, - { - "name": "Joey Badass", - "image_url": "https://www.thestar.com/content/dam/thestar/news/gta/2017/08/24/rapper-joey-bada-cancels-toronto-show-after-staring-at-eclipse/joey-badass.jpg.size.custom.crop.1086x724.jpg" - }, - { - "name": "John Cena", - "image_url": "http://www.picshunger.com/wp-content/uploads/2014/04/Rap.jpg" - }, - { - "name": "Johnny J", - "image_url": "http://media.rapnews.net/ArtistPics/JohnnyJ_rnn.jpg" - }, - { - "name": "Johntá Austin", - "image_url": "http://www.rap-up.com/app/uploads/2010/05/johnta-austin.jpg" - }, - { - "name": "Joji Miller", - "image_url": "http://pre11.deviantart.net/2640/th/pre/i/2016/122/1/8/joji_miller__filthy_frank__by_shuploc-da122fv.jpg" - }, - { - "name": "Jon Connor", - "image_url": "http://s3.amazonaws.com/rapgenius/jonconnor.png" - }, - { - "name": "Joyner Lucas", - "image_url": "https://www.sohh.com/wp-content/uploads/Joyner-Lucas-1.png" - }, - { - "name": "JT Money", - "image_url": "http://purple-drank.com/wp-content/uploads/2013/06/JT-Money-New.jpg" - }, - { - "name": "JT the Bigga Figga", - "image_url": "http://a1yola.com/wp-content/uploads/2010/10/JT-The-Bigga-Figga-Dwellin-In-Tha-Labb1-e1299005670778.jpg" - }, - { - "name": "Juelz Santana", - "image_url": "http://ll-media.tmz.com/2016/12/12/1212-juelz-santana-instagram-3.jpg" - }, - { - "name": "Juice (Đus)", - "image_url": "http://images.genius.com/37b135c1e081633b01d3b09bf4e785ed.600x600x1.png" - }, - { - "name": "Juicy J", - "image_url": "http://www.beyondblackwhite.com/wp-content/uploads/2014/01/Juicy-j-cup-1.png" - }, - { - "name": "Junhyung", - "image_url": "http://stuffpoint.com/kpopshineecnbluesujubapexoetc/image/378408-kpopshineecnbluesujub-a-pexoetc-rapper-junhyung.jpg" - }, - { - "name": "Jus Allah", - "image_url": "http://farm5.staticflickr.com/4014/5169574228_84ff1a04f4_z.jpg" - }, - { - "name": "Just Ice", - "image_url": "https://images.genius.com/608c268dba94e441e3f19c1e46207413.879x876x1.jpg" - }, - { - "name": "Juvenile", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/club937.com/files/2012/08/56688577-630x418.jpg" - }, - { - "name": "Kurtis Blow", - "image_url": "http://is4.mzstatic.com/image/thumb/Music/v4/69/fd/9f/69fd9f31-d152-c8ba-57be-80308d6b5d0c/source/1200x1200sr.jpg" - }, - { - "name": "Lauryn Hill", - "image_url": "http://images.musictimes.com/data/images/full/75871/lauryn-hill-tour.jpg" - }, - { - "name": "K Camp", - "image_url": "http://www2.pictures.zimbio.com/gi/K+Camp+American+Authors+Visit+Music+Choice+Furxjl-IhJ7l.jpg" - }, - { - "name": "K'naan", - "image_url": "http://images2.fanpop.com/image/photos/13600000/Stock-Knaan-on-twitter-knaan-club-13681875-483-570.jpg" - }, - { - "name": "K-Dee", - "image_url": "https://i.ytimg.com/vi/ovfrEfeqjf0/hqdefault.jpg" - }, - { - "name": "K-OS", - "image_url": "http://torontorappers.com/newsite/wp-content/uploads/2016/08/k-os-rapper.jpg" - }, - { - "name": "K-Solo", - "image_url": "http://freshnewsbysteph.com/wp-content/uploads/2011/07/k-solo.jpg" - }, - { - "name": "K.E. on the Track", - "image_url": "https://akpopworld.files.wordpress.com/2015/08/sik-k.jpg" - }, - { - "name": "K7", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0001/792/MI0001792148.jpg" - }, - { - "name": "Kafani", - "image_url": "http://gossip-grind.com/wp-content/uploads/2014/09/image18.jpg" - }, - { - "name": "Kam", - "image_url": "http://3.bp.blogspot.com/_qqc1V4I4JkY/RiKG8DfqsLI/AAAAAAAABmU/e7zmHRlRL9M/kam2.jpg" - }, - { - "name": "Kangol Kid", - "image_url": "http://cache4.asset-cache.net/gc/141797166-rapper-kangol-kid-attends-the-back-to-the-gettyimages.jpg" - }, - { - "name": "Kanye West", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.3936461.1523887222!/img/httpImage/image.jpg_gen/derivatives/article_750/604289829cc00020-kanye-west.jpg" - }, - { - "name": "Kap G", - "image_url": "http://remezcla.com/wp-content/uploads/2016/04/kap-g-2016-e1466531632221.jpg" - }, - { - "name": "Kardinal Offishall", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-kardinal-offishall-fame-rap-singer.jpg" - }, - { - "name": "Kastro", - "image_url": "https://i.ytimg.com/vi/tV66DD_rxNU/maxresdefault.jpg" - }, - { - "name": "Kat Dahlia", - "image_url": "http://static.djbooth.net/pics-artist/katdahlia.jpg" - }, - { - "name": "Katie Got Bandz", - "image_url": "http://moodswingmgmt.com/wp-content/uploads/2013/10/Katie_Main.jpg" - }, - { - "name": "KB", - "image_url": "http://www.rapzilla.com/rz/images/kbillboard.jpg" - }, - { - "name": "Keak da Sneak", - "image_url": "http://theboombox.com/files/2017/01/Keak-Da-Sneak-Shot.jpg" - }, - { - "name": "Keith Ape", - "image_url": "http://conversationsabouther.net/wp-content/uploads/2016/08/Keith-Ape.jpg" - }, - { - "name": "Keith Murray", - "image_url": "http://mrdaveyd.files.wordpress.com/2010/10/keith-murray.jpg" - }, - { - "name": "Malcolm David Kelley", - "image_url": "http://cdn.cnwimg.com/searchThumb/wp-content/uploads/2014/09/Malcolm-David-Kelley.jpg" - }, - { - "name": "Kendrick Lamar", - "image_url": "https://stupiddope.com/wp-content/uploads/2018/04/kendrick-lamar-damn-2018-pulitzer-prize-first-rapper-music.jpg" - }, - { - "name": "Kent Jones", - "image_url": "http://static.vibe.com/files/2015/12/kent-jones-binishPR.jpg" - }, - { - "name": "Kerser", - "image_url": "http://dailyurbanculture.com/wp-content/uploads/2014/07/3.13.jpg" - }, - { - "name": "Kevin Abstract", - "image_url": "http://images.greenlabel.com/assets/2015/09/kevin-abstract-2.jpg" - }, - { - "name": "Kevin Gates", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/73/db/e4/73dbe4857b26434fdfaab15b98a622de.jpg" - }, - { - "name": "Kevin McCall", - "image_url": "http://resources3.news.com.au/images/2013/04/29/1226631/457575-kevin-mccall.jpg" - }, - { - "name": "Khia", - "image_url": "http://planetill.com/wp-content/uploads/2012/01/Khia.jpg" - }, - { - "name": "Khleo", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/2b/fb/f7/2bfbf784203a8fbd7a032c6eb77cbaef.jpg" - }, - { - "name": "Kia Shine", - "image_url": "http://i1.ytimg.com/vi/8OZx1vK4PNE/maxresdefault.jpg" - }, - { - "name": "Kid Capri", - "image_url": "https://s3.amazonaws.com/battlerap-production/2014/08/rsz_caprirsz.jpg" - }, - { - "name": "Kid Cudi", - "image_url": "http://www.stasheverything.com/wp-content/uploads/2013/04/Kid-Cudi-pic.jpg" - }, - { - "name": "Kid Frost", - "image_url": "http://media-cache-ak0.pinimg.com/736x/8f/36/61/8f3661da395898b46c6c3c84ce4ecef1.jpg" - }, - { - "name": "Kid Ink", - "image_url": "http://chekadigital.co.za/wp-content/uploads/2013/03/kid-ink.jpg" - }, - { - "name": "Kid Rock", - "image_url": "http://www.feelnumb.com/wp-content/uploads/2011/02/gallery0219.jpg" - }, - { - "name": "Kid Sister", - "image_url": "http://s3.amazonaws.com/rapgenius/jpg_Kid_Sister__MG_2600copy2.jpg" - }, - { - "name": "Kidd Kidd", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.2284876.1436323996!/img/httpImage/image.jpg_gen/derivatives/article_750/webconfitems78f-1-web.jpg" - }, - { - "name": "Killah Priest", - "image_url": "http://media-cache-ak0.pinimg.com/736x/ce/9a/5c/ce9a5c2abde75b828adae9b87dc8e020.jpg" - }, - { - "name": "Killer Mike", - "image_url": "http://www.findnews.co.uk/wp-content/uploads/2018/03/killer-mike-rapper-defends-gun-ownership-in-nra-video.jpg" - }, - { - "name": "Kilo Ali", - "image_url": "http://straightfromthea.com/wp-content/uploads/2014/09/KiloAli.jpg" - }, - { - "name": "King Chip", - "image_url": "http://jasperdowney.files.wordpress.com/2013/09/king-chip.jpg" - }, - { - "name": "King Gordy", - "image_url": "https://s3.amazonaws.com/rapgenius/King%20Gordy%2051.jpg" - }, - { - "name": "King L", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/theboombox.com/files/2012/11/king-l-456-11212.jpg" - }, - { - "name": "King Tee", - "image_url": "http://siccness.net/wp/wp-content/uploads/2015/03/king-t.jpeg" - }, - { - "name": "Kirk Knight", - "image_url": "https://s3.amazonaws.com/rapgenius/dsc_0086.jpg" - }, - { - "name": "Kirko Bangz", - "image_url": "http://api.ning.com/files/SVfVaVsl8W2HxUovnHG5eBzdyEzEx0OOHA5U4cWbuT8ShSldd7ZKPKOi2RnF*LScvMps4AXVqwGVzjyDRdPbhW**MOzSt8V3/151263619.jpg" - }, - { - "name": "Kitty", - "image_url": "http://www.mxdwn.com/wp-content/uploads/2014/06/kitty-pryde2-580x386.jpg" - }, - { - "name": "KJ-52", - "image_url": "http://www.vegasnews.com/wp-content/uploads/KJ-52-570.jpg" - }, - { - "name": "Knero", - "image_url": "https://upload.wikimedia.org/wikipedia/en/a/a8/Knero_performing_during_the_Liberian_Independent_Celebration.jpg" - }, - { - "name": "Knoc-turn'al", - "image_url": "http://images.artistdirect.com/Images/artd/amg/music/bio/1640940_knocturnal_200x200.jpg" - }, - { - "name": "KO", - "image_url": "http://hbr.co.ke/wp-content/uploads/2015/09/K.O-CARACARA-RAPPER-HIP-HOP-MUSIC.jpg" - }, - { - "name": "KOHH", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/6a/1a/12/6a1a128849b04dfde612195054e9568d.jpg" - }, - { - "name": "Kodak Black", - "image_url": "https://media.nbcnewyork.com/images/1200*675/kodakblackout1.jpg" - }, - { - "name": "Kokane", - "image_url": "http://images1.laweekly.com/imager/kokane/u/original/5095855/kokane.jpg" - }, - { - "name": "Kool A.D", - "image_url": "http://s3.amazonaws.com/rapgenius/1362185468_Kool-AD-Okayplayer-interview2.jpg" - }, - { - "name": "Kool G Rap", - "image_url": "http://www.howtorapbook.com/wp-content/uploads/2015/07/040511-music-kool-g-rap.png" - }, - { - "name": "Kool Keith", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-kool-keith-hip-hop-celebrity-star.jpg" - }, - { - "name": "Kool Moe Dee", - "image_url": "https://s3.amazonaws.com/rapgenius/kool_moe_dee.jpg" - }, - { - "name": "Koolade", - "image_url": "http://media-cache-ak0.pinimg.com/736x/48/d5/96/48d596e5d2914e055459440cd92cd802.jpg" - }, - { - "name": "Krayzie Bone", - "image_url": "http://articlebio.com/uploads/bio/2016/03/23/krayzie-bone.jpg" - }, - { - "name": "Kreayshawn", - "image_url": "http://thesuperslice.com/wp-content/uploads/2011/05/Kreayshawn-03.jpg" - }, - { - "name": "Krizz Kaliko", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-krizz-kaliko-hip-hop-star-celebrity.jpg" - }, - { - "name": "KRS-One", - "image_url": "http://ifihavent.files.wordpress.com/2007/07/krs_blaze02991.jpg" - }, - { - "name": "Kung Fu Vampire", - "image_url": "https://kungfuvampire.com/wp-content/uploads/2015/03/KFV-Love-Bites_cover1200x1200.jpeg" - }, - { - "name": "Kurious", - "image_url": "http://theciphershow.com/image/uploads/kurious.jpg" - }, - { - "name": "Kurtis Blow", - "image_url": "http://is4.mzstatic.com/image/thumb/Music/v4/69/fd/9f/69fd9f31-d152-c8ba-57be-80308d6b5d0c/source/1200x1200sr.jpg" - }, - { - "name": "Kurupt", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/1/19/Kurupt_Young_Gotti_in_Abu_Dhabi.jpg" - }, - { - "name": "Kutt Calhoun", - "image_url": "http://cdn.ticketfly.com/i/00/02/01/08/95-atxl.jpg" - }, - { - "name": "Kwamé", - "image_url": "http://3.bp.blogspot.com/-jgNTkFb4SrQ/UD5DgQgbdPI/AAAAAAAAAJI/13r9_kMiDkY/s1600/kwame_classic1.jpg" - }, - { - "name": "Kyle", - "image_url": "http://www.dailypublic.com/sites/default/files/2015/Apr/kyle.jpg" - }, - { - "name": "Lil' Kim", - "image_url": "http://4.bp.blogspot.com/-EngYxv2jBPA/UTknA4a939I/AAAAAAAAzBU/nb9YucfMRzs/s1600/41.jpg" - }, - { - "name": "Lil Wayne", - "image_url": "http://matchmusik.files.wordpress.com/2012/01/lil-wayne.jpg" - }, - { - "name": "L.T. Hutton", - "image_url": "http://www1.pictures.zimbio.com/gi/L+T+Hutton+Tupac+Production+Celebration+Santa+0yDUOUDKzhxl.jpg" - }, - { - "name": "La Chat", - "image_url": "http://uptwnxs.com/wp-content/uploads/2013/09/lachat.png" - }, - { - "name": "La the Darkman", - "image_url": "http://cdn5.hiphoplead.com/static/2010/02/la-the-darkman2.jpg" - }, - { - "name": "Lady Luck", - "image_url": "http://www.rapgrid.com/sites/default/files/rapper-photo/lady-luck.jpg" - }, - { - "name": "The Lady of Rage", - "image_url": "https://static1.squarespace.com/static/520ed800e4b0229123208764/526f4c76e4b0096d44b292ac/526f4c78e4b0096d44b292ad/1383025791721/1.jpg" - }, - { - "name": "Lakey The Kid", - "image_url": "http://nahright.com/wp-content/uploads/2014/10/Lakey.jpg" - }, - { - "name": "Lakim Shabazz", - "image_url": "http://uniqueheat.files.wordpress.com/2011/09/lakim-shabazz-11.jpg" - }, - { - "name": "Lakutis", - "image_url": "http://first-avenue.com/sites/default/files/styles/medium/public/images/performers/lakutis11.jpg" - }, - { - "name": "Large Professor", - "image_url": "http://www.hiphopnometry.org/wp-content/uploads/2015/03/download.jpg" - }, - { - "name": "Lauryn Hill", - "image_url": "http://images.musictimes.com/data/images/full/75871/lauryn-hill-tour.jpg" - }, - { - "name": "Lazarus", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/d/dc/Lazarus_%28rapper%29.jpeg" - }, - { - "name": "LE", - "image_url": "http://hiphopenquirer.com/wp-content/uploads/2013/01/le.jpg" - }, - { - "name": "Lecrae", - "image_url": "http://blog.beliefnet.com/wholenotes/files/2012/06/Lecrae1.jpg" - }, - { - "name": "Left Brain", - "image_url": "http://s3.amazonaws.com/rapgenius/tumblr_m81jhwwa8m1qa42jro1_1280.jpg" - }, - { - "name": "Lex Luger", - "image_url": "http://static01.nyt.com/images/2011/11/06/magazine/06luger/06luger-popup-v2.jpg" - }, - { - "name": "Lil' B", - "image_url": "http://celebrityinsider.org/wp-content/uploads/2018/04/Cardi-B-Nicki-Minaj-Lil-Scrappy.jpg" - }, - { - "name": "Lil' Bibby", - "image_url": "http://www.billboard.com/files/styles/article_main_image/public/media/462817929-rapper-lil-bibby-enters-the-sirius-xm-650.jpg" - }, - { - "name": "Lil' Debbie", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/4/42/Lil_Debbie_on_March_14%2C_2013.jpg" - }, - { - "name": "Lil' Dicky", - "image_url": "http://b-sides.tv/wp-content/uploads/2015/10/lil-dicky.jpg" - }, - { - "name": "Lil' Durk", - "image_url": "http://www.trbimg.com/img-5693fc6e/turbine/ct-chicago-rapper-lil-durk-announces-tour-drops-new-video-20160111" - }, - { - "name": "Lil' Eazy-E", - "image_url": "http://s3.amazonaws.com/hiphopdx-production/2014/09/Lil-Eazy-E_09-05-2014.jpg" - }, - { - "name": "Lil' Flip", - "image_url": "http://cdn.cnwimg.com/wp-content/uploads/2010/12/083011-lil-flip-lil-flip.png" - }, - { - "name": "Lil' Herb", - "image_url": "https://urbanstylzclothing.files.wordpress.com/2015/09/herb.jpg" - }, - { - "name": "Lil' Jon", - "image_url": "http://s3.amazonaws.com/rapgenius/lil-jon-w.jpg" - }, - { - "name": "Lil' Joseph", - "image_url": "http://beardfist.com/images/lil_joseph.png" - }, - { - "name": "Lil' Mama", - "image_url": "http://images2.fanpop.com/image/photos/11900000/Lil-Mama-3-female-rappers-11934192-440-348.jpg" - }, - { - "name": "Lil' Peep", - "image_url": "http://www.sobrietyresources.org/wp-content/uploads/2017/11/Lil-Peep-920x584.jpg" - }, - { - "name": "Lil' Phat", - "image_url": "http://api.ning.com/files/pQ8PE*Dabum6BY5-C2af3tLPWvIZgBdgFHHT*JOkvQAU8VjVm9v*Tl*M5TmfXHOqV4ji67tMnQY9zl7p-2QdcmKmsJPGcl6Y/WTFRussianMobsterChargedInTheMurderOfRapperLilPhatAtAtlantaHospitalInAOrderedShootingVideoInside.jpg" - }, - { - "name": "Lil' Pump", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2017/05/170530-Lil-Pump-800x600.jpg" - }, - { - "name": "Lil' Reese", - "image_url": "https://assets.dnainfo.com/generated/chicago_photo/2013/06/tavares-taylor-1372019233.jpg/extralarge.jpg" - }, - { - "name": "Lil' Ric", - "image_url": "http://a1yola.com/wp-content/uploads/2010/10/Lil-Ric.jpg" - }, - { - "name": "Lil' Ru", - "image_url": "http://authenticcore.files.wordpress.com/2009/06/lil-ru.jpg" - }, - { - "name": "Lil' Scrappy", - "image_url": "http://www.memphisrap.com/mr-uploads/2014/04/Lil-Scrappy-rapper-photo.jpg" - }, - { - "name": "Lil' Skies", - "image_url": "http://dailychiefers.com/wp-content/media/2017/08/Screen-Shot-2017-08-14-at-1.47.54-PM-1160x1088.png" - }, - { - "name": "Lil' Twist", - "image_url": "http://static.vibe.com/files/2015/03/Lil-Twist.jpg" - }, - { - "name": "Lil' Uzi Vert", - "image_url": "https://static.vibe.com/files/2017/05/Lil-Uzi-Vert-photo-1494953208-640x635.jpg" - }, - { - "name": "Lil' Wayne", - "image_url": "http://matchmusik.files.wordpress.com/2012/01/lil-wayne.jpg" - }, - { - "name": "Lil' Wyte", - "image_url": "https://bloximages.chicago2.vip.townnews.com/siouxcityjournal.com/content/tncms/assets/v3/editorial/f/16/f16599f8-8e54-5711-bb60-dae0d43f7a57/4f20871f57f10.image.jpg" - }, - { - "name": "Lil' Xan", - "image_url": "http://dailychiefers.com/wp-content/media/2017/05/lil-xan-1160x1119.png" - }, - { - "name": "Lil' Yachty", - "image_url": "http://i.dailymail.co.uk/i/newpix/2018/04/16/09/4B34F62B00000578-5620125-To_celebrate_her_15th_birthday_rapper_Danielle_Bregoli_released_-a-23_1523865744678.jpg" - }, - { - "name": "Lil' Zane", - "image_url": "http://eotm.files.wordpress.com/2010/07/lil_zane_677x600.jpg" - }, - { - "name": "Lil' Cease", - "image_url": "http://www1.pictures.zimbio.com/gi/Lil+Cease+Celebs+BET+Networks+New+York+Upfront+MTLFBnBGM_Gl.jpg" - }, - { - "name": "Lil' Fizz", - "image_url": "http://img.spokeo.com/public/900-600/lil_fizz_2007_07_10.jpg" - }, - { - "name": "Lil' Flip", - "image_url": "http://cdn.cnwimg.com/wp-content/uploads/2010/12/083011-lil-flip-lil-flip.png" - }, - { - "name": "Lil' Keke", - "image_url": "http://rapdose.com/wp-content/uploads/2014/04/Lil-Keke.jpg" - }, - { - "name": "Lil' Kim", - "image_url": "http://4.bp.blogspot.com/-EngYxv2jBPA/UTknA4a939I/AAAAAAAAzBU/nb9YucfMRzs/s1600/41.jpg" - }, - { - "name": "Lil' O", - "image_url": "http://purple-drank.com/wp-content/uploads/2011/06/Lil-O-Grind-Hard-Pray-Harder.jpg" - }, - { - "name": "Lil' Ronnie", - "image_url": "http://s3.amazonaws.com/hiphopdx-production/2016/10/Lil-Ronny-Instagram-e1477254471301-824x620.jpg" - }, - { - "name": "Lil' Troy", - "image_url": "http://photos1.blogger.com/x/blogger/2167/1769/1600/398335/liltroy.jpg" - }, - { - "name": "Lil' Wil", - "image_url": "http://www.rap-up.com/app/uploads/2018/04/lil-uzi-vert-japan.jpg" - }, - { - "name": "Lin Que", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/LinQue_5287-theOne.jpg/220px-LinQue_5287-theOne.jpg" - }, - { - "name": "Lisa Lopes", - "image_url": "http://images.rapgenius.com/3c8e978c534d938aa60f47229515ee66.527x600x1.jpg" - }, - { - "name": "LL Cool J", - "image_url": "http://media.npr.org/assets/img/2012/08/23/ll-cool-j_sq-ad8a68251f21a82c02dec641aad124d6b4de1ca0-s6-c30.jpg" - }, - { - "name": "Lloyd Banks", - "image_url": "https://www.bet.com/music/2018/03/17/lloyd-banks/_jcr_content/image.large2x1image.dimg/__1521337827454__1521335807971/031718-music-lloyd-banks.jpg" - }, - { - "name": "Locksmith", - "image_url": "http://api.ning.com/files/J*jFKCPdofWd1tPJxEdhL67p02O8suSNVExOVE0sZ0Gr*i9CA1T6aus8mXwgRx-xZODjLxtX5Am03SCXd8YZS1dm-MQZU*rN/locksmith.PNG" - }, - { - "name": "Logic", - "image_url": "http://hiphopnewssource.com/wp-content/uploads/2015/01/Logic-rapper.jpg" - }, - { - "name": "LoLa Monroe", - "image_url": "http://talkingpretty.com/wp-content/uploads/2011/12/Lola_Monroe.jpg" - }, - { - "name": "London On Da Track", - "image_url": "http://image1.redbull.com/rbcom/010/2017-02-27/1331846909916_2/0010/1/1500/1000/2/rapper-pell-and-producer-london-on-da-track.jpg" - }, - { - "name": "Loon", - "image_url": "http://rollingout.com/wp-content/uploads/2013/07/loon.jpg" - }, - { - "name": "Lord Finesse", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2012/07/lordfinesse.jpg" - }, - { - "name": "Lord Have Mercy", - "image_url": "http://4.bp.blogspot.com/-e0M8qmkHdr4/T-OOzru_EbI/AAAAAAAAB4c/b2Xzs3Uhx2M/s1600/lord-have-mercy-black-n-white.jpg" - }, - { - "name": "Lord Infamous", - "image_url": "http://www.aceshowbiz.com/images/news/lord-infamous-of-three-6-mafia-died-at-40.jpg" - }, - { - "name": "Lord Jamar", - "image_url": "http://insidejamarifox.com/wp-content/uploads/2013/09/LORDJAMAR.jpg" - }, - { - "name": "Los", - "image_url": "http://cdn.ambrosiaforheads.com/wp-content/uploads/2014/03/los-rapper.jpeg" - }, - { - "name": "Louis Logic", - "image_url": "http://www.mvremix.com/urban/interviews/images/l_l.jpg" - }, - { - "name": "Lovebug Starski", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/1/12/Starski.jpg" - }, - { - "name": "LoveRance", - "image_url": "http://www.famousbirthdays.com/thumbnails/loverance-medium.jpg" - }, - { - "name": "Lowkey", - "image_url": "https://www.thecanary.co/wp-content/uploads/2018/04/Rapper-Lowkey-on-Going-Underground-770x403.jpg" - }, - { - "name": "LRoc", - "image_url": "https://i1.wp.com/www.respectmyregion.com/wp-content/uploads/2015/07/unnamed1-e1438370960295.jpg" - }, - { - "name": "Ludacris", - "image_url": "https://pennylibertygbow.files.wordpress.com/2012/02/ludacris.jpg" - }, - { - "name": "Luis Resto", - "image_url": "http://images.genius.com/1555dd4015e93a37e901dd6bbcf8fd94.502x502x1.jpg" - }, - { - "name": "Luni Coleone", - "image_url": "http://hw-static.worldstarhiphop.com/pics/images/tp/2lieagk.jpg" - }, - { - "name": "Lupe Fiasco", - "image_url": "http://2.bp.blogspot.com/-sB8Ufk5JalU/TnVCVMu-QaI/AAAAAAAAGZk/ih8up0ox8AA/s1600/lupe_fiasco.jpg" - }, - { - "name": "Luther Campbell", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/55/bf/a2/55bfa20740ef48b87c79db4bf83045e6.jpg" - }, - { - "name": "MC Lyte", - "image_url": "http://www2.pictures.zimbio.com/gi/MC+Lyte+Soul+Train+Awards+2012+Glade+Suite+pwqWuYKKQkwl.jpg" - }, - { - "name": "Melle Mel", - "image_url": "http://www4.pictures.zimbio.com/gi/Melle+Mel+GRAMMY+Nominations+Concert+Live+uqqOc_pgcOKl.jpg" - }, - { - "name": "MF Doom", - "image_url": "http://1.bp.blogspot.com/-sV6R16-qWxo/Tm6ZhYV7_aI/AAAAAAAAACI/Hh6dPl1H2L0/s1600/MF%2BDOOM.jpg" - }, - { - "name": "M Trill", - "image_url": "http://www.iwantairplay.com/artist/img/201011041288886539_mtrill%202.jpg" - }, - { - "name": "M-1", - "image_url": "http://i.huffpost.com/gen/2559626/images/o-M1-RAPPER-facebook.jpg" - }, - { - "name": "M.I.A.", - "image_url": "http://media.santabanta.com/newsite/cinemascope/feed/mia20.jpg" - }, - { - "name": "Mac", - "image_url": "http://s3.amazonaws.com/rapgenius/Earlly.jpg" - }, - { - "name": "Mac Dre", - "image_url": "http://s3.amazonaws.com/rapgenius/10683a596c9b82548291.jpg" - }, - { - "name": "Mac Lethal", - "image_url": "http://i.ytimg.com/vi/UV-q4q66SAQ/maxresdefault.jpg" - }, - { - "name": "Mac Mall", - "image_url": "http://s3.amazonaws.com/rapgenius/DSC_2267a.jpg" - }, - { - "name": "Mac Miller", - "image_url": "http://s3.amazonaws.com/rapgenius/1357230347_MacMiller.jpg" - }, - { - "name": "Mac Minister", - "image_url": "http://mtv.mtvnimages.com/uri/mgid:uma:image:mtv.com:3088181" - }, - { - "name": "Machine Gun Kelly", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Machine_Gun_Kelly.jpg/1200px-Machine_Gun_Kelly.jpg" - }, - { - "name": "Mack 10", - "image_url": "http://s3.amazonaws.com/rapgenius/1368457732_77476E77A10496D1F7CAD8DC2CBA9F72.jpg" - }, - { - "name": "Mack Maine", - "image_url": "http://www1.pictures.zimbio.com/gi/Cash+Money+Records+Lil+Wayne+Album+Release+4Y_9ed0dAwal.jpg" - }, - { - "name": "Macklemore", - "image_url": "http://cdn1.bostonmagazine.com/wp-content/uploads/2013/10/macklemore-boston-rappers.jpg" - }, - { - "name": "Mad Lion", - "image_url": "http://ring.cdandlp.com/oldiers/photo_grande/114795296.jpg" - }, - { - "name": "Madchild", - "image_url": "http://i1.wp.com/www.ballerstatus.com/wp-content/uploads/2013/07/madchild.jpg" - }, - { - "name": "Madlib", - "image_url": "http://www.stasheverything.com/wp-content/uploads/2012/08/madlib.jpg" - }, - { - "name": "Maejor Ali", - "image_url": "http://www.rap-up.com/app/uploads/2014/10/maejor-ali-team.jpg" - }, - { - "name": "Magic", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2013/03/mrmagic.jpg" - }, - { - "name": "Magneto Dayo", - "image_url": "http://images1.laweekly.com/imager/magneto-dayo/u/original/4246159/dayophoto.jpg" - }, - { - "name": "Magnolia Shorty", - "image_url": "http://img.wennermedia.com/social/rs-896-rectangle.jpg" - }, - { - "name": "Maino", - "image_url": "https://fanart.tv/fanart/music/2c2cc2fe-0dcf-4995-8199-91fd5f159323/artistbackground/maino-50a1d2727401b.jpg" - }, - { - "name": "Manafest", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-manafest-celebrity-rap-singer.jpg" - }, - { - "name": "Mann", - "image_url": "https://ipeoplewatch.files.wordpress.com/2010/11/mann.png" - }, - { - "name": "Mannie Fresh", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2012/08/Mannie-Fresh-3.jpg" - }, - { - "name": "Marčelo", - "image_url": "http://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Mar%C4%8Delo_2008.jpg/600px-Mar%C4%8Delo_2008.jpg" - }, - { - "name": "Mariah Carey", - "image_url": "http://37.media.tumblr.com/ddcd2842cfaa27ad749eb1c8f0fa87d3/tumblr_mrvw1psZ391szbfero1_500.jpg" - }, - { - "name": "Mark Battles", - "image_url": "http://www.gannett-cdn.com/-mm-/ea1e306e18ab38d38fd0c7bad5df798dc9e6bf2a/c=1-0-1142-858&r=x404&c=534x401/local/-/media/2016/09/12/INGroup/Indianapolis/636092765862586531-MARKBATTLES-1-.jpg" - }, - { - "name": "Marky Mark", - "image_url": "http://i.dailymail.co.uk/i/pix/2014/12/15/23C0FCDA00000578-2874607-Back_in_the_day_Marky_Mark_Mark_Wahlberg_rapper_and_actor_circa_-m-4_1418665581123.jpg" - }, - { - "name": "Marley Marl", - "image_url": "http://www.waxpoetics.com/wp-content/uploads/2014/06/Kool-G-Rap_Promo2_suekwon-1.jpg" - }, - { - "name": "Marvaless", - "image_url": "http://a1yola.com/wp-content/uploads/2011/01/Marvaless-Ghetto-Blues.jpg" - }, - { - "name": "Marz", - "image_url": "http://wadeoradio.com/wp-content/uploads/2013/05/marz_with_hoodie.jpg" - }, - { - "name": "Mase", - "image_url": "http://richglare.com/wp-content/uploads/2014/03/mase.jpg" - }, - { - "name": "Masspike Miles", - "image_url": "http://api.ning.com/files/mWq-Pv8RWzQj-MamUWH9TNTYNoW0BlcruPGRV8J5nMMxDR76Wm0*Jgimy-pJMwxDTY5CcBFnbIJEj2GQDyirFkBKOdLYcw7C/masspikemiles20120105300x300.jpg" - }, - { - "name": "Masta Ace", - "image_url": "http://www.okayplayer.com/wp-content/uploads/2012/04/Masta_Ace_x_DOOM.jpg" - }, - { - "name": "Masta Killa", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-masta-killa-singer-rap-hip-hop.jpg" - }, - { - "name": "Master P", - "image_url": "http://cdn2.hiphopsince1987.com/wp-content/uploads/2014/04/MasterP.jpg" - }, - { - "name": "Master Shortie", - "image_url": "http://cache4.asset-cache.net/gc/98003447-british-rapper-master-shortie-performs-at-the-gettyimages.jpg" - }, - { - "name": "Matt Toka", - "image_url": "http://cdn.baeblemusic.com/bandcontent/matt_toka/matt_toka-498.jpg" - }, - { - "name": "Max B", - "image_url": "http://www.therapscene.com/wp-content/uploads/2016/09/max-b.png" - }, - { - "name": "Maxo Kream", - "image_url": "http://images.livemixtapes.com/artists/nodj/maxo_kream-maxo_187/cover.jpg" - }, - { - "name": "MC Breed", - "image_url": "http://cdn.ambrosiaforheads.com/wp-content/uploads/2015/10/MCBreed_Tupac.jpg" - }, - { - "name": "MC Davo", - "image_url": "https://i.scdn.co/image/8ca10c2e0345c064fd77e23dffd044e095cd09d9" - }, - { - "name": "MC Eiht", - "image_url": "http://happybday.to/sites/pics/mc-eiht-2013-3.jpg" - }, - { - "name": "MC Frontalot", - "image_url": "http://s3.amazonaws.com/media.wbur.org/wordpress/9/files/2011/11/1102_frontalot.jpg" - }, - { - "name": "MC Hammer", - "image_url": "http://i2.cdn.turner.com/cnnnext/dam/assets/111020033101-mc-hammer-story-top.jpg" - }, - { - "name": "MC Jin", - "image_url": "http://blog.asianinny.com/wp-content/uploads/2014/08/Edit-2.jpg" - }, - { - "name": "MC Lyte", - "image_url": "http://www2.pictures.zimbio.com/gi/MC+Lyte+Soul+Train+Awards+2012+Glade+Suite+pwqWuYKKQkwl.jpg" - }, - { - "name": "MC Mong", - "image_url": "http://www.christianitydaily.com/data/images/full/292/mc-mong.jpg" - }, - { - "name": "MC Pressure", - "image_url": "http://resources1.news.com.au/images/2013/09/13/1226718/911813-6111b67a-1b93-11e3-885a-29191a963f6e.jpg" - }, - { - "name": "MC Ren", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-mc-ren-celebrity-rap-star.jpg" - }, - { - "name": "MC Ride", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/7/72/MC_Ride_of_Death_Grips_in_2012.jpg" - }, - { - "name": "MC Serch", - "image_url": "http://www.eurweb.com/wp-content/uploads/2013/09/MC-Serch1.jpg" - }, - { - "name": "MC Shan", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0001/369/MI0001369927.jpg" - }, - { - "name": "MC Solaar", - "image_url": "http://www.potoclips.com/wp-content/uploads/2014/12/MC-Solaar-Zoom-90-11-2014-10.png" - }, - { - "name": "MC Trouble", - "image_url": "http://urbanbridgez.com/ubgblog/wp-content/uploads/2012/06/MCTrouble.jpg" - }, - { - "name": "MC Tunes", - "image_url": "http://i4.manchestereveningnews.co.uk/incoming/article4024647.ece/ALTERNATES/s615/nicky-lockett.jpg" - }, - { - "name": "Meechy Darko", - "image_url": "http://www4.pictures.zimbio.com/gi/Meechy+Darko+Coachella+Valley+Music+Arts+Festival+TkYgAgZECOrl.jpg" - }, - { - "name": "Meek Mill", - "image_url": "https://mk0slamonlinensgt39k.kinstacdn.com/wp-content/uploads/2018/04/meek.jpg" - }, - { - "name": "Melle Mel", - "image_url": "http://www4.pictures.zimbio.com/gi/Melle+Mel+GRAMMY+Nominations+Concert+Live+uqqOc_pgcOKl.jpg" - }, - { - "name": "Mellow Man Ace", - "image_url": "http://www.thecubanhistory.com/wp-content/uploads/2014/09/mellow-man-ace-posing-for-pic-picture.jpg" - }, - { - "name": "Memphis Bleek", - "image_url": "http://www4.pictures.gi.zimbio.com/Jay+Z+Celebrates+Grand+Opening+40+40+Club+BLwPtvgRe4Sl.jpg" - }, - { - "name": "Messy Marv", - "image_url": "http://gossip-grind.com/wp-content/uploads/2013/09/image3.jpg" - }, - { - "name": "Method Man", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.384069!/img/httpImage/image.jpg_gen/derivatives/landscape_1200/alg-rapper-method-man-jpg.jpg" - }, - { - "name": "Metro Boomin", - "image_url": "http://static.stereogum.com/uploads/2017/05/Metro-Boomin-1496168461-compressed.jpg" - }, - { - "name": "MF Doom", - "image_url": "http://1.bp.blogspot.com/-sV6R16-qWxo/Tm6ZhYV7_aI/AAAAAAAAACI/Hh6dPl1H2L0/s1600/MF%2BDOOM.jpg" - }, - { - "name": "MF Grimm", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-mf-grimm-fame-hip-hop-star.jpg" - }, - { - "name": "Mia X", - "image_url": "http://www.blackvibes.com/images/bvc/81/16306-mia-x.jpg" - }, - { - "name": "Mic Geronimo", - "image_url": "http://img.spokeo.com/public/900-600/mic_geronimo_2003_06_04.jpg" - }, - { - "name": "Mick Jenkins", - "image_url": "http://thekoalition.com/images/2015/10/Mick-Jenkins.jpg" - }, - { - "name": "Mickey Factz", - "image_url": "http://www.thefader.com/ys_assets/0005/4038/mfactz_main.jpg" - }, - { - "name": "Mike Dean", - "image_url": "https://vice-images.vice.com/images/content-images-crops/2015/10/23/smoking-weed-and-talking-rap-urban-legends-with-the-biggest-stoner-in-hip-hop-420-body-image-1445632224-size_1000.jpg" - }, - { - "name": "Mike G", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.2687217.1466809127!/img/httpImage/image.jpg_gen/derivatives/article_750/brown25f-2-web.jpg" - }, - { - "name": "Mike Jones", - "image_url": "http://content6.flixster.com/photo/12/68/69/12686960_ori.jpg" - }, - { - "name": "Mike Posner", - "image_url": "http://i.ytimg.com/vi/_z1aJvUTXUY/maxresdefault.jpg" - }, - { - "name": "Mike Shinoda", - "image_url": "http://www.canada.com/entertainment/cms/binary/7181890.jpg" - }, - { - "name": "Mike Stud", - "image_url": "https://cab.blog.gustavus.edu/files/2014/01/STUDITUNES2.jpg" - }, - { - "name": "Mike Will Made It", - "image_url": "http://generations.fr/media/son/_src/mike-will-made-it.jpg" - }, - { - "name": "Mike Zombie", - "image_url": "http://www.lifeistremendez.com/wp-content/uploads/2016/06/MIKE-ZOMBIE.jpg" - }, - { - "name": "Milo", - "image_url": "http://images1.laweekly.com/imager/milo/u/original/4244882/milo2final.jpg" - }, - { - "name": "Mims", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/f/f5/Shawn-Mims_2009-04-10_by-Adam-Bielawski.jpg" - }, - { - "name": "Mino", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/9c/ab/e8/9cabe8d370440fada55bc1e8f338b332.jpg" - }, - { - "name": "Miryo", - "image_url": "http://i1.wp.com/www.koreaboo.com/wp-content/uploads/2015/11/80059600.jpg" - }, - { - "name": "Missy Elliott", - "image_url": "http://thatgrapejuice.net/wp-content/uploads/2011/06/Missy%2BElliott1.jpg" - }, - { - "name": "Mista Grimm", - "image_url": "http://steadydippin.com/wp-content/uploads/Mista-Grimm.jpg" - }, - { - "name": "Mistah F.A.B.", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/5/5f/Mistah_F.A.B._%28cropped%29.jpg" - }, - { - "name": "Mister Cee", - "image_url": "http://ocdn.hiphopdx.com/mister-cee-gq-magazine-january-2014-hip-hop-dj-atlanta-music-01.jpg" - }, - { - "name": "Mitchy Slick", - "image_url": "http://celebnmusic247.com/wp-content/uploads/2013/12/San-Diego-rapper-Lil-Mitchy-Slick-Killed-news-1216-1.jpg" - }, - { - "name": "Mo B. Dick", - "image_url": "http://purple-drank.com/wp-content/uploads/2011/06/Mo-B.-Dick.jpg" - }, - { - "name": "Mod Sun", - "image_url": "https://i.ytimg.com/vi/077gBsOpfLY/maxresdefault.jpg" - }, - { - "name": "Money-B", - "image_url": "https://static1.squarespace.com/static/537f7de4e4b07cc20962a0fe/57d9ce70d482e972e8422601/57d9ced41b631b43099ada2e/1486258009597/money+b+icicles.jpg" - }, - { - "name": "Monie Love", - "image_url": "https://68.media.tumblr.com/34e9804dcd3a4307e744fc0b343318bf/tumblr_mvftdi4xlG1szbfero1_500.jpg" - }, - { - "name": "Monoxide Child", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/Monoxide_Child_at_the_Abominationz_tour_in_Chesterfield%2C_MI_on_April_27th%2C_2013.jpg/220px-Monoxide_Child_at_the_Abominationz_tour_in_Chesterfield%2C_MI_on_April_27th%2C_2013.jpg" - }, - { - "name": "Mopreme Shakur", - "image_url": "https://media.gettyimages.com/photos/rapper-mopreme-shakur-attends-the-2012-estrella-de-moet-program-at-picture-id143587959" - }, - { - "name": "Mos Def", - "image_url": "http://1.bp.blogspot.com/-OfuE_iF9YT4/TarRyZ1raYI/AAAAAAAABJA/JZ6p4D2dMjw/s1600/mos_def1.jpg" - }, - { - "name": "Mr. Capone-E", - "image_url": "http://media-cache-ak0.pinimg.com/736x/06/58/dd/0658ddcf276465f97bee8e59ac5937ef.jpg" - }, - { - "name": "Mr. Cheeks", - "image_url": "http://www.twincities.com/wp-content/uploads/2016/03/08-RapperMrCheeks.jpg" - }, - { - "name": "Mr. Collipark", - "image_url": "http://urltv.tv/wp-content/uploads/2015/02/mr-collipark.png" - }, - { - "name": "Mr. Criminal", - "image_url": "https://nueonline.files.wordpress.com/2010/05/mr-cr.jpg" - }, - { - "name": "Mr. Lif", - "image_url": "https://media2.wnyc.org/i/800/0/c/80/nprproxy/477517970" - }, - { - "name": "Mr. Porter", - "image_url": "https://s3.amazonaws.com/rapgenius/1364090355_l.jpg" - }, - { - "name": "Mr. Serv-On", - "image_url": "http://purple-drank.com/wp-content/uploads/2013/09/Mr.-Serv-On-Gangsta-1-More-Time.jpg" - }, - { - "name": "Mr. Short Khop", - "image_url": "https://www.rapmusicguide.com/amass/images/inventory/4474/Mr.%20Short%20Khop%20-%20Da%20Khop%20Shop.jpg" - }, - { - "name": "Ms. Jade", - "image_url": "http://www.billboard.com/files/media/ms-jade-press-2002-650a.jpg" - }, - { - "name": "Murphy Lee", - "image_url": "http://1.bp.blogspot.com/_72Fq2ASEDsQ/SylcE_hqPzI/AAAAAAAAJSQ/VlycqZcZFQE/s320/murphy_lee_mo-174781.jpg" - }, - { - "name": "Murs", - "image_url": "http://planetill.com/wp-content/uploads/2011/01/murs1a.jpg" - }, - { - "name": "Mystikal", - "image_url": "http://theboombox.com/files/2015/09/mystikal-630x420.jpg" - }, - { - "name": "Myzery", - "image_url": "http://faygoluvers.net/v5/wp-content/uploads/2013/03/MYZERY-INT-2013th.jpg" - }, - { - "name": "Montana of 300", - "image_url": "http://www.rapswag.com/wp-content/uploads/2016/05/montana-of-300.jpg" - }, - { - "name": "Nas", - "image_url": "http://www.howtorapbook.com/wp-content/uploads/2016/04/nas_rapper_reuters_1200.jpg" - }, - { - "name": "Nicki Minaj", - "image_url": "http://www.rap-up.com/app/uploads/2018/04/nicki-minaj-chun-li.jpg" - }, - { - "name": "NBA YoungBoy", - "image_url": "http://feedbox.com/wp-content/uploads/2017/07/rapper-nba-youngboy.jpg" - }, - { - "name": "N.O. Joe", - "image_url": "https://images.genius.com/e6d317c1fc41f258cab262a651d1032d.220x222x1.jpg" - }, - { - "name": "N.O.R.E.", - "image_url": "http://rapradar.com/wp-content/uploads/2016/03/nore-rapradar-2.jpg" - }, - { - "name": "Napoleon", - "image_url": "http://2paclegacy.net/wp-content/uploads/2015/12/Napoleon-Outlawz.jpg" - }, - { - "name": "Nas", - "image_url": "http://www.howtorapbook.com/wp-content/uploads/2016/04/nas_rapper_reuters_1200.jpg" - }, - { - "name": "Nate Dogg", - "image_url": "http://www.evilbeetgossip.com/wp-content/uploads/2011/03/Nate-Dogg-AKA-Nathaniel-Hale.jpg" - }, - { - "name": "Nature", - "image_url": "http://blogordiepgh.com/wp-content/uploads/2016/02/nature2-590x738.jpg" - }, - { - "name": "Nav", - "image_url": "https://www.desiblitz.com/wp-content/uploads/2017/02/Nav-Rapper-Watch-2017-Featued-1.jpg" - }, - { - "name": "Nebu Kiniza", - "image_url": "https://www.famousbirthdays.com/faces/kiniza-nebu-image.jpg" - }, - { - "name": "Necro", - "image_url": "https://m3event.files.wordpress.com/2012/05/necro.png" - }, - { - "name": "Needlz", - "image_url": "http://www.mvremix.com/urban/interviews/images/choppa.jpg" - }, - { - "name": "Nelly", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2015/04/nelly-4ee7ec5a1b162.jpg" - }, - { - "name": "NF", - "image_url": "http://nfrealmusic.umg-wp.com/wp-content/blogs.dir/390/files_mf/1427238717nfbg2.jpg" - }, - { - "name": "Nick Cannon", - "image_url": "http://4.bp.blogspot.com/-WlJB4GcpcJI/TrIsDVJpnCI/AAAAAAAAAQs/KvUN9_OEwRg/s1600/Nick-Cannon-biography.jpg" - }, - { - "name": "Nicki Minaj", - "image_url": "http://www.rap-up.com/app/uploads/2018/04/nicki-minaj-chun-li.jpg" - }, - { - "name": "Nicky da B", - "image_url": "http://www.out.com/sites/out.com/files/2014/09/04/nicky-1%20main.jpg" - }, - { - "name": "Nicole Wray", - "image_url": "http://static.djbooth.net/pics-artist-rec/Nicole_Wray_1.jpg" - }, - { - "name": "Nikki D", - "image_url": "http://4.bp.blogspot.com/-Qc-EvZbPM1Q/UZhHG-wvkUI/AAAAAAAASYY/FRdaUwO8KJg/s1600/nikkid.jpg" - }, - { - "name": "Ninja", - "image_url": "http://www.dieantwoord.com/wp-content/uploads/2016/06/13355485_1803315693234477_1383954422_n.jpg" - }, - { - "name": "Nipsey Hussle", - "image_url": "https://badgerherald.com/media/2013/10/nipsey_headshot.jpg" - }, - { - "name": "Nitty", - "image_url": "http://versetracker.com/sites/default/files/rapper-pictures/r/rum-nitty.jpg" - }, - { - "name": "Nitty Scott MC", - "image_url": "http://www.thesocialmediasamurai.com/wp-content/uploads/2015/06/TMT_6971-Edit_HighResEdit.jpg" - }, - { - "name": "NoClue", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/9/94/RickyBrown2.jpg" - }, - { - "name": "No Malice", - "image_url": "http://www.xxlmag.com/files/2015/08/no-malice-interview.jpg" - }, - { - "name": "Noah 40 Shebib", - "image_url": "http://www.rap-up.com/app/uploads/2018/04/drake-floral.jpg" - }, - { - "name": "Noname", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/Noname_%28rapper%29_2017.jpg/1200px-Noname_%28rapper%29_2017.jpg" - }, - { - "name": "Nonchalant", - "image_url": "http://bandwidth.wamu.org/wp-content/uploads/2014/05/nonchalant-900x503.png" - }, - { - "name": "The Notorious B.I.G.", - "image_url": "http://www.neontommy.com/sites/default/files/NotoriousBIG.jpg" - }, - { - "name": "Nottz", - "image_url": "http://brandnew-hiphop.com/wp-content/uploads/2015/11/rapper-big-pooh-nottz-300z.jpg" - }, - { - "name": "Nujabes", - "image_url": "http://www.posterinvation.com/wp-content/uploads/2017/11/Nujabes-Japanese-Rapper.jpg" - }, - { - "name": "Nump", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/0/08/NUMP_Photo_By_Linda_Poeng.jpg" - }, - { - "name": "Numskull", - "image_url": "http://a1yola.com/wp-content/uploads/2010/10/knumskull.jpeg" - }, - { - "name": "Nyck Caution", - "image_url": "https://s3.amazonaws.com/rapgenius/1370913624_Nyck-Caution.jpeg" - }, - { - "name": "Nyzzy Nyce", - "image_url": "http://cache.vevo.com/Content/VevoImages/artist/F77096AEAB227EDC7749E54A60AD84FD20133151332767.jpg" - }, - { - "name": "O.T. Genasis", - "image_url": "http://www.atlanticrecords.com/sites/g/files/g2000003466/f/styles/post_thumbnail_home/public/201407/O.T.Genasis_NewArtist_StoryImage.jpg" - }, - { - "name": "Obie Trice", - "image_url": "https://i.ytimg.com/vi/k0CukaaPmpk/hqdefault.jpg" - }, - { - "name": "Oddisee", - "image_url": "http://image2.redbull.com/rbcom/010/2015-04-21/1331718387121_2/0012/0/905/0/2616/2573/1500/2/oddisee.jpg" - }, - { - "name": "Offset", - "image_url": "http://celebrityinsider.org/wp-content/uploads/2018/04/Offset.jpg" - }, - { - "name": "OG Maco", - "image_url": "http://rack.0.mshcdn.com/media/ZgkyMDE0LzA5LzE5LzY2L29nbWFjb3VndWVzLjI3ZTY4LmpwZwpwCXRodW1iCTk1MHg1MzQjCmUJanBn/e9db9255/336/og-maco-uguessedit.jpg" - }, - { - "name": "Oh No", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2015/07/Screen-Shot-2015-07-17-at-6.45.39-PM-300x300.png" - }, - { - "name": "OJ da Juiceman", - "image_url": "http://www.stacksmag.net/wp-content/uploads/2013/03/Oj+Da+Juiceman.jpg" - }, - { - "name": "Ol' Dirty Bastard", - "image_url": "http://25.media.tumblr.com/tumblr_llopqgJSCs1qcnjjco1_500.jpg" - }, - { - "name": "Olamide", - "image_url": "http://i0.wp.com/www.currentnewsnow.com/wp-content/uploads/2017/02/olamide-rapper.jpg" - }, - { - "name": "Olivia", - "image_url": "http://4.bp.blogspot.com/-zjgq7UuGNO8/TtdcuvdVFoI/AAAAAAAAEtA/5zoyt9VRv3E/s1600/olivia+longott.jpg" - }, - { - "name": "Omarion", - "image_url": "https://i0.wp.com/favimages.com/wp-content/uploads/2012/08/rapper-omarion-rap-celebrity-singer.jpg" - }, - { - "name": "Omega Red", - "image_url": "https://www.rap-n-blues.com/wp-content/uploads/2010/10/Exclusive-Interview-with-Omega-Red-pt-1-11.jpg" - }, - { - "name": "Omillio Sparks", - "image_url": "https://i.ytimg.com/vi/bgjehnnP7vE/maxresdefault.jpg" - }, - { - "name": "One Be Lo", - "image_url": "https://grownuprap.files.wordpress.com/2015/06/one-be-lo.jpg" - }, - { - "name": "Oneya", - "image_url": "http://4.bp.blogspot.com/_rc6elIZnb9w/StfvPh8AExI/AAAAAAAAAPI/_RXYXzPY8PI/s320/grillz%5B1%5D.png" - }, - { - "name": "Open Mike Eagle", - "image_url": "http://normalimage.cdn.ucbt.net/person_69295.png" - }, - { - "name": "Psy", - "image_url": "http://www.soompi.com/wp-content/uploads/2013/05/psy-yahoo.jpg" - }, - { - "name": "Q-Tip", - "image_url": "http://amarudontv.com/wp-content/uploads/2011/06/q-tip.jpg" - }, - { - "name": "P. Reign", - "image_url": "http://www.thisisyourconscience.com/wp-content/uploads/2011/03/P-Reign.jpg" - }, - { - "name": "P.C.T", - "image_url": "http://media-cache-ec0.pinimg.com/736x/d8/6e/c4/d86ec4828778e4997a77313619d0d370.jpg" - }, - { - "name": "Papa Reu", - "image_url": "http://thesource.com/wp-content/uploads/2015/07/papa-reu-1.jpg" - }, - { - "name": "Papoose", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.281729.1314351515!/img/httpImage/image.jpg_gen/derivatives/article_970/amd-papoose-jpg.jpg" - }, - { - "name": "Paris", - "image_url": "http://cps-static.rovicorp.com/3/JPG_1080/MI0001/400/MI0001400360.jpg" - }, - { - "name": "PARTYNEXTDOOR", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/theboombox.com/files/2013/10/PARTYNEXTDOOR.jpg" - }, - { - "name": "Pastor Troy", - "image_url": "http://veganrapnerd.com/wp-content/uploads/2013/08/pastortroy.jpg" - }, - { - "name": "Paul Wall", - "image_url": "http://aaenglish.files.wordpress.com/2010/07/paul_wall.jpg" - }, - { - "name": "Peedi Peedi", - "image_url": "http://www.yorapper.com/Photos/peedi-peedi-ringtone.jpg" - }, - { - "name": "Peewee Longway", - "image_url": "http://thedailyloud.com/wp-content/uploads/2014/07/PeeWee+Longway.jpg" - }, - { - "name": "Pacewon", - "image_url": "http://s3.amazonaws.com/rapgenius/1362133040_pacewon1.jpg" - }, - { - "name": "Percee P", - "image_url": "https://images.genius.com/5043bb54deda4ff352e7a413107e40fd.455x489x1.jpg" - }, - { - "name": "Petey Pablo", - "image_url": "http://i.perezhilton.com/wp-content/uploads/2012/02/rapper-petey-pablo-goes-to-prison__oPt.jpg" - }, - { - "name": "Pharoahe Monch", - "image_url": "http://thecorner.co.nz/wp-content/uploads/2010/10/monch.jpg" - }, - { - "name": "Pharrell Williams", - "image_url": "http://www.alux.com/wp-content/uploads/2016/05/pharrell-williams8.jpg" - }, - { - "name": "Phat Kat", - "image_url": "http://factmag-images.s3.amazonaws.com/wp-content/uploads/2015/09/Phat-Kat-FACT-Freestyles-Episode-1200x630.png" - }, - { - "name": "Phife Dawg", - "image_url": "http://static.celebuzz.com/uploads/2016/03/phife-dawg-32316.jpg" - }, - { - "name": "Philthy Rich", - "image_url": "http://i2.wp.com/allhiphop.com/wp-content/uploads/2012/02/20120214-133154-1.jpg" - }, - { - "name": "Phyno", - "image_url": "https://i.onthe.io/vllkyt2uq4dmo3ouf.bf39c0a9.jpg" - }, - { - "name": "Pill", - "image_url": "http://missdimplez.com/wp-content/uploads/2011/12/pill-rapper.jpg" - }, - { - "name": "Pimp C", - "image_url": "http://media-cache-ak0.pinimg.com/736x/4a/5d/da/4a5dda5cf63497a7a7323d64036ea588.jpg" - }, - { - "name": "Pinkie Pie", - "image_url": "http://fc09.deviantart.net/fs71/i/2014/106/a/e/rap_pinkie_pie_by_racoonkun-d7eqdf7.png" - }, - { - "name": "Pitbull", - "image_url": "http://images5.fanpop.com/image/photos/25000000/Pitbull-wallpaper-pitbull-rapper-25094094-1024-768.jpg" - }, - { - "name": "Planet Asia", - "image_url": "http://2.bp.blogspot.com/_3i6Ja3TzR3U/TUsqDLt5_eI/AAAAAAAAA5E/o5cNPWjIOUc/s1600/Planet+Asia.jpg" - }, - { - "name": "Planetary", - "image_url": "https://www.universetoday.com/wp-content/uploads/2013/06/star_cluster_planet.jpg" - }, - { - "name": "Plies", - "image_url": "http://siccness.net/wp/wp-content/uploads/2013/01/Plies.png" - }, - { - "name": "Playboi Carti", - "image_url": "http://images.complex.com/complex/image/upload/t_article_image/playboi-carti_hylalw.jpg" - }, - { - "name": "PnB Rock", - "image_url": "http://www.trbimg.com/img-593a8ea1/turbine/mc-rapper-pnb-rocks-show-to-open-easton-s-new-one-centre-square-is-rescheduled-20170609" - }, - { - "name": "PNC", - "image_url": "https://resources.stuff.co.nz/content/dam/images/1/d/s/t/f/0/image.related.StuffLandscapeSixteenByNine.620x349.1gbkoq.png/1483310043452.jpg" - }, - { - "name": "Porta", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/9/93/Christian_Jim%C3%A9nez_Porta.jpg" - }, - { - "name": "Positive K", - "image_url": "https://i.ytimg.com/vi/rKSu3MMqjNA/maxresdefault.jpg" - }, - { - "name": "Post Malone", - "image_url": "http://s3.amazonaws.com/factmag-images/wp-content/uploads/2016/06/Post-Malone-6-15-16-616x440.jpg" - }, - { - "name": "Pras", - "image_url": "http://okp-cdn.okayplayer.com/wp-content/uploads/2015/11/pras-grey.jpg" - }, - { - "name": "Prince Ital Joe", - "image_url": "http://api.ning.com/files/HX2HJ5zFz1VXk6nnlv3UuQYepy3Zaqp7FB99CwchCVK9qZAdUGnd2wgm8KBTPBQ5TPA7viKN70LkASDddCvpMRV*qbrLbSTS/2pacPrinceItalJoe.jpg" - }, - { - "name": "Prince Paul", - "image_url": "http://s3.amazonaws.com/hiphopdx-production/2017/06/DJ-Prince-Paul-789x591.jpg" - }, - { - "name": "Prince Po", - "image_url": "http://s3.amazonaws.com/rapgenius/1354768383_tumblr_m4e3h3JIu51rrnvtco1_500.png" - }, - { - "name": "Problem", - "image_url": "http://media-cache-ec0.pinimg.com/736x/91/12/c5/9112c5e71840687c066eb9bf199a6c8b.jpg" - }, - { - "name": "Prodigy", - "image_url": "http://www.genycis.com/blog/php/prodigy.jpg" - }, - { - "name": "Professor Green", - "image_url": "http://www.thedrum.com/uploads/drum_basic_article/154317/main_images/ProfessorGreen.jpg" - }, - { - "name": "Project Pat", - "image_url": "http://purple-drank.com/wp-content/uploads/2013/04/Project-Pat-New.jpg" - }, - { - "name": "Proof", - "image_url": "http://api.ning.com/files/id8pBTnWr70l7rcG7ybqV4HsnNh-BPxmwnyZ9v0VIyOITru56VjRVTRg9zdpsZMShSK3pPDKlmcbXnYrswx9fJCRZh8Y5ooC/proof.jpg" - }, - { - "name": "Prozak", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-prozak-celebrity-rap-photo.jpg" - }, - { - "name": "Psy", - "image_url": "http://www.soompi.com/wp-content/uploads/2013/05/psy-yahoo.jpg" - }, - { - "name": "Pusha T", - "image_url": "https://images.vice.com/noisey/content-images/article/pusha-t-interview-my-name-is-my-name/Pusha%20T%20Close.jpg" - }, - { - "name": "Queen Latifah", - "image_url": "http://media-cache-ec0.pinimg.com/736x/98/eb/23/98eb236df993bb4d0a7b2bbb6f8887d6.jpg" - }, - { - "name": "Q-Tip", - "image_url": "http://amarudontv.com/wp-content/uploads/2011/06/q-tip.jpg" - }, - { - "name": "Quan", - "image_url": "http://www.collegedj.net/wp-content/uploads/2011/09/Quan-rapper.jpg" - }, - { - "name": "Quavo", - "image_url": "http://www.globallnews.com/wp-content/uploads/2018/04/725393107_quavo_hunchoday_1522628161372_11247409_ver1.0_640_360.jpg" - }, - { - "name": "Quazedelic", - "image_url": "https://ilovemssugar.files.wordpress.com/2009/08/quazedelic.jpg" - }, - { - "name": "Queen Latifah", - "image_url": "http://media-cache-ec0.pinimg.com/736x/98/eb/23/98eb236df993bb4d0a7b2bbb6f8887d6.jpg" - }, - { - "name": "Queen Pen", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/48/19/d1/4819d1636cf36c92194437765638240b.jpg" - }, - { - "name": "The Quiett", - "image_url": "http://korcan50years.files.wordpress.com/2013/06/the-quiett-798x1024.jpg" - }, - { - "name": "Quincy Jones III", - "image_url": "http://theboombox.com/files/2010/05/david-banner-200ak050410.jpg" - }, - { - "name": "Qwazaar", - "image_url": "http://cdn.ticketfly.com/i/00/01/88/84/11-atxl1.jpg" - }, - { - "name": "Qwel", - "image_url": "http://img.karaoke-lyrics.net/img/artists/4536/qwel-247381.jpg" - }, - { - "name": "Rakim", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/162/MI0003162077.jpg" - }, - { - "name": "RZA", - "image_url": "https://static01.nyt.com/images/2012/10/21/arts/21RZA1_SPAN/21RZA1_SPAN-jumbo.jpg" - }, - { - "name": "R. Kelly", - "image_url": "http://i.dailymail.co.uk/i/newpix/2018/04/16/21/4B25AB0C00000578-5622755-image-a-24_1523909544359.jpg" - }, - { - "name": "R.A. the Rugged Man", - "image_url": "http://www.kapu.or.at/sites/default/files/event/image/ruggednew1.jpg" - }, - { - "name": "Raekwon", - "image_url": "http://www.bkhiphopfestival.com/wp-content/uploads/2014/06/Raekwon.jpg" - }, - { - "name": "Rah Digga", - "image_url": "http://hiphopgoldenage.com/wp-content/uploads/2015/08/2012-music-topic-rah-digga.png" - }, - { - "name": "Rahzel", - "image_url": "http://www.blackouthiphop.com/blog/wp-content/uploads/2011/04/rahzel.jpg" - }, - { - "name": "Rakim", - "image_url": "http://cps-static.rovicorp.com/3/JPG_400/MI0003/162/MI0003162077.jpg" - }, - { - "name": "Rampage", - "image_url": "http://www.blackouthiphop.com/blog/wp-content/uploads/2012/01/rampage.jpg" - }, - { - "name": "Rap Monster", - "image_url": "http://xinspirit.files.wordpress.com/2013/06/rap-monster.jpg" - }, - { - "name": "Rappin' 4-Tay", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2016/10/Rappin-4-Tay_10-13-2016-596x447.jpg" - }, - { - "name": "Rapsody", - "image_url": "http://rollingout.com/wp-content/uploads/2014/05/rapsody.jpg" - }, - { - "name": "Ramey Dawoud", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/7/70/Kashta_Live.jpg" - }, - { - "name": "Ras Kass", - "image_url": "http://cdn4.hiphoplead.com/static/2012/03/Ras-Kass.jpg" - }, - { - "name": "Rasheeda", - "image_url": "http://media-cache-ak0.pinimg.com/736x/df/01/4f/df014fca71f89f5efc4d58f27b1beb2a.jpg" - }, - { - "name": "Ravi", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Ravi_-_2016_Gaon_Chart_K-pop_Awards_red_carpet.jpg/1200px-Ravi_-_2016_Gaon_Chart_K-pop_Awards_red_carpet.jpg" - }, - { - "name": "Ray Cash", - "image_url": "http://www.hipstrumentals.com/wp-content/uploads/2012/12/Ray-Cash-Bumpin-My-Music.jpg" - }, - { - "name": "Ray J", - "image_url": "http://www3.pictures.zimbio.com/pc/Rapper+Ray+J+spotted+Tru+night+club+Hollywood+0orxsqqyQ49x.jpg" - }, - { - "name": "Ray Luv", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Ray_Luv_performing_at_5th_Annual_AHSC_1.JPG/1200px-Ray_Luv_performing_at_5th_Annual_AHSC_1.JPG" - }, - { - "name": "Raz Fresco", - "image_url": "http://exclaim.ca/images/razfresco2.jpg" - }, - { - "name": "RBX", - "image_url": "http://www.longbeachindependent.com/wp-content/uploads/2015/03/rbx-rapper-long-beach1.jpg" - }, - { - "name": "The Real Roxanne", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/dd/e9/61/dde961ccc373460deb2bab6b8df479b8.jpg" - }, - { - "name": "Really Doe", - "image_url": "http://cdn.smosh.com/sites/default/files/ftpuploads/bloguploads/1113/least-badass-name-really-doe.jpg" - }, - { - "name": "Red Café", - "image_url": "http://www1.pictures.zimbio.com/gi/Red+Cafe+Interscope+Geffen+Promotions+Department+MlqC_HJwxQMl.jpg" - }, - { - "name": "Red Spyda", - "image_url": "http://16762-presscdn-0-89.pagely.netdna-cdn.com/wp-content/uploads/2012/08/red-spyda.png" - }, - { - "name": "Redfoo", - "image_url": "http://media.gettyimages.com/photos/rapper-redfoo-of-lmfao-arrives-for-party-rock-mondays-at-marquee-in-picture-id131805210" - }, - { - "name": "Redman", - "image_url": "http://djstorm.files.wordpress.com/2011/02/redman1.jpg" - }, - { - "name": "Reef the Lost Cauze", - "image_url": "http://thekey.xpn.org/aatk/files/2016/02/ReefCaliph-9726-620x413.jpg" - }, - { - "name": "Reema Major", - "image_url": "http://www.bet.com/topics/r/reema-major/_jcr_content/image.heroimage.dimg/__1411088698102/080312-topic-music-reema-major-rapper.jpg" - }, - { - "name": "Reks", - "image_url": "http://hypeverse.files.wordpress.com/2012/10/reks.jpg" - }, - { - "name": "Remy Ma", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/d9/79/0e/d9790e372e4df2baf687770b711968bf.jpg" - }, - { - "name": "Rhymefest", - "image_url": "http://i.huffpost.com/gen/2824474/images/h-CHE-RHYMEFEST-SMITH-348x516.jpg" - }, - { - "name": "Rich Boy", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/theboombox.com/files/2009/01/rich-boy_011509_200.jpg" - }, - { - "name": "Rich Brian", - "image_url": "https://www.tinymixtapes.com/sites/default/files/imagecache/Article_Width/1801/rich-chigga-amen-cover-low-res.jpg" - }, - { - "name": "Rich Homie Quan", - "image_url": "http://www.judiciaryreport.com/images_4/rich-homie-quan-4-10-15-1.png" - }, - { - "name": "Rich The Kid", - "image_url": "https://gazettereview.com/wp-content/uploads/2017/05/rich4.jpg" - }, - { - "name": "Richie Rich", - "image_url": "http://www.rule4080.com/wp-content/uploads/2011/08/Richie_Rich_rapper.jpeg" - }, - { - "name": "Rick Rock", - "image_url": "http://s3.amazonaws.com/rapgenius/rick_rock.jpg" - }, - { - "name": "Rick Ross", - "image_url": "http://www.thefamouspeople.com/profiles/images/rick-ross-2.jpg" - }, - { - "name": "Rico Love", - "image_url": "http://media.gettyimages.com/photos/record-producer-singer-songwriter-and-rapper-rico-love-is-interviewed-picture-id177991029" - }, - { - "name": "Riff Raff", - "image_url": "http://images1.laweekly.com/imager/riff-raff/u/original/4248950/rrone.jpg" - }, - { - "name": "Rittz", - "image_url": "http://theciphershow.com/image/uploads/rittz.jpg" - }, - { - "name": "RJ", - "image_url": "http://images1.laweekly.com/imager/u/original/6044341/rj-kenneth-wynn.jpg" - }, - { - "name": "Rob Sonic", - "image_url": "http://cdn.ticketfly.com/i/00/01/30/07/57-exl.jpeg" - }, - { - "name": "Rob Stone", - "image_url": "https://i1.wp.com/hypebeast.com/image/ht/2016/08/rob-stone-chill-bill-remix1.png" - }, - { - "name": "Roc Marciano", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2010/09/marciano_304.jpg" - }, - { - "name": "Rockie Fresh", - "image_url": "http://www.missinfo.tv/wp-content/uploads/2014/03/rockie-fresh.jpg.jpg" - }, - { - "name": "Rocko", - "image_url": "http://www.bet.com/content/dam/betcom/images/2013/06/Shows/Music-News/mn13_rockoadon_final.jpg" - }, - { - "name": "Roger Troutman", - "image_url": "https://www.thefamousbirthdays.com/photo/en/c/c6/wk_60128_40208_large.jpg" - }, - { - "name": "Romeo Miller", - "image_url": "http://www1.pictures.zimbio.com/gi/BET+Awards+11+Arrivals+XFI1wmisCnBx.jpg" - }, - { - "name": "Ronnie Radke", - "image_url": "http://www.altpress.com/images/uploads/feature_header_images/ronnie_radke_list_2015.jpg" - }, - { - "name": "Roots Manuva", - "image_url": "http://dis.resized.images.s3.amazonaws.com/940x535/27742.jpeg" - }, - { - "name": "Roscoe", - "image_url": "http://www4.pictures.stylebistro.com/gi/Roscoe%2BDash%2BScarves%2BPatterned%2BScarf%2B8lklV6z5LqUl.jpg" - }, - { - "name": "Roscoe Dash", - "image_url": "http://www.africamusiclaw.com/wp-content/uploads/2012/09/Rapper-Roscoe-Dash-Says-Wale-and-Kanye-Did-not-Give-Credits.jpg" - }, - { - "name": "Rowdy Rebel", - "image_url": "https://images.vice.com/noisey/content-images/article/rowdy-rebel-interview/Screen-Shot-2014-09-19-at-1-26-44-PM.jpg" - }, - { - "name": "Roxanne Shanté", - "image_url": "https://s-media-cache-ak0.pinimg.com/564x/aa/0a/11/aa0a117dbca70d9867e8ec57cda0209f.jpg" - }, - { - "name": "Royce da 5'9", - "image_url": "http://www.ihiphop.com/wp-content/uploads/2011/08/royce.jpg" - }, - { - "name": "Russ", - "image_url": "http://dailychiefers.com/wp-content/media/2016/04/russ.jpg" - }, - { - "name": "Rucka Rucka Ali", - "image_url": "https://www.thefamouspeople.com/profiles/images/rucka-rucka-ali-1.jpg" - }, - { - "name": "Rydah J. Klyde", - "image_url": "http://siccness.net/wp/wp-content/uploads/2016/08/dj-fresh-rydah-j-klyde.jpg" - }, - { - "name": "Rye Rye", - "image_url": "http://www.bet.com/topics/r/rye-rye/_jcr_content/image.heroimage.dimg/__1411951278058/051512-shows-106-park-rye-rye-9.jpg" - }, - { - "name": "RZA", - "image_url": "http://www.sosoactive.com/wp-content/uploads/2014/04/rza-2.jpg" - }, - { - "name": "Roy Woods", - "image_url": "http://bendxl.com/wp-content/uploads/2015/07/ROYWoodsOVO.jpg" - }, - { - "name": "Slick Rick", - "image_url": "http://jobbiecrew.com/wp-content/uploads/2015/04/0slickrick2.jpg" - }, - { - "name": "Snoop Dogg", - "image_url": "https://media.nbcnewyork.com/images/1200*675/Snoop+Dogg3.jpg" - }, - { - "name": "Skabo", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/8/83/Bosko.jpg" - }, - { - "name": "Saafir", - "image_url": "http://www.okayplayer.com/wp-content/uploads/2013/02/saafir.jpg" - }, - { - "name": "Sabac Red", - "image_url": "http://wildstylemag.com/wp-content/uploads/Sabac-Red.gif" - }, - { - "name": "Sacario", - "image_url": "http://www.hellhoundmusic.com/wp-content/uploads/2013/11/sacario-1.jpg" - }, - { - "name": "Sadat X", - "image_url": "http://factmag-images.s3.amazonaws.com/wp-content/uploads/2012/10/sadat-x-10.25.2012.j.jpeg" - }, - { - "name": "Sadistik", - "image_url": "http://potholesinmyblog.com/wp-content/uploads/2013/01/sadistik-mic.jpg" - }, - { - "name": "Sage Francis", - "image_url": "https://consequenceofsound.files.wordpress.com/2014/03/sage-francis_1276598819.jpg" - }, - { - "name": "Sage the Gemini", - "image_url": "https://images.vice.com/noisey/content-images/article/sage-the-gemini-doesnt-listen-to-rap/E9FA1B25DC26BCB317C236E1CD46175920132510124230269.jpg" - }, - { - "name": "Saigon", - "image_url": "http://thekoalition.com/images/2011/01/Saigon.jpg" - }, - { - "name": "Sam Sneed", - "image_url": "http://www.post-gazette.com/image/2013/10/17/Sam-Sneed.jpg" - }, - { - "name": "Sammy Adams", - "image_url": "https://s3.amazonaws.com/rapgenius/1374121236_Sam_Adams-Bostons_Boy_Album.jpg" - }, - { - "name": "San E", - "image_url": "http://www.soompi.com/wp-content/uploads/2014/10/1013-san-e.jpg" - }, - { - "name": "San Quinn", - "image_url": "http://www.therealspill.com/uploads/2/0/6/4/2064107/5641474.jpg" - }, - { - "name": "Sarkodie", - "image_url": "http://www.thecable.ng/wp-content/uploads/2015/06/sak1.jpg" - }, - { - "name": "Sauce Money", - "image_url": "http://images.rapgenius.com/709f4d670c0505627849b8664f8276de.422x530x1.jpg" - }, - { - "name": "Savage", - "image_url": "https://i1.wp.com/hypebeast.com/image/2016/08/off-white-2016-fw-collection-21-savage-lookbook-2.jpg" - }, - { - "name": "Scarface", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2015/10/SCARFACE.jpg" - }, - { - "name": "Schoolboy Q", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2014/02/Schoolboy_Q_Speaks_on_best_tde_rapper.jpg" - }, - { - "name": "Schoolly D", - "image_url": "http://www.rapmusicguide.com/blog/wp-content/uploads/2013/12/Schoolly-D-arms.jpg" - }, - { - "name": "Scott Storch", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2014/02/Scott-Storch_02-17-2014-300x300.jpg" - }, - { - "name": "Scotty", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/1/1b/Scotty_%28Scotty_ATL%29.jpg" - }, - { - "name": "Scram Jones", - "image_url": "http://i1.wp.com/allhiphop.com/wp-content/uploads/2013/10/scram-jones.jpg" - }, - { - "name": "Scribe", - "image_url": "http://static2.stuff.co.nz/1296207390/967/4595967.jpg" - }, - { - "name": "Scrilla", - "image_url": "http://media.nbcmiami.com/images/1200*675/Young-Scrilla.jpg" - }, - { - "name": "Scrufizzer", - "image_url": "http://jumpoff.tv/assets/images/made/assets/images/posts/12dec11_scrufizzer_war_MAIN_580_352.jpg" - }, - { - "name": "Sean Combs", - "image_url": "http://s1.ibtimes.com/sites/www.ibtimes.com/files/styles/lg/public/2012/04/20/265198-rapper-sean-diddy-combs.jpg" - }, - { - "name": "Sean Paul", - "image_url": "http://wac.450f.edgecastcdn.net/80450F/theboombox.com/files/2009/12/seanp_bbx_200_122309.jpg" - }, - { - "name": "Sean Price", - "image_url": "http://img2.timeinc.net/people/i/2015/news/150824/sean-price-435.jpg" - }, - { - "name": "Sean T", - "image_url": "http://www.talentedprofiles.com/wp-content/uploads/2016/02/Rapper-Big-Sean-600x600_t.jpg" - }, - { - "name": "Serengeti", - "image_url": "http://www.anticon.com/sites/default/files/imagecache/artist/White%20Collar%209.jpg" - }, - { - "name": "Serius Jones", - "image_url": "http://www2.pictures.zimbio.com/gi/Serius+Jones+Sean+Diddy+Combs+Hosts+Pool+Party+3ntNwfoXEMil.jpg" - }, - { - "name": "Sev Statik", - "image_url": "http://i.ytimg.com/vi/vbQhHs2_10s/maxresdefault.jpg" - }, - { - "name": "Sha Money XL", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-sha-money-xl-celebrity-singer-photos.jpg" - }, - { - "name": "Shabazz the Disciple", - "image_url": "http://i1.ytimg.com/vi/UzKEnCSozmA/maxresdefault.jpg" - }, - { - "name": "Shad", - "image_url": "http://www.chartattack.com/wp-content/uploads/2014/04/shad.jpg" - }, - { - "name": "Shade Sheist", - "image_url": "http://beatsandrhymesfc.com/wp-content/uploads/2012/04/ss-bb.jpg" - }, - { - "name": "Shady Nate", - "image_url": "http://bloximages.newyork1.vip.townnews.com/montereycountyweekly.com/content/tncms/assets/v3/editorial/e/b5/eb5a2b49-dc95-557a-9967-09f87b6818a1/519523d286802.image.jpg" - }, - { - "name": "Shaggy", - "image_url": "http://img.karaoke-lyrics.net/img/artists/10440/shaggy-132443.jpg" - }, - { - "name": "Shaggy 2 Dope", - "image_url": "http://www.faygoluvers.net/v5/wp-content/uploads/2013/05/Shaggy-2-Dope1.jpg" - }, - { - "name": "Shaquille O'Neal", - "image_url": "https://nextshark-vxdsockgvw3ki.stackpathdns.com/wp-content/uploads/2018/04/maxresdefault.jpg" - }, - { - "name": "Shawnna", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2012/05/Shawnna.jpg" - }, - { - "name": "Shawty Lo", - "image_url": "http://jusflippin.com/wp-content/uploads/2011/07/Shawty-Lo.jpg" - }, - { - "name": "Sheek Louch", - "image_url": "http://highlineballroom.com/assets/Sheek-Louch.jpg" - }, - { - "name": "Shing02", - "image_url": "http://media-cache-ec0.pinimg.com/736x/78/23/8e/78238e9ab30512cfb4d52af6ccb40292.jpg" - }, - { - "name": "Sho Baraka", - "image_url": "https://i0.wp.com/allhiphop.com/wp-content/uploads/2017/02/rapper-sho-baraka-banned-from-ch.jpg" - }, - { - "name": "Shock G", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/c/ca/ShkWiki9.jpg" - }, - { - "name": "Shorty", - "image_url": "http://starcasm.net/wp-content/uploads/2010/12/Magnolia-Shorty-490x445.jpg" - }, - { - "name": "Shorty Mack", - "image_url": "https://i0.wp.com/www.hip-hopvibe.com/wp-content/uploads/2013/01/Shorty-Mack.jpg" - }, - { - "name": "Shwayze", - "image_url": "http://www.aceshowbiz.com/images/wennpic/wenn5188509.jpg" - }, - { - "name": "Shy Glizzy", - "image_url": "http://www.trbimg.com/img-57a2315f/turbine/bal-shy-glizzy-young-jefe-2-cut-it-royal-farms-arena" - }, - { - "name": "Shyheim", - "image_url": "http://www.eurweb.com/wp-content/uploads/2014/07/Shyheim_04-24-2013-300x300.jpg" - }, - { - "name": "Shyne", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2012/01/Shyne-2.jpg" - }, - { - "name": "Silentó", - "image_url": "https://cmgajcmusic.files.wordpress.com/2015/06/silento-2.jpg" - }, - { - "name": "Silkk the Shocker", - "image_url": "http://2.bp.blogspot.com/_B1LlYh6iKqs/TK0gNRXZJ_I/AAAAAAAAC7g/vT3VW7tT8C4/s1600/silkk-the-shocker.jpg" - }, - { - "name": "Silla", - "image_url": "http://rap.de/wp-content/uploads/silla-rapde.png" - }, - { - "name": "Simon D", - "image_url": "http://3.bp.blogspot.com/-o789bRAl2C8/UaSCreHk8lI/AAAAAAAAD6A/671amIdPmxA/s1600/simon+d.jpg" - }, - { - "name": "Sir Jinx", - "image_url": "http://www.dubcnn.com/wp-content/uploads/2012/12/jinx-pic1000001.png" - }, - { - "name": "Sir Mix-a-Lot", - "image_url": "https://usatftw.files.wordpress.com/2018/03/pjimage-23-e1520876763340.jpg" - }, - { - "name": "Sirah", - "image_url": "http://www3.pictures.zimbio.com/gi/Sirah+55th+Annual+GRAMMY+Awards+Press+Room+AQYNmS4HQPUl.jpg" - }, - { - "name": "Skee-Lo", - "image_url": "http://whatisplayinginmyitunes.files.wordpress.com/2012/12/skee-lo.jpg" - }, - { - "name": "Skeme", - "image_url": "http://rapdose.com/wp-content/uploads/2014/05/Skeme-Believe.jpg" - }, - { - "name": "Skepta", - "image_url": "http://runthetrap.com/wp-content/uploads/2015/03/skepta-50bb835c0adb7.jpg" - }, - { - "name": "Skinnyman", - "image_url": "http://eslhiphop.com/wp-content/uploads/2013/06/skinnyman.png" - }, - { - "name": "Skooly", - "image_url": "https://i.ytimg.com/vi/6mZHPf-ZvFA/maxresdefault.jpg" - }, - { - "name": "Skyzoo", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2009/05/skyzoo.jpg" - }, - { - "name": "SL Jones", - "image_url": "http://www.audibletreats.com/Media/newspics/SL_Jones-08.jpg" - }, - { - "name": "Sleepy Brown", - "image_url": "http://s3.amazonaws.com/rapgenius/sleepy-brown-129.jpg" - }, - { - "name": "Slick Rick", - "image_url": "http://jobbiecrew.com/wp-content/uploads/2015/04/0slickrick2.jpg" - }, - { - "name": "Slim Jxmmi", - "image_url": "https://images.genius.com/7e898276f657a3d38d0febfee65a7280.640x640x1.jpg" - }, - { - "name": "Slim Thug", - "image_url": "http://1.bp.blogspot.com/-qfh469KzybM/TkT0T9dTN9I/AAAAAAAAAY8/uB-uoTiwCv4/s1600/Slim-Thug-Rapper-Gun.jpg" - }, - { - "name": "Slug", - "image_url": "http://unspokenstyle.files.wordpress.com/2011/01/slug.jpg" - }, - { - "name": "Smitty", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Smitty-rapper.jpg/1200px-Smitty-rapper.jpg" - }, - { - "name": "Smoke DZA", - "image_url": "http://massappeal.com/wp-content/uploads/2014/03/smoke-dza-.png" - }, - { - "name": "Smooth", - "image_url": "https://i.ytimg.com/vi/KmtRq-iucII/maxresdefault.jpg" - }, - { - "name": "Smoothe da Hustler", - "image_url": "https://i.ytimg.com/vi/e_zc1qBlu-c/maxresdefault.jpg" - }, - { - "name": "Sniper J", - "image_url": "http://medias.2kmusic.com/uploads/2010/03/19/img-1269021506-cb5ef53ef55ea9e90358d91d4e4b25f7.jpg" - }, - { - "name": "Snoop Dogg", - "image_url": "https://media.nbcnewyork.com/images/1200*675/Snoop+Dogg3.jpg" - }, - { - "name": "Snootie Wild", - "image_url": "https://images.genius.com/e0d17ec7650545545dabdf923613065c.600x600x1.jpg" - }, - { - "name": "Snow Tha Product", - "image_url": "http://blog.krizzkaliko.com/wp-content/uploads/2012/11/SNow-On-Kaliko.jpg" - }, - { - "name": "Soce the elemental wizard", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Soce.jpg/1200px-Soce.jpg" - }, - { - "name": "Sole", - "image_url": "http://lobermanhiphop.files.wordpress.com/2013/02/sole.jpg" - }, - { - "name": "Solzilla", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Sol_%28Seattle_rapper%29_03.jpg/1200px-Sol_%28Seattle_rapper%29_03.jpg" - }, - { - "name": "Sonny Digital", - "image_url": "https://nationofbillions.com/wp-content/uploads/2016/07/SONNYDIGITAL_1_Wireless.jpg" - }, - { - "name": "SonReal", - "image_url": "http://www.digitaljournal.com/img/2/7/4/3/7/7/i/1/7/0/o/BWsuit_15.JPG" - }, - { - "name": "Sonsee", - "image_url": "http://img2-ak.lst.fm/i/u/avatar170s/ed38054f95d64ddf9b5e6a53f9702497.jpg" - }, - { - "name": "Soopafly", - "image_url": "http://cache1.asset-cache.net/xc/136582867-rapper-soopafly-visits-the-late-show-with-filmmagic.jpg" - }, - { - "name": "Soulja Boy", - "image_url": "http://www.wallpaperup.com/uploads/wallpapers/2014/03/04/284914/a82261adb443e8646b88831a16649ffe.jpg" - }, - { - "name": "Soulja Slim", - "image_url": "http://listofdeadrappers.files.wordpress.com/2011/09/soulja_slim.jpg" - }, - { - "name": "South Park Mexican", - "image_url": "http://ww3.hdnux.com/photos/04/30/72/1150654/0/960x540.jpg" - }, - { - "name": "Southside", - "image_url": "https://i.ytimg.com/vi/L95z8KjFPZo/maxresdefault.jpg" - }, - { - "name": "SpaceGhostPurrp", - "image_url": "http://images1.miaminewtimes.com/imager/spaceghostpurrp-will-not-abide-the-fakes/u/original/6387038/7915708.0.jpg" - }, - { - "name": "Special Ed", - "image_url": "http://i1.ytimg.com/vi/XXOHX9HBeXk/maxresdefault.jpg" - }, - { - "name": "Spice 1", - "image_url": "http://s3.amazonaws.com/rapgenius/1378684361_tumblr_mi7aw9N3cj1qzx6s2o1_500.jpg" - }, - { - "name": "Spider Loc", - "image_url": "http://www.datwav.com/wp-content/uploads/2017/06/G-Unit-Rapper-Spider-Loc.jpg" - }, - { - "name": "Spoonie Gee", - "image_url": "http://www.boogitybeat.com/images/BB13068.jpg" - }, - { - "name": "Spose", - "image_url": "http://img3.wikia.nocookie.net/__cb20111103180051/rap/images/d/d7/Spose.png" - }, - { - "name": "Spot", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/6/6c/SPOT_January_2012_Photoshoot.jpg" - }, - { - "name": "Stalley", - "image_url": "http://blahblahblahscience.com/wp-content/uploads/2014/09/stalley.jpg" - }, - { - "name": "Starlito", - "image_url": "http://factmag-images.s3.amazonaws.com/wp-content/uploads/2010/02/starlito-sq-39399222.jpg" - }, - { - "name": "Stat Quo", - "image_url": "http://hiphopscholar.files.wordpress.com/2008/09/stat_quo.jpg" - }, - { - "name": "Static Major", - "image_url": "http://3.bp.blogspot.com/_DuzxFBl8bfQ/TKdHgLphHHI/AAAAAAAAAlA/e4rL00OHn-I/s1600/static+major+1.jpg" - }, - { - "name": "Statik Selektah", - "image_url": "http://www.tunecore.com/blog/wp-content/uploads/2015/05/statik.selektah-actual_wide-970ed5943d6d8b812a0e74a38aac7f8a2d1ae196-s6-c30.jpg" - }, - { - "name": "Steady B", - "image_url": "http://ring.cdandlp.com/lower/photo_grande/115261787.jpg" - }, - { - "name": "Stevie Joe", - "image_url": "http://siccness.net/wp/wp-content/uploads/2016/02/Stevie_Joe.jpg" - }, - { - "name": "Stevie Stone", - "image_url": "http://faygoluvers.net/v5/wp-content/uploads/2012/09/steviestone101912.jpg" - }, - { - "name": "Stezo", - "image_url": "http://phaseonemusic.com/wp-content/uploads/2012/02/STEZO+FREAK+THE+FUNK+COVER+1.jpg" - }, - { - "name": "Stitches", - "image_url": "https://pmchollywoodlife.files.wordpress.com/2015/12/stitches-the-game-insta-ftr.jpg" - }, - { - "name": "Sticky Fingaz", - "image_url": "http://interestingcelebrities.com/pictures/sticky_fingaz.jpg" - }, - { - "name": "Stoka", - "image_url": "https://www.hhunity.org/wp-content/uploads/2014/11/Stoka-Agram-Audio.png" - }, - { - "name": "Stoupe the Enemy of Mankind", - "image_url": "http://images.rapgenius.com/8ed6eb4ea61413e34c0a67f5e04f3b3c.600x340x1.jpg" - }, - { - "name": "Stormzy", - "image_url": "https://d.ibtimes.co.uk/en/full/1475476/stormzy.jpg" - }, - { - "name": "Stretch", - "image_url": "http://img2.wikia.nocookie.net/__cb20130717123821/hip-hop-music/images/6/69/Stretch.jpg" - }, - { - "name": "Styles P", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-styles-p-singer-celebrity-hip-hop.jpg" - }, - { - "name": "Substantial", - "image_url": "http://www.hiphopsite.com/wp-content/uploads/2010/06/Substantial.jpg" - }, - { - "name": "Suga Free", - "image_url": "http://s3.amazonaws.com/rapgenius/1361309346_l.jpg" - }, - { - "name": "Suffa", - "image_url": "http://i.dailymail.co.uk/i/pix/2015/02/28/262B331400000578-2973396-In_the_zone_The_Suffa_MC_from_the_Hilltop_Hoods_also_took_to_the-a-13_1425134138732.jpg" - }, - { - "name": "Swagg Man", - "image_url": "http://www.famousbirthdays.com/headshots/swagg-man-7.jpg" - }, - { - "name": "Sweet Tee", - "image_url": "http://www.rapindustry.com/sweet_tee_in.jpg" - }, - { - "name": "Swings", - "image_url": "http://cdn.koreaboo.com/wp-content/uploads/2014/11/htm_20141106171738c010c0111.jpg" - }, - { - "name": "Swizz Beatz", - "image_url": "http://www1.pictures.zimbio.com/gi/Swizz+Beatz+40th+American+Music+Awards+Arrivals+cTuFcCa4A2sl.jpg" - }, - { - "name": "SwizZz", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-swizzz-star-rap-hip-hop.jpg" - }, - { - "name": "Syd Tha Kyd", - "image_url": "https://i1.wp.com/hypebeast.com/image/2012/03/syd-tha-kyd-by-lance-bangs-edit-0.jpg" - }, - { - "name": "SZA", - "image_url": "http://www.billboard.com/files/styles/promo_650/public/media/sza-650.jpg" - }, - { - "name": "T.I.", - "image_url": "http://media-cache-ec0.pinimg.com/736x/d8/6e/c4/d86ec4828778e4997a77313619d0d370.jpg" - }, - { - "name": "Tyler the Creator", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/fa/52/a6/fa52a6dd8c76a533092056f173900e80.jpg" - }, - { - "name": "T La Rock", - "image_url": "http://okp-cdn.okayplayer.com/wp-content/uploads/2017/10/Screen-Shot-2017-10-20-at-9.55.59-PM-715x719.png" - }, - { - "name": "T-Bone", - "image_url": "http://www.christianmusic.com/PHOTOS/t_bone-2.jpg" - }, - { - "name": "T-Nutty", - "image_url": "http://siccness.net/wp/wp-content/uploads/2016/02/nutty.png" - }, - { - "name": "T-Pain", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2014/02/grammy-award-winning-rapper-t-pain-Talks-Chance-The-Rapper.jpg" - }, - { - "name": "T-Wayne", - "image_url": "https://images.rapgenius.com/822e2be4f84c2ecdada82f017f75b7fb.960x960x1.jpg" - }, - { - "name": "T. Mills", - "image_url": "http://www2.pictures.zimbio.com/gi/T+Mills+Arrivals+Young+Hollywood+Awards+Part+NJl90h8MOpal.jpg" - }, - { - "name": "T.I.", - "image_url": "http://media-cache-ec0.pinimg.com/736x/d8/6e/c4/d86ec4828778e4997a77313619d0d370.jpg" - }, - { - "name": "T.O.P", - "image_url": "http://i2.asntown.net/h2/Korea/7/kpop-bigbang/TOP-bigbang-fashion06.jpg" - }, - { - "name": "Tabi Bonney", - "image_url": "https://thisguysworld.files.wordpress.com/2010/07/tabi.jpg" - }, - { - "name": "Tablo", - "image_url": "http://images5.fanpop.com/image/photos/30500000/Tablo-tablo-30511618-333-500.jpg" - }, - { - "name": "Taio Cruz", - "image_url": "http://colunas.multishowfm.globoradio.globo.com/platb/files/806/2010/11/Taio-Cruz-.jpg" - }, - { - "name": "Talib Kweli", - "image_url": "http://media2.fdncms.com/orlando/imager/u/original/2404812/talib_kweli.jpg" - }, - { - "name": "Target", - "image_url": "http://static5.businessinsider.com/image/4e9876896bb3f74864000017/rapper-rick-ross-was-the-target-of-a-drive-by-shooting-in-florida.jpg" - }, - { - "name": "Tay Dizm", - "image_url": "http://static.djbooth.net/pics-artist-rec/Tay_Dizm_1.jpg" - }, - { - "name": "Tay-K", - "image_url": "https://hypb.imgix.net/image/2017/10/ybn-nahmir-tay-k-the-race-remix-0.jpg" - }, - { - "name": "TD Cruze", - "image_url": "http://www.berliner-kurier.de/image/26201312/max/600/450/49d662a34b7203e4183c320bf0785416/UF/ted-cruz.jpg" - }, - { - "name": "Teairra Marí", - "image_url": "http://hw-img.datpiff.com/mb740e75/Teairra_Marie_Unfinished_Business-front-large.jpg" - }, - { - "name": "Tech N9ne", - "image_url": "http://vegasimpulse.files.wordpress.com/2012/04/tech-n9ne-2.jpg" - }, - { - "name": "Tedashii", - "image_url": "http://images.christianpost.com/full/75663/tedashii.jpg" - }, - { - "name": "TeeFlii", - "image_url": "http://www3.pictures.zimbio.com/gi/TeeFlii+BET+AWARDS+14+Day+1+Kp7kNUhHFvyl.jpg" - }, - { - "name": "Tee Grizzley", - "image_url": "https://i1.wp.com/hypebeast.com/image/2016/11/detroit-rapper-tee-grizzley-first-day-out-video-0.jpg" - }, - { - "name": "Tekitha", - "image_url": "http://rollingout.com/wp-content/uploads/2015/12/Anthony-Hamilton-380x280.jpg" - }, - { - "name": "Tela", - "image_url": "http://nebula.wsimg.com/88482f2967eb7b323b8df1fd1eff4d3a" - }, - { - "name": "Termanology", - "image_url": "http://static.djbooth.net/pics-artist/termanology.jpg" - }, - { - "name": "Terrace Martin", - "image_url": "https://lastfm-img2.akamaized.net/i/u/57e7f81f5b8f4d2daea075100bf0473a.png" - }, - { - "name": "Teyana Taylor", - "image_url": "http://worldofblackheroes.files.wordpress.com/2012/03/teyana-taylor-17.jpg" - }, - { - "name": "Tha Chill", - "image_url": "http://steadydippin.com/wp-content/uploads/Tha-Chill.jpg" - }, - { - "name": "Tha City Paper", - "image_url": "http://hw-img.datpiff.com/ma42439e/Tha_City_Paper_Paper_Aka_Tha_City_Paper_paper_Vie-front-large.jpg" - }, - { - "name": "Tha Trademarc", - "image_url": "http://s3.amazonaws.com/rapgenius/1363145335_John%20Cena%20%20Tha%20Trademarc%20JOHN_CENA___THA_TRADEMARCcolti.jpg" - }, - { - "name": "The-Dream", - "image_url": "http://thatgrapejuice.net/wp-content/uploads/2010/06/the-dream1.jpg" - }, - { - "name": "Theophilus London", - "image_url": "http://www2.pictures.zimbio.com/gi/Theophilus+London+Carlos+Campos+Presentation+NeU7Y7HGrqwl.jpg" - }, - { - "name": "Tiffany Foxx", - "image_url": "http://bloximages.newyork1.vip.townnews.com/stltoday.com/content/tncms/assets/v3/editorial/b/2c/b2cd2408-9233-5bff-9a8b-5d39bafa4029/50928305bb4ba.preview-620.jpg" - }, - { - "name": "Tim Dog", - "image_url": "http://assets.rollingstone.com/assets/2014/article/tim-dog-rapper-accused-of-faking-death-confirmed-dead-20140916/168425/large_rect/1401x788-retna2059832.jpg" - }, - { - "name": "Timaya", - "image_url": "http://www.360nobs.com/wp-content/uploads/2015/01/Jahbless1.jpg" - }, - { - "name": "Timbaland", - "image_url": "http://live.drjays.com/wp-content/uploads/2009/12/timbaland.jpg" - }, - { - "name": "Timbe", - "image_url": "http://live.drjays.com/wp-content/uploads/2009/12/timbaland.jpg" - }, - { - "name": "Tinie Tempah", - "image_url": "http://dollyumez.files.wordpress.com/2013/01/tinie-tempah.jpg" - }, - { - "name": "Tink (musician)", - "image_url": "https://cocoocd.files.wordpress.com/2013/04/228090_507024229347791_1052484062_n.jpg" - }, - { - "name": "TobyMac", - "image_url": "https://s-media-cache-ak0.pinimg.com/564x/e7/a8/9e/e7a89ea48cbf5822fb909ec267b79499.jpg" - }, - { - "name": "Tone Lōc", - "image_url": "http://media-cache-ak0.pinimg.com/736x/68/72/60/6872607c9c8e2dacc6a52a776d4b843a.jpg" - }, - { - "name": "Tone Trump", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2012/06/tonetrump.jpg" - }, - { - "name": "Tonedeff", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/5/51/Tonedefflive.png" - }, - { - "name": "Toni Blackman", - "image_url": "http://www.womex.com/virtual/image/artist/toni_blackman_big_29989.jpg" - }, - { - "name": "Tony Yayo", - "image_url": "http://zmldajoker.com/wp-content/uploads/2012/08/Tony-Yayo.jpg" - }, - { - "name": "Too Short", - "image_url": "https://escobar300.files.wordpress.com/2011/08/tooshort.jpg" - }, - { - "name": "Torch (American)", - "image_url": "http://1.bp.blogspot.com/-XU66PTiuIIA/T36hTMQCXJI/AAAAAAAAAcU/5rfwuQ6TWAU/s1600/Torch_(US_rapper).jpg" - }, - { - "name": "Torch (German)", - "image_url": "http://leaveyournineathome.files.wordpress.com/2007/10/torch-blauer_samt.jpg" - }, - { - "name": "Tory Lanez", - "image_url": "http://cdn.ratedrnb.com/2016/10/tory-lanez.jpg" - }, - { - "name": "Tracey Lee", - "image_url": "http://madrapper.com/wp-content/uploads/2010/12/Lee1.jpg" - }, - { - "name": "Trae tha Truth", - "image_url": "https://media.thehypemagazine.com/wp-content/uploads/2018/04/trae-tha-truth-paras-griffin-1024x683.jpg" - }, - { - "name": "Tragedy Khadafi", - "image_url": "http://assets2.vice.com/images/content-images/2014/12/29/tragedy-khadafi-is-still-queensbridges-realest-456-body-image-1419881556.jpg" - }, - { - "name": "Travis Scott", - "image_url": "http://www1.pictures.zimbio.com/gi/Travis+Scott+Arrivals+BET+Awards+54GWNTvi2gDl.jpg" - }, - { - "name": "Traxamillion", - "image_url": "https://cbarap.files.wordpress.com/2014/05/traxamillion.jpg" - }, - { - "name": "Tray Deee", - "image_url": "https://unitedgangs.files.wordpress.com/2013/07/ta36tw1.jpg" - }, - { - "name": "Treach", - "image_url": "https://ionehellobeautiful.files.wordpress.com/2016/01/14520871976376.jpg" - }, - { - "name": "Trey Songz", - "image_url": "http://www.creativefan.com/important/cf/2012/08/trey-songz-tattoo/trey-songz-body-tattoo.jpg" - }, - { - "name": "Trick Daddy", - "image_url": "https://i1.wp.com/celebritybio.org/wp-content/uploads/2014/08/Trick-Daddy-Net-Worth.jpg" - }, - { - "name": "Trick-Trick", - "image_url": "http://ctt.marketwire.com/" - }, - { - "name": "Trina", - "image_url": "http://www.missxpose.com/wp-content/uploads/2011/11/trina-bet-photo-shoot-4.jpg" - }, - { - "name": "Trinidad James", - "image_url": "https://cbshot937.files.wordpress.com/2012/12/trinidad_james10.jpg" - }, - { - "name": "Trip Lee", - "image_url": "http://www.eewmagazine.com/images/Trip-Lee-Good-life.jpg" - }, - { - "name": "Trippie Redd", - "image_url": "http://hiphopheads.net/wp-content/uploads/2017/08/Trippie-Redd-1.jpg" - }, - { - "name": "Tristan Wilds", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/8c/a2/44/8ca24487846ce97afbae1ff967fb78c5--tristan-wilds-rapper.jpg" - }, - { - "name": "Troy Ave", - "image_url": "http://www.ballerstatus.com/wp-content/uploads/2016/05/tave.jpg" - }, - { - "name": "Tupac Shakur", - "image_url": "http://sahiphopmag.co.za/wp-content/uploads/2016/05/Rap-artist-Tupac-Shakur.jpg" - }, - { - "name": "Turf Talk", - "image_url": "http://www.wallpaperup.com/uploads/wallpapers/2013/12/01/181203/19ce36a2c8ceed416ad66fa6d96db889.jpg" - }, - { - "name": "Turk", - "image_url": "http://www.brothersonsports.com/wp-content/uploads/2014/12/turkandwayne.jpg" - }, - { - "name": "Tweedy Bird Loc", - "image_url": "http://steadydippin.com/wp-content/uploads/Tweedy-Bird-Loc.jpg" - }, - { - "name": "Twista", - "image_url": "http://www.trbimg.com/img-53503146/turbine/ct-twista-chicago-rap-durty-nellies-20140417-001/2048/1365x2048" - }, - { - "name": "Twisted Insane", - "image_url": "http://assets.audiomack.com/rap-ebashit/f6e96e7ed6b0e965062aaed0ab4a9983.jpeg" - }, - { - "name": "Ty Dolla Sign", - "image_url": "http://www.rapbasement.com/wp-content/uploads/2015/01/tydolla.jpg" - }, - { - "name": "Tyga", - "image_url": "http://www.eurweb.com/wp-content/uploads/2015/07/tyga.jpg" - }, - { - "name": "Tyler Joseph", - "image_url": "https://s-media-cache-ak0.pinimg.com/564x/02/1c/b9/021cb958f946bfa08f64aea9d90b1a5b.jpg" - }, - { - "name": "Tyler The Creator", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/fa/52/a6/fa52a6dd8c76a533092056f173900e80.jpg" - }, - { - "name": "Tyra Bolling", - "image_url": "http://www.famousbirthdays.com/faces/bolling-tyra-image.jpg" - }, - { - "name": "Verbal Jint", - "image_url": "https://www.allkpop.com/upload/2017/09/af_org/22120146/verbal-jint.jpg" - }, - { - "name": "U-God", - "image_url": "http://assets.rollingstone.com/assets/2014/albumreview/wu-tang-clan-a-better-tomorrow-20141218/178198/large_rect/1418859659/1401x788-Wu_Tang_Clan_JW_WBR_107(2).JPG" - }, - { - "name": "Ugly God", - "image_url": "http://dailychiefers.com/wp-content/media/2016/03/ugly-god.jpg" - }, - { - "name": "Uncle Murda", - "image_url": "http://hiphop-n-more.com/wp-content/uploads/2015/01/uncle-murda-2014-rap-up.jpg" - }, - { - "name": "Unk", - "image_url": "http://antoniofam.files.wordpress.com/2011/05/dj_unk.jpg" - }, - { - "name": "U$O", - "image_url": "http://images.stiften.dk/22/63622_1200_0_0_35_1921_1200_2.jpg" - }, - { - "name": "Wyclef Jean", - "image_url": "http://media.gettyimages.com/photos/rapper-wyclef-jean-performs-in-concert-at-brooklyn-bowl-on-march-29-picture-id518080012" - }, - { - "name": "V-Nasty", - "image_url": "http://images.complex.com/complex/image/upload/c_limit,w_680/fl_lossy,pg_1,q_auto/daxoo4uvasmysgziontg.jpg" - }, - { - "name": "V.I.C.", - "image_url": "http://www.bigbloc.com/proto/images/hiphop/V.I.C._.jpg" - }, - { - "name": "Vado", - "image_url": "http://messymandella.files.wordpress.com/2012/10/vado_.jpg" - }, - { - "name": "Vakill", - "image_url": "http://thamidwest.com/wp-content/uploads/Vakill.png" - }, - { - "name": "Val Young", - "image_url": "https://i.ytimg.com/vi/uxy12OgQL54/maxresdefault.jpg" - }, - { - "name": "Valete", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/0/04/ValeteRapper.jpg" - }, - { - "name": "Vanilla Ice", - "image_url": "http://i.dailymail.co.uk/i/pix/2008/04_02/iceLFI1104_468x762.jpg" - }, - { - "name": "Vast Aire", - "image_url": "http://exclaim.ca/images/vast1.jpg" - }, - { - "name": "Verbal Jint", - "image_url": "https://www.allkpop.com/upload/2017/09/af_org/22120146/verbal-jint.jpg" - }, - { - "name": "Verse Simmonds", - "image_url": "https://i0.wp.com/allhiphop.com/wp-content/uploads/2012/02/verse-simmonds-1.png" - }, - { - "name": "Vic Mensa", - "image_url": "http://media-cache-ak0.pinimg.com/736x/91/aa/c1/91aac1b95e805342d2225913359ebd2b.jpg" - }, - { - "name": "Vince Staples", - "image_url": "http://i.dailymail.co.uk/i/newpix/2018/04/16/21/4B25AB0C00000578-5622755-image-a-24_1523909544359.jpg" - }, - { - "name": "Vinnie Paz", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-vinnie-paz-celebrity-song-rap.jpg" - }, - { - "name": "Violent J", - "image_url": "http://www.celebdirtylaundry.com/wp-content/uploads/violent-j-stereo-stolen.jpg" - }, - { - "name": "Viper", - "image_url": "http://static.qobuz.com/images/covers/14/83/3610154048314_600.jpg" - }, - { - "name": "VL Mike", - "image_url": "http://themusicsover.com/wp-content/uploads/2008/04/vlmike2.jpg" - }, - { - "name": "Xzibit", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.330091.1314424298!/img/httpImage/image.jpg_gen/derivatives/article_1200/amd-xzibit-jpg.jpg" - }, - { - "name": "Waka Flocka Flame", - "image_url": "http://2.bp.blogspot.com/-32xuBz8T-dk/T8oPlIH1PjI/AAAAAAAAqb0/pVwsKQXbigU/s1600/99_waka-flocka.jpg" - }, - { - "name": "Wale", - "image_url": "http://upbjmu.files.wordpress.com/2010/04/wale.jpg" - }, - { - "name": "Warren G", - "image_url": "http://images.huffingtonpost.com/2014-06-23-WarrenG.jpg" - }, - { - "name": "Warryn Campbell", - "image_url": "http://www.famousbirthdays.com/thumbnails/campbell-warryn-medium.jpg" - }, - { - "name": "Watsky", - "image_url": "http://www.gannett-cdn.com/-mm-/ac1394dbdcca6a36cbf486633b129cd813095ac3/r=x404&c=534x401/local/-/media/USATODAY/USATODAY/2013/05/23/1369356877000-image-1305232056_4_3.jpg" - }, - { - "name": "Wax (rapper)", - "image_url": "http://s3.amazonaws.com/rapgenius/Big_Wax_in_Front_of_a_Fence.jpg" - }, - { - "name": "WC", - "image_url": "http://www3.pictures.zimbio.com/fp/Wc+WC+Performing+In+Vancouver+RlfP1TE81Vzl.jpg" - }, - { - "name": "Webbie", - "image_url": "https://messymandella.files.wordpress.com/2012/08/16.jpg" - }, - { - "name": "The Weeknd", - "image_url": "http://factmag-images.s3.amazonaws.com/wp-content/uploads/2013/02/the_weeknd_0205131.jpg" - }, - { - "name": "Westside Gunn", - "image_url": "http://247hiphopnews.com/wp-content/uploads/2017/06/WestSide-Gunn-JAYFORCE.COM_.png" - }, - { - "name": "Wikluh Sky", - "image_url": "https://a4-images.myspacecdn.com/images01/12/691bb215a8b471aa86fc0f11a46ddfdb/full.jpg" - }, - { - "name": "Will Smith", - "image_url": "http://www.clashmusic.com/sites/default/files/styles/article_feature/public/legacy/files/willsmith-freshprince.jpg" - }, - { - "name": "will.i.am", - "image_url": "http://i1.tribune.com.pk/wp-content/uploads/2013/04/538192-image-1366470972-209-640x480.JPG" - }, - { - "name": "Willie D", - "image_url": "http://siccness.net/wp/wp-content/uploads/2013/02/Willie-D-blackchair1.jpg" - }, - { - "name": "Willie the Kid", - "image_url": "http://static.djbooth.net/pics-artist/williethekid.jpg" - }, - { - "name": "Willow Smith", - "image_url": "http://i.dailymail.co.uk/i/pix/2010/12/26/article-1341770-0C9599D1000005DC-328_468x531.jpg" - }, - { - "name": "Willy Northpole", - "image_url": "http://www.bet.com/topics/w/willy-northpole/_jcr_content/image.heroimage.dimg/__1378865366064/081012-topic-music-willy-northpole.jpg" - }, - { - "name": "Wish Bone", - "image_url": "http://djrushmusic.files.wordpress.com/2011/05/wish.jpg" - }, - { - "name": "Witchdoctor", - "image_url": "http://www.cocaineblunts.com/blunts/wp-content/uploads/2007/11/witch3.jpg" - }, - { - "name": "Wiz Khalifa", - "image_url": "http://farm8.staticflickr.com/7154/6622364753_1b2ab906b4.jpg" - }, - { - "name": "Wizkid", - "image_url": "http://www.thenet.ng/wp-content/uploads/2012/07/wale_wizkid-1.jpg" - }, - { - "name": "Wrekonize", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/a6/03/8d/a6038dcfe1d2068294fd3991603bedcf.jpg" - }, - { - "name": "Wyclef Jean", - "image_url": "http://media.gettyimages.com/photos/rapper-wyclef-jean-performs-in-concert-at-brooklyn-bowl-on-march-29-picture-id518080012" - }, - { - "name": "X-Raided", - "image_url": "http://siccness.net/wp/wp-content/uploads/2016/03/xraided.jpg" - }, - { - "name": "XV", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/XV_performance_Dancefestopia_2013_2014-01-28_00-39.jpg/1200px-XV_performance_Dancefestopia_2013_2014-01-28_00-39.jpg" - }, - { - "name": "Xzibit", - "image_url": "http://assets.nydailynews.com/polopoly_fs/1.330091.1314424298!/img/httpImage/image.jpg_gen/derivatives/article_1200/amd-xzibit-jpg.jpg" - }, - { - "name": "XXXTentacion", - "image_url": "https://dancehallhiphop.com/wp-content/uploads/2017/12/XXXTentacion-rapper-800x565.jpg" - }, - { - "name": "X Clan", - "image_url": "http://www.xxlmag.com/files/2015/03/x-clan-feat2.jpg" - }, - { - "name": "Yolandi Visser", - "image_url": "https://i.pinimg.com/originals/d0/fd/b2/d0fdb22ac606edd8f1bd15abd8d67faa.jpg" - }, - { - "name": "Young Jeezy", - "image_url": "http://pennylibertygbow.files.wordpress.com/2012/02/youngjeezy3.jpg" - }, - { - "name": "Ya Boy", - "image_url": "http://www.yorapper.com/Photos/ya-boy-rapper.jpg" - }, - { - "name": "Yaki Kadafi", - "image_url": "https://40.media.tumblr.com/d340fd47b49c53270809188a633dd53b/tumblr_nnhcxjUrQ41tm7i3uo1_500.jpg" - }, - { - "name": "Yazz The Greatest", - "image_url": "http://l7.alamy.com/zooms/ed94feec8a1b4ca9a29552f71c0625ac/philadelphia-pa-usa-15th-may-2016-american-rapper-yazz-the-greatest-g1ppkr.jpg" - }, - { - "name": "YBN Nahmir", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/YBN-Nahmir-2018.png/1200px-YBN-Nahmir-2018.png" - }, - { - "name": "YC", - "image_url": "http://www2.pictures.zimbio.com/gi/YC+BET+Awards+11+Press+Room+RRqra9BH48Ql.jpg" - }, - { - "name": "YDG", - "image_url": "http://www.bntnews.co.uk/images/news/2014/z7rqhmv4d3wjw5nffyob82va89tuo6me.jpg" - }, - { - "name": "Yelawolf", - "image_url": "http://3.bp.blogspot.com/-tOoHr9RZwUA/TjvmXyGFLWI/AAAAAAAAALg/tNy7P4_BWy4/s1600/Yelawolf-994x1024.jpg" - }, - { - "name": "YFN Lucci", - "image_url": "https://www.trapworldhiphop.com/wp-content/uploads/YFN-Lucci.jpg" - }, - { - "name": "YG", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/39/40/81/3940811a323f82520ca394aa7cec923f.jpg" - }, - { - "name": "Yo Gotti", - "image_url": "https://s-media-cache-ak0.pinimg.com/736x/b8/25/70/b82570e97603b42d3ff6f8dc26d2d0c5.jpg" - }, - { - "name": "Yo Yo Honey Singh", - "image_url": "http://media2.intoday.in/indiatoday/images/stories/honey-4_650_090214091121.jpg" - }, - { - "name": "Yoon Mi-rae", - "image_url": "http://media.tumblr.com/tumblr_m5sq6s8kb41r84myb.jpg" - }, - { - "name": "Young Bleed", - "image_url": "http://udgsounds.com/wp-content/uploads/2016/01/young-bleed-pic-for-interview-1.jpg" - }, - { - "name": "Young Buck", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-young-buck-hip-hop-fame-rap.jpg" - }, - { - "name": "Young Chop", - "image_url": "http://rollingout.com/wp-content/uploads/2014/11/young_chop.jpg" - }, - { - "name": "Young Chris", - "image_url": "http://hustlebunny.com/content/2012/08/young-chris-rapper.jpg" - }, - { - "name": "Young Dolph", - "image_url": "http://img.wennermedia.com/social/rs-young-dolph-v1-d999645e-11e4-4549-b0f6-61af8728931a.jpg" - }, - { - "name": "Young Dre the Truth", - "image_url": "https://www.rapmusicguide.com/amass/images/inventory/9868/young%20dre-rev-4.jpg" - }, - { - "name": "Young Dro", - "image_url": "http://www.sohh.com/wp-content/uploads/2014/06/young-dro-2012-11-10-300x3001.jpg" - }, - { - "name": "Young Greatness", - "image_url": "http://hw-static.hiphopearly.com/images/tracks/3/Trappin-t32631-large.jpg" - }, - { - "name": "Young Jeezy", - "image_url": "http://pennylibertygbow.files.wordpress.com/2012/02/youngjeezy3.jpg" - }, - { - "name": "Young M.A", - "image_url": "https://cmga360music.files.wordpress.com/2017/03/youngma_img_9284_mike-marquez.jpg" - }, - { - "name": "Young Maylay", - "image_url": "http://www.hip-hopvibe.com/wp-content/uploads/2013/03/Young-Maylay-3.jpg" - }, - { - "name": "Young MC", - "image_url": "http://www1.pictures.zimbio.com/gi/Young+MC+Screening+Lionsgate+Films+Expendables+A6aEODh_pRRl.jpg" - }, - { - "name": "Young Noble", - "image_url": "https://www.strangemusicinc.com/wp-content/uploads/2011/09/Noble.jpg" - }, - { - "name": "Young Scooter", - "image_url": "http://www.4umf.com/wp-content/uploads/2013/04/Rapper-Young-Scooter-Arrested.jpg" - }, - { - "name": "Young Thug", - "image_url": "https://djbooth.net/.image/t_share/MTU0NzgzMTA4OTEzMTc3NzI3/chance-the-rapper-project-with-young-thug.jpg" - }, - { - "name": "YoungBoy Never Broke Again", - "image_url": "http://image.nola.com/home/nola-media/width600/img/crime_impact/photo/youngboy-never-broke-again-499fcd968041c808.jpg" - }, - { - "name": "Your Old Droog", - "image_url": "https://2.bp.blogspot.com/-LBeysv-KSHw/V1oDq2dOglI/AAAAAAAAIZc/NiQPZ0DOaBgHnZiILbvwTUJfSHT7vNMTQCLcB/s1600/Your%2BOld%2BDroog.jpg" - }, - { - "name": "Yubin", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-yubin-celebrity-hip-hop-singer-style.jpg" - }, - { - "name": "Yukmouth", - "image_url": "http://content9.flixster.com/photo/13/58/04/13580447_ori.jpg" - }, - { - "name": "Yung Berg", - "image_url": "http://4.bp.blogspot.com/_sBPAPP8w_cA/TLgdFYjm7FI/AAAAAAAAAtU/blx-d1WJmMQ/s1600/yung%20berg.jpg" - }, - { - "name": "Yung Joc", - "image_url": "http://www.judiciaryreport.com/images_4/yung-joc-5-12-15-3.jpg" - }, - { - "name": "Yung L.A.", - "image_url": "http://2.bp.blogspot.com/_WcsRR3fKzyU/TPZx4zZP_9I/AAAAAAAAAXY/z7SrxMG6ehs/s1600/yung_la_1.jpg" - }, - { - "name": "Yung Lean", - "image_url": "http://content.acclaimmag.com/content/uploads/2016/04/yung-lean3-600x400.jpg" - }, - { - "name": "Yung Ro", - "image_url": "http://c3.cduniverse.ws/resized/250x500/music/135/7350135.jpg" - }, - { - "name": "Yung Wun", - "image_url": "http://independentmusicpromotions.com/wp-content/uploads/2011/12/16279_Yung-Wun-pr04.jpg" - }, - { - "name": "Yung6ix", - "image_url": "https://mojidelano.com/wp-content/uploads/2017/03/Yung6ix-1.jpg" - }, - { - "name": "YZ", - "image_url": "http://www.cocaineblunts.com/blunts/wp-content/uploads/2009/06/yz.jpg" - }, - { - "name": "Z-Ro", - "image_url": "http://hiphop-n-more.com/wp-content/uploads/2013/04/z-ro-4.jpg" - }, - { - "name": "Zack de la Rocha", - "image_url": "http://favimages.com/wp-content/uploads/2012/08/rapper-zack-de-la-rocha-singer-rap-photoshoot-young.jpg" - }, - { - "name": "Zaytoven", - "image_url": "https://s3.amazonaws.com/hiphopdx-production/2016/03/Zaytoven-Bankroll-Fresh-e1457227555254-824x620.png" - }, - { - "name": "Zebra Katz", - "image_url": "http://thencrowd14.com/wp-content/uploads/2016/11/04-371x500.jpg" - }, - { - "name": "Zelooperz", - "image_url": "https://i-d-images.vice.com/images/articles/meta/2016/01/26/untitled-article-1453817283.jpg" - }, - { - "name": "Zico", - "image_url": "https://i.pinimg.com/736x/05/4a/2e/054a2eb016aa2d165fb3d933c0b47207.jpg" - } -]
\ No newline at end of file diff --git a/pysite/migrations/tables/oauth_data/__init__.py b/pysite/migrations/tables/oauth_data/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/oauth_data/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/oauth_data/v1.py b/pysite/migrations/tables/oauth_data/v1.py deleted file mode 100644 index 9ace6bf9..00000000 --- a/pysite/migrations/tables/oauth_data/v1.py +++ /dev/null @@ -1,13 +0,0 @@ -from rethinkdb import ReqlOpFailedError - - -def run(db, table, table_obj): - """ - Create a secondary index on the "snowflake" key, so we can easily get documents by matching that key - """ - - try: - db.run(db.query(table).index_create("snowflake")) - db.run(db.query(table).index_wait("snowflake")) - except ReqlOpFailedError: - print("Index already exists.") diff --git a/pysite/migrations/tables/pydoc_links/__init__.py b/pysite/migrations/tables/pydoc_links/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/pydoc_links/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/pydoc_links/initial_data.json b/pysite/migrations/tables/pydoc_links/initial_data.json deleted file mode 100644 index e5b21357..00000000 --- a/pysite/migrations/tables/pydoc_links/initial_data.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "package": "aiohttp", - "base_url": "https://aiohttp.readthedocs.io/en/stable/", - "inventory_url": "https://aiohttp.readthedocs.io/en/stable/objects.inv" - }, - { - "package": "discord", - "base_url": "https://discordpy.readthedocs.io/en/rewrite/", - "inventory_url": "https://discordpy.readthedocs.io/en/rewrite/objects.inv" - }, - { - "package": "django", - "base_url": "https://docs.djangoproject.com/en/dev/", - "inventory_url": "https://docs.djangoproject.com/en/dev/_objects/" - }, - { - "package": "stdlib", - "base_url": "https://docs.python.org/3.7/", - "inventory_url": "https://docs.python.org/3.7/objects.inv" - } -] diff --git a/pysite/migrations/tables/snake_facts/__init__.py b/pysite/migrations/tables/snake_facts/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/snake_facts/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/snake_facts/initial_data.json b/pysite/migrations/tables/snake_facts/initial_data.json deleted file mode 100644 index 49b5a80e..00000000 --- a/pysite/migrations/tables/snake_facts/initial_data.json +++ /dev/null @@ -1,233 +0,0 @@ -[ - { - "fact": "The decapitated head of a dead snake can still bite, even hours after death. These types of bites usually contain huge amounts of venom." - }, - { - "fact": "What is considered the most “dangerous” snake depends on both a specific country’s health care and the availability of antivenom following a bite. Based on these criteria, the most dangerous snake in the world is the saw-scaled viper, which bites and kills more people each year than any other snake." - }, - { - "fact": "Snakes live on everywhere on Earth except Ireland, Iceland, New Zealand, and the North and South Poles.a" - }, - { - "fact": "Of the approximately 725 species of venomous snakes worldwide, 250 can kill a human with one bite." - }, - { - "fact": "Snakes evolved from a four-legged reptilian ancestor—most likely a small, burrowing, land-bound lizard—about 100 million years ago. Some snakes, such as pythons and boas, still have traces of back legs." - }, - { - "fact": "The fear of snakes (ophiophobia or herpetophobia) is one of the most common phobias worldwide. Approximately 1/3 of all adult humans areophidiophobic , which suggests that humans have an innate, evolutionary fear of snakes." - }, - { - "fact": "The top 5 most venomous snakes in the world are the inland taipan, the eastern brown snake, the coastal taipan, the tiger snake, and the black tiger snake." - }, - { - "fact": "The warmer a snake’s body, the more quickly it can digest its prey. Typically, it takes 3–5 days for a snake to digest its meal. For very large snakes, such as the anaconda, digestion can take weeks." - }, - { - "fact": "Some animals, such as the Mongoose, are immune to snake venom." - }, - { - "fact": "To avoid predators, some snakes can poop whenever they want. They make themselves so dirty and smelly that predators will run away." - }, - { - "fact": "The heaviest snake in the world is the anaconda. It weighs over 595 pounds (270 kg) and can grow to over 30 feet (9m) long. It has been known to eat caimans, capybaras, and jaguars." - }, - { - "fact": "The Brahminy Blind Snake, or flowerpot snake, is the only snake species made up of solely females and, as such, does not need a mate to reproduce. It is also the most widespread terrestrial snake in the world." - }, - { - "fact": "If a person suddenly turned into a snake, they would be about 4 times longer than they are now and only a few inches thick. While humans have 24 ribs, some snakes can have more than 400." - }, - { - "fact": "The most advanced snake species in the world is believed to be the black mamba. It has the most highly evolved venom delivery system of any snake on Earth. It can strike up to 12 times in a row, though just one bite is enough to kill a grown man.o" - }, - { - "fact": "The inland taipan is the world’s most toxic snake, meaning it has both the most toxic venom and it injects the most venom when it bites. Its venom sacs hold enough poison to kill up to 80 people." - }, - { - "fact": "The death adder has the fastest strike of any snake in the world. It can attack, inject venom, and go back to striking position in under 0.15 seconds." - }, - { - "fact": "While snakes do not have external ears or eardrums, their skin, muscles, and bones carry sound vibrations to their inner ears." - }, - { - "fact": "Some snakes have been known to explode after eating a large meal. For example, a 13-foot python blew up after it tried to eat a 6-foot alligator. The python was found with the alligator’s tail protruding from its midsection. Its head was missing." - }, - { - "fact": "The word “snake” is from the Proto-Indo-European root *sneg -, meaning “to crawl, creeping thing.” The word “serpent” is from the Proto-Indo-European root *serp -, meaning “to crawl, creep.”" - }, - { - "fact": "Rattlesnake rattles are made of rings of keratin, which is the same material as human hair and fingernails. A rattler will add a new ring each time it sheds its skin." - }, - { - "fact": "Some snakes have over 200 teeth. The teeth aren’t used for chewing but they point backward to prevent prey from escaping the snake’s throat." - }, - { - "fact": "There are about 500 genera and 3,000 different species of snakes. All of them are predators." - }, - { - "fact": "Naturalist Paul Rosolie attempted to be the first person to survive being swallowed by an anaconda in 2014. Though he was wearing a specially designed carbon fiber suit equipped with a breathing system, cameras, and a communication system, he ultimately called off his stunt when he felt like the anaconda was breaking his arm as it tightened its grip around his body." - }, - { - "fact": "There are five recognized species of flying snakes. Growing up to 4 feet, some types can glide up to 330 feet through the air." - }, - { - "fact": "Scales cover every inch of a snake’s body, even its eyes. Scales are thick, tough pieces of skin made from keratin, which is the same material human nails and hair are made from." - }, - { - "fact": "The most common snake in North America is the garter (gardener) snake. This snake is also Massachusetts’s state reptile. While previously thought to be nonvenomous, garter snakes do, in fact, produce a mild neurotoxic venom that is harmless to humans." - }, - { - "fact": "Snakes do not lap up water like mammals do. Instead, they dunk their snouts underwater and use their throats to pump water into their stomachs." - }, - { - "fact": "A snake’s fangs usually last about 6–10 weeks. When a fang wears out, a new one grows in its place." - }, - { - "fact": "Because the end of a snake’s tongue is forked, the two tips taste different amounts of chemicals. Essentially, a snake “smells in stereo” and can even tell which direction a smell is coming from. It identifies scents on its tongue using pits in the roof of its mouth called the Jacobson’s organ." - }, - { - "fact": "The amount of food a snake eats determines how many offspring it will have. The Arafura file snake eats the least and lays just one egg every decade." - }, - { - "fact": "While smaller snakes, such a tree- or- ground-dwelling snakes, use their tongues to follow the scent trails of prey (such as spiders, birds, and other snakes). Larger snakes, such as boas, have heat-sensing organs called labial (lip) pits in their snouts." - }, - { - "fact": "Snakes typically need to eat only 6–30 meals each year to be healthy." - }, - { - "fact": "Snakes like to lie on roads and rocky areas because stones and rocks absorb heat from the sun, which warms them. Basking on these surfaces warms a snake quickly so it can move. If the temperature reaches below 50° Fahrenheit, a snake’s body does not work properly." - }, - { - "fact": "The Mozambique spitting cobra can spit venom over 8 feet away. It can spit from any position, including lying on the ground or raised up. It prefers to aim for its victim’s eyes." - }, - { - "fact": "Snakes cannot chew, so they must swallow their food whole. They are able to stretch their mouths very wide because they have a very flexible lower jaw. Snakes can eat other animals that are 75%–100% bigger than their own bodies." - }, - { - "fact": "To keep from choking on large prey, a snake will push the end of its trachea, or windpipe, out of its mouth, similar to the way a snorkel works." - }, - { - "fact": "The Gaboon viper has the longest fangs of any snake, reaching about 2 inches (5 cm) long." - }, - { - "fact": "Anacondas can hold their breath for up to 10 minutes under water. Additionally, similar to crocodiles, anacondas have eyes and nostrils that can poke above the water’s surface to increase their stealth and hunting prowess." - }, - { - "fact": "The longest snake ever recorded is the reticulated python. It can reach over 33 feet long, which is big enough to swallow a pig, a deer, or even a person." - }, - { - "fact": "Sea snakes with their paddle-shaped tails can dive over 300 feet into the ocean." - }, - { - "fact": "If a snake is threatened soon after a meal, it will often regurgitate its food so it can quickly escape the perceived threat. A snake’s digestive system can dissolve everything but a prey’s hair, feathers, and claws." - }, - { - "fact": "Snakes do not have eyelids; rather, a single transparent scale called a brille protects their eyes. Most snakes see very well, especially if the object is moving." - }, - { - "fact": "The world’s longest venomous snake is the king cobra from Asia. It can grow up to 18 feet, rear almost as high as a person, growl loudly, and inject enough venom to kill an elephant." - }, - { - "fact": "The king cobra is thought to be one of the most intelligent of all snakes. Additionally, unlike most snakes, who do not care for their young, king cobras are careful parents who defend and protect their eggs from enemies." - }, - { - "fact": "Not all snakes have fangs—only those that kill their prey with venom have them. When their fangs are not in use, they fold them back into the roof of the mouth (except for the coral snake, whose fangs do not fold back)." - }, - { - "fact": "Some venomous snakes have died after biting and poisoning themselves by mistake." - }, - { - "fact": "Elephant trunk snakes are almost completely aquatic. They cannot slither because they lack the broad scales in the belly that help other snakes move on land. Rather, elephant trunk snakes have large knobby scales to hold onto slippery fish and constrict them underwater." - }, - { - "fact": "The shortest known snake is the thread snake. It is about 4 inches long and lives on the island of Barbados in the Caribbean. It is said to be as “thin as spaghetti” and it feeds primarily on termites and larvae." - }, - { - "fact": "In 2009, a farm worker in East Africa survived an epic 3-hour battle with a 12-foot python after accidentally stepping on the large snake. It coiled around the man and carried him into a tree. The man wrapped his shirt over the snake’s mouth to prevent it from swallowing him, and he was finally rescued by police after calling for help on his cell phone." - }, - { - "fact": "The venom from a Brazilian pit viper is used in a drug to treat high blood pressure." - }, - { - "fact": "The word “cobra” means “hooded.” Some cobras have large spots on the back of their hood that look like eyes to make them appear intimating even from behind." - }, - { - "fact": "Some desert snakes, such as the African rock python, sleep during the hottest parts of the desert summer. This summer sleep is similar to hibernation and is called “aestivation.”" - }, - { - "fact": "The black mamba is the world’s fastest snake and the world’s second-longest venomous snake in the world, after the king cobra. Found in East Africa, it can reach speeds of up to 12 mph (19kph). It’s named not from the color of its scales, which is olive green, but from the inside of its mouth, which is inky black. Its venom is highly toxic, and without anti-venom, death in humans usually occurs within 7–15 hours." - }, - { - "fact": "Although a snake’s growth rate slows as it gets older, a snake never stops growing." - }, - { - "fact": "While a snake cannot hear the music of a snake charmer, the snake responds to the vibrations of the charmer’s tapping foot or to the movement of the flute." - }, - { - "fact": "Most snakes are not harmful to humans and they help balance the ecosystem by keeping the population of rats, mice, and birds under control." - }, - { - "fact": "The largest snake fossil ever found is the Titanoboa. It lived over 60 million years ago and reached over 50 feet (15 meters) long. It weighed more than 20 people and ate crocodiles and giant tortoises." - }, - { - "fact": "Two-headed snakes are similar to conjoined twins: an embryo begins to split to create identical twins, but the process does not finish. Such snakes rarely survive in the wild because the two heads have duplicate senses, they fight over food, and one head may try to eat the other head." - }, - { - "fact": "Snakes can be grouped into two sections: primitive snakes and true (typical) snakes. Primitive snakes—such as blind snakes, worm snakes, and thread snakes—represent the earliest forms of snakes. True snakes, such as rat snakes and king snakes, are more evolved and more active." - }, - { - "fact": "The oldest written record that describes snakes is in the Brooklyn Papyrus, which is a medical papyrus dating from ancient Egypt (450 B.C.)." - }, - { - "fact": "Approximately 70% of snakes lay eggs. Those that lay eggs are called oviparous. The other 30% of snakes live in colder climates and give birth to live young because it is too cold for eggs outside the body to develop and hatch." - }, - { - "fact": "Most snakes have an elongated right lung, many have a smaller left lung, and a few even have a third lung. They do not have a sense of taste, and most of their organs are organized linearly." - }, - { - "fact": "The most rare and endangered snake is the St. Lucia racer. There are only 18 to 100 of these snakes left." - }, - { - "fact": "Snakes kill over 40,000 people a year—though, with unreported incidents, the total may be over 100,000. About half of these deaths are in India." - }, - { - "fact": "In some cultures, eating snakes is considered a delicacy. For example, snake soup has been a popular Cantonese delicacy for over 2,000 years." - }, - { - "fact": "In some Asian countries, it is believed that drinking the blood of snakes, particularly the cobra, will increase sexual virility. The blood is usually drained from a live snake and then mixed with liquor." - }, - { - "fact": "In the United States, fewer than 1 in 37,500 people are bitten by venomous snakes each year (7,000–8,000 bites per year), and only 1 in 50 million people will die from snake bite (5–6 fatalities per year). In the U.S., a person is 9 times more likely to die from being struck by lightening than to die from a venomous snakebite." - }, - { - "fact": "Some members of the U.S. Army Special Forces are taught to kill and eat snakes during their survival training, which has earned them the nickname “Snake Eaters.”" - }, - { - "fact": "One of the great feats of the legendary Greek hero Perseus was to kill Medusa, a female monster whose hair consisted of writhing, venomous snakes." - }, - { - "fact": "The symbol of the snake is one of the most widespread and oldest cultural symbols in history. Snakes often represent the duality of good and evil and of life and death." - }, - { - "fact": "Because snakes shed their skin, they are often symbols of rebirth, transformation, and healing. For example, Asclepius, the god of medicine, carries a staff encircled by a snake." - }, - { - "fact": "The snake has held various meanings throughout history. For example, The Egyptians viewed the snake as representing royalty and deity. In the Jewish rabbinical tradition and in Hinduism, it represents sexual passion and desire. And the Romans interpreted the snake as a symbol of eternal love." - }, - { - "fact": "Anacondas mate in a huge “breeding ball.” The ball consists of 1 female and nearly 12 males. They stay in a “mating ball” for up to a month." - }, - { - "fact": "Depending on the species, snakes can live from 4 to over 25 years." - }, - { - "fact": "Snakes that are poisonous have pupils that are shaped like a diamond. Nonpoisonous snakes have round pupils." - }, - { - "fact": "Endangered snakes include the San Francisco garter snake, eastern indigo snake, the king cobra, and Dumeril’s boa." - }, - { - "fact": "A mysterious, new “mad snake disease” causes captive pythons and boas to tie themselves in knots. Other symptoms include “stargazing,” which is when snakes stare upwards for long periods of time. Snake experts believe a rodent virus causes the fatal disease." - } -]
\ No newline at end of file diff --git a/pysite/migrations/tables/snake_idioms/__init__.py b/pysite/migrations/tables/snake_idioms/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/snake_idioms/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/snake_idioms/initial_data.json b/pysite/migrations/tables/snake_idioms/initial_data.json deleted file mode 100644 index 37148c42..00000000 --- a/pysite/migrations/tables/snake_idioms/initial_data.json +++ /dev/null @@ -1,275 +0,0 @@ -[ - { - "idiom": "snek it up" - }, - { - "idiom": "get ur snek on" - }, - { - "idiom": "snek ur heart out" - }, - { - "idiom": "snek 4 ever" - }, - { - "idiom": "i luve snek" - }, - { - "idiom": "snek bff" - }, - { - "idiom": "boyfriend snek" - }, - { - "idiom": "dont snek ur homies" - }, - { - "idiom": "garden snek" - }, - { - "idiom": "snektie" - }, - { - "idiom": "snek keks" - }, - { - "idiom": "birthday snek!" - }, - { - "idiom": "snek tonight?" - }, - { - "idiom": "snek hott lips" - }, - { - "idiom": "snek u latr" - }, - { - "idiom": "netflx and snek" - }, - { - "idiom": "holy snek prey4u" - }, - { - "idiom": "ghowst snek hauntt u" - }, - { - "idiom": "ipekek snek syrop" - }, - { - "idiom": "2 snek 2 furius" - }, - { - "idiom": "the shawsnek redumpton" - }, - { - "idiom": "snekler's list" - }, - { - "idiom": "snekablanca" - }, - { - "idiom": "romeo n snekulet" - }, - { - "idiom": "citizn snek" - }, - { - "idiom": "gon wit the snek" - }, - { - "idiom": "dont step on snek" - }, - { - "idiom": "the wizrd uf snek" - }, - { - "idiom": "forrest snek" - }, - { - "idiom": "snek of musik" - }, - { - "idiom": "west snek story" - }, - { - "idiom": "snek wars eposide XI" - }, - { - "idiom": "2001: a snek odyssuuy" - }, - { - "idiom": "E.T. the snekstra terrastriul" - }, - { - "idiom": "snekkin' inth rain" - }, - { - "idiom": "dr sneklove" - }, - { - "idiom": "snekley kubrik" - }, - { - "idiom": "willium snekspeare" - }, - { - "idiom": "snek on tutanic" - }, - { - "idiom": "a snekwork orunge" - }, - { - "idiom": "the snek the bad n the ogly" - }, - { - "idiom": "the sneksorcist" - }, - { - "idiom": "gudd snek huntin" - }, - { - "idiom": "leonurdo disnekrio" - }, - { - "idiom": "denzal snekington" - }, - { - "idiom": "snekuel l jocksons" - }, - { - "idiom": "kevn snek" - }, - { - "idiom": "snekthony hopkuns" - }, - { - "idiom": "hugh snekman" - }, - { - "idiom": "snek but it glow in durk" - }, - { - "idiom": "snek but u cn ride it" - }, - { - "idiom": "snek but slep in ur bed" - }, - { - "idiom": "snek but mad frum plastk" - }, - { - "idiom": "snek but bulong 2 ur frnd" - }, - { - "idiom": "sneks on plene" - }, - { - "idiom": "baby snek" - }, - { - "idiom": "trouser snek" - }, - { - "idiom": "momo snek" - }, - { - "idiom": "fast snek" - }, - { - "idiom": "super slow snek" - }, - { - "idiom": "old snek" - }, - { - "idiom": "slimy snek" - }, - { - "idiom": "snek attekk" - }, - { - "idiom": "snek get wrekk" - }, - { - "idiom": "snek you long time" - }, - { - "idiom": "carpenter snek" - }, - { - "idiom": "drain snek" - }, - { - "idiom": "eat ur face snek" - }, - { - "idiom": "kawaii snek" - }, - { - "idiom": "dis snek is soft" - }, - { - "idiom": "snek is 4 yers uld" - }, - { - "idiom": "pls feed snek, is hingry" - }, - { - "idiom": "snek? snek? sneeeeek!!" - }, - { - "idiom": "solid snek" - }, - { - "idiom": "big bos snek" - }, - { - "idiom": "snek republic" - }, - { - "idiom": "snekoslovakia" - }, - { - "idiom": "snek please!" - }, - { - "idiom": "i brok my snek :(" - }, - { - "idiom": "star snek the nxt generatin" - }, - { - "idiom": "azsnek tempul" - }, - { - "idiom": "discosnek" - }, - { - "idiom": "bottlsnek" - }, - { - "idiom": "turtlsnek" - }, - { - "idiom": "cashiers snek" - }, - { - "idiom": "mega snek!!" - }, - { - "idiom": "one tim i saw snek neked" - }, - { - "idiom": "snek cnt clim trees" - }, - { - "idiom": "snek in muth is jus tongue" - }, - { - "idiom": "juan snek" - }, - { - "idiom": "photosnek" - } -]
\ No newline at end of file diff --git a/pysite/migrations/tables/snake_names/__init__.py b/pysite/migrations/tables/snake_names/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/snake_names/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/snake_names/initial_data.json b/pysite/migrations/tables/snake_names/initial_data.json deleted file mode 100644 index 8ba9dbd7..00000000 --- a/pysite/migrations/tables/snake_names/initial_data.json +++ /dev/null @@ -1,2170 +0,0 @@ -[ - { - "name": "Acanthophis", - "scientific": "Acanthophis" - }, - { - "name": "Aesculapian snake", - "scientific": "Aesculapian snake" - }, - { - "name": "African beaked snake", - "scientific": "Rufous beaked snake" - }, - { - "name": "African puff adder", - "scientific": "Bitis arietans" - }, - { - "name": "African rock python", - "scientific": "African rock python" - }, - { - "name": "African twig snake", - "scientific": "Twig snake" - }, - { - "name": "Agkistrodon piscivorus", - "scientific": "Agkistrodon piscivorus" - }, - { - "name": "Ahaetulla", - "scientific": "Ahaetulla" - }, - { - "name": "Amazonian palm viper", - "scientific": "Bothriopsis bilineata" - }, - { - "name": "American copperhead", - "scientific": "Agkistrodon contortrix" - }, - { - "name": "Amethystine python", - "scientific": "Amethystine python" - }, - { - "name": "Anaconda", - "scientific": "Anaconda" - }, - { - "name": "Andaman cat snake", - "scientific": "Boiga andamanensis" - }, - { - "name": "Andrea's keelback", - "scientific": "Amphiesma andreae" - }, - { - "name": "Annulated sea snake", - "scientific": "Hydrophis cyanocinctus" - }, - { - "name": "Arafura file snake", - "scientific": "Acrochordus arafurae" - }, - { - "name": "Arizona black rattlesnake", - "scientific": "Crotalus oreganus cerberus" - }, - { - "name": "Arizona coral snake", - "scientific": "Coral snake" - }, - { - "name": "Aruba rattlesnake", - "scientific": "Crotalus durissus unicolor" - }, - { - "name": "Asian cobra", - "scientific": "Indian cobra" - }, - { - "name": "Asian keelback", - "scientific": "Amphiesma vibakari" - }, - { - "name": "Asp (reptile)", - "scientific": "Asp (reptile)" - }, - { - "name": "Assam keelback", - "scientific": "Amphiesma pealii" - }, - { - "name": "Australian copperhead", - "scientific": "Austrelaps" - }, - { - "name": "Australian scrub python", - "scientific": "Amethystine python" - }, - { - "name": "Baird's rat snake", - "scientific": "Pantherophis bairdi" - }, - { - "name": "Banded Flying Snake", - "scientific": "Banded flying snake" - }, - { - "name": "Banded cat-eyed snake", - "scientific": "Banded cat-eyed snake" - }, - { - "name": "Banded krait", - "scientific": "Banded krait" - }, - { - "name": "Barred wolf snake", - "scientific": "Lycodon striatus" - }, - { - "name": "Beaked sea snake", - "scientific": "Enhydrina schistosa" - }, - { - "name": "Beauty rat snake", - "scientific": "Beauty rat snake" - }, - { - "name": "Beddome's cat snake", - "scientific": "Boiga beddomei" - }, - { - "name": "Beddome's coral snake", - "scientific": "Beddome's coral snake" - }, - { - "name": "Bird snake", - "scientific": "Twig snake" - }, - { - "name": "Black-banded trinket snake", - "scientific": "Oreocryptophis porphyraceus" - }, - { - "name": "Black-headed snake", - "scientific": "Western black-headed snake" - }, - { - "name": "Black-necked cobra", - "scientific": "Black-necked spitting cobra" - }, - { - "name": "Black-necked spitting cobra", - "scientific": "Black-necked spitting cobra" - }, - { - "name": "Black-striped keelback", - "scientific": "Buff striped keelback" - }, - { - "name": "Black-tailed horned pit viper", - "scientific": "Mixcoatlus melanurus" - }, - { - "name": "Black headed python", - "scientific": "Black-headed python" - }, - { - "name": "Black krait", - "scientific": "Greater black krait" - }, - { - "name": "Black mamba", - "scientific": "Black mamba" - }, - { - "name": "Black rat snake", - "scientific": "Rat snake" - }, - { - "name": "Black tree cobra", - "scientific": "Cobra" - }, - { - "name": "Blind snake", - "scientific": "Scolecophidia" - }, - { - "name": "Blonde hognose snake", - "scientific": "Hognose" - }, - { - "name": "Blood python", - "scientific": "Python brongersmai" - }, - { - "name": "Blue krait", - "scientific": "Bungarus candidus" - }, - { - "name": "Blunt-headed tree snake", - "scientific": "Imantodes cenchoa" - }, - { - "name": "Boa constrictor", - "scientific": "Boa constrictor" - }, - { - "name": "Bocourt's water snake", - "scientific": "Subsessor" - }, - { - "name": "Boelen python", - "scientific": "Morelia boeleni" - }, - { - "name": "Boidae", - "scientific": "Boidae" - }, - { - "name": "Boiga", - "scientific": "Boiga" - }, - { - "name": "Boomslang", - "scientific": "Boomslang" - }, - { - "name": "Brahminy blind snake", - "scientific": "Indotyphlops braminus" - }, - { - "name": "Brazilian coral snake", - "scientific": "Coral snake" - }, - { - "name": "Brazilian smooth snake", - "scientific": "Hydrodynastes gigas" - }, - { - "name": "Brown snake (disambiguation)", - "scientific": "Brown snake" - }, - { - "name": "Brown tree snake", - "scientific": "Brown tree snake" - }, - { - "name": "Brown white-lipped python", - "scientific": "Leiopython" - }, - { - "name": "Buff striped keelback", - "scientific": "Buff striped keelback" - }, - { - "name": "Bull snake", - "scientific": "Bull snake" - }, - { - "name": "Burmese keelback", - "scientific": "Burmese keelback water snake" - }, - { - "name": "Burmese krait", - "scientific": "Burmese krait" - }, - { - "name": "Burmese python", - "scientific": "Burmese python" - }, - { - "name": "Burrowing viper", - "scientific": "Atractaspidinae" - }, - { - "name": "Buttermilk racer", - "scientific": "Coluber constrictor anthicus" - }, - { - "name": "California kingsnake", - "scientific": "California kingsnake" - }, - { - "name": "Cantor's pitviper", - "scientific": "Trimeresurus cantori" - }, - { - "name": "Cape cobra", - "scientific": "Cape cobra" - }, - { - "name": "Cape coral snake", - "scientific": "Aspidelaps lubricus" - }, - { - "name": "Cape gopher snake", - "scientific": "Cape gopher snake" - }, - { - "name": "Carpet viper", - "scientific": "Echis" - }, - { - "name": "Cat-eyed night snake", - "scientific": "Banded cat-eyed snake" - }, - { - "name": "Cat-eyed snake", - "scientific": "Banded cat-eyed snake" - }, - { - "name": "Cat snake", - "scientific": "Boiga" - }, - { - "name": "Central American lyre snake", - "scientific": "Trimorphodon biscutatus" - }, - { - "name": "Central ranges taipan", - "scientific": "Taipan" - }, - { - "name": "Chappell Island tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Checkered garter snake", - "scientific": "Checkered garter snake" - }, - { - "name": "Checkered keelback", - "scientific": "Checkered keelback" - }, - { - "name": "Children's python", - "scientific": "Children's python" - }, - { - "name": "Chinese cobra", - "scientific": "Chinese cobra" - }, - { - "name": "Coachwhip snake", - "scientific": "Masticophis flagellum" - }, - { - "name": "Coastal taipan", - "scientific": "Coastal taipan" - }, - { - "name": "Cobra", - "scientific": "Cobra" - }, - { - "name": "Collett's snake", - "scientific": "Collett's snake" - }, - { - "name": "Common adder", - "scientific": "Vipera berus" - }, - { - "name": "Common cobra", - "scientific": "Chinese cobra" - }, - { - "name": "Common garter snake", - "scientific": "Common garter snake" - }, - { - "name": "Common ground snake", - "scientific": "Western ground snake" - }, - { - "name": "Common keelback (disambiguation)", - "scientific": "Common keelback" - }, - { - "name": "Common tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Common worm snake", - "scientific": "Indotyphlops braminus" - }, - { - "name": "Congo snake", - "scientific": "Amphiuma" - }, - { - "name": "Congo water cobra", - "scientific": "Naja christyi" - }, - { - "name": "Coral snake", - "scientific": "Coral snake" - }, - { - "name": "Corn snake", - "scientific": "Corn snake" - }, - { - "name": "Coronado Island rattlesnake", - "scientific": "Crotalus oreganus caliginis" - }, - { - "name": "Crossed viper", - "scientific": "Vipera berus" - }, - { - "name": "Crotalus cerastes", - "scientific": "Crotalus cerastes" - }, - { - "name": "Crotalus durissus", - "scientific": "Crotalus durissus" - }, - { - "name": "Crotalus horridus", - "scientific": "Timber rattlesnake" - }, - { - "name": "Crowned snake", - "scientific": "Tantilla" - }, - { - "name": "Cuban boa", - "scientific": "Chilabothrus angulifer" - }, - { - "name": "Cuban wood snake", - "scientific": "Tropidophis melanurus" - }, - { - "name": "Dasypeltis", - "scientific": "Dasypeltis" - }, - { - "name": "Desert death adder", - "scientific": "Desert death adder" - }, - { - "name": "Desert kingsnake", - "scientific": "Desert kingsnake" - }, - { - "name": "Desert woma python", - "scientific": "Woma python" - }, - { - "name": "Diamond python", - "scientific": "Morelia spilota spilota" - }, - { - "name": "Dog-toothed cat snake", - "scientific": "Boiga cynodon" - }, - { - "name": "Down's tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Dubois's sea snake", - "scientific": "Aipysurus duboisii" - }, - { - "name": "Durango rock rattlesnake", - "scientific": "Crotalus lepidus klauberi" - }, - { - "name": "Dusty hognose snake", - "scientific": "Hognose" - }, - { - "name": "Dwarf beaked snake", - "scientific": "Dwarf beaked snake" - }, - { - "name": "Dwarf boa", - "scientific": "Boa constrictor" - }, - { - "name": "Dwarf pipe snake", - "scientific": "Anomochilus" - }, - { - "name": "Eastern brown snake", - "scientific": "Eastern brown snake" - }, - { - "name": "Eastern coral snake", - "scientific": "Micrurus fulvius" - }, - { - "name": "Eastern diamondback rattlesnake", - "scientific": "Eastern diamondback rattlesnake" - }, - { - "name": "Eastern green mamba", - "scientific": "Eastern green mamba" - }, - { - "name": "Eastern hognose snake", - "scientific": "Eastern hognose snake" - }, - { - "name": "Eastern mud snake", - "scientific": "Mud snake" - }, - { - "name": "Eastern racer", - "scientific": "Coluber constrictor" - }, - { - "name": "Eastern tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Eastern water cobra", - "scientific": "Cobra" - }, - { - "name": "Elaps harlequin snake", - "scientific": "Micrurus fulvius" - }, - { - "name": "Eunectes", - "scientific": "Eunectes" - }, - { - "name": "European Smooth Snake", - "scientific": "Smooth snake" - }, - { - "name": "False cobra", - "scientific": "False cobra" - }, - { - "name": "False coral snake", - "scientific": "Coral snake" - }, - { - "name": "False water cobra", - "scientific": "Hydrodynastes gigas" - }, - { - "name": "Fierce snake", - "scientific": "Inland taipan" - }, - { - "name": "Flying snake", - "scientific": "Chrysopelea" - }, - { - "name": "Forest cobra", - "scientific": "Forest cobra" - }, - { - "name": "Forsten's cat snake", - "scientific": "Boiga forsteni" - }, - { - "name": "Fox snake", - "scientific": "Fox snake" - }, - { - "name": "Gaboon viper", - "scientific": "Gaboon viper" - }, - { - "name": "Garter snake", - "scientific": "Garter snake" - }, - { - "name": "Giant Malagasy hognose snake", - "scientific": "Hognose" - }, - { - "name": "Glossy snake", - "scientific": "Glossy snake" - }, - { - "name": "Gold-ringed cat snake", - "scientific": "Boiga dendrophila" - }, - { - "name": "Gold tree cobra", - "scientific": "Pseudohaje goldii" - }, - { - "name": "Golden tree snake", - "scientific": "Chrysopelea ornata" - }, - { - "name": "Gopher snake", - "scientific": "Pituophis catenifer" - }, - { - "name": "Grand Canyon rattlesnake", - "scientific": "Crotalus oreganus abyssus" - }, - { - "name": "Grass snake", - "scientific": "Grass snake" - }, - { - "name": "Gray cat snake", - "scientific": "Boiga ocellata" - }, - { - "name": "Great Plains rat snake", - "scientific": "Pantherophis emoryi" - }, - { - "name": "Green anaconda", - "scientific": "Green anaconda" - }, - { - "name": "Green rat snake", - "scientific": "Rat snake" - }, - { - "name": "Green tree python", - "scientific": "Green tree python" - }, - { - "name": "Grey-banded kingsnake", - "scientific": "Gray-banded kingsnake" - }, - { - "name": "Grey Lora", - "scientific": "Leptophis stimsoni" - }, - { - "name": "Halmahera python", - "scientific": "Morelia tracyae" - }, - { - "name": "Harlequin coral snake", - "scientific": "Micrurus fulvius" - }, - { - "name": "Herald snake", - "scientific": "Caduceus" - }, - { - "name": "High Woods coral snake", - "scientific": "Coral snake" - }, - { - "name": "Hill keelback", - "scientific": "Amphiesma monticola" - }, - { - "name": "Himalayan keelback", - "scientific": "Amphiesma platyceps" - }, - { - "name": "Hognose snake", - "scientific": "Hognose" - }, - { - "name": "Hognosed viper", - "scientific": "Porthidium" - }, - { - "name": "Hook Nosed Sea Snake", - "scientific": "Enhydrina schistosa" - }, - { - "name": "Hoop snake", - "scientific": "Hoop snake" - }, - { - "name": "Hopi rattlesnake", - "scientific": "Crotalus viridis nuntius" - }, - { - "name": "Indian cobra", - "scientific": "Indian cobra" - }, - { - "name": "Indian egg-eater", - "scientific": "Indian egg-eating snake" - }, - { - "name": "Indian flying snake", - "scientific": "Chrysopelea ornata" - }, - { - "name": "Indian krait", - "scientific": "Bungarus" - }, - { - "name": "Indigo snake", - "scientific": "Drymarchon" - }, - { - "name": "Inland carpet python", - "scientific": "Morelia spilota metcalfei" - }, - { - "name": "Inland taipan", - "scientific": "Inland taipan" - }, - { - "name": "Jamaican boa", - "scientific": "Jamaican boa" - }, - { - "name": "Jan's hognose snake", - "scientific": "Hognose" - }, - { - "name": "Japanese forest rat snake", - "scientific": "Euprepiophis conspicillatus" - }, - { - "name": "Japanese rat snake", - "scientific": "Japanese rat snake" - }, - { - "name": "Japanese striped snake", - "scientific": "Japanese striped snake" - }, - { - "name": "Kayaudi dwarf reticulated python", - "scientific": "Reticulated python" - }, - { - "name": "Keelback", - "scientific": "Natricinae" - }, - { - "name": "Khasi Hills keelback", - "scientific": "Amphiesma khasiense" - }, - { - "name": "King Island tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "King brown", - "scientific": "Mulga snake" - }, - { - "name": "King cobra", - "scientific": "King cobra" - }, - { - "name": "King rat snake", - "scientific": "Rat snake" - }, - { - "name": "King snake", - "scientific": "Kingsnake" - }, - { - "name": "Krait", - "scientific": "Bungarus" - }, - { - "name": "Krefft's tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Lance-headed rattlesnake", - "scientific": "Crotalus polystictus" - }, - { - "name": "Lancehead", - "scientific": "Bothrops" - }, - { - "name": "Large shield snake", - "scientific": "Pseudotyphlops" - }, - { - "name": "Leptophis ahaetulla", - "scientific": "Leptophis ahaetulla" - }, - { - "name": "Lesser black krait", - "scientific": "Lesser black krait" - }, - { - "name": "Long-nosed adder", - "scientific": "Eastern hognose snake" - }, - { - "name": "Long-nosed tree snake", - "scientific": "Western hognose snake" - }, - { - "name": "Long-nosed whip snake", - "scientific": "Ahaetulla nasuta" - }, - { - "name": "Long-tailed rattlesnake", - "scientific": "Rattlesnake" - }, - { - "name": "Longnosed worm snake", - "scientific": "Leptotyphlops macrorhynchus" - }, - { - "name": "Lyre snake", - "scientific": "Trimorphodon" - }, - { - "name": "Madagascar ground boa", - "scientific": "Acrantophis madagascariensis" - }, - { - "name": "Malayan krait", - "scientific": "Bungarus candidus" - }, - { - "name": "Malayan long-glanded coral snake", - "scientific": "Calliophis bivirgata" - }, - { - "name": "Malayan pit viper", - "scientific": "Pit viper" - }, - { - "name": "Mamba", - "scientific": "Mamba" - }, - { - "name": "Mamushi", - "scientific": "Mamushi" - }, - { - "name": "Manchurian Black Water Snake", - "scientific": "Elaphe schrenckii" - }, - { - "name": "Mandarin rat snake", - "scientific": "Mandarin rat snake" - }, - { - "name": "Mangrove snake (disambiguation)", - "scientific": "Mangrove snake" - }, - { - "name": "Many-banded krait", - "scientific": "Many-banded krait" - }, - { - "name": "Many-banded tree snake", - "scientific": "Many-banded tree snake" - }, - { - "name": "Many-spotted cat snake", - "scientific": "Boiga multomaculata" - }, - { - "name": "Massasauga rattlesnake", - "scientific": "Massasauga" - }, - { - "name": "Mexican black kingsnake", - "scientific": "Mexican black kingsnake" - }, - { - "name": "Mexican green rattlesnake", - "scientific": "Crotalus basiliscus" - }, - { - "name": "Mexican hognose snake", - "scientific": "Hognose" - }, - { - "name": "Mexican parrot snake", - "scientific": "Leptophis mexicanus" - }, - { - "name": "Mexican racer", - "scientific": "Coluber constrictor oaxaca" - }, - { - "name": "Mexican vine snake", - "scientific": "Oxybelis aeneus" - }, - { - "name": "Mexican west coast rattlesnake", - "scientific": "Crotalus basiliscus" - }, - { - "name": "Micropechis ikaheka", - "scientific": "Micropechis ikaheka" - }, - { - "name": "Midget faded rattlesnake", - "scientific": "Crotalus oreganus concolor" - }, - { - "name": "Milk snake", - "scientific": "Milk snake" - }, - { - "name": "Moccasin snake", - "scientific": "Agkistrodon piscivorus" - }, - { - "name": "Modest keelback", - "scientific": "Amphiesma modestum" - }, - { - "name": "Mojave desert sidewinder", - "scientific": "Crotalus cerastes" - }, - { - "name": "Mojave rattlesnake", - "scientific": "Crotalus scutulatus" - }, - { - "name": "Mole viper", - "scientific": "Atractaspidinae" - }, - { - "name": "Moluccan flying snake", - "scientific": "Chrysopelea" - }, - { - "name": "Montpellier snake", - "scientific": "Malpolon monspessulanus" - }, - { - "name": "Mud adder", - "scientific": "Mud adder" - }, - { - "name": "Mud snake", - "scientific": "Mud snake" - }, - { - "name": "Mussurana", - "scientific": "Mussurana" - }, - { - "name": "Narrowhead Garter Snake", - "scientific": "Garter snake" - }, - { - "name": "Nicobar Island keelback", - "scientific": "Amphiesma nicobariense" - }, - { - "name": "Nicobar cat snake", - "scientific": "Boiga wallachi" - }, - { - "name": "Night snake", - "scientific": "Night snake" - }, - { - "name": "Nilgiri keelback", - "scientific": "Nilgiri keelback" - }, - { - "name": "North eastern king snake", - "scientific": "Eastern hognose snake" - }, - { - "name": "Northeastern hill krait", - "scientific": "Northeastern hill krait" - }, - { - "name": "Northern black-tailed rattlesnake", - "scientific": "Crotalus molossus" - }, - { - "name": "Northern tree snake", - "scientific": "Dendrelaphis calligastra" - }, - { - "name": "Northern water snake", - "scientific": "Northern water snake" - }, - { - "name": "Northern white-lipped python", - "scientific": "Leiopython" - }, - { - "name": "Oaxacan small-headed rattlesnake", - "scientific": "Crotalus intermedius gloydi" - }, - { - "name": "Okinawan habu", - "scientific": "Okinawan habu" - }, - { - "name": "Olive sea snake", - "scientific": "Aipysurus laevis" - }, - { - "name": "Opheodrys", - "scientific": "Opheodrys" - }, - { - "name": "Orange-collared keelback", - "scientific": "Rhabdophis himalayanus" - }, - { - "name": "Ornate flying snake", - "scientific": "Chrysopelea ornata" - }, - { - "name": "Oxybelis", - "scientific": "Oxybelis" - }, - { - "name": "Palestine viper", - "scientific": "Vipera palaestinae" - }, - { - "name": "Paradise flying snake", - "scientific": "Chrysopelea paradisi" - }, - { - "name": "Parrot snake", - "scientific": "Leptophis ahaetulla" - }, - { - "name": "Patchnose snake", - "scientific": "Salvadora (snake)" - }, - { - "name": "Pelagic sea snake", - "scientific": "Yellow-bellied sea snake" - }, - { - "name": "Peninsula tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Perrotet's shieldtail snake", - "scientific": "Plectrurus perrotetii" - }, - { - "name": "Persian rat snake", - "scientific": "Rat snake" - }, - { - "name": "Pine snake", - "scientific": "Pine snake" - }, - { - "name": "Pit viper", - "scientific": "Pit viper" - }, - { - "name": "Plains hognose snake", - "scientific": "Western hognose snake" - }, - { - "name": "Prairie kingsnake", - "scientific": "Lampropeltis calligaster" - }, - { - "name": "Pygmy python", - "scientific": "Pygmy python" - }, - { - "name": "Pythonidae", - "scientific": "Pythonidae" - }, - { - "name": "Queen snake", - "scientific": "Queen snake" - }, - { - "name": "Rat snake", - "scientific": "Rat snake" - }, - { - "name": "Rattler", - "scientific": "Rattlesnake" - }, - { - "name": "Rattlesnake", - "scientific": "Rattlesnake" - }, - { - "name": "Red-bellied black snake", - "scientific": "Red-bellied black snake" - }, - { - "name": "Red-headed krait", - "scientific": "Red-headed krait" - }, - { - "name": "Red-necked keelback", - "scientific": "Rhabdophis subminiatus" - }, - { - "name": "Red-tailed bamboo pitviper", - "scientific": "Trimeresurus erythrurus" - }, - { - "name": "Red-tailed boa", - "scientific": "Boa constrictor" - }, - { - "name": "Red-tailed pipe snake", - "scientific": "Cylindrophis ruffus" - }, - { - "name": "Red blood python", - "scientific": "Python brongersmai" - }, - { - "name": "Red diamond rattlesnake", - "scientific": "Crotalus ruber" - }, - { - "name": "Reticulated python", - "scientific": "Reticulated python" - }, - { - "name": "Ribbon snake", - "scientific": "Ribbon snake" - }, - { - "name": "Ringed hognose snake", - "scientific": "Hognose" - }, - { - "name": "Rosy boa", - "scientific": "Rosy boa" - }, - { - "name": "Rough green snake", - "scientific": "Opheodrys aestivus" - }, - { - "name": "Rubber boa", - "scientific": "Rubber boa" - }, - { - "name": "Rufous beaked snake", - "scientific": "Rufous beaked snake" - }, - { - "name": "Russell's viper", - "scientific": "Russell's viper" - }, - { - "name": "San Francisco garter snake", - "scientific": "San Francisco garter snake" - }, - { - "name": "Sand boa", - "scientific": "Erycinae" - }, - { - "name": "Sand viper", - "scientific": "Sand viper" - }, - { - "name": "Saw-scaled viper", - "scientific": "Echis" - }, - { - "name": "Scarlet kingsnake", - "scientific": "Scarlet kingsnake" - }, - { - "name": "Sea snake", - "scientific": "Hydrophiinae" - }, - { - "name": "Selayer reticulated python", - "scientific": "Reticulated python" - }, - { - "name": "Shield-nosed cobra", - "scientific": "Shield-nosed cobra" - }, - { - "name": "Shield-tailed snake", - "scientific": "Uropeltidae" - }, - { - "name": "Sikkim keelback", - "scientific": "Sikkim keelback" - }, - { - "name": "Sind krait", - "scientific": "Sind krait" - }, - { - "name": "Smooth green snake", - "scientific": "Smooth green snake" - }, - { - "name": "South American hognose snake", - "scientific": "Hognose" - }, - { - "name": "South Andaman krait", - "scientific": "South Andaman krait" - }, - { - "name": "South eastern corn snake", - "scientific": "Corn snake" - }, - { - "name": "Southern Pacific rattlesnake", - "scientific": "Crotalus oreganus helleri" - }, - { - "name": "Southern black racer", - "scientific": "Southern black racer" - }, - { - "name": "Southern hognose snake", - "scientific": "Southern hognose snake" - }, - { - "name": "Southern white-lipped python", - "scientific": "Leiopython" - }, - { - "name": "Southwestern blackhead snake", - "scientific": "Tantilla hobartsmithi" - }, - { - "name": "Southwestern carpet python", - "scientific": "Morelia spilota imbricata" - }, - { - "name": "Southwestern speckled rattlesnake", - "scientific": "Crotalus mitchellii pyrrhus" - }, - { - "name": "Speckled hognose snake", - "scientific": "Hognose" - }, - { - "name": "Speckled kingsnake", - "scientific": "Lampropeltis getula holbrooki" - }, - { - "name": "Spectacled cobra", - "scientific": "Indian cobra" - }, - { - "name": "Sri Lanka cat snake", - "scientific": "Boiga ceylonensis" - }, - { - "name": "Stiletto snake", - "scientific": "Atractaspidinae" - }, - { - "name": "Stimson's python", - "scientific": "Stimson's python" - }, - { - "name": "Striped snake", - "scientific": "Japanese striped snake" - }, - { - "name": "Sumatran short-tailed python", - "scientific": "Python curtus" - }, - { - "name": "Sunbeam snake", - "scientific": "Xenopeltis" - }, - { - "name": "Taipan", - "scientific": "Taipan" - }, - { - "name": "Tan racer", - "scientific": "Coluber constrictor etheridgei" - }, - { - "name": "Tancitaran dusky rattlesnake", - "scientific": "Crotalus pusillus" - }, - { - "name": "Tanimbar python", - "scientific": "Reticulated python" - }, - { - "name": "Tasmanian tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Tawny cat snake", - "scientific": "Boiga ochracea" - }, - { - "name": "Temple pit viper", - "scientific": "Pit viper" - }, - { - "name": "Tentacled snake", - "scientific": "Erpeton tentaculatum" - }, - { - "name": "Texas Coral Snake", - "scientific": "Coral snake" - }, - { - "name": "Texas blind snake", - "scientific": "Leptotyphlops dulcis" - }, - { - "name": "Texas garter snake", - "scientific": "Texas garter snake" - }, - { - "name": "Texas lyre snake", - "scientific": "Trimorphodon biscutatus vilkinsonii" - }, - { - "name": "Texas night snake", - "scientific": "Hypsiglena jani" - }, - { - "name": "Thai cobra", - "scientific": "King cobra" - }, - { - "name": "Three-lined ground snake", - "scientific": "Atractus trilineatus" - }, - { - "name": "Tic polonga", - "scientific": "Russell's viper" - }, - { - "name": "Tiger rattlesnake", - "scientific": "Crotalus tigris" - }, - { - "name": "Tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Tigre snake", - "scientific": "Spilotes pullatus" - }, - { - "name": "Timber rattlesnake", - "scientific": "Timber rattlesnake" - }, - { - "name": "Tree snake", - "scientific": "Brown tree snake" - }, - { - "name": "Tri-color hognose snake", - "scientific": "Hognose" - }, - { - "name": "Trinket snake", - "scientific": "Trinket snake" - }, - { - "name": "Tropical rattlesnake", - "scientific": "Crotalus durissus" - }, - { - "name": "Twig snake", - "scientific": "Twig snake" - }, - { - "name": "Twin-Barred tree snake", - "scientific": "Banded flying snake" - }, - { - "name": "Twin-spotted rat snake", - "scientific": "Rat snake" - }, - { - "name": "Twin-spotted rattlesnake", - "scientific": "Crotalus pricei" - }, - { - "name": "Uracoan rattlesnake", - "scientific": "Crotalus durissus vegrandis" - }, - { - "name": "Viperidae", - "scientific": "Viperidae" - }, - { - "name": "Wall's keelback", - "scientific": "Amphiesma xenura" - }, - { - "name": "Wart snake", - "scientific": "Acrochordidae" - }, - { - "name": "Water adder", - "scientific": "Agkistrodon piscivorus" - }, - { - "name": "Water moccasin", - "scientific": "Agkistrodon piscivorus" - }, - { - "name": "West Indian racer", - "scientific": "Antiguan racer" - }, - { - "name": "Western blind snake", - "scientific": "Leptotyphlops humilis" - }, - { - "name": "Western carpet python", - "scientific": "Morelia spilota" - }, - { - "name": "Western coral snake", - "scientific": "Coral snake" - }, - { - "name": "Western diamondback rattlesnake", - "scientific": "Western diamondback rattlesnake" - }, - { - "name": "Western green mamba", - "scientific": "Western green mamba" - }, - { - "name": "Western ground snake", - "scientific": "Western ground snake" - }, - { - "name": "Western hognose snake", - "scientific": "Western hognose snake" - }, - { - "name": "Western mud snake", - "scientific": "Mud snake" - }, - { - "name": "Western tiger snake", - "scientific": "Tiger snake" - }, - { - "name": "Western woma python", - "scientific": "Woma python" - }, - { - "name": "White-lipped keelback", - "scientific": "Amphiesma leucomystax" - }, - { - "name": "Wolf snake", - "scientific": "Lycodon capucinus" - }, - { - "name": "Woma python", - "scientific": "Woma python" - }, - { - "name": "Wutu", - "scientific": "Bothrops alternatus" - }, - { - "name": "Wynaad keelback", - "scientific": "Amphiesma monticola" - }, - { - "name": "Yellow-banded sea snake", - "scientific": "Yellow-bellied sea snake" - }, - { - "name": "Yellow-bellied sea snake", - "scientific": "Yellow-bellied sea snake" - }, - { - "name": "Yellow-lipped sea snake", - "scientific": "Yellow-lipped sea krait" - }, - { - "name": "Yellow-striped rat snake", - "scientific": "Rat snake" - }, - { - "name": "Yellow anaconda", - "scientific": "Yellow anaconda" - }, - { - "name": "Yellow cobra", - "scientific": "Cape cobra" - }, - { - "name": "Yunnan keelback", - "scientific": "Amphiesma parallelum" - }, - { - "name": "Abaco Island boa", - "scientific": "Epicrates exsul" - }, - { - "name": "Agkistrodon bilineatus", - "scientific": "Agkistrodon bilineatus" - }, - { - "name": "Amazon tree boa", - "scientific": "Corallus hortulanus" - }, - { - "name": "Andaman cobra", - "scientific": "Andaman cobra" - }, - { - "name": "Angolan python", - "scientific": "Python anchietae" - }, - { - "name": "Arabian cobra", - "scientific": "Arabian cobra" - }, - { - "name": "Asp viper", - "scientific": "Vipera aspis" - }, - { - "name": "Ball Python", - "scientific": "Ball python" - }, - { - "name": "Ball python", - "scientific": "Ball python" - }, - { - "name": "Bamboo pitviper", - "scientific": "Trimeresurus gramineus" - }, - { - "name": "Banded pitviper", - "scientific": "Trimeresurus fasciatus" - }, - { - "name": "Banded water cobra", - "scientific": "Naja annulata" - }, - { - "name": "Barbour's pit viper", - "scientific": "Mixcoatlus barbouri" - }, - { - "name": "Bismarck ringed python", - "scientific": "Bothrochilus" - }, - { - "name": "Black-speckled palm-pitviper", - "scientific": "Bothriechis nigroviridis" - }, - { - "name": "Bluntnose viper", - "scientific": "Macrovipera lebetina" - }, - { - "name": "Bornean pitviper", - "scientific": "Trimeresurus borneensis" - }, - { - "name": "Borneo short-tailed python", - "scientific": "Borneo python" - }, - { - "name": "Bothrops jararacussu", - "scientific": "Bothrops jararacussu" - }, - { - "name": "Bredl's python", - "scientific": "Morelia bredli" - }, - { - "name": "Brongersma's pitviper", - "scientific": "Trimeresurus brongersmai" - }, - { - "name": "Brown spotted pitviper", - "scientific": "Trimeresurus mucrosquamatus" - }, - { - "name": "Brown water python", - "scientific": "Liasis fuscus" - }, - { - "name": "Burrowing cobra", - "scientific": "Egyptian cobra" - }, - { - "name": "Bush viper", - "scientific": "Atheris" - }, - { - "name": "Calabar python", - "scientific": "Calabar python" - }, - { - "name": "Caspian cobra", - "scientific": "Caspian cobra" - }, - { - "name": "Centralian carpet python", - "scientific": "Morelia bredli" - }, - { - "name": "Chinese tree viper", - "scientific": "Trimeresurus stejnegeri" - }, - { - "name": "Coastal carpet python", - "scientific": "Morelia spilota mcdowelli" - }, - { - "name": "Colorado desert sidewinder", - "scientific": "Crotalus cerastes laterorepens" - }, - { - "name": "Common lancehead", - "scientific": "Bothrops atrox" - }, - { - "name": "Cyclades blunt-nosed viper", - "scientific": "Macrovipera schweizeri" - }, - { - "name": "Dauan Island water python", - "scientific": "Liasis fuscus" - }, - { - "name": "De Schauensee's anaconda", - "scientific": "Eunectes deschauenseei" - }, - { - "name": "Dumeril's boa", - "scientific": "Acrantophis dumerili" - }, - { - "name": "Dusky pigmy rattlesnake", - "scientific": "Sistrurus miliarius barbouri" - }, - { - "name": "Dwarf sand adder", - "scientific": "Bitis peringueyi" - }, - { - "name": "Egyptian cobra", - "scientific": "Egyptian cobra" - }, - { - "name": "Elegant pitviper", - "scientific": "Trimeresurus elegans" - }, - { - "name": "Emerald tree boa", - "scientific": "Emerald tree boa" - }, - { - "name": "Equatorial spitting cobra", - "scientific": "Equatorial spitting cobra" - }, - { - "name": "European asp", - "scientific": "Vipera aspis" - }, - { - "name": "Eyelash palm-pitviper", - "scientific": "Bothriechis schlegelii" - }, - { - "name": "Eyelash pit viper", - "scientific": "Bothriechis schlegelii" - }, - { - "name": "Eyelash viper", - "scientific": "Bothriechis schlegelii" - }, - { - "name": "False horned viper", - "scientific": "Pseudocerastes" - }, - { - "name": "Fan-Si-Pan horned pitviper", - "scientific": "Trimeresurus cornutus" - }, - { - "name": "Fea's viper", - "scientific": "Azemiops" - }, - { - "name": "Fifty pacer", - "scientific": "Deinagkistrodon" - }, - { - "name": "Flat-nosed pitviper", - "scientific": "Trimeresurus puniceus" - }, - { - "name": "Godman's pit viper", - "scientific": "Cerrophidion godmani" - }, - { - "name": "Great Lakes bush viper", - "scientific": "Atheris nitschei" - }, - { - "name": "Green palm viper", - "scientific": "Bothriechis lateralis" - }, - { - "name": "Green tree pit viper", - "scientific": "Trimeresurus gramineus" - }, - { - "name": "Guatemalan palm viper", - "scientific": "Bothriechis aurifer" - }, - { - "name": "Guatemalan tree viper", - "scientific": "Bothriechis bicolor" - }, - { - "name": "Hagen's pitviper", - "scientific": "Trimeresurus hageni" - }, - { - "name": "Hairy bush viper", - "scientific": "Atheris hispida" - }, - { - "name": "Himehabu", - "scientific": "Ovophis okinavensis" - }, - { - "name": "Hogg Island boa", - "scientific": "Boa constrictor imperator" - }, - { - "name": "Honduran palm viper", - "scientific": "Bothriechis marchi" - }, - { - "name": "Horned desert viper", - "scientific": "Cerastes cerastes" - }, - { - "name": "Horseshoe pitviper", - "scientific": "Trimeresurus strigatus" - }, - { - "name": "Hundred pacer", - "scientific": "Deinagkistrodon" - }, - { - "name": "Hutton's tree viper", - "scientific": "Tropidolaemus huttoni" - }, - { - "name": "Indian python", - "scientific": "Python molurus" - }, - { - "name": "Indian tree viper", - "scientific": "Trimeresurus gramineus" - }, - { - "name": "Indochinese spitting cobra", - "scientific": "Indochinese spitting cobra" - }, - { - "name": "Indonesian water python", - "scientific": "Liasis mackloti" - }, - { - "name": "Javan spitting cobra", - "scientific": "Javan spitting cobra" - }, - { - "name": "Jerdon's pitviper", - "scientific": "Trimeresurus jerdonii" - }, - { - "name": "Jumping viper", - "scientific": "Atropoides" - }, - { - "name": "Jungle carpet python", - "scientific": "Morelia spilota cheynei" - }, - { - "name": "Kanburian pit viper", - "scientific": "Trimeresurus kanburiensis" - }, - { - "name": "Kaulback's lance-headed pitviper", - "scientific": "Trimeresurus kaulbacki" - }, - { - "name": "Kaznakov's viper", - "scientific": "Vipera kaznakovi" - }, - { - "name": "Kham Plateau pitviper", - "scientific": "Protobothrops xiangchengensis" - }, - { - "name": "Lachesis (genus)", - "scientific": "Lachesis (genus)" - }, - { - "name": "Large-eyed pitviper", - "scientific": "Trimeresurus macrops" - }, - { - "name": "Large-scaled tree viper", - "scientific": "Trimeresurus macrolepis" - }, - { - "name": "Leaf-nosed viper", - "scientific": "Eristicophis" - }, - { - "name": "Leaf viper", - "scientific": "Atheris squamigera" - }, - { - "name": "Levant viper", - "scientific": "Macrovipera lebetina" - }, - { - "name": "Long-nosed viper", - "scientific": "Vipera ammodytes" - }, - { - "name": "Macklot's python", - "scientific": "Liasis mackloti" - }, - { - "name": "Madagascar tree boa", - "scientific": "Sanzinia" - }, - { - "name": "Malabar rock pitviper", - "scientific": "Trimeresurus malabaricus" - }, - { - "name": "Malcolm's tree viper", - "scientific": "Trimeresurus sumatranus malcolmi" - }, - { - "name": "Mandalay cobra", - "scientific": "Mandalay spitting cobra" - }, - { - "name": "Mangrove pit viper", - "scientific": "Trimeresurus purpureomaculatus" - }, - { - "name": "Mangshan pitviper", - "scientific": "Trimeresurus mangshanensis" - }, - { - "name": "McMahon's viper", - "scientific": "Eristicophis" - }, - { - "name": "Mexican palm-pitviper", - "scientific": "Bothriechis rowleyi" - }, - { - "name": "Monocled cobra", - "scientific": "Monocled cobra" - }, - { - "name": "Motuo bamboo pitviper", - "scientific": "Trimeresurus medoensis" - }, - { - "name": "Mozambique spitting cobra", - "scientific": "Mozambique spitting cobra" - }, - { - "name": "Namaqua dwarf adder", - "scientific": "Bitis schneideri" - }, - { - "name": "Namib dwarf sand adder", - "scientific": "Bitis peringueyi" - }, - { - "name": "New Guinea carpet python", - "scientific": "Morelia spilota variegata" - }, - { - "name": "Nicobar bamboo pitviper", - "scientific": "Trimeresurus labialis" - }, - { - "name": "Nitsche's bush viper", - "scientific": "Atheris nitschei" - }, - { - "name": "Nitsche's tree viper", - "scientific": "Atheris nitschei" - }, - { - "name": "Northwestern carpet python", - "scientific": "Morelia spilota variegata" - }, - { - "name": "Nubian spitting cobra", - "scientific": "Nubian spitting cobra" - }, - { - "name": "Oenpelli python", - "scientific": "Oenpelli python" - }, - { - "name": "Olive python", - "scientific": "Liasis olivaceus" - }, - { - "name": "Pallas' viper", - "scientific": "Gloydius halys" - }, - { - "name": "Palm viper", - "scientific": "Bothriechis lateralis" - }, - { - "name": "Papuan python", - "scientific": "Apodora" - }, - { - "name": "Peringuey's adder", - "scientific": "Bitis peringueyi" - }, - { - "name": "Philippine cobra", - "scientific": "Philippine cobra" - }, - { - "name": "Philippine pitviper", - "scientific": "Trimeresurus flavomaculatus" - }, - { - "name": "Pope's tree viper", - "scientific": "Trimeresurus popeorum" - }, - { - "name": "Portuguese viper", - "scientific": "Vipera seoanei" - }, - { - "name": "Puerto Rican boa", - "scientific": "Puerto Rican boa" - }, - { - "name": "Rainbow boa", - "scientific": "Rainbow boa" - }, - { - "name": "Red spitting cobra", - "scientific": "Red spitting cobra" - }, - { - "name": "Rhinoceros viper", - "scientific": "Bitis nasicornis" - }, - { - "name": "Rhombic night adder", - "scientific": "Causus maculatus" - }, - { - "name": "Rinkhals", - "scientific": "Rinkhals" - }, - { - "name": "Rinkhals cobra", - "scientific": "Rinkhals" - }, - { - "name": "River jack", - "scientific": "Bitis nasicornis" - }, - { - "name": "Rough-scaled bush viper", - "scientific": "Atheris hispida" - }, - { - "name": "Rough-scaled python", - "scientific": "Rough-scaled python" - }, - { - "name": "Rough-scaled tree viper", - "scientific": "Atheris hispida" - }, - { - "name": "Royal python", - "scientific": "Ball python" - }, - { - "name": "Rungwe tree viper", - "scientific": "Atheris nitschei rungweensis" - }, - { - "name": "Sakishima habu", - "scientific": "Trimeresurus elegans" - }, - { - "name": "Savu python", - "scientific": "Liasis mackloti savuensis" - }, - { - "name": "Schlegel's viper", - "scientific": "Bothriechis schlegelii" - }, - { - "name": "Schultze's pitviper", - "scientific": "Trimeresurus schultzei" - }, - { - "name": "Sedge viper", - "scientific": "Atheris nitschei" - }, - { - "name": "Sharp-nosed viper", - "scientific": "Deinagkistrodon" - }, - { - "name": "Siamese palm viper", - "scientific": "Trimeresurus puniceus" - }, - { - "name": "Side-striped palm-pitviper", - "scientific": "Bothriechis lateralis" - }, - { - "name": "Snorkel viper", - "scientific": "Deinagkistrodon" - }, - { - "name": "Snouted cobra", - "scientific": "Snouted cobra" - }, - { - "name": "Sonoran sidewinder", - "scientific": "Crotalus cerastes cercobombus" - }, - { - "name": "Southern Indonesian spitting cobra", - "scientific": "Javan spitting cobra" - }, - { - "name": "Southern Philippine cobra", - "scientific": "Samar cobra" - }, - { - "name": "Spiny bush viper", - "scientific": "Atheris hispida" - }, - { - "name": "Spitting cobra", - "scientific": "Spitting cobra" - }, - { - "name": "Spotted python", - "scientific": "Spotted python" - }, - { - "name": "Sri Lankan pit viper", - "scientific": "Trimeresurus trigonocephalus" - }, - { - "name": "Stejneger's bamboo pitviper", - "scientific": "Trimeresurus stejnegeri" - }, - { - "name": "Storm water cobra", - "scientific": "Naja annulata" - }, - { - "name": "Sumatran tree viper", - "scientific": "Trimeresurus sumatranus" - }, - { - "name": "Temple viper", - "scientific": "Tropidolaemus wagleri" - }, - { - "name": "Tibetan bamboo pitviper", - "scientific": "Trimeresurus tibetanus" - }, - { - "name": "Tiger pit viper", - "scientific": "Trimeresurus kanburiensis" - }, - { - "name": "Timor python", - "scientific": "Python timoriensis" - }, - { - "name": "Tokara habu", - "scientific": "Trimeresurus tokarensis" - }, - { - "name": "Tree boa", - "scientific": "Emerald tree boa" - }, - { - "name": "Undulated pit viper", - "scientific": "Ophryacus undulatus" - }, - { - "name": "Ursini's viper", - "scientific": "Vipera ursinii" - }, - { - "name": "Wagler's pit viper", - "scientific": "Tropidolaemus wagleri" - }, - { - "name": "West African brown spitting cobra", - "scientific": "Mozambique spitting cobra" - }, - { - "name": "White-lipped tree viper", - "scientific": "Trimeresurus albolabris" - }, - { - "name": "Wirot's pit viper", - "scientific": "Trimeresurus puniceus" - }, - { - "name": "Yellow-lined palm viper", - "scientific": "Bothriechis lateralis" - }, - { - "name": "Zebra spitting cobra", - "scientific": "Naja nigricincta" - }, - { - "name": "Yarara", - "scientific": "Bothrops jararaca" - }, - { - "name": "Wetar Island python", - "scientific": "Liasis macklot" - }, - { - "name": "Urutus", - "scientific": "Bothrops alternatus" - }, - { - "name": "Titanboa", - "scientific": "Titanoboa" - } -]
\ No newline at end of file diff --git a/pysite/migrations/tables/snake_quiz/__init__.py b/pysite/migrations/tables/snake_quiz/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/snake_quiz/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/snake_quiz/initial_data.json b/pysite/migrations/tables/snake_quiz/initial_data.json deleted file mode 100644 index 8c426b22..00000000 --- a/pysite/migrations/tables/snake_quiz/initial_data.json +++ /dev/null @@ -1,200 +0,0 @@ -[ - { - "id": 0, - "question": "How long have snakes been roaming the Earth for?", - "options": { - "a": "3 million years", - "b": "30 million years", - "c": "130 million years", - "d": "200 million years" - }, - "answerkey": "c" - }, - { - "id": 1, - "question": "What characteristics do all snakes share?", - "options": { - "a": "They are carnivoes", - "b": "They are all programming languages", - "c": "They're all cold-blooded", - "d": "They are both carnivores and cold-blooded" - }, - "answerkey": "c" - }, - { - "id": 2, - "question": "How do snakes hear?", - "options": { - "a": "With small ears", - "b": "Through their skin", - "c": "Through their tail", - "d": "They don't use their ears at all" - }, - "answerkey": "b" - }, - { - "id": 3, - "question": "What can't snakes see?", - "options": { - "a": "Colour", - "b": "Light", - "c": "Both of the above", - "d": "Other snakes" - }, - "answerkey": "a" - }, - { - "id": 4, - "question": "What unique vision ability do boas and pythons possess?", - "options": { - "a": "Night vision", - "b": "Infrared vision", - "c": "See through walls", - "d": "They don't have vision" - }, - "answerkey": "b" - }, - { - "id": 5, - "question": "How does a snake smell?", - "options": { - "a": "Quite pleasant", - "b": "Through its nose", - "c": "Through its tongues", - "d": "Both through its nose and its tongues" - }, - "answerkey": "d" - }, - { - "id": 6, - "question": "Where are Jacobson's organs located in snakes?", - "options": { - "a": "Mouth", - "b": "Tail", - "c": "Stomach", - "d": "Liver" - }, - "answerkey": "a" - }, - { - "id": 7, - "question": "Snakes have very similar internal organs compared to humans. Snakes, however; lack the following:", - "options": { - "a": "A diaphragm", - "b": "Intestines", - "c": "Lungs", - "d": "Kidney" - }, - "answerkey": "a" - }, - { - "id": 8, - "question": "Snakes have different shaped lungs than humans. What do snakes have?", - "options": { - "a": "An elongated right lung", - "b": "A small left lung", - "c": "Both of the above", - "d": "None of the above" - }, - "answerkey": "c" - }, - { - "id": 9, - "question": "What's true about two-headed snakes?", - "options": { - "a": "They're a myth!", - "b": "They rarely survive in the wild", - "c": "They're very dangerous", - "d": "They can kiss each other" - }, - "answerkey": "b" - }, - { - "id": 10, - "question": "What substance covers a snake's skin?", - "options": { - "a": "Calcium", - "b": "Keratin", - "c": "Copper", - "d": "Iron" - }, - "answerkey": "b" - }, - { - "id": 11, - "question": "What snake doesn't have to have a mate to lay eggs?", - "options": { - "a": "Copperhead", - "b": "Cornsnake", - "c": "Kingsnake", - "d": "Flower pot snake" - }, - "answerkey": "d" - }, - { - "id": 12, - "question": "What snake is the longest?", - "options": { - "a": "Green anaconda", - "b": "Reticulated python", - "c": "King cobra", - "d": "Kingsnake" - }, - "answerkey": "b" - }, - { - "id": 13, - "question": "Though invasive species can now be found in the Everglades, in which three continents are pythons (members of the family Pythonidae) found in the wild?", - "options": { - "a": "Africa, Asia and Australia", - "b": "Africa, Australia and Europe", - "c": "Africa, Australia and South America", - "d": "Africa, Asia and South America" - }, - "answerkey": "a" - }, - { - "id": 14, - "question": "Pythons are held as some of the most dangerous snakes on earth, but are often confused with anacondas. Which of these is *not* a difference between pythons and anacondas?", - "options": { - "a": "Pythons suffocate their prey, anacondas crush them", - "b": "Pythons lay eggs, anacondas give birth to live young", - "c": "Pythons grow longer, anacondas grow heavier", - "d": "Pythons generally spend less time in water than anacondas do" - }, - "answerkey": "a" - }, - { - "id": 15, - "question": "Pythons are unable to chew their food, and so swallow prey whole. Which of these methods is most commonly demonstrated to help a python to swallow large prey?", - "options": { - "a": "The python's stomach pressure is reduced, so prey is sucked in", - "b": "An extra set of upper teeth 'walk' along the prey", - "c": "The python holds its head up, so prey falls into its stomach", - "d": "Prey is pushed against a barrier and is forced down the python's throat" - }, - "answerkey": "b" - }, - { - "id": 16, - "question": "Pythons, like many large constrictors, possess vestigial hind limbs. Whilst these 'spurs' serve no purpose in locomotion, how are they put to use by some male pythons? ", - "options": { - "a": "To store sperm", - "b": "To release pheromones", - "c": "To grip females during mating", - "d": "To fight off rival males" - }, - "answerkey": "c" - }, - { - "id": 17, - "question": "Pythons tend to travel by the rectilinear method (in straight lines) when on land, as opposed to the concertina method (s-shaped movement). Why do large pythons tend not to use the concertina method? ", - "options": { - "a": "Their spine is too inflexible", - "b": "They move too slowly", - "c": "The scales on their backs are too rigid", - "d": "They are too heavy" - }, - "answerkey": "d" - } -] diff --git a/pysite/migrations/tables/special_snakes/__init__.py b/pysite/migrations/tables/special_snakes/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/special_snakes/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/special_snakes/initial_data.json b/pysite/migrations/tables/special_snakes/initial_data.json deleted file mode 100644 index 8159f914..00000000 --- a/pysite/migrations/tables/special_snakes/initial_data.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "name": "Bob Ross", - "info": "Robert Norman Ross (October 29, 1942 – July 4, 1995) was an American painter, art instructor, and television host. He was the creator and host of The Joy of Painting, an instructional television program that aired from 1983 to 1994 on PBS in the United States, and also aired in Canada, Latin America, and Europe.", - "image_list": [ - "https://d3atagt0rnqk7k.cloudfront.net/wp-content/uploads/2016/09/23115633/bob-ross-1-1280x800.jpg" - ] - }, - { - "name": "Mystery Snake", - "info": "The Mystery Snake is rumored to be a thin, serpentine creature that hides in spaghetti dinners. It has yellow, pasta-like scales with a completely smooth texture, and is quite glossy. ", - "image_list": [ - "https://img.thrfun.com/img/080/349/spaghetti_dinner_l1.jpg" - ] - } -]
\ No newline at end of file diff --git a/pysite/migrations/tables/users/__init__.py b/pysite/migrations/tables/users/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/users/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/users/v1.py b/pysite/migrations/tables/users/v1.py deleted file mode 100644 index 9ba70142..00000000 --- a/pysite/migrations/tables/users/v1.py +++ /dev/null @@ -1,11 +0,0 @@ -def run(db, table, table_obj): - """ - Remove stored email addresses from every user document - """ - - for document in db.get_all(table): - if "email" in document: - del document["email"] - - db.insert(table, document, conflict="update", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/users/v2.py b/pysite/migrations/tables/users/v2.py deleted file mode 100644 index 820d0d6d..00000000 --- a/pysite/migrations/tables/users/v2.py +++ /dev/null @@ -1,11 +0,0 @@ -def run(db, table, table_obj): - """ - Remove stored email addresses from every user document - "apparently `update` doesn't update" update - """ - - for document in db.get_all(table): - if "email" in document: - del document["email"] - - db.insert(table, document, conflict="replace", durability="soft") - db.sync(table) diff --git a/pysite/migrations/tables/wiki/__init__.py b/pysite/migrations/tables/wiki/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/migrations/tables/wiki/__init__.py +++ /dev/null diff --git a/pysite/migrations/tables/wiki/v1.py b/pysite/migrations/tables/wiki/v1.py deleted file mode 100644 index 22670342..00000000 --- a/pysite/migrations/tables/wiki/v1.py +++ /dev/null @@ -1,11 +0,0 @@ -def run(db, table, table_obj): - """ - Ensure that there are no wiki articles that don't have titles - """ - - for document in db.pluck(table, table_obj.primary_key, "title"): - if not document.get("title"): - document["title"] = "No Title" - - db.insert(table, document, conflict="update", durability="soft") - db.sync(table) diff --git a/pysite/mixins.py b/pysite/mixins.py deleted file mode 100644 index 98528891..00000000 --- a/pysite/mixins.py +++ /dev/null @@ -1,214 +0,0 @@ -from typing import Any, Dict -from weakref import ref - -from flask import Blueprint -from kombu import Connection -from rethinkdb.ast import Table - -from pysite.constants import ( - BOT_EVENT_QUEUE, BotEventTypes, - RMQ_HOST, RMQ_PASSWORD, RMQ_PORT, RMQ_USERNAME -) -from pysite.database import RethinkDB -from pysite.oauth import OAuthBackend - - -BOT_EVENT_REQUIRED_PARAMS = { - "mod_log": ("level", "title", "message"), - "send_message": ("target", "message"), - "send_embed": ("target",), - "add_role": ("target", "role_id", "reason"), - "remove_role": ("target", "role_id", "reason") -} - - -class DBMixin: - """ - Mixin for classes that make use of RethinkDB. It can automatically create a table with the specified primary - key using the attributes set at class-level. - - This class is intended to be mixed in alongside one of the other view classes. For example: - - >>> class MyView(APIView, DBMixin): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... table_name = "my_table" # Name of the table to create - ... table_primary_key = "username" # Primary key to set for this table - - This class will also work with Websockets: - - >>> class MyWebsocket(WS, DBMixin): - ... name = "my_websocket" - ... path = "/my_websocket" - ... table_name = "my_table" - ... table_primary_key = "username" - - You may omit `table_primary_key` and it will be defaulted to RethinkDB's default column - "id". - """ - - table_name = "" # type: str - table_primary_key = "id" # type: str - - @classmethod - def setup(cls: "DBMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view by creating the table specified by the class attributes - this will also deal with multiple - inheritance by calling `super().setup()` as appropriate. - - :param manager: Instance of the current RouteManager (used to get a handle for the database object) - :param blueprint: Current Flask blueprint - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - cls._db = ref(manager.db) - - @property - def table(self) -> Table: - return self.db.query(self.table_name) - - @property - def db(self) -> RethinkDB: - return self._db() - - -class RMQMixin: - """ - Mixin for classes that make use of RabbitMQ. It allows routes to send JSON-encoded messages to specific RabbitMQ - queues. - - This class is intended to be mixed in alongside one of the other view classes. For example: - - >>> class MyView(APIView, RMQMixin): - ... name = "my_view" # Flask internal name for this route - ... path = "/my_view" # Actual URL path to reach this route - ... queue_name = "my_queue" # Name of the RabbitMQ queue to send on - - Note that the queue name is optional if all you want to do is send bot events. - - This class will also work with Websockets: - - >>> class MyWebsocket(WS, RMQMixin): - ... name = "my_websocket" - ... path = "/my_websocket" - ... queue_name = "my_queue" - """ - - queue_name = "" - - @classmethod - def setup(cls: "RMQMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the view by calling `super().setup()` as appropriate. - - :param manager: Instance of the current RouteManager (used to get a handle for the database object) - :param blueprint: Current Flask blueprint - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - @property - def rmq_connection(self) -> Connection: - """ - Get a Kombu AMQP connection object - use this in a context manager so that it gets closed after you're done - - If you're just trying to send a message, check out `rmq_send` and `rmq_bot_event` instead. - """ - - return Connection(hostname=RMQ_HOST, userid=RMQ_USERNAME, password=RMQ_PASSWORD, port=RMQ_PORT) - - def rmq_send(self, data: Dict[str, Any], routing_key: str = None): - """ - Send some data to the RabbitMQ queue - - >>> self.rmq_send({ - ... "text": "My hovercraft is full of eels!", - ... "source": "Dirty Hungarian Phrasebook" - ... }) - ... - - This will be delivered to the queue immediately. - """ - - if routing_key is None: - routing_key = self.queue_name - - with self.rmq_connection as c: - producer = c.Producer() - producer.publish(data, routing_key=routing_key) - - def rmq_bot_event(self, event_type: BotEventTypes, data: Dict[str, Any]): - """ - Send an event to the queue responsible for delivering events to the bot - - >>> self.rmq_bot_event(BotEventTypes.send_message, { - ... "channel": CHANNEL_MOD_LOG, - ... "message": "This is a plain-text message for @everyone, from the site!" - ... }) - ... - - This will be delivered to the bot and actioned immediately, or when the bot comes online if it isn't already - connected. - """ - - if not isinstance(event_type, BotEventTypes): - raise ValueError("`event_type` must be a member of the the `pysite.constants.BotEventTypes` enum") - - event_type = event_type.value - required_params = BOT_EVENT_REQUIRED_PARAMS[event_type] - - for param in required_params: - if param not in data: - raise KeyError(f"Event is missing required parameter: {param}") - - return self.rmq_send( - {"event": event_type, "data": data}, - routing_key=BOT_EVENT_QUEUE, - ) - - -class OAuthMixin: - """ - Mixin for the classes that need access to a logged in user's information. This class should be used - to grant route's access to user information, such as name, email, id, ect. - - There will almost never be a need for someone to inherit this, as BaseView does that for you. - - This class will add 3 properties to your route: - - * logged_in (bool): True if user is registered with the site, False else wise. - - * user_data (dict): A dict that looks like this: - - { - "user_id": Their discord ID, - "username": Their discord username (without discriminator), - "discriminator": Their discord discriminator, - "email": Their email, in which is connected to discord - } - - user_data returns None, if the user isn't logged in. - - * oauth (OAuthBackend): The instance of pysite.oauth.OAuthBackend, connected to the RouteManager. - """ - - @classmethod - def setup(cls: "OAuthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - if hasattr(super(), "setup"): - super().setup(manager, blueprint) # pragma: no cover - - cls._oauth = ref(manager.oauth_backend) - - @property - def logged_in(self) -> bool: - return self.user_data is not None - - @property - def user_data(self) -> dict: - return self.oauth.user_data() - - @property - def oauth(self) -> OAuthBackend: - return self._oauth() diff --git a/pysite/oauth.py b/pysite/oauth.py deleted file mode 100644 index 86e7cdde..00000000 --- a/pysite/oauth.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging -from uuid import uuid4, uuid5 - -from flask import session -from flask_dance.consumer.backend import BaseBackend -from flask_dance.contrib.discord import discord - -from pysite.constants import DISCORD_API_ENDPOINT, OAUTH_DATABASE - - -class OAuthBackend(BaseBackend): - """ - This is the backend for the oauth - - This is used to manage users that have completed - an oauth dance. It contains 3 functions, get, set, - and delete, however we only use set. - - Inherits: - flake_dance.consumer.backend.BaseBackend - pysite.mixins.DBmixin - - Properties: - key: The app's secret, we use it too make session IDs - """ - - def __init__(self, manager): - super().__init__() - self.db = manager.db - self.key = manager.app.secret_key - self.db.create_table(OAUTH_DATABASE, primary_key="id") - - def get(self, *args, **kwargs): # Not used - pass - - def set(self, blueprint, token): - user = self.get_user() - sess_id = str(uuid5(uuid4(), self.key)) - self.add_user(token, user, sess_id) - - def delete(self, blueprint): # Not used - pass - - def add_user(self, token_data: dict, user_data: dict, session_id: str): - session["session_id"] = session_id - - self.db.insert( - OAUTH_DATABASE, - { - "id": session_id, - "access_token": token_data["access_token"], - "refresh_token": token_data["refresh_token"], - "expires_at": token_data["expires_at"], - "snowflake": user_data["id"] - }, - conflict="replace" - ) - - self.db.insert( - "users", - { - "user_id": user_data["id"], - "username": user_data["username"], - "discriminator": user_data["discriminator"] - }, - conflict="update" - ) - - def get_user(self) -> dict: - resp = discord.get(DISCORD_API_ENDPOINT + "/users/@me") # 'discord' is a request.Session with oauth information - if resp.status_code != 200: - logging.warning("Unable to get user information: " + str(resp.json())) - return resp.json() - - def user_data(self): - user_id = session.get("session_id") - if user_id: # If the user is logged in, get user info. - creds = self.db.get(OAUTH_DATABASE, user_id) - if creds: - return self.db.get("users", creds["snowflake"]) - - def logout(self): - sess_id = session.get("session_id") - if sess_id and self.db.get(OAUTH_DATABASE, sess_id): # If user exists in db, - self.db.delete(OAUTH_DATABASE, sess_id) # remove them (at least, their session) - session.clear() diff --git a/pysite/queues.py b/pysite/queues.py deleted file mode 100644 index 7a200208..00000000 --- a/pysite/queues.py +++ /dev/null @@ -1,5 +0,0 @@ -from kombu import Queue - -QUEUES = { # RabbitMQ Queue definitions, they'll be declared at gunicorn start time - "bot_events": Queue("bot_events", durable=True) -} diff --git a/pysite/route_manager.py b/pysite/route_manager.py deleted file mode 100644 index 79fb67ac..00000000 --- a/pysite/route_manager.py +++ /dev/null @@ -1,146 +0,0 @@ -import importlib -import inspect -import logging -import os - -from flask import Blueprint, Flask, _request_ctx_stack -from flask_dance.contrib.discord import make_discord_blueprint -from flask_sockets import Sockets -from gunicorn_config import _when_ready as when_ready - -from pysite.base_route import APIView, BaseView, ErrorView, RedirectView, RouteView, TemplateView -from pysite.constants import ( - CSRF, DEBUG_MODE, DISCORD_OAUTH_AUTHORIZED, DISCORD_OAUTH_ID, DISCORD_OAUTH_REDIRECT, - DISCORD_OAUTH_SCOPE, DISCORD_OAUTH_SECRET, PREFERRED_URL_SCHEME) -from pysite.database import RethinkDB -from pysite.oauth import OAuthBackend -from pysite.websockets import WS - -TEMPLATES_PATH = "../templates" -STATIC_PATH = "../static" - - -class RouteManager: - def __init__(self): - - # Set up the app and the database - self.app = Flask( - __name__, template_folder=TEMPLATES_PATH, static_folder=STATIC_PATH, static_url_path="/static", - ) - self.sockets = Sockets(self.app) - - self.db = RethinkDB() - self.log = logging.getLogger(__name__) - self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY", "super_secret") - self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.local:8080") - self.app.config["PREFERRED_URL_SCHEME"] = PREFERRED_URL_SCHEME - self.app.config["WTF_CSRF_CHECK_DEFAULT"] = False # We only want to protect specific routes - - # Trim blocks so that {% block %} statements in templates don't generate blank lines - self.app.jinja_env.trim_blocks = True - self.app.jinja_env.lstrip_blocks = True - - # We make the token valid for the lifetime of the session because of the wiki - you might spend some - # time editing an article, and it seems that session lifetime is a good analogue for how long you have - # to edit - self.app.config["WTF_CSRF_TIME_LIMIT"] = None - - if DEBUG_MODE: - # Migrate the database, as we would in prod - when_ready(output_func=self.db.log.info) - - self.app.before_request(self.db.before_request) - self.app.teardown_request(self.db.teardown_request) - - CSRF.init_app(self.app) # Set up CSRF protection - - # Load the oauth blueprint - self.oauth_backend = OAuthBackend(self) - self.oauth_blueprint = make_discord_blueprint( - DISCORD_OAUTH_ID, - DISCORD_OAUTH_SECRET, - DISCORD_OAUTH_SCOPE, - login_url=DISCORD_OAUTH_REDIRECT, - authorized_url=DISCORD_OAUTH_AUTHORIZED, - redirect_to="main.auth.done", - backend=self.oauth_backend - ) - self.log.debug(f"Loading Blueprint: {self.oauth_blueprint.name}") - self.app.register_blueprint(self.oauth_blueprint) - self.log.debug("") - - # Load the main blueprint - self.main_blueprint = Blueprint("main", __name__) - self.log.debug(f"Loading Blueprint: {self.main_blueprint.name}") - self.load_views(self.main_blueprint, "pysite/views/main") - self.load_views(self.main_blueprint, "pysite/views/error_handlers") - self.app.register_blueprint(self.main_blueprint) - self.log.debug("") - - # Load the subdomains - self.subdomains = ["api", "staff", "wiki"] - - for sub in self.subdomains: - try: - sub_blueprint = Blueprint(sub, __name__, subdomain=sub) - self.log.debug(f"Loading Blueprint: {sub_blueprint.name}") - self.load_views(sub_blueprint, f"pysite/views/{sub}") - self.app.register_blueprint(sub_blueprint) - except Exception: - logging.getLogger(__name__).exception(f"Failed to register blueprint for subdomain: {sub}") - - # Load the websockets - self.ws_blueprint = Blueprint("ws", __name__) - - self.log.debug("Loading websocket routes...") - self.load_views(self.ws_blueprint, "pysite/views/ws") - self.sockets.register_blueprint(self.ws_blueprint, url_prefix="/ws") - - self.app.before_request(self.https_fixing_hook) # Try to fix HTTPS issues - - def https_fixing_hook(self): - """ - Attempt to fix HTTPS issues by modifying the request context stack - """ - - if _request_ctx_stack is not None: - reqctx = _request_ctx_stack.top - reqctx.url_adapter.url_scheme = PREFERRED_URL_SCHEME - - def run(self): - from gevent.pywsgi import WSGIServer - from geventwebsocket.handler import WebSocketHandler - - server = WSGIServer( - ("0.0.0.0", int(os.environ.get("WEBPAGE_PORT", 8080))), # noqa: B104, S104 - self.app, handler_class=WebSocketHandler - ) - server.serve_forever() - - def load_views(self, blueprint, location="pysite/views"): - for filename in os.listdir(location): - if os.path.isdir(f"{location}/{filename}"): - # Recurse if it's a directory; load ALL the views! - self.load_views(blueprint, location=f"{location}/{filename}") - continue - - if filename.endswith(".py") and not filename.startswith("__init__"): - module = importlib.import_module(f"{location}/{filename}".replace("/", ".")[:-3]) - - for cls_name, cls in inspect.getmembers(module): - if ( - inspect.isclass(cls) and - cls is not BaseView and - cls is not ErrorView and - cls is not RouteView and - cls is not APIView and - cls is not WS and - cls is not TemplateView and - cls is not RedirectView and - ( - BaseView in cls.__mro__ or - WS in cls.__mro__ - ) - ): - cls.setup(self, blueprint) - self.log.debug(f">> View loaded: {cls.name: <15} ({module.__name__}.{cls_name})") diff --git a/pysite/rst/__init__.py b/pysite/rst/__init__.py deleted file mode 100644 index e58fbe8c..00000000 --- a/pysite/rst/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import re - -from docutils.core import publish_parts -from docutils.parsers.rst.directives import register_directive -from docutils.parsers.rst.roles import register_canonical_role - -from pysite.rst.directives import ButtonDirective -from pysite.rst.roles import fira_code_role, icon_role, page_role, url_for_role - -RST_TEMPLATE = """.. contents:: - -{0}""" - -CONTENTS_REGEX = re.compile(r"""<div class=\"contents topic\" id=\"contents\">(.*?)</div>""", re.DOTALL) -HREF_REGEX = re.compile(r"""<a class=\"reference internal\" href=\"(.*?)\".*?>(.*?)</a>""") - -TABLE_FRAGMENT = """<table class="uk-table uk-table-divider table-bordered uk-table-striped">""" - - -def render(rst: str, link_headers=True): - if link_headers: - rst = RST_TEMPLATE.format(rst) - - html = publish_parts( - source=rst, writer_name="html5", settings_overrides={ - "halt_level": 2, "syntax_highlight": "short", "initial_header_level": 3 - } - )["html_body"] - - data = { - "html": html, - "headers": [] - } - - if link_headers: - match = CONTENTS_REGEX.search(html) # Find the contents HTML - - if match: - data["html"] = html.replace(match.group(0), "") # Remove the contents from the document HTML - depth = 0 - headers = [] - current_header = {} - - group = match.group(1) - - # Sanitize the output so we can more easily parse it - group = group.replace("<li>", "<li>\n") - group = group.replace("</li>", "\n</li>") - group = group.replace("<p>", "<p>\n") - group = group.replace("</p>", "\n</p>") - - for line in group.split("\n"): - line = line.strip() # Remove excess whitespace - - if not line: # Nothing to process - continue - - if line.startswith("<li>") and depth <= 2: - # We've found a header, or the start of a header group - depth += 1 - elif line.startswith("</li>") and depth >= 0: - # That's the end of a header or header group - - if depth == 1: - # We just dealt with an entire header group, so store it - headers.append(current_header.copy()) # Store a copy, since we're clearing the dict - current_header.clear() - - depth -= 1 - elif line.startswith("<a") and depth <= 2: - # We've found an actual URL - match = HREF_REGEX.match(line) # Parse the line for the ID and header title - - if depth == 1: # Top-level header, so just store it in the current header - current_header["id"] = match.group(1) - - title = match.group(2) - - if title.startswith("<i"): # We've found an icon, which needs to have a space after it - title = title.replace("</i> ", "</i> ") - - current_header["title"] = title - else: # Second-level (or deeper) header, should be stored in a list of sub-headers - sub_headers = current_header.get("sub_headers", []) - title = match.group(2) - - if title.startswith("<i"): # We've found an icon, which needs to have a space after it - title = title.replace("</i> ", "</i> ") - - sub_headers.append({ - "id": match.group(1), - "title": title - }) - current_header["sub_headers"] = sub_headers - - data["headers"] = headers - - data["html"] = data["html"].replace("<table>", TABLE_FRAGMENT) # Style the tables properly - - return data - - -register_canonical_role("fira_code", fira_code_role) -register_canonical_role("icon", icon_role) -register_canonical_role("page", page_role) -register_canonical_role("url_for", url_for_role) - -register_directive("button", ButtonDirective) diff --git a/pysite/rst/directives/__init__.py b/pysite/rst/directives/__init__.py deleted file mode 100644 index b4359200..00000000 --- a/pysite/rst/directives/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -from docutils import nodes -from docutils.parsers.rst import Directive -from docutils.parsers.rst.directives import unchanged, unchanged_required -from flask import url_for -from jinja2 import escape - -BUTTON_TYPES = ("default", "primary", "secondary", "danger", "darkish", "darker") - -ICON_WEIGHT_TABLE = { - "light": "fal", - "regular": "far", - "solid": "fas", - "branding": "fab" -} -ICON_WEIGHTS = tuple(ICON_WEIGHT_TABLE.values()) - - -class ButtonDirective(Directive): - has_content = True - - option_spec = { - "icon": unchanged_required, - "text": unchanged_required, - "type": unchanged, - "url": unchanged, - } - - def run(self): - icon = self.options.get("icon", "") - button_type = self.options.get("type", "primary") - - text = self.options["text"] - url = self.options["url"] - - if icon: - parts = [escape(x) for x in icon.split("/")] - - if len(parts) != 2: - raise self.error("Icon specification must be in the form <type>/<name>") - elif parts: - weight = parts[0] - - if weight not in ICON_WEIGHTS: - weight = ICON_WEIGHT_TABLE.get(weight) - - if not weight: - raise self.error( - "Icon type must be one of light, regular, solid or " - "branding, or a font-awesome weight class" - ) - - icon_html = f"""<i class="uk-icon fa-fw {weight} fa-{parts[1]}"></i>""" - else: - icon_html = "" - - if button_type not in BUTTON_TYPES: - self.error(f"Button type must be one of {', '.join(BUTTON_TYPES[:-1])} or {[-1]}") - - if url.startswith("flask://"): - url = url_for(url.split("://", 1)[1]) - elif url.startswith("wiki://"): - url = url_for("wiki.page", page=url.split("://", 1)[1]) - html = f"""<a class="uk-button uk-button-{button_type}" href=\"{url}\">{icon_html} {text}</a>""" - - return [nodes.raw(html, html, format="html", **{})] diff --git a/pysite/rst/roles.py b/pysite/rst/roles.py deleted file mode 100644 index d83f07f9..00000000 --- a/pysite/rst/roles.py +++ /dev/null @@ -1,125 +0,0 @@ -from docutils import nodes -from docutils.parsers.rst.roles import set_classes -from docutils.parsers.rst.states import Inliner -from flask import url_for -from jinja2 import escape - - -def icon_role(_role: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: dict = None, _content: dict = None): - if options is None: - options = {} - - set_classes(options) - - if "/" in text: - parts = [escape(x) for x in text.split("/")] - else: - msg = inliner.reporter.error("Icon specification must be in the form <type>/<name>", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - if len(parts) != 2: - msg = inliner.reporter.error("Icon specification must be in the form <type>/<name>", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - else: - if parts[0] == "light": - weight = "fal" - elif parts[0] == "regular": - weight = "far" - elif parts[0] == "solid": - weight = "fas" - elif parts[0] == "branding": - weight = "fab" - else: - msg = inliner.reporter.error("Icon type must be one of light, regular, solid or branding", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - html = f"""<i class="uk-icon fa-fw {weight} fa-{parts[1]}"></i>""" - - node = nodes.raw(html, html, format="html", **options) - return [node], [] - - -def url_for_role(_role: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: dict = None, _content: dict = None): - if options is None: - options = {} - - set_classes(options) - - if "/" in text: - parts = [escape(x) for x in text.split("/")] - else: - msg = inliner.reporter.error("URL specification must be in the form <page.name>/<text>", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - if len(parts) != 2: - msg = inliner.reporter.error("URL specification must be in the form <page.name>/<text>", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - else: - try: - url = url_for(parts[0]) - name = parts[1] - - html = f"""<a href="{url}">{name}</a>""" - - node = nodes.raw(html, html, format="html", **options) - return [node], [] - except Exception as e: - msg = inliner.reporter.error(str(e), line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - -def page_role(_role: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: dict = None, _content: dict = None): - if options is None: - options = {} - - set_classes(options) - - if "/" in text: - parts = [escape(x) for x in text.rsplit("/", 1)] - else: - msg = inliner.reporter.error("Page specification must be in the form <page_slug>/<text>", line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - try: - url = url_for("wiki.page", page=parts[0]) - name = parts[1] - - html = f"""<a href="{url}">{name}</a>""" - - node = nodes.raw(html, html, format="html", **options) - return [node], [] - except Exception as e: - msg = inliner.reporter.error(str(e), line=lineno) - prb = inliner.problematic(text, rawtext, msg) - - return [prb], [msg] - - -def fira_code_role(_role: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: dict = None, _content: dict = None): - if options is None: - options = {} - - set_classes(options) - - html = f"""<span class="fira-code">{text}</span>""" - node = nodes.raw(html, html, format="html", **options) - - return [node], [] diff --git a/pysite/service_discovery.py b/pysite/service_discovery.py deleted file mode 100644 index a03341c4..00000000 --- a/pysite/service_discovery.py +++ /dev/null @@ -1,26 +0,0 @@ -import datetime -import socket -import time -from contextlib import closing - -from pysite.constants import RMQ_HOST, RMQ_PORT - -THIRTY_SECONDS = datetime.timedelta(seconds=30) - - -def wait_for_rmq(): - start = datetime.datetime.now() - - while True: - if datetime.datetime.now() - start > THIRTY_SECONDS: - return False - - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - try: - state = sock.connect_ex((RMQ_HOST, RMQ_PORT)) - if state == 0: - return True - except socket.gaierror: - pass - - time.sleep(0.5) diff --git a/pysite/tables.py b/pysite/tables.py deleted file mode 100644 index 65a4db16..00000000 --- a/pysite/tables.py +++ /dev/null @@ -1,292 +0,0 @@ -from typing import List, NamedTuple - - -class Table(NamedTuple): - primary_key: str - keys: List[str] - locked: bool = True - - -TABLES = { - "bot_events": Table( # Events to be sent to the bot via websocket - primary_key="id", - keys=sorted([ - "id", - "data" - ]) - ), - - "clean_logs": Table( # Logs of cleanups done by the clean bot commands - primary_key="id", - keys=sorted([ - "id", - "log_data" - ]) - ), - - "hiphopify": Table( # Users in hiphop prison - primary_key="user_id", - keys=sorted([ - "user_id", - "end_timestamp", - "forced_nick" - ]) - ), - - "hiphopify_namelist": Table( # Names and images of hiphop artists - primary_key="name", - keys=sorted([ - "name", - "image_url" - ]), - locked=False - ), - - "code_jams": Table( # Information about each code jam - primary_key="number", - keys=sorted([ - "date_end", # datetime - "date_start", # datetime - "end_html", # str - "end_rst", # str - "info_rst", # str - "info_html", # str - "number", # int - "participants", # list[str] - "repo", # str - "state", # str - "task_html", # str - "task_rst", # str - "teams", # list[str] - "theme", # str - "title", # str - "winning_team" # str - ]) - ), - - "code_jam_forms": Table( # Application forms for each jam - primary_key="number", - keys=sorted([ - "number", # int - "preamble_rst", # str - "preamble_html", # str - "questions" # list[dict[str, str]] {title, type, input_type, options?} - ]) - ), - - "code_jam_questions": Table( # Application form questions - primary_key="id", - keys=sorted([ - "data", # dict - "id", # uuid - "optional", # bool - "title", # str - "type", # str - ]) - ), - - "code_jam_responses": Table( # Application form responses - primary_key="id", - keys=sorted([ - "id", # uuid - "snowflake", # str - "jam", # int - "answers", # list [{question, answer, metadata}] - "approved" # bool - ]) - ), - - "code_jam_teams": Table( # Teams for each jam - primary_key="id", - keys=sorted([ - "id", # uuid - "name", # str - "members", # list[str] - "repo", # str - "jam" # int - ]) - ), - - "code_jam_infractions": Table( # Individual infractions for each user - primary_key="id", - keys=sorted([ - "id", # uuid - "participant", # str - "reason", # str - "number", # int (optionally -1 for permanent) - "decremented_for" # list[int] - ]) - ), - - "code_jam_participants": Table( # Info for each participant - primary_key="id", - keys=sorted([ - "id", # str - "gitlab_username", # str - "timezone" # str - ]) - ), - - "member_chunks": Table( - primary_key="id", - keys=sorted([ - "id", # str - "chunk", # list - ]) - ), - - "oauth_data": Table( # OAuth login information - primary_key="id", - keys=sorted([ - "id", - "access_token", - "expires_at", - "refresh_token", - "snowflake" - ]) - ), - - "off_topic_names": Table( # Names for the off-topic category channels - primary_key="name", - keys=("name",), - locked=False - ), - - "snake_facts": Table( # Snake facts - primary_key="fact", - keys=sorted([ - "fact" - ]), - locked=False - ), - - "snake_idioms": Table( # Snake idioms - primary_key="idiom", - keys=sorted([ - "idiom" - ]), - locked=False - ), - - "snake_names": Table( # Snake names - primary_key="name", - keys=sorted([ - "name", - "scientific" - ]), - locked=False - ), - - "snake_quiz": Table( # Snake questions and answers - primary_key="id", - keys=sorted([ - "id", - "question", - "options", - "answerkey" - ]), - locked=False - ), - - "special_snakes": Table( # Special case snakes for the snake converter - primary_key="name", - keys=sorted([ - "name", - "info", - "image_list", - ]), - locked=False - ), - - "tags": Table( # Tag names and values - primary_key="tag_name", - keys=sorted([ - "tag_name", - "tag_content" - ]), - locked=False - ), - - "users": Table( # Users from the Discord server - primary_key="user_id", - keys=sorted([ - "avatar", - "user_id", - "roles", - "username", - "discriminator" - ]) - ), - - "wiki": Table( # Wiki articles - primary_key="slug", - keys=sorted([ - "slug", - "headers", - "html", - "rst", - "text", - "title" - ]) - ), - - "wiki_revisions": Table( # Revisions of wiki articles - primary_key="id", - keys=sorted([ - "id", - "date", - "post", - "slug", - "user" - ]) - ), - - "_versions": Table( # Table migration versions - primary_key="table", - keys=sorted([ - "table", - "version" - ]) - ), - - "pydoc_links": Table( # pydoc_links - primary_key="package", - keys=sorted([ - "base_url", - "inventory_url", - "package" - ]), - locked=False - ), - - "bot_settings": Table( - primary_key="key", - keys=sorted([ - "key", # str - "value" # any - ]) - ), - - "bot_infractions": Table( - primary_key="id", - keys=sorted([ - "id", # str - "user_id", # str - "actor_id", # str - "reason", # str - "type", # str - "inserted_at", # datetime - "expires_at", # datetime - "closed", # bool - "legacy_rowboat_id" # str - ]) - ), - - "watched_users": Table( # Users being monitored by the bot's BigBrother cog - primary_key="user_id", - keys=sorted([ - "user_id", - "channel_id" - ]) - ) -} diff --git a/pysite/utils/time.py b/pysite/utils/time.py deleted file mode 100644 index 334408a4..00000000 --- a/pysite/utils/time.py +++ /dev/null @@ -1,62 +0,0 @@ -from datetime import datetime, timedelta - -from rethinkdb import make_timezone - - -UNITS = { - 's': lambda value: value, - 'm': lambda value: value * 60, - 'h': lambda value: value * 60 * 60, - 'd': lambda value: value * 60 * 60 * 24, - 'w': lambda value: value * 60 * 60 * 24 * 7 -} - - -def parse_duration(duration: str) -> datetime: - """ - Parses a string like '3w' into a datetime 3 weeks from now. - - Also supports strings like 1w2d or 1h25m. - - This function is adapted from a bot called ROWBOAT, written by b1naryth1ef. - See https://github.com/b1naryth1ef/rowboat/blob/master/rowboat/util/input.py - - :param duration: a string containing the number and a time unit shorthand. - :return: A datetime representing now + the duration - """ - - if not duration: - raise ValueError("No duration provided.") - - value = 0 - digits = '' - - for char in duration: - - # Add all numbers to the digits string - if char.isdigit(): - digits += char - continue - - # If it's not a number and not one of the letters in UNITS, it must be invalid. - if char not in UNITS or not digits: - raise ValueError("Invalid duration") - - # Otherwise, call the corresponding lambda to convert the value, and keep iterating. - value += UNITS[char](int(digits)) - digits = '' - - return datetime.now(make_timezone("00:00")) + timedelta(seconds=value + 1) - - -def is_expired(rdb_datetime: datetime) -> bool: - """ - Takes a rethinkdb datetime (timezone aware) and - figures out if it has expired yet. - - Always compares with UTC 00:00 - - :param rdb_timestamp: A datetime as stored in rethinkdb. - :return: True if the datetime is in the past. - """ - return datetime.now(make_timezone("00:00")) > rdb_datetime diff --git a/pysite/utils/words.py b/pysite/utils/words.py deleted file mode 100644 index 0959ec3e..00000000 --- a/pysite/utils/words.py +++ /dev/null @@ -1,73 +0,0 @@ -import random -from typing import Iterator, List, Tuple - -adjectives = ( - "abortive", "abounding", "abrasive", "absent", "acceptable", "adamant", "adhesive", "adjoining", "aggressive", - "alike", "alleged", "aloof", "ambitious", "amused", "aspiring", "available", "awake", "axiomatic", "barbarous", - "bashful", "beautiful", "befitting", "beneficial", "blushing", "boundless", "brawny", "certain", "childlike", - "cluttered", "courageous", "crooked", "damp", "deadpan", "debonair", "decorous", "defiant", "delirious", - "detailed", "disturbed", "divergent", "drab", "dramatic", "drunk", "electric", "enormous", "erect", "evanescent", - "excellent", "exultant", "faded", "famous", "far-flung", "fascinated", "faulty", "festive", "fine", "fixed", - "flaky", "flat", "fluttering", "foregoing", "frail", "fresh", "frightened", "funny", "furtive", "gainful", "glib", - "godly", "half", "hallowed", "handsome", "hard", "heavenly", "hesitant", "high", "honorable", "hot", "hungry", - "hurt", "hushed", "hypnotic", "ill-fated", "illegal", "important", "incompetent", "inconclusive", "infamous", - "innocent", "insidious", "instinctive", "jazzy", "jumbled", "kind", "knowing", "late", "laughable", "lean", - "loving", "madly", "majestic", "married", "materialistic", "measly", "mighty", "misty", "murky", "mushy", - "mysterious", "needy", "next", "nice", "nondescript", "nutritious", "omniscient", "ossified", "overconfident", - "panoramic", "parallel", "parched", "pastoral", "plant", "possible", "pricey", "prickly", "private", "productive", - "pumped", "purple", "purring", "quixotic", "rabid", "rare", "real", "receptive", "resolute", "right", "rightful", - "ritzy", "rough", "ruddy", "rude", "salty", "sassy", "satisfying", "scandalous", "sedate", "selective", "separate", - "shrill", "sincere", "slow", "small", "smooth", "sordid", "sour", "spicy", "spiky", "spiteful", "spooky" "spotty", - "steady", "subdued", "successful", "supreme", "sweltering", "synonymous", "talented", "tasty", "teeny", "telling", - "temporary", "tender", "tense", "tenuous", "thinkable", "thoughtless", "tiny", "tough", "trashy", "two", - "uncovered", "uninterested", "unruly", "unsuitable", "used", "useful", "vagabond", "verdant", "vivacious", - "voiceless", "waggish", "wasteful", "wealthy", "whole", "wise", "woebegone", "workable", "wrong", "young", -) - -nouns = ( - "actions", "activities", "additions", "advertisements", "afterthoughts", "airplanes", "amounts", "angles", "ants", - "baskets", "baths", "battles", "bees", "beginners", "behaviors", "beliefs", "bells", "berries", "bikes", - "birthdays", "bits", "boats", "boys", "breaths", "bubbles", "bulbs", "bursts", "butter", "cables", "camps", "cans", - "captions", "cars", "carpenters", "cats", "cemeteries", "changes", "channels", "chickens", "classes", "clubs", - "committees", "covers", "cracks", "crates", "crayons", "crowds", "decisions", "degrees", "details", "directions", - "dresses", "drops", "dusts", "errors", "examples", "expansions", "falls", "fangs", "feelings", "firemen", - "flowers", "fog", "feet", "fowls", "frogs", "glasses", "gloves", "grandmothers", "grounds", "guns", "haircuts", - "halls", "harmonies", "hats", "hopes", "horns", "horses", "ideas", "inks", "insects", "interests", "inventions", - "irons", "islands", "jails", "jeans", "jellyfish", "laborers", "lakes", "letters", "lockets", "matches", "measures", - "mice", "milk", "motions", "moves", "nerves", "numbers", "pans", "pancakes", "persons", "pets", "pickles", "pies", - "pizzas", "plantations", "plastics", "ploughs", "pockets", "potatoes", "powders", "properties", "reactions", - "regrets", "riddles", "rivers", "rocks", "sails", "scales", "scarecrows", "scarves", "scenes", "schools", - "sciences", "shakes", "shapes", "shirts", "silvers", "sinks", "snakes", "sneezes", "sofas", "songs", "sounds", - "spades", "sparks", "stages", "stamps", "stars", "stations", "stews", "stomachs", "suggestions", "suits", "swings", - "tables", "tents", "territories", "tests", "textures", "things", "thoughts", "threads", "tigers", "toads", "toes", - "tomatoes", "trains", "treatments", "troubles", "tubs", "turkeys", "umbrellas", "uncles", "vacations", "veils", - "voices", "volcanoes", "volleyballs", "walls", "wars", "waters", "waves", "wilderness", "women", "words", "works", - "worms", "wounds", "writings", "yams", "yards", "yarns", "zebras" -) - - -def get_adjectives(num: int = 1) -> List[str]: - """ - Get a list of random, unique adjectives - """ - - return random.sample(adjectives, num) - - -def get_nouns(num: int = 1) -> List[str]: - """ - Get a list of random, unique nouns - """ - - return random.sample(nouns, num) - - -def get_word_pairs(num: int = 1) -> Iterator[Tuple[str, str]]: - """ - Get an iterator over random, unique (adjective, noun) pairs - """ - - return zip( - get_adjectives(num), - get_nouns(num) - ) diff --git a/pysite/views/__init__.py b/pysite/views/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/__init__.py +++ /dev/null diff --git a/pysite/views/api/__init__.py b/pysite/views/api/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/__init__.py b/pysite/views/api/bot/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/bot/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/bigbrother.py b/pysite/views/api/bot/bigbrother.py deleted file mode 100644 index 89697811..00000000 --- a/pysite/views/api/bot/bigbrother.py +++ /dev/null @@ -1,118 +0,0 @@ -import json - -from flask import jsonify -from schema import And, Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -GET_SCHEMA = Schema({ - # This is passed as a GET parameter, so it has to be a string - Optional('user_id'): And(str, str.isnumeric, error="`user_id` must be a numeric string") -}) - -POST_SCHEMA = Schema({ - 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string"), - 'channel_id': And(str, str.isnumeric, error="`channel_id` must be a numeric string") -}) - -DELETE_SCHEMA = Schema({ - 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string") -}) - - -NOT_A_NUMBER_JSON = json.dumps({ - 'error_message': "The given `user_id` parameter is not a valid number" -}) -NOT_FOUND_JSON = json.dumps({ - 'error_message': "No entry for the requested user ID could be found." -}) - - -class BigBrotherView(APIView, DBMixin): - path = '/bot/bigbrother' - name = 'bot.bigbrother' - table_name = 'watched_users' - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params): - """ - Without query parameters, returns a list of all monitored users. - A parameter `user_id` can be specified to return a single entry, - or a dictionary with the string field 'error_message' that tells why it failed. - - If the returned status is 200, has got either a list of entries - or a single object (see above). - - If the returned status is 400, the `user_id` parameter was incorrectly specified. - If the returned status is 404, the given `user_id` could not be found. - See the 'error_message' field in the JSON response for more information. - - The user ID must be provided as query parameter. - API key must be provided as header. - """ - - user_id = params.get('user_id') - if user_id is not None: - data = self.db.get(self.table_name, user_id) - if data is None: - return NOT_FOUND_JSON, 404 - return jsonify(data) - - else: - data = self.db.pluck(self.table_name, ('user_id', 'channel_id')) or [] - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - """ - Adds a new entry to the database. - Entries take the following form: - { - "user_id": ..., # The user ID of the user being monitored, as a string. - "channel_id": ... # The channel ID that the user's messages will be relayed to, as a string. - } - - If an entry for the given `user_id` already exists, it will be updated with the new channel ID. - - Returns 204 (ok, empty response) on success. - - Data must be provided as JSON. - API key must be provided as header. - """ - - self.db.insert( - self.table_name, - { - 'user_id': data['user_id'], - 'channel_id': data['channel_id'] - }, - conflict='update' - ) - - return '', 204 - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.params) - def delete(self, params): - """ - Removes an entry for the given `user_id`. - - Returns 204 (ok, empty response) on success. - Returns 400 if the given `user_id` is invalid. - - The user ID must be provided as query parameter. - API key must be provided as header. - """ - - self.db.delete( - self.table_name, - params['user_id'] - ) - - return '', 204 diff --git a/pysite/views/api/bot/clean.py b/pysite/views/api/bot/clean.py deleted file mode 100644 index 82d1e735..00000000 --- a/pysite/views/api/bot/clean.py +++ /dev/null @@ -1,48 +0,0 @@ -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -POST_SCHEMA = Schema({ - 'log_data': [ - { - "author": str, - "user_id": str, - "content": str, - "role_id": str, - "timestamp": str, - "embeds": object, - "attachments": [str], - } - ] -}) - - -class CleanView(APIView, DBMixin): - path = '/bot/clean' - name = 'bot.clean' - table_name = 'clean_logs' - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - """ - Receive some log_data from a bulk deletion, - and store it in the database. - - Returns an ID which can be used to get the data - from the /bot/clean_logs/<id> endpoint. - """ - - # Insert and return the id to use for GET - insert = self.db.insert( - self.table_name, - { - "log_data": data["log_data"] - } - ) - - return jsonify({"log_id": insert['generated_keys'][0]}) diff --git a/pysite/views/api/bot/doc.py b/pysite/views/api/bot/doc.py deleted file mode 100644 index c1d6020c..00000000 --- a/pysite/views/api/bot/doc.py +++ /dev/null @@ -1,98 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -GET_SCHEMA = Schema([ - { - Optional("package"): str - } -]) - -POST_SCHEMA = Schema([ - { - "package": str, - "base_url": str, - "inventory_url": str - } -]) - -DELETE_SCHEMA = Schema([ - { - "package": str - } -]) - - -class DocView(APIView, DBMixin): - path = "/bot/docs" - name = "bot.docs" - table_name = "pydoc_links" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Fetches documentation metadata from the database. - - - If `package` parameters are provided, fetch metadata - for the given packages, or `[]` if none matched. - - - If `package` is not provided, return all - packages known to the database. - - Data must be provided as params. - API key must be provided as header. - """ - - if params: - packages = (param['package'] for param in params if 'package' in param) - data = self.db.get_all(self.table_name, *packages, index='package') or [] - else: - data = self.db.pluck(self.table_name, ("package", "base_url", "inventory_url")) or [] - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - Adds one or more new documentation metadata objects. - - If the `package` passed in the data - already exists, it will be updated instead. - - Data must be provided as JSON. - API key must be provided as header. - """ - - packages_to_insert = ( - { - "package": json_object["package"], - "base_url": json_object["base_url"], - "inventory_url": json_object["inventory_url"] - } for json_object in json_data - ) - - self.db.insert(self.table_name, *packages_to_insert, conflict="update") - return jsonify({"success": True}) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, json_data): - """ - Deletes a documentation metadata object. - Expects the `package` to be deleted to - be specified as a request parameter. - - Data must be provided as params. - API key must be provided as header. - """ - - packages = (json_object["package"]for json_object in json_data) - changes = self.db.delete(self.table_name, *packages, return_changes=True) - return jsonify(changes) diff --git a/pysite/views/api/bot/hiphopify.py b/pysite/views/api/bot/hiphopify.py deleted file mode 100644 index ce4dfa4a..00000000 --- a/pysite/views/api/bot/hiphopify.py +++ /dev/null @@ -1,170 +0,0 @@ -import logging - -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin -from pysite.utils.time import is_expired, parse_duration - -log = logging.getLogger(__name__) - -GET_SCHEMA = Schema({ - "user_id": str -}) - -POST_SCHEMA = Schema({ - "user_id": str, - "duration": str, - Optional("forced_nick"): str -}) - -DELETE_SCHEMA = Schema({ - "user_id": str -}) - - -class HiphopifyView(APIView, DBMixin): - path = "/bot/hiphopify" - name = "bot.hiphopify" - prison_table = "hiphopify" - name_table = "hiphopify_namelist" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Check if the user is currently in hiphop-prison. - - If user is currently servin' his sentence in the big house, - return the name stored in the forced_nick column of prison_table. - - If user cannot be found in prison, or - if his sentence has expired, return nothing. - - Data must be provided as params. - API key must be provided as header. - """ - - user_id = params.get("user_id") - - log.debug(f"Checking if user ({user_id}) is permitted to change their nickname.") - data = self.db.get(self.prison_table, user_id) or {} - - if data and data.get("end_timestamp"): - log.trace("User exists in the prison_table.") - end_time = data.get("end_timestamp") - if is_expired(end_time): - log.trace("...But their sentence has already expired.") - data = {} # Return nothing if the sentence has expired. - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - Imprisons a user in hiphop-prison. - - If a forced_nick was provided by the caller, the method will force - this nick. If not, a random hiphop nick will be selected from the - name_table. - - Data must be provided as JSON. - API key must be provided as header. - """ - - user_id = json_data.get("user_id") - duration = json_data.get("duration") - forced_nick = json_data.get("forced_nick") - - log.debug(f"Attempting to imprison user ({user_id}).") - - # Get random name and picture if no forced_nick was provided. - if not forced_nick: - log.trace("No forced_nick provided. Fetching a random rapper name and image.") - rapper_data = self.db.sample(self.name_table, 1)[0] - forced_nick = rapper_data.get('name') - - # If forced nick was provided, try to look up the forced_nick in the database. - # If a match cannot be found, just default to Lil' Jon for the image. - else: - log.trace(f"Forced nick provided ({forced_nick}). Trying to match it with the database.") - rapper_data = ( - self.db.get(self.name_table, forced_nick) - or self.db.get(self.name_table, "Lil' Joseph") - ) - - image_url = rapper_data.get('image_url') - log.trace(f"Using the nickname {forced_nick} and the image_url {image_url}.") - - # Convert duration to valid timestamp - try: - log.trace("Parsing the duration and converting it to a timestamp") - end_timestamp = parse_duration(duration) - except ValueError: - log.warning(f"The duration could not be parsed, or was invalid. The duration was '{duration}'.") - return jsonify({ - "success": False, - "error_message": "Invalid duration" - }) - - log.debug("Everything seems to be in order, inserting the data into the prison_table.") - self.db.insert( - self.prison_table, - { - "user_id": user_id, - "end_timestamp": end_timestamp, - "forced_nick": forced_nick - }, - conflict="update" # If it exists, update it. - ) - - return jsonify({ - "success": True, - "end_timestamp": end_timestamp, - "forced_nick": forced_nick, - "image_url": image_url - }) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, json_data): - """ - Releases a user from hiphop-prison. - - Data must be provided as JSON. - API key must be provided as header. - """ - - user_id = json_data.get("user_id") - - log.debug(f"Attempting to release user ({user_id}) from hiphop-prison.") - prisoner_data = self.db.get(self.prison_table, user_id) - sentence_expired = None - - log.trace(f"Checking if the user ({user_id}) is currently in hiphop-prison.") - if prisoner_data and prisoner_data.get("end_timestamp"): - sentence_expired = is_expired(prisoner_data['end_timestamp']) - - if prisoner_data and not sentence_expired: - log.debug("User is currently in hiphop-prison. Deleting the record and releasing the prisoner.") - self.db.delete( - self.prison_table, - user_id - ) - return jsonify({"success": True}) - elif not prisoner_data: - log.warning(f"User ({user_id}) is not currently in hiphop-prison.") - return jsonify({ - "success": False, - "error_message": "User is not currently in hiphop-prison!" - }) - elif sentence_expired: - log.warning(f"User ({user_id}) was in hiphop-prison, but has already been released.") - return jsonify({ - "success": False, - "error_message": "User has already been released from hiphop-prison!" - }) diff --git a/pysite/views/api/bot/infractions.py b/pysite/views/api/bot/infractions.py deleted file mode 100644 index eee40b82..00000000 --- a/pysite/views/api/bot/infractions.py +++ /dev/null @@ -1,572 +0,0 @@ -""" -INFRACTIONS API - -"GET" endpoints in this API may take the following optional parameters, depending on the endpoint: - - active: filters infractions that are active (true), expired (false), or either (not present/any) - - expand: expands the result data with the information about the users (slower) - - dangling: filters infractions that are active, or inactive infractions that have not been closed manually. - - search: filters the "reason" field to match the given RE2 query. - -Infraction Schema: - This schema is used when an infraction's data is returned. - - Root object: - "id" (str): the UUID of the infraction. - "inserted_at" (str): the date and time of the creation of this infraction (RFC1123 format). - "expires_at" (str): the date and time of the expiration of this infraction (RC1123 format), may be null. - The significance of this field being null depends on the type of infraction. Duration-based infractions - have a "null" expiration if they are permanent. Other infraction types do not have expirations. - "active" (bool): whether the infraction is still active. Note that the check for expiration of - duration-based infractions is done by the API, so you should check for expiration using this "active" field. - "user" (object): the user to which the infraction was applied. - "user_id" (str): the Discord ID of the user. - "username" (optional str): the username of the user. This field is only present if the query was expanded. - "discriminator" (optional int): the username discriminator of the user. This field is only present if the - query was expanded. - "avatar" (optional str): the avatar URL of the user. This field is only present if the query was expanded. - "actor" (object): the user which applied the infraction. - This object uses the same schema as the "user" field. - "type" (str): the type of the infraction. - "reason" (str): the reason for the infraction. - - -Endpoints: - - GET /bot/infractions - Gets a list of all infractions, regardless of type or user. - Parameters: "active", "expand", "dangling", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id> - Gets a list of all infractions for a user. - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/type/<type> - Gets a list of all infractions of the given type (ban, mute, etc.) - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id>/<type> - Gets a list of all infractions of the given type for a user. - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id>/<type>/current - Gets the active infraction (if any) of the given type for a user. - Parameters: "expand". - This endpoint returns an object with the "infraction" key, which is either set to null (no infraction) - or the query's corresponding infraction. It will not return an infraction if the type of the infraction - isn't duration-based (e.g. kick, warning, etc.) - - GET /bot/infractions/id/<infraction_id> - Gets the infraction (if any) for the given ID. - Parameters: "expand". - This endpoint returns an object with the "infraction" key, which is either set to null (no infraction) - or the infraction corresponding to the ID. - - POST /bot/infractions - Creates an infraction for a user. - Parameters (JSON payload): - "type" (str): the type of the infraction (must be a valid infraction type). - "reason" (str): the reason of the infraction. - "user_id" (str): the Discord ID of the user who is being given the infraction. - "actor_id" (str): the Discord ID of the user who submitted the infraction. - "duration" (optional str): the duration of the infraction. This is ignored for infractions - which are not duration-based. For other infraction types, omitting this field may imply permanence. - "expand" (optional bool): whether to expand the infraction user data once the infraction is inserted and returned. - - PATCH /bot/infractions - Updates an infractions. - Parameters (JSON payload): - "id" (str): the ID of the infraction to update. - "reason" (optional str): if provided, the new reason for the infraction. - "duration" (optional str): if provided, updates the expiration of the infraction to the time of UPDATING - plus the duration. If set to null, the expiration is also set to null (may imply permanence). - "active" (optional bool): if provided, activates or deactivates the infraction. This does not do anything - if the infraction isn't duration-based, or if the infraction has already expired. This marks the infraction - as closed. - "expand" (optional bool): whether to expand the infraction user data once the infraction is updated and returned. -""" - -import datetime -from typing import NamedTuple - -import rethinkdb -from flask import jsonify -from schema import Optional, Or, Schema - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin -from pysite.utils.time import parse_duration - - -class InfractionType(NamedTuple): - timed_infraction: bool # whether the infraction is active until it expires. - - -RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -EXCLUDED_FIELDS = "user_id", "actor_id", "closed", "_timed" -INFRACTION_ORDER = rethinkdb.desc("active"), rethinkdb.desc("inserted_at") - -INFRACTION_TYPES = { - "warning": InfractionType(timed_infraction=False), - "mute": InfractionType(timed_infraction=True), - "ban": InfractionType(timed_infraction=True), - "kick": InfractionType(timed_infraction=False), - "superstar": InfractionType(timed_infraction=True) # hiphopify -} - -GET_SCHEMA = Schema({ - Optional("active"): str, - Optional("expand"): str, - Optional("dangling"): str, - Optional("search"): str -}) - -GET_ACTIVE_SCHEMA = Schema({ - Optional("expand"): str -}) - -CREATE_INFRACTION_SCHEMA = Schema({ - "type": lambda tp: tp in INFRACTION_TYPES, - "reason": Or(str, None), - "user_id": str, # Discord user ID - "actor_id": str, # Discord user ID - Optional("duration"): str, # If not provided, may imply permanence depending on the infraction - Optional("expand"): bool -}) - -UPDATE_INFRACTION_SCHEMA = Schema({ - "id": str, - Optional("reason"): Or(str, None), - Optional("duration"): Or(str, None), - Optional("active"): bool -}) - -IMPORT_INFRACTIONS_SCHEMA = Schema([ - { - "id": str, - "active": bool, - "actor": { - "id": str - }, - "created_at": str, - "expires_at": Or(str, None), - "reason": Or(str, None), - "type": { - "name": str - }, - "user": { - "id": str - } - } -], ignore_extra_keys=True) - - -class InfractionsView(APIView, DBMixin): - path = "/bot/infractions" - name = "bot.infractions" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params: dict = None): - if "dangling" in params: - return _infraction_list_filtered(self, params, {"_timed": True, "closed": False}) - else: - return _infraction_list_filtered(self, params, {}) - - @api_key - @api_params(schema=CREATE_INFRACTION_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - deactivate_infraction_query = None - - infraction_type = data["type"] - user_id = data["user_id"] - actor_id = data["actor_id"] - reason = data["reason"] - duration_str = data.get("duration") - expand = data.get("expand") - expires_at = None - inserted_at = datetime.datetime.now(tz=datetime.timezone.utc) - - if infraction_type not in INFRACTION_TYPES: - return self.error(ErrorCodes.incorrect_parameters, "Invalid infraction type.") - - # check if the user already has an active infraction of this type - # if so, we need to disable that infraction and create a new infraction - if INFRACTION_TYPES[infraction_type].timed_infraction: - active_infraction_query = \ - self.db.query(self.table_name).merge(_merge_active_check()) \ - .filter({"user_id": user_id, "type": infraction_type, "active": True}) \ - .limit(1).nth(0).default(None) - - active_infraction = self.db.run(active_infraction_query) - if active_infraction: - deactivate_infraction_query = \ - self.db.query(self.table_name) \ - .get(active_infraction["id"]) \ - .update({"active": False, "closed": True}) - - if duration_str: - try: - expires_at = parse_duration(duration_str) - except ValueError: - return self.error( - ErrorCodes.incorrect_parameters, - "Invalid duration format." - ) - - infraction_insert_doc = { - "actor_id": actor_id, - "user_id": user_id, - "type": infraction_type, - "reason": reason, - "inserted_at": inserted_at, - "expires_at": expires_at - } - - infraction_id = self.db.insert(self.table_name, infraction_insert_doc)["generated_keys"][0] - - if deactivate_infraction_query: - self.db.run(deactivate_infraction_query) - - query = self.db.query(self.table_name).get(infraction_id) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - @api_key - @api_params(schema=UPDATE_INFRACTION_SCHEMA, validation_type=ValidationTypes.json) - def patch(self, data): - expand = data.get("expand") - update_collection = { - "id": data["id"] - } - - if "reason" in data: - update_collection["reason"] = data["reason"] - - if "active" in data: - update_collection["active"] = data["active"] - update_collection["closed"] = not data["active"] - - if "duration" in data: - duration_str = data["duration"] - if duration_str is None: - update_collection["expires_at"] = None - else: - try: - update_collection["expires_at"] = parse_duration(duration_str) - except ValueError: - return self.error( - ErrorCodes.incorrect_parameters, - "Invalid duration format." - ) - - query_update = self.db.query(self.table_name).update(update_collection) - result_update = self.db.run(query_update) - - if not result_update["replaced"]: - return jsonify({ - "success": False, - "error_message": "Unknown infraction / nothing was changed." - }) - - # return the updated infraction - query = self.db.query(self.table_name).get(data["id"]) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - infraction = self.db.run(query) - - return jsonify({ - "infraction": infraction, - "success": True - }) - - -class InfractionById(APIView, DBMixin): - path = "/bot/infractions/id/<string:infraction_id>" - name = "bot.infractions.id" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_ACTIVE_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, infraction_id): - params = params or {} - expand = parse_bool(params.get("expand"), default=False) - - query = self.db.query(self.table_name).get(infraction_id) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - -class ListInfractionsByUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>" - name = "bot.infractions.user" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id): - return _infraction_list_filtered(self, params, { - "user_id": user_id - }) - - -class ListInfractionsByTypeView(APIView, DBMixin): - path = "/bot/infractions/type/<string:type>" - name = "bot.infractions.type" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, type): - return _infraction_list_filtered(self, params, { - "type": type - }) - - -class ListInfractionsByTypeAndUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>/<string:type>" - name = "bot.infractions.user.type" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id, type): - return _infraction_list_filtered(self, params, { - "user_id": user_id, - "type": type - }) - - -class CurrentInfractionByTypeAndUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>/<string:infraction_type>/current" - name = "bot.infractions.user.type.current" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_ACTIVE_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id, infraction_type): - params = params or {} - expand = parse_bool(params.get("expand"), default=False) - - query_filter = { - "user_id": user_id, - "type": infraction_type - } - query = _merged_query(self, expand, query_filter).filter({ - "active": True - }).order_by(rethinkdb.desc("data")).limit(1).nth(0).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - -class ImportRowboatInfractionsView(APIView, DBMixin): - path = "/bot/infractions/import" - name = "bot.infractions.import" - table_name = "bot_infractions" - - @api_key - @api_params(schema=IMPORT_INFRACTIONS_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - # keep track of the un-bans, to apply after the import is complete. - unbans = [] - infractions = [] - - # previously imported infractions - imported_infractions = self.db.run( - self.db.query(self.table_name).filter( - lambda row: row.has_fields("legacy_rowboat_id") - ).fold([], lambda acc, row: acc.append(row["legacy_rowboat_id"])).coerce_to("array") - ) - - for rowboat_infraction_data in data: - legacy_rowboat_id = rowboat_infraction_data["id"] - if legacy_rowboat_id in imported_infractions: - continue - infraction_type = rowboat_infraction_data["type"]["name"] - if infraction_type == "unban": - unbans.append(rowboat_infraction_data) - continue - # adjust infraction types - if infraction_type == "tempmute": - infraction_type = "mute" - if infraction_type == "tempban": - infraction_type = "ban" - if infraction_type not in INFRACTION_TYPES: - # unknown infraction type - continue - active = rowboat_infraction_data["active"] - reason = rowboat_infraction_data["reason"] or "<No reason>" - user_id = rowboat_infraction_data["user"]["id"] - actor_id = rowboat_infraction_data["actor"]["id"] - inserted_at_str = rowboat_infraction_data["created_at"] - try: - inserted_at = parse_rfc1123(inserted_at_str) - except ValueError: - continue - expires_at_str = rowboat_infraction_data["expires_at"] - if expires_at_str is not None: - try: - expires_at = parse_rfc1123(expires_at_str) - except ValueError: - continue - else: - expires_at = None - infractions.append({ - "legacy_rowboat_id": legacy_rowboat_id, - "active": active, - "reason": reason, - "user_id": user_id, - "actor_id": actor_id, - "inserted_at": inserted_at, - "expires_at": expires_at, - "type": infraction_type - }) - - insertion_query = self.db.query(self.table_name).insert(infractions) - inserted_count = self.db.run(insertion_query)["inserted"] - - # apply unbans - for unban_data in unbans: - inserted_at_str = unban_data["created_at"] - user_id = unban_data["user"]["id"] - try: - inserted_at = parse_rfc1123(inserted_at_str) - except ValueError: - continue - self.db.run( - self.db.query(self.table_name).filter( - lambda row: (row["user_id"].eq(user_id)) & - (row["type"].eq("ban")) & - (row["inserted_at"] < inserted_at) - ).pluck("id").merge(lambda row: { - "active": False - }).coerce_to("array").for_each(lambda doc: self.db.query(self.table_name).get(doc["id"]).update(doc)) - ) - - return jsonify({ - "success": True, - "inserted_count": inserted_count - }) - - -def _infraction_list_filtered(view, params=None, query_filter=None): - params = params or {} - query_filter = query_filter or {} - active = parse_bool(params.get("active")) - expand = parse_bool(params.get("expand"), default=False) - search = params.get("search") - - if active is not None: - query_filter["active"] = active - - query = _merged_query(view, expand, query_filter) - - if search is not None: - query = query.filter( - lambda row: rethinkdb.branch( - row["reason"].eq(None), - False, - row["reason"].match(search) - ) - ) - - query = query.order_by(*INFRACTION_ORDER) - - return jsonify(view.db.run(query.coerce_to("array"))) - - -def _merged_query(view, expand, query_filter): - return view.db.query(view.table_name).merge(_merge_active_check()).filter(query_filter) \ - .merge(_merge_expand_users(view, expand)).without(*EXCLUDED_FIELDS) - - -def _merge_active_check(): - # Checks if the "closed" field has been set to true (manual infraction removal). - # If not, the "active" field is set to whether the infraction has expired. - def _merge(row): - return { - "active": - rethinkdb.branch( - _is_timed_infraction(row["type"]), - rethinkdb.branch( - (row["closed"].default(False).eq(True)) | (row["active"].default(True).eq(False)), - False, - rethinkdb.branch( - row["expires_at"].eq(None), - True, - row["expires_at"] > rethinkdb.now() - ) - ), - False - ), - "closed": row["closed"].default(False), - "_timed": _is_timed_infraction(row["type"]) - } - - return _merge - - -def _merge_expand_users(view, expand): - def _do_expand(user_id): - if not user_id: - return None - # Expands the user information, if it is in the database. - - if expand: - return view.db.query("users").get(user_id).default({ - "user_id": user_id - }) - - return { - "user_id": user_id - } - - def _merge(row): - return { - "user": _do_expand(row["user_id"].default(None)), - "actor": _do_expand(row["actor_id"].default(None)) - } - - return _merge - - -def _is_timed_infraction(type_var): - # this method generates an ReQL expression to check if the given type - # is a "timed infraction" (i.e it can expire or be permanent) - - timed_infractions = filter(lambda key: INFRACTION_TYPES[key].timed_infraction, INFRACTION_TYPES.keys()) - expr = rethinkdb.expr(False) - for infra_type in timed_infractions: - expr = expr | type_var.eq(infra_type) - return expr - - -def parse_rfc1123(time_str): - return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) - - -def parse_bool(a_string, default=None): - # Not present, null or any: returns default (defaults to None) - # false, no, or 0: returns False - # anything else: True - if a_string is None or a_string == "null" or a_string == "any": - return default - if a_string.lower() == "false" or a_string.lower() == "no" or a_string == "0": - return False - return True diff --git a/pysite/views/api/bot/off_topic_names.py b/pysite/views/api/bot/off_topic_names.py deleted file mode 100644 index 1c75428e..00000000 --- a/pysite/views/api/bot/off_topic_names.py +++ /dev/null @@ -1,108 +0,0 @@ -import random - -from flask import jsonify, request -from schema import And, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -OFF_TOPIC_NAME = And( - str, - len, - lambda name: all(c.isalnum() or c == '-' for c in name), - str.islower, - lambda name: len(name) <= 96, - error=( - "The channel name must be a non-blank string consisting only of" - " lowercase regular characters and '-' with a maximum length of 96" - ) -) - -DELETE_SCHEMA = Schema({ - 'name': OFF_TOPIC_NAME -}) - -POST_SCHEMA = Schema({ - 'name': OFF_TOPIC_NAME -}) - - -class OffTopicNamesView(APIView, DBMixin): - path = "/bot/off-topic-names" - name = "bot.off_topic_names" - table_name = "off_topic_names" - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.params) - def delete(self, params): - """ - Removes a single off-topic name from the database. - Returns the result of the deletion call. - - API key must be provided as header. - Name to delete must be provided as the `name` query argument. - """ - - result = self.db.delete( - self.table_name, - params['name'], - return_changes=True - ) - - return jsonify(result) - - @api_key - def get(self): - """ - Fetch all known off-topic channel names from the database. - Returns a list of strings, the strings being the off-topic names. - - If the query argument `random_items` is provided (a non-negative integer), - then this view will return `random_items` random names from the database - instead of returning all items at once. - - API key must be provided as header. - """ - - names = [ - entry['name'] for entry in self.db.get_all(self.table_name) - ] - - if 'random_items' in request.args: - random_count = request.args['random_items'] - if not random_count.isdigit(): - response = {'message': "`random_items` must be a valid integer"} - return jsonify(response), 400 - - samples = random.sample(names, int(random_count)) - return jsonify(samples) - - return jsonify(names) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.params) - def post(self, data): - """ - Add a new off-topic channel name to the database. - Expects the new channel's name as the `name` argument. - The name must consist only of alphanumeric characters or minus signs, - and must not be empty or exceed 96 characters. - - Data must be provided as params. - API key must be provided as header. - """ - - if self.db.get(self.table_name, data['name']) is not None: - response = { - 'message': "An entry with the given name already exists" - } - return jsonify(response), 400 - - self.db.insert( - self.table_name, - {'name': data['name']} - ) - return jsonify({'message': 'ok'}) diff --git a/pysite/views/api/bot/settings.py b/pysite/views/api/bot/settings.py deleted file mode 100644 index a633a68a..00000000 --- a/pysite/views/api/bot/settings.py +++ /dev/null @@ -1,56 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -# todo: type safety -SETTINGS_KEYS_DEFAULTS = { - "defcon_enabled": False, - "defcon_days": 1 -} - -GET_SCHEMA = Schema({ - Optional("keys"): str -}) - - -def settings_schema(): - schema_dict = {Optional(key): type(SETTINGS_KEYS_DEFAULTS[key]) for key in SETTINGS_KEYS_DEFAULTS.keys()} - return Schema(schema_dict) - - -class ServerSettingsView(APIView, DBMixin): - path = "/bot/settings" - name = "bot.settings" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - keys_raw = None - if params: - keys_raw = params.get("keys") - - keys = filter(lambda key: key in SETTINGS_KEYS_DEFAULTS, - keys_raw.split(",")) if keys_raw else SETTINGS_KEYS_DEFAULTS.keys() - - result = {key: (self.db.get("bot_settings", key) or {}).get("value") or SETTINGS_KEYS_DEFAULTS[key] for key in - keys} - return jsonify(result) - - @api_key - @api_params(schema=settings_schema(), validation_type=ValidationTypes.json) - def put(self, json_data): - # update in database - - for key, value in json_data.items(): - self.db.insert("bot_settings", { - "key": key, - "value": value - }, conflict="update") - - return jsonify({ - "success": True - }) diff --git a/pysite/views/api/bot/snake_cog/__init__.py b/pysite/views/api/bot/snake_cog/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/bot/snake_cog/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/snake_cog/snake_facts.py b/pysite/views/api/bot/snake_cog/snake_facts.py deleted file mode 100644 index 4e8c8a5d..00000000 --- a/pysite/views/api/bot/snake_cog/snake_facts.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeFactsView(APIView, DBMixin): - path = "/bot/snake_facts" - name = "bot.snake_facts" - table = "snake_facts" - - @api_key - def get(self): - """ - Returns a random fact from the snake_facts table. - - API key must be provided as header. - """ - - log.trace("Fetching a random fact from the snake_facts database") - question = self.db.sample(self.table, 1)[0]["fact"] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/snake_idioms.py b/pysite/views/api/bot/snake_cog/snake_idioms.py deleted file mode 100644 index 9d879871..00000000 --- a/pysite/views/api/bot/snake_cog/snake_idioms.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeIdiomView(APIView, DBMixin): - path = "/bot/snake_idioms" - name = "bot.snake_idioms" - table = "snake_idioms" - - @api_key - def get(self): - """ - Returns a random idiom from the snake_idioms table. - - API key must be provided as header. - """ - - log.trace("Fetching a random idiom from the snake_idioms database") - question = self.db.sample(self.table, 1)[0]["idiom"] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/snake_names.py b/pysite/views/api/bot/snake_cog/snake_names.py deleted file mode 100644 index d9e0c6b8..00000000 --- a/pysite/views/api/bot/snake_cog/snake_names.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging - -from flask import jsonify -from schema import Optional, Schema - - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - -GET_SCHEMA = Schema([ - { - Optional("get_all"): str - } -]) - - -class SnakeNamesView(APIView, DBMixin): - path = "/bot/snake_names" - name = "bot.snake_names" - table = "snake_names" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Returns all snake names random name from the snake_names table. - - API key must be provided as header. - """ - - get_all = None - - if params: - get_all = params[0].get("get_all") - - if get_all: - log.trace("Returning all snake names from the snake_names table") - snake_names = self.db.get_all(self.table) - - else: - log.trace("Fetching a single random snake name from the snake_names table") - snake_names = self.db.sample(self.table, 1)[0] - - return jsonify(snake_names) diff --git a/pysite/views/api/bot/snake_cog/snake_quiz.py b/pysite/views/api/bot/snake_cog/snake_quiz.py deleted file mode 100644 index 359077d7..00000000 --- a/pysite/views/api/bot/snake_cog/snake_quiz.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeQuizView(APIView, DBMixin): - path = "/bot/snake_quiz" - name = "bot.snake_quiz" - table = "snake_quiz" - - @api_key - def get(self): - """ - Returns a random question from the snake_quiz table. - - API key must be provided as header. - """ - - log.trace("Fetching a random question from the snake_quiz database") - question = self.db.sample(self.table, 1)[0] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/special_snakes.py b/pysite/views/api/bot/snake_cog/special_snakes.py deleted file mode 100644 index 294c16c9..00000000 --- a/pysite/views/api/bot/snake_cog/special_snakes.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SpecialSnakesView(APIView, DBMixin): - path = "/bot/special_snakes" - name = "bot.special_snakes" - table = "special_snakes" - - @api_key - def get(self): - """ - Returns all special snake objects from the database - - API key must be provided as header. - """ - - log.trace("Returning all special snakes in the database") - snake_names = self.db.get_all(self.table) - - return jsonify(snake_names) diff --git a/pysite/views/api/bot/tags.py b/pysite/views/api/bot/tags.py deleted file mode 100644 index 4394c224..00000000 --- a/pysite/views/api/bot/tags.py +++ /dev/null @@ -1,107 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -GET_SCHEMA = Schema({ - Optional("tag_name"): str -}) - -POST_SCHEMA = Schema({ - "tag_name": str, - "tag_content": str -}) - -DELETE_SCHEMA = Schema({ - "tag_name": str -}) - - -class TagsView(APIView, DBMixin): - path = "/bot/tags" - name = "bot.tags" - table_name = "tags" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Fetches tags from the database. - - - If tag_name is provided, it fetches - that specific tag. - - - If tag_category is provided, it fetches - all tags in that category. - - - If nothing is provided, it will - fetch a list of all tag_names. - - Data must be provided as params. - API key must be provided as header. - """ - - tag_name = None - - if params: - tag_name = params.get("tag_name") - - if tag_name: - data = self.db.get(self.table_name, tag_name) or {} - else: - data = self.db.pluck(self.table_name, "tag_name") or [] - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - If the tag_name doesn't exist, this - saves a new tag in the database. - - If the tag_name already exists, - this will edit the existing tag. - - Data must be provided as JSON. - API key must be provided as header. - """ - - tag_name = json_data.get("tag_name") - tag_content = json_data.get("tag_content") - - self.db.insert( - self.table_name, - { - "tag_name": tag_name, - "tag_content": tag_content - }, - conflict="update" # If it exists, update it. - ) - - return jsonify({"success": True}) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, data): - """ - Deletes a tag from the database. - - Data must be provided as JSON. - API key must be provided as header. - """ - - tag_name = data.get("tag_name") - tag_exists = self.db.get(self.table_name, tag_name) - - if tag_exists: - self.db.delete( - self.table_name, - tag_name - ) - return jsonify({"success": True}) - - return jsonify({"success": False}) diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py deleted file mode 100644 index a3a0c7a8..00000000 --- a/pysite/views/api/bot/user.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging - -import rethinkdb -from flask import jsonify, request -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -SCHEMA = Schema([ - { - "avatar": str, - "discriminator": str, - "roles": [str], - "user_id": str, - "username": str - } -]) - -GET_SCHEMA = Schema([ - { - "user_id": str - } -]) - -DELETE_SCHEMA = Schema([ - { - "user_id": str, - - Optional("avatar"): str, - Optional("discriminator"): str, - Optional("roles"): [str], - Optional("username"): str - } -]) - -BANNABLE_STATES = ("preparing", "running") - - -class UserView(APIView, DBMixin): - path = "/bot/users" - name = "bot.users" - - chunks_table = "member_chunks" - infractions_table = "code_jam_infractions" - jams_table = "code_jams" - oauth_table_name = "oauth_data" - participants_table = "code_jam_participants" - responses_table = "code_jam_responses" - table_name = "users" - teams_table = "code_jam_teams" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, data): - logging.getLogger(__name__).debug(f"Size of request: {len(request.data)} bytes") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No user IDs supplied") - - data = [x["user_id"] for x in data] - - result = self.db.run( - self.db.query(self.table_name) - .filter(lambda document: rethinkdb.expr(data).contains(document["user_id"])), - coerce=list - ) - - return jsonify({"data": result}) # pragma: no cover - - @api_key - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - logging.getLogger(__name__).debug(f"Size of request: {len(request.data)} bytes") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No users supplied") - - self.db.insert(self.chunks_table, {"chunk": data}) - - return jsonify({"success": True}) # pragma: no cover - - @api_key - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def put(self, data): - changes = self.db.insert( - self.table_name, *data, - conflict="update" - ) - - return jsonify(changes) # pragma: no cover - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, data): - user_ids = [user["user_id"] for user in data] - - changes = {} - - # changes = self.db.run( - # self.db.query(self.table_name) - # .get_all(*user_ids) - # .delete() - # ) - - oauth_deletions = self.db.run( - self.db.query(self.oauth_table_name) - .get_all(*user_ids, index="snowflake") - .delete() - ).get("deleted", 0) - - profile_deletions = self.db.run( - self.db.query(self.participants_table) - .get_all(*user_ids) - .delete() - ).get("deleted", 0) - - bans = 0 - response_deletions = 0 - - for user_id in user_ids: - banned = False - responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) - - for response in responses: - jam = response["jam"] - jam_obj = self.db.get(self.jams_table, jam) - - if jam_obj: - if jam_obj["state"] in BANNABLE_STATES: - banned = True - - self.db.delete(self.responses_table, response["id"]) - response_deletions += 1 - - teams = self.db.run( - self.db.query(self.teams_table).filter(lambda row: row["members"].contains(user_id)), - coerce=list - ) - - for team in teams: - team["members"].remove(user_id) - - self.db.insert(self.teams_table, team, conflict="replace", durability="soft") - - self.db.sync(self.teams_table) - - if banned: - self.db.insert( - self.infractions_table, { - "participant": user_id, - "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", - "number": -1, - "decremented_for": [] - } - ) - bans += 1 - - changes["deleted_oauth"] = oauth_deletions - changes["deleted_jam_profiles"] = profile_deletions - changes["deleted_responses"] = response_deletions - changes["jam_bans"] = bans - - return jsonify(changes) # pragma: no cover diff --git a/pysite/views/api/bot/user_complete.py b/pysite/views/api/bot/user_complete.py deleted file mode 100644 index 877eee34..00000000 --- a/pysite/views/api/bot/user_complete.py +++ /dev/null @@ -1,143 +0,0 @@ -import logging - -from flask import jsonify, request - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -BANNABLE_STATES = ("preparing", "running") - -log = logging.getLogger(__name__) - - -class UserView(APIView, DBMixin): - path = "/bot/users/complete" - name = "bot.users.complete" - - chunks_table = "member_chunks" - infractions_table = "code_jam_infractions" - jams_table = "code_jams" - oauth_table_name = "oauth_data" - participants_table = "code_jam_participants" - responses_table = "code_jam_responses" - table_name = "users" - teams_table = "code_jam_teams" - - @api_key - @api_params(validation_type=ValidationTypes.none) - def post(self, _): - log.debug(f"Size of request: {len(request.data)} bytes") - - documents = self.db.get_all(self.chunks_table) - chunks = [] - - for doc in documents: - log.info(f"Got member chunk with {len(doc['chunk'])} users") - chunks.append(doc["chunk"]) - - self.db.delete(self.chunks_table, doc["id"], durability="soft") - self.db.sync(self.chunks_table) - - log.info(f"Got {len(chunks)} member chunks") - - data = [] - - for chunk in chunks: - data += chunk - - log.info(f"Got {len(data)} members") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No users supplied") - - deletions = 0 - oauth_deletions = 0 - profile_deletions = 0 - response_deletions = 0 - bans = 0 - - user_ids = [user["user_id"] for user in data] - - all_users = self.db.run(self.db.query(self.table_name), coerce=list) - - for user in all_users: - if user["user_id"] not in user_ids: - self.db.delete(self.table_name, user["user_id"], durability="soft") - deletions += 1 - - all_oauth_data = self.db.run(self.db.query(self.oauth_table_name), coerce=list) - - for item in all_oauth_data: - if item["snowflake"] not in user_ids: - user_id = item["snowflake"] - - oauth_deletions += self.db.delete( - self.oauth_table_name, item["id"], durability="soft", return_changes=True - ).get("deleted", 0) - profile_deletions += self.db.delete( - self.participants_table, user_id, durability="soft", return_changes=True - ).get("deleted", 0) - - banned = False - responses = self.db.run( - self.db.query(self.responses_table).filter({"snowflake": user_id}), - coerce=list - ) - - for response in responses: - jam = response["jam"] - jam_obj = self.db.get(self.jams_table, jam) - - if jam_obj: - if jam_obj["state"] in BANNABLE_STATES: - banned = True - - self.db.delete(self.responses_table, response["id"], durability="soft") - response_deletions += 1 - - teams = self.db.run( - self.db.query(self.teams_table).filter(lambda row: row["members"].contains(user_id)), - coerce=list - ) - - for team in teams: - team["members"].remove(user_id) - - self.db.insert(self.teams_table, team, conflict="replace", durability="soft") - - if banned: - self.db.insert( - self.infractions_table, { - "participant": user_id, - "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", - "number": -1, - "decremented_for": [] - }, durability="soft" - ) - bans += 1 - - del user_ids - - changes = self.db.insert( - self.table_name, *data, - conflict="update", - durability="soft" - ) - - self.db.sync(self.infractions_table) - self.db.sync(self.oauth_table_name) - self.db.sync(self.participants_table) - self.db.sync(self.responses_table) - self.db.sync(self.table_name) - self.db.sync(self.teams_table) - - changes["deleted"] = deletions - changes["deleted_oauth"] = oauth_deletions - changes["deleted_jam_profiles"] = profile_deletions - changes["deleted_responses"] = response_deletions - changes["jam_bans"] = bans - - return jsonify(changes) # pragma: no cover diff --git a/pysite/views/api/error_view.py b/pysite/views/api/error_view.py deleted file mode 100644 index 89b4d6ad..00000000 --- a/pysite/views/api/error_view.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import jsonify -from werkzeug.exceptions import HTTPException - -from pysite.base_route import ErrorView - - -class APIErrorView(ErrorView): - name = "api.error_all" - error_code = range(400, 600) - register_on_app = False - - def __init__(self): - - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.return_error) - - def return_error(self, error: HTTPException): - """ - Return a basic JSON object representing the HTTP error, - as well as propagating its status code - """ - - message = str(error) - code = 500 - - if isinstance(error, HTTPException): - message = error.description - code = error.code - - return jsonify({ - "error_code": -1, - "error_message": message - }), code diff --git a/pysite/views/api/healthcheck.py b/pysite/views/api/healthcheck.py deleted file mode 100644 index c873d674..00000000 --- a/pysite/views/api/healthcheck.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import jsonify - -from pysite.base_route import APIView - - -class HealthCheckView(APIView): - path = "/healthcheck" - name = "api.healthcheck" - - def get(self): - return jsonify({"status": "ok"}) diff --git a/pysite/views/api/index.py b/pysite/views/api/index.py deleted file mode 100644 index 5111162c..00000000 --- a/pysite/views/api/index.py +++ /dev/null @@ -1,10 +0,0 @@ -from pysite.base_route import APIView -from pysite.constants import ErrorCodes - - -class IndexView(APIView): - path = "/" - name = "api.index" - - def get(self): - return self.error(ErrorCodes.unknown_route) diff --git a/pysite/views/api/robots_txt.py b/pysite/views/api/robots_txt.py deleted file mode 100644 index d4406d54..00000000 --- a/pysite/views/api/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True), rules={"*": ["/"]} - ), content_type="text/plain" - ) diff --git a/pysite/views/api/sitemap_xml.py b/pysite/views/api/sitemap_xml.py deleted file mode 100644 index 26a786b0..00000000 --- a/pysite/views/api/sitemap_xml.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import Response - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml") diff --git a/pysite/views/error_handlers/http_4xx.py b/pysite/views/error_handlers/http_4xx.py deleted file mode 100644 index 731204f9..00000000 --- a/pysite/views/error_handlers/http_4xx.py +++ /dev/null @@ -1,31 +0,0 @@ -from flask import request -from werkzeug.exceptions import HTTPException - -from pysite.base_route import ErrorView -from pysite.constants import ERROR_DESCRIPTIONS - - -class Error400View(ErrorView): - name = "errors.4xx" - error_code = range(400, 430) - - def __init__(self): - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.error) - - def error(self, error: HTTPException): - error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") - - return self.render( - "errors/error.html", code=error.code, req=request, error_title=error_desc, - error_message=f"{error_desc} If you believe we have made a mistake, please " - "<a href='https://gitlab.com/python-discord/projects/site/issues'>" - "open an issue on our GitLab</a>." - ), error.code diff --git a/pysite/views/error_handlers/http_5xx.py b/pysite/views/error_handlers/http_5xx.py deleted file mode 100644 index 489eb5e5..00000000 --- a/pysite/views/error_handlers/http_5xx.py +++ /dev/null @@ -1,41 +0,0 @@ -from flask import request -from werkzeug.exceptions import HTTPException, InternalServerError - -from pysite.base_route import ErrorView -from pysite.constants import ERROR_DESCRIPTIONS - - -class Error500View(ErrorView): - name = "errors.5xx" - error_code = range(500, 600) - - def __init__(self): - - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.error) - - def error(self, error: HTTPException): - - # We were sometimes recieving errors from RethinkDB, which were not originating from Werkzeug. - # To fix this, this section checks whether they have a code (which werkzeug adds) and if not - # change the error to a Werkzeug InternalServerError. - - if not hasattr(error, "code"): - error = InternalServerError() - - error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") - - return self.render( - "errors/error.html", code=error.code, req=request, error_title=error_desc, - error_message="An error occurred while processing this request, please try " - "again later. If you believe we have made a mistake, please " - "<a href='https://gitlab.com/python-discord/projects/site/issues'>file an issue on our" - " GitLab</a>." - ), error.code diff --git a/pysite/views/main/__init__.py b/pysite/views/main/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/__init__.py +++ /dev/null diff --git a/pysite/views/main/abort.py b/pysite/views/main/abort.py deleted file mode 100644 index ecfe8f91..00000000 --- a/pysite/views/main/abort.py +++ /dev/null @@ -1,11 +0,0 @@ -from werkzeug.exceptions import InternalServerError - -from pysite.base_route import RouteView - - -class EasterEgg500(RouteView): - path = "/500" - name = "500" - - def get(self): - raise InternalServerError diff --git a/pysite/views/main/about/__init__.py b/pysite/views/main/about/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/about/__init__.py +++ /dev/null diff --git a/pysite/views/main/about/channels.py b/pysite/views/main/about/channels.py deleted file mode 100644 index 2e5496f9..00000000 --- a/pysite/views/main/about/channels.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class ChannelsView(TemplateView): - path = "/about/channels" - name = "about.channels" - template = "main/about/channels.html" diff --git a/pysite/views/main/about/index.py b/pysite/views/main/about/index.py deleted file mode 100644 index 6f5ef1c8..00000000 --- a/pysite/views/main/about/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/about/" - name = "about.index" - template = "main/about/index.html" diff --git a/pysite/views/main/about/partners.py b/pysite/views/main/about/partners.py deleted file mode 100644 index 4fe321a5..00000000 --- a/pysite/views/main/about/partners.py +++ /dev/null @@ -1,19 +0,0 @@ -import json -from logging import getLogger - -from pysite.base_route import RouteView - -try: - with open("static/partners.json") as fh: - partners = json.load(fh) -except Exception: - getLogger("Partners").exception("Failed to load partners.json") - categories = None - - -class PartnersView(RouteView): - path = "/about/partners" - name = "about.partners" - - def get(self): - return self.render("main/about/partners.html", partners=partners) diff --git a/pysite/views/main/about/privacy.py b/pysite/views/main/about/privacy.py deleted file mode 100644 index a08aa22b..00000000 --- a/pysite/views/main/about/privacy.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class PrivacyView(TemplateView): - path = "/about/privacy" - name = "about.privacy" - template = "main/about/privacy.html" diff --git a/pysite/views/main/about/rules.py b/pysite/views/main/about/rules.py deleted file mode 100644 index a40110a1..00000000 --- a/pysite/views/main/about/rules.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class RulesView(TemplateView): - path = "/about/rules" - name = "about.rules" - template = "main/about/rules.html" diff --git a/pysite/views/main/auth/__init__.py b/pysite/views/main/auth/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/auth/__init__.py +++ /dev/null diff --git a/pysite/views/main/auth/done.py b/pysite/views/main/auth/done.py deleted file mode 100644 index 6e892906..00000000 --- a/pysite/views/main/auth/done.py +++ /dev/null @@ -1,18 +0,0 @@ -from flask import redirect, session, url_for - -from pysite.base_route import RouteView - - -class AuthDoneView(RouteView): - path = "/auth/done" - name = "auth.done" - - def get(self): - if self.logged_in: - target = session.get("redirect_target") - - if target: - del session["redirect_target"] - return redirect(url_for(target["url"], **target.get("kwargs", {}))) - - return redirect(url_for("main.index")) diff --git a/pysite/views/main/bot/cleanlog.py b/pysite/views/main/bot/cleanlog.py deleted file mode 100644 index 9c719b3e..00000000 --- a/pysite/views/main/bot/cleanlog.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, DEVELOPERS_ROLE, ROLE_COLORS -from pysite.decorators import require_roles -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class CleanLogView(RouteView, DBMixin, OAuthMixin): - path = "/bot/clean_logs/<log_id>" - name = "bot.clean_logs" - - table_name = "clean_logs" - template = "main/bot/clean_logs.html" - - @require_roles(ALL_STAFF_ROLES) - def get(self, log_id): - """ - Get the requested clean log and spit it out - in a beautiful template. - """ - - data = self.db.get(self.table_name, log_id) - - if data is None: - return "ID could not be found in the database", 404 - - messages = data["log_data"] - - for message in messages: - message['color'] = ROLE_COLORS.get(message['role_id'], ROLE_COLORS[DEVELOPERS_ROLE]) - - return self.render(self.template, messages=messages) diff --git a/pysite/views/main/error.py b/pysite/views/main/error.py deleted file mode 100644 index 07286eb4..00000000 --- a/pysite/views/main/error.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import abort - -from pysite.base_route import RouteView - - -class ErrorView(RouteView): - path = "/error/<int:code>" - name = "error" - - def get(self, code): - try: - return abort(code) - except LookupError: - return abort(500) diff --git a/pysite/views/main/index.py b/pysite/views/main/index.py deleted file mode 100644 index 874961bb..00000000 --- a/pysite/views/main/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/" - name = "index" - template = "main/index.html" diff --git a/pysite/views/main/info/__init__.py b/pysite/views/main/info/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/info/__init__.py +++ /dev/null diff --git a/pysite/views/main/info/faq.py b/pysite/views/main/info/faq.py deleted file mode 100644 index 8878e180..00000000 --- a/pysite/views/main/info/faq.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/info/faq" - name = "info.faq" - template = "main/info/faq.html" diff --git a/pysite/views/main/info/help.py b/pysite/views/main/info/help.py deleted file mode 100644 index 6a82a9ed..00000000 --- a/pysite/views/main/info/help.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class HelpView(TemplateView): - path = "/info/help" - name = "info.help" - template = "main/info/help.html" diff --git a/pysite/views/main/info/index.py b/pysite/views/main/info/index.py deleted file mode 100644 index 97678ee4..00000000 --- a/pysite/views/main/info/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/info/" - name = "info.index" - template = "main/info/index.html" diff --git a/pysite/views/main/info/jams.py b/pysite/views/main/info/jams.py deleted file mode 100644 index b654ec1d..00000000 --- a/pysite/views/main/info/jams.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import RedirectView - - -class JamsView(RedirectView): - path = "/info/jams" - name = "info.jams" - page = "main.jams.index" diff --git a/pysite/views/main/info/resources.py b/pysite/views/main/info/resources.py deleted file mode 100644 index 541b9ba1..00000000 --- a/pysite/views/main/info/resources.py +++ /dev/null @@ -1,58 +0,0 @@ -import json -from logging import getLogger - -from pysite.base_route import RouteView - -ICON_STYLES = { - "branding": "fab", - "regular": "far", - "solid": "fas", - "light": "fal" -} - -logger = getLogger("Resources") - -try: - with open("static/resources.json") as fh: - categories = json.load(fh) - - for category, items in categories.items(): - to_remove = [] - - for name, resource in items["resources"].items(): - for url_obj in resource["urls"]: - icon = url_obj["icon"].lower() - - if "/" not in icon: - to_remove.append(name) - logger.error( - f"Resource {name} in category {category} has an invalid icon. Icons should be of the" - f"form `style/name`." - ) - continue - - style, icon_name = icon.split("/") - - if style not in ICON_STYLES: - to_remove.append(name) - logger.error( - f"Resource {name} in category {category} has an invalid icon style. Icon style must " - f"be one of {', '.join(ICON_STYLES.keys())}." - ) - continue - - url_obj["classes"] = f"{ICON_STYLES[style]} fa-{icon_name}" - - for name in to_remove: - del items["resources"][name] -except Exception: - getLogger("Resources").exception("Failed to load resources.json") - categories = None - - -class ResourcesView(RouteView): - path = "/info/resources" - name = "info.resources" - - def get(self): - return self.render("main/info/resources.html", categories=categories) diff --git a/pysite/views/main/jams/__init__.py b/pysite/views/main/jams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/jams/__init__.py +++ /dev/null diff --git a/pysite/views/main/jams/index.py b/pysite/views/main/jams/index.py deleted file mode 100644 index 0cd9a287..00000000 --- a/pysite/views/main/jams/index.py +++ /dev/null @@ -1,52 +0,0 @@ -import rethinkdb - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class JamsIndexView(RouteView, DBMixin): - path = "/jams" - name = "jams.index" - table_name = "code_jams" - - teams_table = "code_jam_teams" - - def get(self): - query = ( - self.db.query(self.table_name) - .filter(rethinkdb.row["state"] != "planning") - .merge( - lambda jam_obj: { - "teams": - self.db.query(self.teams_table) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id"]) - .coerce_to("array") - } - ) - .order_by(rethinkdb.desc("number")) - .limit(5) - ) - - jams = self.db.run(query, coerce=list) - for jam in jams: - if "winning_team" in jam and jam["winning_team"]: - jam["winning_team"] = self.db.get(self.teams_table, jam["winning_team"]) - else: - jam["winning_team"] = None - pass - return self.render("main/jams/index.html", jams=jams, has_applied_to_jam=self.has_applied_to_jam) - - def get_jam_response(self, jam, user_id): - query = self.db.query("code_jam_responses").filter({"jam": jam, "snowflake": user_id}) - result = self.db.run(query, coerce=list) - - if result: - return result[0] - return None - - def has_applied_to_jam(self, jam): - # whether the user has applied to this jam - if not self.logged_in: - return False - return self.get_jam_response(jam, self.user_data["user_id"]) diff --git a/pysite/views/main/jams/info.py b/pysite/views/main/jams/info.py deleted file mode 100644 index fd4615e9..00000000 --- a/pysite/views/main/jams/info.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class JamsInfoView(TemplateView): - path = "/jams/info" - name = "jams.info" - template = "main/jams/info.html" diff --git a/pysite/views/main/jams/jam_team_list.py b/pysite/views/main/jams/jam_team_list.py deleted file mode 100644 index 452a073f..00000000 --- a/pysite/views/main/jams/jam_team_list.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging - -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamListView(RouteView, DBMixin, OAuthMixin): - path = "/jams/teams/<int:jam_id>" - name = "jams.jam_team_list" - - table_name = "code_jam_teams" - jams_table = "code_jams" - - def get(self, jam_id): - jam_obj = self.db.get(self.jams_table, jam_id) - if not jam_obj: - raise NotFound() - - # Get all the participants of this jam - # Note: the group function will return a dict with user_ids as keys, however each element will be an array - participants_query = self.db.query("users").get_all(*jam_obj["participants"], index="user_id").group("user_id") - participants = self.db.run(participants_query) - - # Get all the teams, leaving the team members as only an array of IDs - query = self.db.query(self.table_name).get_all(self.table_name, *jam_obj["teams"]).pluck( - ["id", "name", "members", "repo"]).coerce_to("array") - jam_obj["teams"] = self.db.run(query) - - # Populate each team's members using the previously queried participant list - for team in jam_obj["teams"]: - team["members"] = [participants[user_id][0] for user_id in team["members"]] - - return self.render( - "main/jams/team_list.html", - jam=jam_obj, - teams=jam_obj["teams"], - member_ids=self.member_ids - ) - - def member_ids(self, members): - return [member["user_id"] for member in members] diff --git a/pysite/views/main/jams/join.py b/pysite/views/main/jams/join.py deleted file mode 100644 index 4db59630..00000000 --- a/pysite/views/main/jams/join.py +++ /dev/null @@ -1,247 +0,0 @@ -import datetime -from email.utils import parseaddr - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_JAM_LOGS -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin, RMQMixin - - -class JamsJoinView(RouteView, DBMixin, OAuthMixin, RMQMixin): - path = "/jams/join/<int:jam>" - name = "jams.join" - - table_name = "code_jams" - forms_table = "code_jam_forms" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - participants_table = "code_jam_participants" - infractions_table = "code_jam_infractions" - - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not self.user_data: - return self.redirect_login(jam=jam) - - infractions = self.get_infractions(self.user_data["user_id"]) - - for infraction in infractions: - if infraction["number"] == -1: # Indefinite ban - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if infraction["number"]: # Got some jams left - if jam not in infraction["decremented_for"]: - # Make sure they haven't already tried to apply for this jam - infraction["number"] -= 1 - infraction["decremented_for"].append(jam) - - self.db.insert(self.infractions_table, infraction, conflict="replace") - - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if jam in infraction["decremented_for"]: - # They already tried to apply for this jam - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - participant = self.db.get(self.participants_table, self.user_data["user_id"]) - - if not participant: - return redirect(url_for("main.jams.profile", form=jam)) - - if self.get_response(jam, self.user_data["user_id"]): - return self.render("main/jams/already.html", jam=jam_obj) - - form_obj = self.db.get(self.forms_table, jam) - questions = [] - - if form_obj: - for question in form_obj["questions"]: - questions.append(self.db.get(self.questions_table, question)) - - return self.render( - "main/jams/join.html", jam=jam_obj, form=form_obj, - questions=questions, question_ids=[q["id"] for q in questions] - ) - - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not self.user_data: - return self.redirect_login(jam=jam) - - infractions = self.get_infractions(self.user_data["user_id"]) - - for infraction in infractions: - if infraction["number"] == -1: # Indefinite ban - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction) - - if infraction["number"]: # Got some jams left - if jam not in infraction["decremented_for"]: - # Make sure they haven't already tried to apply for this jam - infraction["number"] -= 1 - infraction["decremented_for"].append(jam) - - self.db.insert(self.infractions_table, infraction, conflict="replace") - - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if jam in infraction["decremented_for"]: - # They already tried to apply for this jam - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - participant = self.db.get(self.participants_table, self.user_data["user_id"]) - - if not participant: - return redirect(url_for("main.jams.profile")) - - if self.get_response(jam, self.user_data["user_id"]): - return self.render("main/jams/already.html", jam=jam_obj) - - form_obj = self.db.get(self.forms_table, jam) - - if not form_obj: - return NotFound() - - questions = [] - - for question in form_obj["questions"]: - questions.append(self.db.get(self.questions_table, question)) - - answers = [] - - for question in questions: - value = request.form.get(question["id"]) - answer = {"question": question["id"]} - - if not question["optional"] and value is None: - return BadRequest() - - if question["type"] == "checkbox": - if value == "on": - answer["value"] = True - elif not question["optional"]: - return BadRequest() - else: - answer["value"] = False - - elif question["type"] == "email": - if value: - address = parseaddr(value) - - if address == ("", ""): - return BadRequest() - - answer["value"] = value - - elif question["type"] in ["number", "range", "slider"]: - if value is not None: - value = int(value) - - if value > int(question["data"]["max"]) or value < int(question["data"]["min"]): - return BadRequest() - - answer["value"] = value - - elif question["type"] == "radio": - if value: - if value not in question["data"]["options"]: - return BadRequest() - - answer["value"] = value - - elif question["type"] in ["text", "textarea"]: - answer["value"] = value - - answers.append(answer) - - user_id = self.user_data["user_id"] - - response = { - "snowflake": user_id, - "jam": jam, - "approved": False, - "answers": answers - } - - self.db.insert(self.responses_table, response) - self.log_success() - - return self.render("main/jams/thanks.html", jam=jam_obj) - - def get_response(self, jam, user_id): - query = self.db.query(self.responses_table).filter({"jam": jam, "snowflake": user_id}) - result = self.db.run(query, coerce=list) - - if result: - return result[0] - return None - - def get_infractions(self, user_id): - query = self.db.query(self.infractions_table).filter({"participant": user_id}) - return self.db.run(query, coerce=list) - - def log_banned(self, number, reason): - user_data = self.user_data - - user_id = user_data["user_id"] - username = user_data["username"] - discriminator = user_data["discriminator"] - - message = f"Failed code jam signup from banned user: {user_id} ({username}#{discriminator})\n\n" - - if number == -1: - message += f"This user has been banned indefinitely. Reason: '{reason}'" - elif number < 1: - message += f"This application has expired the infraction. Reason: '{reason}'" - else: - message += f"This user has {number} more applications left before they're unbanned. Reason: '{reason}'" - - self.rmq_bot_event( - BotEventTypes.mod_log, - { - "level": "warning", "title": "Code Jams: Applications", - "message": message - } - ) - - def log_success(self): - user_data = self.user_data - - user_id = user_data["user_id"] - username = user_data["username"] - discriminator = user_data["discriminator"] - - self.rmq_bot_event( - BotEventTypes.mod_log, - { - "level": "info", "title": "Code Jams: Applications", - "message": f"Successful code jam signup from user: {user_id} " - f"({username}#{discriminator})" - } - ) - - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_JAM_LOGS, - "title": "Code Jams: Applications", - "description": f"Successful code jam signup from user: {user_id} " - f"({username}#{discriminator})", - "colour": 0x2ecc71, # Green from d.py - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/main/jams/profile.py b/pysite/views/main/jams/profile.py deleted file mode 100644 index e918c135..00000000 --- a/pysite/views/main/jams/profile.py +++ /dev/null @@ -1,71 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin - - -class JamsProfileView(RouteView, DBMixin, OAuthMixin): - path = "/jams/profile" - name = "jams.profile" - - table_name = "code_jam_participants" - - def get(self): - if not self.user_data: - return self.redirect_login() - - participant = self.db.get(self.table_name, self.user_data["user_id"]) - existing = True - - if not participant: - participant = {"id": self.user_data["user_id"]} - existing = False - - form = request.args.get("form") - - if form: - try: - form = int(form) - except ValueError: - pass # Someone trying to have some fun I guess - - return self.render( - "main/jams/profile.html", participant=participant, form=form, existing=existing - ) - - @csrf - def post(self): - if not self.user_data: - return self.redirect_login() - - participant = self.db.get(self.table_name, self.user_data["user_id"]) - - if not participant: - participant = {"id": self.user_data["user_id"]} - - gitlab_username = request.form.get("gitlab_username") - timezone = request.form.get("timezone") - - if not gitlab_username or not timezone: - return BadRequest() - - participant["gitlab_username"] = gitlab_username - participant["timezone"] = timezone - - self.db.insert(self.table_name, participant, conflict="replace") - - form = request.args.get("form") - - if form: - try: - form = int(form) - except ValueError: - pass # Someone trying to have some fun I guess - else: - return redirect(url_for("main.jams.join", jam=form)) - - return self.render( - "main/jams/profile.html", participant=participant, done=True, existing=True - ) diff --git a/pysite/views/main/jams/retract.py b/pysite/views/main/jams/retract.py deleted file mode 100644 index 277426b5..00000000 --- a/pysite/views/main/jams/retract.py +++ /dev/null @@ -1,83 +0,0 @@ -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin - -BANNABLE_STATES = ("preparing", "running") - - -class JamsProfileView(RouteView, DBMixin, OAuthMixin): - path = "/jams/retract" - name = "jams.retract" - - table_name = "code_jam_participants" - infractions_table = "code_jam_infractions" - jams_table = "code_jams" - responses_table = "code_jam_responses" - - def get(self): - if not self.user_data: - return self.redirect_login() - - user_id = self.user_data["user_id"] - participant = self.db.get(self.table_name, user_id) - - banned = False - - if participant: - responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) - - for response in responses: - jam = response["jam"] - jam_obj = self.db.get(self.jams_table, jam) - - if jam_obj: - if jam_obj["state"] in BANNABLE_STATES: - banned = True - break - - return self.render( - "main/jams/retract.html", participant=participant, banned=banned - ) - - @csrf - def post(self): - if not self.user_data: - return self.redirect_login() - - user_id = self.user_data["user_id"] - participant = self.db.get(self.table_name, user_id) - - if not participant: - return BadRequest() - - banned = False - - responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) - - for response in responses: - jam = response["jam"] - jam_obj = self.db.get(self.jams_table, jam) - - if jam_obj: - if jam_obj["state"] in BANNABLE_STATES: - banned = True - - self.db.delete(self.responses_table, response["id"]) - - self.db.delete(self.table_name, participant["id"]) - - if banned: - self.db.insert( - self.infractions_table, { - "participant": user_id, - "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", - "number": -1, - "decremented_for": [] - } - ) - - return self.render( - "main/jams/retracted.html", participant=participant, banned=banned - ) diff --git a/pysite/views/main/jams/team_edit_repo.py b/pysite/views/main/jams/team_edit_repo.py deleted file mode 100644 index 03e752bc..00000000 --- a/pysite/views/main/jams/team_edit_repo.py +++ /dev/null @@ -1,151 +0,0 @@ -import logging -import re -from urllib.parse import quote - -import requests -from flask import jsonify, request -from rethinkdb import ReqlNonExistenceError -from urllib3.util import parse_url -from werkzeug.exceptions import NotFound, Unauthorized - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, GITLAB_ACCESS_TOKEN -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamEditRepo(APIView, DBMixin, OAuthMixin): - path = "/jams/teams/<string:team_id>/edit_repo" - name = "jams.team.edit_repo" - - table_name = "code_jam_teams" - jams_table = "code_jams" - - gitlab_projects_api_endpoint = "https://gitlab.com/api/v4/projects/{0}" - - @csrf - def post(self, team_id): - if not self.user_data: - return self.redirect_login() - - try: - query = self.db.query(self.table_name).get(team_id).merge( - lambda team: { - "jam": self.db.query("code_jams").get(team["jam"]) - } - ) - - team = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - # Only team members can use this route - if not self.user_data["user_id"] in team["members"]: - raise Unauthorized() - - repo_url = request.form.get("repo_url").strip() - - # Check if repo is a valid GitLab repo URI - url = parse_url(repo_url) - - if url.host != "gitlab.com" or url.path is None: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a GitLab repository." - ) - - project_path = url.path.strip("/") # /user/repository/ --> user/repository - if len(project_path.split("/")) < 2: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - word_regex = re.compile("^[\-\.\w]+$") # Alphanumerical, underscores, periods, and dashes - for segment in project_path.split("/"): - if not word_regex.fullmatch(segment): - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - project_path_encoded = quote(project_path, safe='') # Replaces / with %2F, etc. - - # If validation returns something else than True, abort - validation = self.validate_project(team, project_path_encoded) - if validation is not True: - return validation - - # Update the team repo - # Note: the team repo is only stored using its path (e.g. user/repository) - team_obj = self.db.get(self.table_name, team_id) - team_obj["repo"] = project_path - self.db.insert(self.table_name, team_obj, conflict="update") - - return jsonify( - { - "project_path": project_path - } - ) - - def validate_project(self, team, project_path): - # Check on GitLab if the project exists - # NB: certain fields (such as "forked_from_project") need an access token - # to be visible. Set the GITLAB_ACCESS_TOKEN env variable to solve this - query_response = self.request_project(project_path) - - if query_response.status_code != 200: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - # Check if the jam's base repo has been set by staff - # If not, just ignore the fork check and proceed - if "repo" not in team["jam"]: - return True - jam_repo = team["jam"]["repo"] - - # Check if the provided repo is a forked repo - project_data = query_response.json() - if "forked_from_project" not in project_data: - return self.error( - ErrorCodes.incorrect_parameters, - "This repository is not a fork of the jam's repository." - ) - - # Check if the provided repo is forking the base repo - forked_from_project = project_data["forked_from_project"] - - # The jam repo is stored in full (e.g. https://gitlab.com/user/repository) - jam_repo_path = quote(parse_url(jam_repo).path.strip("/"), safe='') - - # Get info about the code jam repo - jam_repo_response = self.request_project(jam_repo_path) - - # Something went wrong, fail silently - if jam_repo_response.status_code != 200: - return True - - # Check if the IDs for the code jam repo and the fork source match - jam_repo_data = jam_repo_response.json() - if jam_repo_data["id"] != forked_from_project["id"]: - return self.error( - ErrorCodes.incorrect_parameters, - "This repository is not a fork of the jam's repository." - ) - - # All good - return True - - def request_project(self, project_path): - # Request the project details using a private access token - return requests.get( - self.gitlab_projects_api_endpoint.format(project_path), - params={ - "private_token": GITLAB_ACCESS_TOKEN - } - ) diff --git a/pysite/views/main/jams/team_view.py b/pysite/views/main/jams/team_view.py deleted file mode 100644 index 6b5d86ce..00000000 --- a/pysite/views/main/jams/team_view.py +++ /dev/null @@ -1,53 +0,0 @@ -import datetime -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamView(RouteView, DBMixin, OAuthMixin): - path = "/jams/team/<string:team_id>" - name = "jams.team_view" - - table_name = "code_jam_teams" - - def get(self, team_id: str): - try: - query = self.db.query(self.table_name).get(team_id).merge( - lambda team: { - "members": - self.db.query("users") - .filter(lambda user: team["members"].contains(user["user_id"])) - .merge( - lambda user: { - "gitlab_username": self.db.query("code_jam_participants").filter( - {"id": user["user_id"]} - ).coerce_to("array")[0]["gitlab_username"] - } - ).coerce_to("array"), - "jam": self.db.query("code_jams").get(team["jam"]) - } - ) - - team = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - # check if the current user is a member of this team - # (this is for edition privileges) - is_own_team = self.logged_in and self.user_data["user_id"] in [member["user_id"] for member in team["members"]] - - return self.render( - "main/jams/team_view.html", - team=team, is_own_team=is_own_team, day_delta=self.day_delta - ) - - def day_delta(self, date, delta): - # util to add or subtract days from a date - return date + datetime.timedelta(days=delta) diff --git a/pysite/views/main/jams/user_team_list.py b/pysite/views/main/jams/user_team_list.py deleted file mode 100644 index 226cc4b0..00000000 --- a/pysite/views/main/jams/user_team_list.py +++ /dev/null @@ -1,37 +0,0 @@ -import rethinkdb - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - - -class JamsUserTeamListView(RouteView, DBMixin, OAuthMixin): - path = "/jams/my_teams" - name = "jams.user_team_list" - - def get(self): - # list teams a user is (or was) a part of - if not self.user_data: - return self.redirect_login() - - query = self.db.query("code_jam_teams").filter( - lambda team: team["members"].contains(self.user_data["user_id"]) - ).merge( - lambda team: { - "members": - self.db.query("users") - .filter(lambda user: team["members"].contains(user["user_id"])) - .merge(lambda user: { - "gitlab_username": - self.db.query("code_jam_participants").filter({"id": user["user_id"]}) - .coerce_to("array")[0]["gitlab_username"] - }).coerce_to("array"), - "jam": self.db.query("code_jams").get(team["jam"]) - } - ).order_by(rethinkdb.desc("jam.number")) - teams = self.db.run(query) - - return self.render( - "main/jams/team_list.html", - user_teams=True, - teams=teams - ) diff --git a/pysite/views/main/logout.py b/pysite/views/main/logout.py deleted file mode 100644 index 64326371..00000000 --- a/pysite/views/main/logout.py +++ /dev/null @@ -1,16 +0,0 @@ -from flask import redirect, session, url_for - -from pysite.base_route import RouteView - - -class LogoutView(RouteView): - path = "/auth/logout" - name = "logout" - - def get(self): - if self.logged_in: - # remove user's session - del session["session_id"] - self.oauth.logout() - - return redirect(url_for("main.index")) diff --git a/pysite/views/main/redirects/__init__.py b/pysite/views/main/redirects/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/redirects/__init__.py +++ /dev/null diff --git a/pysite/views/main/redirects/github.py b/pysite/views/main/redirects/github.py deleted file mode 100644 index 9e9c0cb8..00000000 --- a/pysite/views/main/redirects/github.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class GitHubView(RedirectView): - path = "/github" - name = "github" - page = "https://gitlab.com/python-discord/" - code = 302 diff --git a/pysite/views/main/redirects/gitlab.py b/pysite/views/main/redirects/gitlab.py deleted file mode 100644 index 4b2b60b4..00000000 --- a/pysite/views/main/redirects/gitlab.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class GitLabView(RedirectView): - path = "/gitlab" - name = "gitlab" - page = "https://gitlab.com/python-discord/" - code = 302 diff --git a/pysite/views/main/redirects/invite.py b/pysite/views/main/redirects/invite.py deleted file mode 100644 index 72e0d144..00000000 --- a/pysite/views/main/redirects/invite.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class InviteView(RedirectView): - path = "/invite" - name = "invite" - page = "https://discord.gg/8NWhsvT" - code = 302 diff --git a/pysite/views/main/redirects/stats.py b/pysite/views/main/redirects/stats.py deleted file mode 100644 index 57a56b3d..00000000 --- a/pysite/views/main/redirects/stats.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class StatsView(RedirectView): - path = "/stats" - name = "stats" - page = "https://p.datadoghq.com/sb/ac8680a8c-c01b556f01b96622fd4f57545b81d568" - code = 302 diff --git a/pysite/views/main/robots_txt.py b/pysite/views/main/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/main/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/main/sitemap_xml.py b/pysite/views/main/sitemap_xml.py deleted file mode 100644 index 98893c21..00000000 --- a/pysite/views/main/sitemap_xml.py +++ /dev/null @@ -1,69 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - urls = [ - { - "type": "url", - "url": url_for("main.index", _external=True), - "priority": 1.0, # Max priority - - "images": [ - { - "caption": "Python Discord Logo", - "url": url_for("static", filename="logos/logo_discord.png", _external=True) - }, - { - "caption": "Python Discord Banner", - "url": url_for("static", filename="logos/logo_banner.png", _external=True) - } - ] - }, - - { - "type": "url", - "url": url_for("main.jams.index", _external=True), - "priority": 0.9 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.about.privacy", _external=True), - "priority": 0.8 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.about.rules", _external=True), - "priority": 0.8 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.info.help", _external=True), - "priority": 0.7 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.info.faq", _external=True), - "priority": 0.7 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.info.resources", _external=True), - "priority": 0.7 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.about.partners", _external=True), - "priority": 0.6 # Normal priority - }, - ] - - return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml") diff --git a/pysite/views/main/ws_test.py b/pysite/views/main/ws_test.py deleted file mode 100644 index a0b6215f..00000000 --- a/pysite/views/main/ws_test.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from pysite.base_route import RouteView - - -class WSTest(RouteView): - path = "/ws_test" - name = "ws_test" - - def get(self): - return self.render( - "main/ws_test.html", - server_name=os.environ.get("SERVER_NAME", "localhost") - ) diff --git a/pysite/views/main/ws_test_rst.py b/pysite/views/main/ws_test_rst.py deleted file mode 100644 index e80acc55..00000000 --- a/pysite/views/main/ws_test_rst.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from pysite.base_route import RouteView - - -class WSTest(RouteView): - path = "/ws_test_rst" - name = "ws_test_rst" - - def get(self): - return self.render( - "main/ws_test_rst.html", - server_name=os.environ.get("SERVER_NAME", "localhost") - ) diff --git a/pysite/views/staff/__init__.py b/pysite/views/staff/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/__init__.py +++ /dev/null diff --git a/pysite/views/staff/index.py b/pysite/views/staff/index.py deleted file mode 100644 index a090ebdd..00000000 --- a/pysite/views/staff/index.py +++ /dev/null @@ -1,31 +0,0 @@ -from pprint import pformat - -from flask import current_app - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, DEBUG_MODE, TABLE_MANAGER_ROLES -from pysite.decorators import require_roles - - -class StaffView(RouteView): - path = "/" - name = "index" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - return self.render( - "staff/index.html", manager=self.is_table_editor(), - app_config=pformat(current_app.config, indent=4, width=120) - ) - - def is_table_editor(self): - if DEBUG_MODE: - return True - - data = self.user_data - - for role in TABLE_MANAGER_ROLES: - if role in data.get("roles", []): - return True - - return False diff --git a/pysite/views/staff/jams/__init__.py b/pysite/views/staff/jams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py deleted file mode 100644 index dfcbf2de..00000000 --- a/pysite/views/staff/jams/actions.py +++ /dev/null @@ -1,597 +0,0 @@ -from flask import jsonify, request -from rethinkdb import ReqlNonExistenceError - -from pysite.base_route import APIView -from pysite.constants import ALL_STAFF_ROLES, BotEventTypes, CHANNEL_JAM_LOGS, ErrorCodes, JAMMERS_ROLE -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin -from pysite.utils.words import get_word_pairs - -GET_ACTIONS = ("questions",) -POST_ACTIONS = ( - "associate_question", "disassociate_question", "infraction", "questions", "state", "approve_application", - "unapprove_application", "create_team", "generate_teams", "set_team_member", - "reroll_team", "set_winning_team", "unset_winning_team" -) -DELETE_ACTIONS = ("infraction", "question", "team") - -KEYS = ("action",) -QUESTION_KEYS = ("optional", "title", "type") - - -class ActionView(APIView, DBMixin, RMQMixin): - path = "/jams/action" - name = "jams.action" - - table_name = "code_jams" - forms_table = "code_jam_forms" - infractions_table = "code_jam_infractions" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - teams_table = "code_jam_teams" - users_table = "users" - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def get(self): - action = request.args.get("action") - - if action not in GET_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "questions": - questions = self.db.get_all(self.questions_table) - - return jsonify({"questions": questions}) - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def post(self): - if request.is_json: - data = request.get_json(force=True) - action = data["action"] if "action" in data else None - else: - action = request.form.get("action") - - if action not in POST_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "associate_question": - form = int(request.form.get("form")) - question = request.form.get("question") - - form_obj = self.db.get(self.forms_table, form) - - if not form_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - if question_obj["id"] not in form_obj["questions"]: - form_obj["questions"].append(question_obj["id"]) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"question": question_obj}) - else: - return self.error( - ErrorCodes.incorrect_parameters, - f"Question {question} already associated with form {form}" - ) - - if action == "disassociate_question": - form = int(request.form.get("form")) - question = request.form.get("question") - - form_obj = self.db.get(self.forms_table, form) - - if not form_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - if question_obj["id"] in form_obj["questions"]: - form_obj["questions"].remove(question_obj["id"]) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"question": question_obj}) - else: - return self.error( - ErrorCodes.incorrect_parameters, - f"Question {question} not already associated with form {form}" - ) - - if action == "state": - jam = int(request.form.get("jam")) - state = request.form.get("state") - - if not all((jam, state)): - return self.error(ErrorCodes.incorrect_parameters) - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["state"] = state - self.db.insert(self.table_name, jam_obj, conflict="update") - - return jsonify({}) - - if action == "questions": - data = request.get_json(force=True) - - for key in QUESTION_KEYS: - if key not in data: - return self.error(ErrorCodes.incorrect_parameters, f"Missing key: {key}") - - title = data["title"] - optional = data["optional"] - question_type = data["type"] - question_data = data.get("data", {}) - - if question_type in ["number", "range", "slider"]: - if "max" not in question_data or "min" not in question_data: - return self.error( - ErrorCodes.incorrect_parameters, f"{question_type} questions must have both max and min values" - ) - - result = self.db.insert( - self.questions_table, - { - "title": title, - "optional": optional, - "type": question_type, - "data": { - "max": question_data["max"], - "min": question_data["min"] - } - }, - conflict="error" - ) - elif question_type == "radio": - if "options" not in question_data: - return self.error( - ErrorCodes.incorrect_parameters, f"{question_type} questions must have both options" - ) - - result = self.db.insert( - self.questions_table, - { - "title": title, - "optional": optional, - "type": question_type, - "data": { - "options": question_data["options"] - } - }, - conflict="error" - ) - else: - result = self.db.insert( - self.questions_table, - { # No extra data for other types of question - "title": title, - "optional": optional, - "type": question_type - }, - conflict="error" - ) - - return jsonify({"id": result["generated_keys"][0]}) - - if action == "infraction": - participant = request.form.get("participant") - reason = request.form.get("reason") - - if not participant or not reason or "number" not in request.form: - return self.error( - ErrorCodes.incorrect_parameters, "Infractions must have a participant, reason and number" - ) - - number = int(request.form.get("number")) - - result = self.db.insert(self.infractions_table, { - "participant": participant, - "reason": reason, - "number": number, - "decremented_for": [] - }) - - return jsonify({"id": result["generated_keys"][0]}) - - if action == "create_team": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - jam_data = self.db.get(self.table_name, jam) - - if not jam_data: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - word_pairs = get_word_pairs() - adjective, noun = list(word_pairs)[0] - - team = { - "name": f"{adjective} {noun}".title(), - "members": [], - "jam": jam - } - - result = self.db.insert(self.teams_table, team) - team["id"] = result["generated_keys"][0] - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["teams"].append(team["id"]) - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"team": team}) - - if action == "generate_teams": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"], "approved": True}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": ["snowflake", "answers"]}) - .zip() - .order_by("username") - .coerce_to("array"), - "teams": - self.db.query(self.teams_table) - .outer_join(self.db.query(self.table_name), - lambda team_row, jams_row: jams_row["teams"].contains(team_row["id"])) - .pluck({"left": ["id", "name", "members"]}) - .zip() - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - if jam_data["teams"]: - return self.error( - ErrorCodes.incorrect_parameters, "Jam already has teams" - ) - - num_participants = len(jam_data["participants"]) - num_teams = num_participants // 3 - - if num_participants % 3: - num_teams += 1 - - word_pairs = get_word_pairs(num_teams) - teams = [] - - for adjective, noun in word_pairs: - team = { - "name": f"{adjective} {noun}".title(), - "members": [] - } - - result = self.db.insert(self.teams_table, team, durability="soft") - team["id"] = result["generated_keys"][0] - teams.append(team) - - self.db.sync(self.teams_table) - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["teams"] = [team["id"] for team in teams] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"teams": teams}) - - if action == "set_team_member": - jam = request.form.get("jam", type=int) - member = request.form.get("member") - team = request.form.get("team") - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - if not member: - return self.error( - ErrorCodes.incorrect_parameters, "Member ID required" - ) - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"], "approved": True}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": ["snowflake", "answers"]}) - .zip() - .order_by("username") - .coerce_to("array"), - "teams": - self.db.query(self.teams_table) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id", "name", "members", "jam"]) - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - if not jam_data["teams"]: - return self.error( - ErrorCodes.incorrect_parameters, "Jam has no teams" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - for jam_team_obj in jam_data["teams"]: - if jam_team_obj["id"] == team: - if member not in jam_team_obj["members"]: - jam_team_obj["members"].append(member) - - self.db.insert(self.teams_table, jam_team_obj, conflict="replace") - else: - if member in jam_team_obj["members"]: - jam_team_obj["members"].remove(member) - - self.db.insert(self.teams_table, jam_team_obj, conflict="replace") - - return jsonify({"result": True}) - - if action == "reroll_team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - word_pairs = get_word_pairs() - adjective, noun = list(word_pairs)[0] - - team_obj["name"] = f"{adjective} {noun}".title() - - self.db.insert(self.teams_table, team_obj, conflict="replace") - - return jsonify({"name": team_obj["name"]}) - - if action == "set_winning_team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - jam_number = team_obj["jam"] - jam_obj = self.db.get(self.table_name, jam_number) - jam_obj["winning_team"] = team - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"result": "success"}) - - if action == "unset_winning_team": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - jam_obj = self.db.get(self.table_name, jam) - if not jam_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - jam_obj["winning_team"] = None - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"result": "success"}) - - if action == "approve_application": - app = request.form.get("id") - - if not app: - return self.error( - ErrorCodes.incorrect_parameters, "Application ID required" - ) - - app_obj = self.db.get(self.responses_table, app) - - if not app_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown application ID" - ) - - app_obj["approved"] = True - - self.db.insert(self.responses_table, app_obj, conflict="replace") - - jam_obj = self.db.get(self.table_name, app_obj["jam"]) - - snowflake = app_obj["snowflake"] - participants = jam_obj.get("participants", []) - - if snowflake not in participants: - participants.append(snowflake) - jam_obj["participants"] = participants - self.db.insert(self.table_name, jam_obj, conflict="replace") - - self.rmq_bot_event( - BotEventTypes.add_role, - { - "reason": "Code jam application approved", - "role_id": JAMMERS_ROLE, - "target": snowflake, - } - ) - - self.rmq_bot_event( - BotEventTypes.send_message, - { - "message": f"Congratulations <@{snowflake}> - you've been approved, " - f"and we've assigned you the Jammer role!", - "target": CHANNEL_JAM_LOGS, - } - ) - - return jsonify({"result": "success"}) - - if action == "unapprove_application": - app = request.form.get("id") - - if not app: - return self.error( - ErrorCodes.incorrect_parameters, "Application ID required" - ) - - app_obj = self.db.get(self.responses_table, app) - - if not app_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown application ID" - ) - - app_obj["approved"] = False - - self.db.insert(self.responses_table, app_obj, conflict="replace") - - jam_obj = self.db.get(self.table_name, app_obj["jam"]) - - snowflake = app_obj["snowflake"] - participants = jam_obj.get("participants", []) - - if snowflake in participants: - participants.remove(snowflake) - jam_obj["participants"] = participants - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - self.rmq_bot_event( - BotEventTypes.remove_role, - { - "reason": "Code jam application unapproved", - "role_id": JAMMERS_ROLE, - "target": snowflake, - } - ) - - return jsonify({"result": "success"}) - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def delete(self): - action = request.form.get("action") - - if action not in DELETE_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "question": - question = request.form.get("id") - - if not question: - return self.error(ErrorCodes.incorrect_parameters, f"Missing key: id") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - self.db.delete(self.questions_table, question) - - for form_obj in self.db.get_all(self.forms_table): - if question in form_obj["questions"]: - form_obj["questions"].remove(question) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"id": question}) - - if action == "infraction": - infraction = request.form.get("id") - - if not infraction: - return self.error(ErrorCodes.incorrect_parameters, "Missing key id") - - infraction_obj = self.db.get(self.infractions_table, infraction) - - if not infraction_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown infraction: {infraction}") - - self.db.delete(self.infractions_table, infraction) - - return jsonify({"id": infraction_obj["id"]}) - - if action == "team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - jam_obj = self.db.get(self.table_name, team_obj["jam"]) - if jam_obj: - jam_obj["teams"].remove(team) - self.db.insert(self.table_name, jam_obj, conflict="update") - - self.db.delete(self.teams_table, team) - - return jsonify({"result": True}) diff --git a/pysite/views/staff/jams/create.py b/pysite/views/staff/jams/create.py deleted file mode 100644 index ef61cbef..00000000 --- a/pysite/views/staff/jams/create.py +++ /dev/null @@ -1,61 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/create" - name = "jams.create" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - number = self.get_next_number() - return self.render("staff/jams/create.html", number=number) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self): - data = {} - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - data[key] = arg - - data["state"] = "planning" - data["number"] = self.get_next_number() - - # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy - date_start = datetime.datetime.strptime(data["date_start"], "%Y-%m-%d %H:%M") - date_start = date_start.replace(tzinfo=datetime.timezone.utc) - - date_end = datetime.datetime.strptime(data["date_end"], "%Y-%m-%d %H:%M") - date_end = date_end.replace(tzinfo=datetime.timezone.utc) - - data["date_start"] = date_start - data["date_end"] = date_end - - self.db.insert(self.table_name, data) - - return redirect(url_for("staff.jams.index")) - - def get_next_number(self) -> int: - count = self.db.run(self.table.count(), coerce=int) - - if count: - max_num = self.db.run(self.table.max("number"))["number"] - - return max_num + 1 - return 1 diff --git a/pysite/views/staff/jams/edit_basics.py b/pysite/views/staff/jams/edit_basics.py deleted file mode 100644 index 462cba14..00000000 --- a/pysite/views/staff/jams/edit_basics.py +++ /dev/null @@ -1,55 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/basics" - name = "jams.edit.basics" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - return self.render("staff/jams/edit_basics.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy - date_start = datetime.datetime.strptime(jam_obj["date_start"], "%Y-%m-%d %H:%M") - date_start = date_start.replace(tzinfo=datetime.timezone.utc) - - date_end = datetime.datetime.strptime(jam_obj["date_end"], "%Y-%m-%d %H:%M") - date_end = date_end.replace(tzinfo=datetime.timezone.utc) - - jam_obj["date_start"] = date_start - jam_obj["date_end"] = date_end - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/edit_ending.py b/pysite/views/staff/jams/edit_ending.py deleted file mode 100644 index 43a36ebc..00000000 --- a/pysite/views/staff/jams/edit_ending.py +++ /dev/null @@ -1,54 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - -REQUIRED_KEYS = ["end_rst"] -ALLOWED_STATES = ["judging", "finished"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/ending" - name = "jams.edit.ending" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - return self.render("staff/jams/edit_ending.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - jam_obj["end_html"] = render(jam_obj["end_rst"], link_headers=False)["html"] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/edit_info.py b/pysite/views/staff/jams/edit_info.py deleted file mode 100644 index 4944ae67..00000000 --- a/pysite/views/staff/jams/edit_info.py +++ /dev/null @@ -1,55 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - -REQUIRED_KEYS = ["info_rst", "repo", "task_rst", "theme"] -ALLOWED_STATES = ["planning", "announced", "preparing", "finished"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/info" - name = "jams.edit.info" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - return self.render("staff/jams/edit_info.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - jam_obj["task_html"] = render(jam_obj["task_rst"], link_headers=False)["html"] - jam_obj["info_html"] = render(jam_obj["info_rst"], link_headers=False)["html"] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/forms/__init__.py b/pysite/views/staff/jams/forms/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/forms/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/forms/preamble_edit.py b/pysite/views/staff/jams/forms/preamble_edit.py deleted file mode 100644 index 59b4678b..00000000 --- a/pysite/views/staff/jams/forms/preamble_edit.py +++ /dev/null @@ -1,45 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - - -class StaffView(RouteView, DBMixin): - path = "/jams/form/<int:jam>/preamble" - name = "jams.forms.preamble.edit" - - table_name = "code_jam_forms" - jams_table = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.jams_table, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.table_name, jam) - return self.render("staff/jams/forms/preamble_edit.html", jam=jam_obj, form=form_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.table_name, jam) - - preamble_rst = request.form.get("preamble_rst") - - form_obj["preamble_rst"] = preamble_rst - form_obj["preamble_html"] = render(preamble_rst, link_headers=False)["html"] - - self.db.insert(self.table_name, form_obj, conflict="replace") - - return redirect(url_for("staff.jams.forms.view", jam=jam)) diff --git a/pysite/views/staff/jams/forms/questions_edit.py b/pysite/views/staff/jams/forms/questions_edit.py deleted file mode 100644 index d46c4ef3..00000000 --- a/pysite/views/staff/jams/forms/questions_edit.py +++ /dev/null @@ -1,75 +0,0 @@ -import json - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/questions/<question>" - name = "jams.forms.questions.edit" - - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, question): - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return NotFound() - - question_obj["data"] = question_obj.get("data", {}) - - return self.render( - "staff/jams/forms/questions_edit.html", question=question_obj - ) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, question): - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return NotFound() - - title = request.form.get("title") - optional = request.form.get("optional") - question_type = request.form.get("type") - - if not title or not optional or not question_type: - return BadRequest() - - question_obj["title"] = title - question_obj["optional"] = optional == "optional" - question_obj["type"] = question_type - - if question_type == "radio": - options = request.form.get("options") - - if not options: - return BadRequest() - - options = json.loads(options)["options"] # No choice this time - question_obj["data"] = {"options": options} - - elif question_type in ("number", "range", "slider"): - question_min = request.form.get("min") - question_max = request.form.get("max") - - if question_min is None or question_max is None: - return BadRequest() - - question_obj["data"] = { - "min": question_min, - "max": question_max - } - - self.db.insert(self.questions_table, question_obj, conflict="replace") - - return redirect(url_for("staff.jams.forms.questions")) diff --git a/pysite/views/staff/jams/forms/questions_view.py b/pysite/views/staff/jams/forms/questions_view.py deleted file mode 100644 index 50ad009e..00000000 --- a/pysite/views/staff/jams/forms/questions_view.py +++ /dev/null @@ -1,22 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/questions" - name = "jams.forms.questions" - - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - questions = self.db.get_all(self.questions_table) - - return self.render( - "staff/jams/forms/questions_view.html", questions=questions, - question_ids=[q["id"] for q in questions] - ) diff --git a/pysite/views/staff/jams/forms/view.py b/pysite/views/staff/jams/forms/view.py deleted file mode 100644 index 8d4e16ad..00000000 --- a/pysite/views/staff/jams/forms/view.py +++ /dev/null @@ -1,46 +0,0 @@ -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/<int:jam>" - name = "jams.forms.view" - - table_name = "code_jams" - forms_table = "code_jam_forms" - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.forms_table, jam) - - if not form_obj: - form_obj = { - "number": jam, - "questions": [], - "preamble_rst": "", - "preamble_html": "" - } - - self.db.insert(self.forms_table, form_obj) - - if form_obj["questions"]: - questions = self.db.get_all(self.questions_table, *[q for q in form_obj["questions"]]) - else: - questions = [] - - return self.render( - "staff/jams/forms/view.html", jam=jam_obj, form=form_obj, - questions=questions, question_ids=[q["id"] for q in questions] - ) diff --git a/pysite/views/staff/jams/index.py b/pysite/views/staff/jams/index.py deleted file mode 100644 index 40a8387c..00000000 --- a/pysite/views/staff/jams/index.py +++ /dev/null @@ -1,15 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, JAM_STATES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - - -class StaffView(RouteView, DBMixin): - path = "/jams" - name = "jams.index" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - jams = self.db.get_all(self.table_name) - return self.render("staff/jams/index.html", jams=jams, states=JAM_STATES) diff --git a/pysite/views/staff/jams/infractions/__init__.py b/pysite/views/staff/jams/infractions/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/infractions/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/infractions/view.py b/pysite/views/staff/jams/infractions/view.py deleted file mode 100644 index 235f99ac..00000000 --- a/pysite/views/staff/jams/infractions/view.py +++ /dev/null @@ -1,29 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/infractions" - name = "jams.infractions" - - table_name = "code_jam_infractions" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - infractions = self.db.get_all(self.table_name) - - for document in infractions: - user_obj = self.db.get(self.users_table, document["participant"]) - - if user_obj: - document["participant"] = user_obj - - return self.render( - "staff/jams/infractions/view.html", infractions=infractions, - infraction_ids=[i["id"] for i in infractions] - ) diff --git a/pysite/views/staff/jams/participants.py b/pysite/views/staff/jams/participants.py deleted file mode 100644 index 52f9bdec..00000000 --- a/pysite/views/staff/jams/participants.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] -log = logging.getLogger(__name__) - - -class StaffView(RouteView, DBMixin): - path = "/jams/participants/<int:jam>" - name = "jams.participants" - - forms_table = "code_jam_forms" - participants_table = "code_jam_participants" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - table_name = "code_jams" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam: int): - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"]}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": "snowflake"}) - .zip() - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - form_obj = self.db.get(self.forms_table, jam) - questions = {} - - if form_obj: - for question in form_obj["questions"]: - questions[question] = self.db.get(self.questions_table, question) - - return self.render( - "staff/jams/participants.html", - jam=jam_data, form=form_obj, questions=questions - ) diff --git a/pysite/views/staff/jams/teams/__init__.py b/pysite/views/staff/jams/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/teams/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/teams/view.py b/pysite/views/staff/jams/teams/view.py deleted file mode 100644 index 662cc084..00000000 --- a/pysite/views/staff/jams/teams/view.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ("title", "date_start", "date_end") -log = logging.getLogger(__name__) - - -class StaffView(RouteView, DBMixin): - path = "/jams/teams/<int:jam>" - name = "jams.teams" - - table_name = "code_jam_teams" - - forms_table = "code_jam_forms" - jams_table = "code_jams" - participants_table = "code_jam_participants" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam: int): - try: - query = self.db.query(self.jams_table).get(jam).merge( - # Merge the jam document with a custom document defined below - lambda jam_obj: { # The lambda lets us manipulate the jam document server-side - "participants": - # Query the responses table - self.db.query(self.responses_table) - # Filter: approved responses for this jam only # noqa: E131 - .filter({"jam": jam_obj["number"], "approved": True}) - # Join each response document with documents from the user table that match the user that - # created this response - this is the efficient way to do things, inner/outer joins - # are slower as they only support explicit predicates - .eq_join("snowflake", self.db.query(self.users_table)) - # Remove the user ID from the left side (the response document) - .without({"left": ["snowflake"]}) - .zip() # Combine the left and right documents together - .order_by("username") # Reorder the documents by username - .coerce_to("array"), # Coerce the document stream into an array - "profiles": - # Query the responses table (again) - # We do this because RethinkDB just returns empty lists if you join on another join - self.db.query(self.responses_table) - # Filter: approved responses for this jam only # noqa: E131 - .filter({"jam": jam_obj["number"], "approved": True}) - # Join each response document with documents from the participant profiles table - # this time - .eq_join("snowflake", self.db.query(self.participants_table)) - # Remove the user ID and answers from the left side (the response document) - .without({"left": ["snowflake", "answers"]}) - .zip() # Combine the left and right documents together - .order_by("username") # Reorder the documents by username - .coerce_to("array"), # Coerce the document stream into an array - "form": self.db.query(self.forms_table).get(jam), # Just get the correct form object - "teams": - self.db.query(self.table_name) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id", "name", "members"]) - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - questions = {} - - for question in jam_data["form"]["questions"]: - questions[question] = self.db.get(self.questions_table, question) - - teams = {} - participants = {} - assigned = [] - - for team in jam_data["teams"]: - teams[team["id"]] = team - - for member in team["members"]: - assigned.append(member) - - for user in jam_data["participants"]: - participants[user["user_id"]] = user - - for profile in jam_data["profiles"]: - participants[profile["id"]]["profile"] = profile - - return self.render( - "staff/jams/teams/view.html", - jam=jam_data, teams=teams, - participants=participants, assigned=assigned, - questions=questions - ) diff --git a/pysite/views/staff/render.py b/pysite/views/staff/render.py deleted file mode 100644 index 0152e568..00000000 --- a/pysite/views/staff/render.py +++ /dev/null @@ -1,62 +0,0 @@ -import re - -from docutils.utils import SystemMessage -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import EDITOR_ROLES, ValidationTypes -from pysite.decorators import api_params, csrf, require_roles -from pysite.rst import render - -SCHEMA = Schema([{ - "data": str -}]) - -MESSAGE_REGEX = re.compile(r"<string>:(\d+): \([A-Z]+/\d\) (.*)") - - -class RenderView(APIView): - path = "/render" # "path" means that it accepts slashes - name = "render" - - @csrf - @require_roles(*EDITOR_ROLES) - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - if not len(data): - return jsonify({"error": "No data!"}) - - data = data[0]["data"] - try: - html = render(data, link_headers=False)["html"] - - return jsonify({"data": html}) - except SystemMessage as e: - lines = str(e) - data = { - "error": lines, - "error_lines": [] - } - - if "\n" in lines: - lines = lines.split("\n") - else: - lines = [lines] - - for message in lines: - match = MESSAGE_REGEX.match(message) - - if match: - data["error_lines"].append( - { - "row": int(match.group(1)) - 3, - "column": 0, - "type": "error", - "text": match.group(2) - } - ) - - return jsonify(data) - except Exception as e: - return jsonify({"error": str(e)}) diff --git a/pysite/views/staff/robots_txt.py b/pysite/views/staff/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/staff/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/staff/sitemap_xml.py b/pysite/views/staff/sitemap_xml.py deleted file mode 100644 index 26a786b0..00000000 --- a/pysite/views/staff/sitemap_xml.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import Response - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml") diff --git a/pysite/views/staff/tables/__init__.py b/pysite/views/staff/tables/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/tables/__init__.py +++ /dev/null diff --git a/pysite/views/staff/tables/edit.py b/pysite/views/staff/tables/edit.py deleted file mode 100644 index 7de63ad2..00000000 --- a/pysite/views/staff/tables/edit.py +++ /dev/null @@ -1,110 +0,0 @@ -import json - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableEditView(RouteView, DBMixin): - path = "/tables/<table>/edit" - name = "tables.edit" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table): - obj = TABLES.get(table) - - if not obj: - # Unknown table - raise NotFound() - - if obj.locked: - return redirect(url_for("staff.tables.table", table=table, page=1), code=303) - - key = request.args.get("key") - - old_primary = None - - if key: - db_obj = self.db.get(table, key) - old_primary = key # Provide the current document's primary key, in case it's modified - - document = json.dumps( # Editor uses JSON - db_obj, - indent=4 - ) - else: - document = json.dumps( # Generate default document from key schema - {k: "" for k in obj.keys}, - indent=4 - ) - - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, - document=document, old_primary=old_primary - ) - - @require_roles(*TABLE_MANAGER_ROLES) - @csrf - def post(self, table): - obj = TABLES.get(table) - - if not obj: - # Unknown table - raise NotFound() - - if obj.locked: - raise BadRequest() - - data = request.form.get("json") - old_primary = request.form.get("old_primary") - - if not data: - # No data given (for some reason) - document = json.dumps( - {k: "" for k in obj.keys}, - indent=4 - ) - - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=document, - message="Please provide some data to save", old_primary=old_primary - ) - - try: - data = json.loads(data) - except json.JSONDecodeError as e: - # Invalid JSON - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=data, - message=f"Invalid JSON, please try again: {e}", old_primary=old_primary - ) - - if not data[obj.primary_key]: - # No primary key value provided - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=data, - message=f"Please provide a value for the primary key: {obj.primary_key}", old_primary=old_primary - ) - - if old_primary is None: - self.db.insert( # This is a new object, so just insert it - table, data - ) - elif old_primary == data[obj.primary_key]: - self.db.insert( # This is an update without a primary key change, replace the whole document - table, data, conflict="replace" - ) - else: - self.db.delete( # This is a primary key change, so we need to remove the old object - table, old_primary - ) - self.db.insert( - table, data, - ) - - return redirect(url_for("staff.tables.table", table=table, page=1), code=303) diff --git a/pysite/views/staff/tables/index.py b/pysite/views/staff/tables/index.py deleted file mode 100644 index 0d84aeb4..00000000 --- a/pysite/views/staff/tables/index.py +++ /dev/null @@ -1,13 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.tables import TABLES - - -class TablesView(RouteView): - path = "/tables" - name = "tables.index" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self): - return self.render("staff/tables/index.html", tables=TABLES) diff --git a/pysite/views/staff/tables/table.py b/pysite/views/staff/tables/table.py deleted file mode 100644 index f47d7793..00000000 --- a/pysite/views/staff/tables/table.py +++ /dev/null @@ -1,63 +0,0 @@ -from math import ceil - -from flask import request -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableView(RouteView, DBMixin): - path = "/tables/<table>/<page>" - name = "tables.table" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table, page): - search = request.args.get("search") - search_key = request.args.get("search-key") - - pages = page - obj = TABLES.get(table) - - if not obj: - return NotFound() - - if search: - new_search = f"(?i){search}" # Case-insensitive search - search_key = search_key or obj.primary_key - - query = self.db.query(table).filter(lambda d: d[search_key].match(new_search)) - else: - query = self.db.query(table) - - if page != "all": - try: - page = int(page) - except ValueError: - # Not an integer - return BadRequest() - - count = self.db.run(query.count(), coerce=int) - pages = max(ceil(count / 10), 1) # Pages if we have 10 documents per page, always at least one - - if page < 1 or page > pages: - # If the page is too small or too big, well, that's an error - return BadRequest() - - documents = self.db.run( # Get only the documents for this page - query.skip((page - 1) * 10).limit(10), - coerce=list - ) - else: - documents = self.db.run(query, coerce=list) - - documents = [dict(sorted(d.items())) for d in documents] - - return self.render( - "staff/tables/table.html", - table=table, documents=documents, table_obj=obj, - page=page, pages=pages, search=search, search_key=search_key - ) diff --git a/pysite/views/staff/tables/table_bare.py b/pysite/views/staff/tables/table_bare.py deleted file mode 100644 index abd6cb19..00000000 --- a/pysite/views/staff/tables/table_bare.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableView(RouteView, DBMixin): - path = "/tables/<table>" - name = "tables.table_bare" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table): - if table not in TABLES: - raise NotFound() - - search = request.args.get("search") - - args = { - "table": table, - "page": 1 - } - - if search is not None: - args["search"] = search - - return redirect(url_for("staff.tables.table", **args)) diff --git a/pysite/views/tests/__init__.py b/pysite/views/tests/__init__.py deleted file mode 100644 index adfc1286..00000000 --- a/pysite/views/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py deleted file mode 100644 index f99e3f3c..00000000 --- a/pysite/views/tests/index.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_params - -LIST_SCHEMA = Schema([{"test": str}]) -DICT_SCHEMA = Schema({"segfault": str}) - - -class TestParamsView(APIView): - path = "/testparams" - name = "testparams" - - @api_params(schema=DICT_SCHEMA, validation_type=ValidationTypes.params) - def get(self, data): - return jsonify(data) - - @api_params(schema=LIST_SCHEMA, validation_type=ValidationTypes.params) - def post(self, data): - jsonified = jsonify(data) - return jsonified diff --git a/pysite/views/wiki/__init__.py b/pysite/views/wiki/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/wiki/__init__.py +++ /dev/null diff --git a/pysite/views/wiki/delete.py b/pysite/views/wiki/delete.py deleted file mode 100644 index 728570a9..00000000 --- a/pysite/views/wiki/delete.py +++ /dev/null @@ -1,64 +0,0 @@ -import datetime - -from flask import redirect, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin - - -class DeleteView(RouteView, DBMixin, RMQMixin): - path = "/delete/<path:page>" # "path" means that it accepts slashes - name = "delete" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj: - title = obj.get("title", "") - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page) - - return self.render("wiki/page_delete.html", page=page, title=title, can_edit=True) - else: - raise NotFound() - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - obj = self.db.get(self.table_name, page) - - if not obj: - raise NotFound() - - self.db.delete(self.table_name, page) - self.db.delete(self.revision_table_name, page) - - revisions = self.db.filter(self.revision_table_name, lambda revision: revision["slug"] == page) - - for revision in revisions: - self.db.delete(self.revision_table_name, revision["id"]) - - self.audit_log(obj) - - return redirect(url_for("wiki.page", page="home"), code=303) # Redirect, ensuring a GET - - def audit_log(self, obj): - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": f"Page Deletion", - "description": f"**{obj['title']}** was deleted by **{self.user_data.get('username')}**", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/edit.py b/pysite/views/wiki/edit.py deleted file mode 100644 index 949c9942..00000000 --- a/pysite/views/wiki/edit.py +++ /dev/null @@ -1,149 +0,0 @@ -import datetime -import html -import re - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, DEBUG_MODE, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin -from pysite.rst import render - -STRIP_REGEX = re.compile(r"<[^<]+?>") - - -class EditView(RouteView, DBMixin, RMQMixin): - path = "/edit/<path:page>" # "path" means that it accepts slashes - name = "edit" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - rst = "" - title = "" - preview = "<p>Preview will appear here.</p>" - - obj = self.db.get(self.table_name, page) - - if obj: - rst = obj.get("rst", "") - title = obj.get("title", "") - preview = obj.get("html", preview) - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page, can_edit=True) - - lock_expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=5) - - # There are a couple of cases where we will not need to lock a page. One of these is if the application is - # current set to debug mode. The other of these cases is if the page is empty, because if the page is empty - # we will only have a partially filled out page if the user quits before saving. - if obj: - if not DEBUG_MODE and obj.get("rst"): - self.db.insert( - self.table_name, - { - "slug": page, - "lock_expiry": lock_expiry.timestamp(), - "lock_user": self.user_data.get("user_id") - }, - conflict="update" - ) - - return self.render("wiki/page_edit.html", page=page, rst=rst, title=title, preview=preview, can_edit=True) - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - rst = request.form.get("rst") - title = request.form["title"] - - if not rst or not rst.strip(): - raise BadRequest() - - if not title or not title.strip(): - raise BadRequest() - - rendered = render(rst) - - obj = { - "slug": page, - "title": request.form["title"], - "rst": rst, - "html": rendered["html"], - "text": html.unescape(STRIP_REGEX.sub("", rendered["html"]).strip()), - "headers": rendered["headers"] - } - - self.db.insert( - self.table_name, - obj, - conflict="replace" - ) - - if not DEBUG_MODE: - # Add the post to the revisions table - revision_payload = { - "slug": page, - "post": obj, - "date": datetime.datetime.utcnow().timestamp(), - "user": self.user_data.get("user_id") - } - - del revision_payload["post"]["slug"] - - current_revisions = self.db.filter(self.revision_table_name, lambda rev: rev["slug"] == page) - sorted_revisions = sorted(current_revisions, key=lambda rev: rev["date"], reverse=True) - - if len(sorted_revisions) > 0: - old_rev = sorted_revisions[0] - else: - old_rev = None - - new_rev = self.db.insert(self.revision_table_name, revision_payload)["generated_keys"][0] - - self.audit_log(page, new_rev, old_rev, obj) - - return redirect(url_for("wiki.page", page=page), code=303) # Redirect, ensuring a GET - - @require_roles(*EDITOR_ROLES) - @csrf - def patch(self, page): - current = self.db.get(self.table_name, page) - if not current: - return "", 404 - - if current.get("lock_expiry"): # If there is a lock present - - # If user patching is not the user with the lock end here - if current["lock_user"] != self.user_data.get("user_id"): - return "", 400 - new_lock = datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # New lock time, 5 minutes in future - self.db.insert(self.table_name, { - "slug": page, - "lock_expiry": new_lock.timestamp() - }, conflict="update") # Update with new lock time - return "", 204 - - def audit_log(self, page, new_id, old_data, new_data): - if not old_data: - link = f"https://wiki.pythondiscord.com/source/{page}" - else: - link = f"https://wiki.pythondiscord.com/history/compare/{old_data['id']}/{new_id}" - - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": "Page Edit", - "description": f"**{new_data['title']}** edited by **{self.user_data.get('username')}**. " - f"[View the diff here]({link})", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/history/compare.py b/pysite/views/wiki/history/compare.py deleted file mode 100644 index 6411ab30..00000000 --- a/pysite/views/wiki/history/compare.py +++ /dev/null @@ -1,70 +0,0 @@ -import difflib - -from pygments import highlight -from pygments.formatters import HtmlFormatter -from pygments.lexers import DiffLexer -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class CompareView(RouteView, DBMixin): - path = "/history/compare/<string:first_rev>/<string:second_rev>" - name = "history.compare" - - table_name = "wiki_revisions" - table_primary_key = "id" - - def get(self, first_rev, second_rev): - before = self.db.get(self.table_name, first_rev) - after = self.db.get(self.table_name, second_rev) - - if not (before and after): - raise NotFound() - - if before["date"] > after["date"]: # Check whether the before was created after the after - raise BadRequest() - - if before["id"] == after["id"]: # The same revision has been requested - raise BadRequest() - - before_text = before["post"]["rst"] - after_text = after["post"]["rst"] - - if not before_text.endswith("\n"): - before_text += "\n" - - if not after_text.endswith("\n"): - after_text += "\n" - - before_text = before_text.splitlines(keepends=True) - after_text = after_text.splitlines(keepends=True) - - if not before["slug"] == after["slug"]: - raise BadRequest() # The revisions are not from the same post - - diff = difflib.unified_diff(before_text, after_text, fromfile=f"{first_rev}.rst", tofile=f"{second_rev}.rst") - diff = "".join(diff) - diff = highlight(diff, DiffLexer(), HtmlFormatter()) - return self.render("wiki/compare_revision.html", - title=after["post"]["title"], - page=before["slug"], - diff=diff, - slug=before["slug"], - can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/history/show.py b/pysite/views/wiki/history/show.py deleted file mode 100644 index 00a1dc27..00000000 --- a/pysite/views/wiki/history/show.py +++ /dev/null @@ -1,41 +0,0 @@ -import datetime - -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class RevisionsListView(RouteView, DBMixin): - path = "/history/show/<path:page>" - name = "history.show" - - table_name = "wiki_revisions" - table_primary_key = "id" - - def get(self, page): - results = self.db.filter(self.table_name, lambda revision: revision["slug"] == page) - if len(results) == 0: - raise NotFound() - - for result in results: - ts = datetime.datetime.fromtimestamp(result["date"]) - result["pretty_time"] = ts.strftime("%d %b %Y") - - results = sorted(results, key=lambda revision: revision["date"], reverse=True) - return self.render("wiki/revision_list.html", page=page, revisions=results, can_edit=self.is_staff()), 200 - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/index.py b/pysite/views/wiki/index.py deleted file mode 100644 index 53a4d269..00000000 --- a/pysite/views/wiki/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class WikiView(RedirectView): - path = "/" - name = "index" - page = "wiki.page" - kwargs = {"page": "home"} diff --git a/pysite/views/wiki/move.py b/pysite/views/wiki/move.py deleted file mode 100644 index 095a1fdb..00000000 --- a/pysite/views/wiki/move.py +++ /dev/null @@ -1,84 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin - - -class MoveView(RouteView, DBMixin, RMQMixin): - path = "/move/<path:page>" # "path" means that it accepts slashes - name = "move" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj: - title = obj.get("title", "") - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page, can_edit=True) - - return self.render("wiki/page_move.html", page=page, title=title, can_edit=True) - else: - raise NotFound() - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - location = request.form.get("location") - - if not location or not location.strip(): - raise BadRequest() - - obj = self.db.get(self.table_name, page) - - if not obj: - raise NotFound() - - title = obj.get("title", "") - other_obj = self.db.get(self.table_name, location) - - if other_obj: - return self.render( - "wiki/page_move.html", page=page, title=title, - message=f"There's already a page at {location} - please pick a different location" - ) - - self.db.delete(self.table_name, page) - - # Move all revisions for the old slug to the new slug. - revisions = self.db.filter(self.revision_table_name, lambda revision: revision["slug"] == obj["slug"]) - - for revision in revisions: - revision["slug"] = location - self.db.insert(self.revision_table_name, revision, conflict="update") - - obj["slug"] = location - - self.db.insert(self.table_name, obj, conflict="update") - - self.audit_log(obj) - - return redirect(url_for("wiki.page", page=location), code=303) # Redirect, ensuring a GET - - def audit_log(self, obj): - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": "Wiki Page Move", - "description": f"**{obj['title']}** was moved by **{self.user_data.get('username')}** to " - f"**{obj['slug']}**", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/page.py b/pysite/views/wiki/page.py deleted file mode 100644 index 26edfcc4..00000000 --- a/pysite/views/wiki/page.py +++ /dev/null @@ -1,36 +0,0 @@ -from flask import redirect, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/wiki/<path:page>" # "path" means that it accepts slashes - name = "page" - table_name = "wiki" - - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj is None: - if self.is_staff(): - return redirect(url_for("wiki.edit", page=page)) - - raise NotFound() - return self.render("wiki/page_view.html", page=page, data=obj, can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/render.py b/pysite/views/wiki/render.py deleted file mode 100644 index 39bdd133..00000000 --- a/pysite/views/wiki/render.py +++ /dev/null @@ -1,62 +0,0 @@ -import re - -from docutils.utils import SystemMessage -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import EDITOR_ROLES, ValidationTypes -from pysite.decorators import api_params, csrf, require_roles -from pysite.rst import render - -SCHEMA = Schema([{ - "data": str -}]) - -MESSAGE_REGEX = re.compile(r"<string>:(\d+): \([A-Z]+/\d\) (.*)", flags=re.S) - - -class RenderView(APIView): - path = "/render" # "path" means that it accepts slashes - name = "render" - - @csrf - @require_roles(*EDITOR_ROLES) - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - if not len(data): - return jsonify({"error": "No data!"}) - - data = data[0]["data"] - try: - html = render(data)["html"] - - return jsonify({"data": html}) - except SystemMessage as e: - lines = str(e) - data = { - "error": lines, - "error_lines": [] - } - - if "\n" in lines: - lines = lines.split("\n") - else: - lines = [lines] - - for message in lines: - match = MESSAGE_REGEX.match(message) - - if match: - data["error_lines"].append( - { - "row": int(match.group(1)) - 3, - "column": 0, - "type": "error", - "text": match.group(2) - } - ) - - return jsonify(data) - except Exception as e: - return jsonify({"error": str(e)}) diff --git a/pysite/views/wiki/robots_txt.py b/pysite/views/wiki/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/wiki/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/wiki/search.py b/pysite/views/wiki/search.py deleted file mode 100644 index 369da943..00000000 --- a/pysite/views/wiki/search.py +++ /dev/null @@ -1,66 +0,0 @@ -import html -import re - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.decorators import csrf -from pysite.mixins import DBMixin - -STRIP_REGEX = re.compile(r"<[^<]+?>") - - -class SearchView(RouteView, DBMixin): - path = "/search" # "path" means that it accepts slashes - name = "search" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - def get(self): - return self.render("wiki/search.html") - - @csrf - def post(self): - given_query = request.form.get("query") - - if not given_query or not given_query.strip(): - raise BadRequest() - - query = f"({re.escape(given_query)})" - - pages = self.db.filter( - self.table_name, - lambda doc: doc["text"].match(f"(?i){query}") - ) - - if len(pages) == 1: - slug = pages[0]["slug"] - return redirect(url_for("wiki.page", page=slug), code=303) - - for obj in pages: - text = obj["text"] - - matches = re.finditer(query, text, flags=re.IGNORECASE) - snippets = [] - - for match in matches: - start = match.start() - 50 - - if start < 0: - start = 0 - - end = match.end() + 50 - - if end > len(text): - end = len(text) - - match_text = text[start:end] - match_text = re.sub(query, r"<strong>\1</strong>", html.escape(match_text), flags=re.IGNORECASE) - - snippets.append(match_text.replace("\n", "<br />")) - - obj["matches"] = snippets - - pages = sorted(pages, key=lambda d: d["title"]) - return self.render("wiki/search_results.html", pages=pages, query=given_query) diff --git a/pysite/views/wiki/sitemap_xml.py b/pysite/views/wiki/sitemap_xml.py deleted file mode 100644 index 9b7f0980..00000000 --- a/pysite/views/wiki/sitemap_xml.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class SitemapXML(RouteView, DBMixin): - path = "/sitemap.xml" - name = "sitemap_xml" - table_name = "wiki" - - def get(self): - urls = [] - - for page in self.db.get_all(self.table_name): - urls.append({ - "change_frequency": "weekly", - "type": "url", - "url": url_for("wiki.page", page=page["slug"], _external=True) - }) - - return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml") diff --git a/pysite/views/wiki/source.py b/pysite/views/wiki/source.py deleted file mode 100644 index 83674447..00000000 --- a/pysite/views/wiki/source.py +++ /dev/null @@ -1,42 +0,0 @@ -from flask import redirect, url_for -from pygments import highlight -from pygments.formatters.html import HtmlFormatter -from pygments.lexers import get_lexer_by_name -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/source/<path:page>" # "path" means that it accepts slashes - name = "source" - table_name = "wiki" - - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj is None: - if self.is_staff(): - return redirect(url_for("wiki.edit", page=page, can_edit=False)) - - raise NotFound() - - rst = obj["rst"] - rst = highlight(rst, get_lexer_by_name("rst"), HtmlFormatter(preclass="code", linenos="inline")) - return self.render("wiki/page_source.html", page=page, data=obj, rst=rst, can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/special/__init__.py b/pysite/views/wiki/special/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/wiki/special/__init__.py +++ /dev/null diff --git a/pysite/views/wiki/special/all_pages.py b/pysite/views/wiki/special/all_pages.py deleted file mode 100644 index d2e02a72..00000000 --- a/pysite/views/wiki/special/all_pages.py +++ /dev/null @@ -1,27 +0,0 @@ -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/special/all_pages" - name = "special.all_pages" - table_name = "wiki" - - def get(self): - pages = self.db.pluck(self.table_name, "title", "slug") - pages = sorted(pages, key=lambda d: d.get("title", "No Title")) - - letters = {} - - for page in pages: - if "title" not in page: - page["title"] = "No Title" - - letter = page["title"][0].upper() - - if letter not in letters: - letters[letter] = [] - - letters[letter].append(page) - - return self.render("wiki/special_all.html", letters=letters) diff --git a/pysite/views/wiki/special/index.py b/pysite/views/wiki/special/index.py deleted file mode 100644 index ccfc7a5a..00000000 --- a/pysite/views/wiki/special/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class PageView(TemplateView): - path = "/special" - name = "special" - template = "wiki/special.html" diff --git a/pysite/views/ws/__init__.py b/pysite/views/ws/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/ws/__init__.py +++ /dev/null diff --git a/pysite/views/ws/bot.py b/pysite/views/ws/bot.py deleted file mode 100644 index 816e7579..00000000 --- a/pysite/views/ws/bot.py +++ /dev/null @@ -1,56 +0,0 @@ -import json -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.constants import BOT_API_KEY -from pysite.mixins import DBMixin -from pysite.websockets import WS - - -class BotWebsocket(WS, DBMixin): - path = "/bot" - name = "ws.bot" - table_name = "bot_events" - - do_changefeed = True - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("Bot | WS opened.") - - def on_message(self, message): - self.log.debug(f"Bot | Message: {message}") - - try: - message = json.loads(message) - except json.JSONDecodeError: - self.send_json({"error": "Message was not valid JSON"}) - return self.socket.close() - - action = message["action"] - - if action == "login": - if message["key"] != BOT_API_KEY: - return self.socket.close() - - self.do_changefeed = True - - for document in self.db.changes(self.table_name, include_initial=True, include_types=True): - if not self.do_changefeed: - break - - if document["type"] not in ["add", "initial"]: - continue - - self.send_json({"action": "event", "event": document["new_val"]}) - self.db.delete(self.table_name, document["id"]) - - self.send_json({"error": f"Unknown action: {action}"}) - - def on_close(self): - self.log.debug("Bot | WS closed.") - self.do_changefeed = False diff --git a/pysite/views/ws/echo.py b/pysite/views/ws/echo.py deleted file mode 100644 index b6f11168..00000000 --- a/pysite/views/ws/echo.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.websockets import WS - - -class EchoWebsocket(WS): - path = "/echo" - name = "ws.echo" - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("Echo | WS opened.") - self.send("Hey, welcome!") - - def on_message(self, message): - self.log.debug(f"Echo | Message: {message}") - self.send(message) - - def on_close(self): - self.log.debug("Echo | WS closed.") diff --git a/pysite/views/ws/rst.py b/pysite/views/ws/rst.py deleted file mode 100644 index f2b2db24..00000000 --- a/pysite/views/ws/rst.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.rst import render -from pysite.websockets import WS - - -class RSTWebsocket(WS): - path = "/rst" - name = "ws.rst" - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("RST | WS opened.") - self.send("Hey, welcome!") - - def on_message(self, message): - self.log.debug(f"RST | Message: {message}") - - try: - data = render(message)["html"] - except Exception as e: - self.log.exception("Parsing error") - data = str(e) - - self.send(data) - - def on_close(self): - self.log.debug("RST | WS closed.") diff --git a/pysite/websockets.py b/pysite/websockets.py deleted file mode 100644 index 213daace..00000000 --- a/pysite/websockets.py +++ /dev/null @@ -1,123 +0,0 @@ -import json - -from flask import Blueprint -from geventwebsocket.websocket import WebSocket - - -class WS: - """ - Base class for representing a Websocket. - - At minimum, you must implement the `on_message(self, message)` function. Without it, you won't be able to handle - any messages, and an error will be thrown! - - If you need access to the database, you can mix-in DBMixin, just like any view class: - - >>> class DBWebsocket(WS, DBMixin): - ... name = "db_websocket" - ... path = "/db_websocket" # This will be prefixed with "/ws" by the blueprint - ... table = "ws" - ... - ... def on_message(self, message): - ... self.send( - ... json.loads(self.db.get(self.table_name, message)) - ... ) - - Please note that an instance of this class is created for every websocket connected to the path. This does, however, - mean that you can store any state required by your websocket. - """ - - path = "" # type: str - name = "" # type: str - - _connections = None - - def __init__(self, socket: WebSocket): - self.socket = socket - - def __new__(cls, *args, **kwargs): - if cls._connections is None: - cls._connections = [] - - return super().__new__(cls) - - def on_open(self): - """ - Called once when the websocket is opened. Optional. - """ - - def on_message(self, message: str): - """ - Called when a message is received by the websocket. - """ - - raise NotImplementedError() - - def on_close(self): - """ - Called once when the websocket is closed. Optional. - """ - - def send(self, message, binary=None): - """ - Send a message to the currently-connected websocket, if it's open. - - Nothing will happen if the websocket is closed. - """ - - if not self.socket.closed: - self.socket.send(message, binary=binary) - - def send_json(self, data): - return self.send(json.dumps(data)) - - @classmethod - def send_all(cls, message, binary=None): - for connection in cls._connections: - connection.send(message, binary=binary) - - @classmethod - def send_all_json(cls, data): - for connection in cls._connections: - connection.send_json(data) - - @classmethod - def setup(cls: "type(WS)", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): - """ - Set up the websocket object, calling `setup()` on any superclasses as necessary (for example, on the DB - mixin). - - This function will set up a websocket handler so that it behaves in a class-oriented way. It's up to you to - deal with message handling yourself, however. - """ - - if hasattr(super(), "setup"): - super().setup(manager, blueprint) - - if not cls.path or not cls.name: - raise RuntimeError("Websockets must have both `path` and `name` defined") - - cls.manager = manager - - def handle(socket: WebSocket): - """ - Wrap the current WS class, dispatching events to it as necessary. We're using gevent, so there's - no need to worry about blocking here. - """ - - ws = cls(socket) # Instantiate the current class, passing it the WS object - cls._connections.append(ws) - try: - ws.on_open() # Call the "on_open" handler - - while not socket.closed: # As long as the socket is open... - message = socket.receive() # Wait for a message - - if not socket.closed: # If the socket didn't just close (there's always a None message on closing) - ws.on_message(message) # Call the "on_message" handler - - ws.on_close() # The socket just closed, call the "on_close" handler - finally: - cls._connections.remove(ws) - - blueprint.route(cls.path)(handle) # Register the handling function to the WS blueprint |