diff --git a/alembic/versions/cac1f275a725_add_emailhostban_table.py b/alembic/versions/cac1f275a725_add_emailhostban_table.py new file mode 100644 index 00000000..b3c0e827 --- /dev/null +++ b/alembic/versions/cac1f275a725_add_emailhostban_table.py @@ -0,0 +1,37 @@ +"""Add EmailBan table. + +Revision ID: cac1f275a725 +Revises: 231e2c1e26da +Create Date: 2016-12-12 23:41:56.438144 + +""" + +# revision identifiers, used by Alembic. +revision = 'cac1f275a725' +down_revision = '231e2c1e26da' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('email_bans', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pattern', sa.Unicode(length=255), nullable=False), + sa.Column('date', sa.DateTime(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.Column('reason', sa.Unicode(length=255), nullable=False), + sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('pattern') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('email_host_bans') + ### end Alembic commands ### diff --git a/alembic/versions/ea30a8851312_add_new_user_group.py b/alembic/versions/ea30a8851312_add_new_user_group.py new file mode 100644 index 00000000..a1a9c010 --- /dev/null +++ b/alembic/versions/ea30a8851312_add_new_user_group.py @@ -0,0 +1,31 @@ +"""Add new user group. + +Revision ID: ea30a8851312 +Revises: cac1f275a725 +Create Date: 2016-12-13 23:10:06.709741 + +""" + +# revision identifiers, used by Alembic. +revision = 'ea30a8851312' +down_revision = 'cac1f275a725' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.execute("COMMIT;") + op.execute("ALTER TYPE users_group ADD VALUE 'new';") + op.execute("ALTER TYPE users_group ADD VALUE 'deactivated';") + op.execute("""UPDATE users SET "group"='deactivated' WHERE "group"='guest';""") + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + pass + ### end Alembic commands ### diff --git a/extras/ensure_database.py b/extras/ensure_database.py index cfa94dc4..b8bf3ab0 100755 --- a/extras/ensure_database.py +++ b/extras/ensure_database.py @@ -64,4 +64,5 @@ def init_db(redis: StrictRedis): del db del redis -subprocess.call(sys.argv[1:]) +status = subprocess.call(sys.argv[1:]) +sys.exit(status) diff --git a/newparp/__init__.py b/newparp/__init__.py index 376bacfb..4a699d3f 100644 --- a/newparp/__init__.py +++ b/newparp/__init__.py @@ -2,6 +2,7 @@ import logging from flask import Flask, abort, redirect, request, send_from_directory +from flask_mail import Mail from werkzeug.routing import BaseConverter from newparp.helpers import check_csrf_token @@ -12,17 +13,12 @@ redis_disconnect, set_cookie, ) -from newparp import views -from newparp.views import ( - account, admin, characters, chat, chat_api, chat_list, errors, guides, - roulette, search, search_characters, settings, -) -from newparp.views.admin import spamless, spamless2 app = Flask(__name__) app.url_map.strict_slashes = False + # Config app.config["SERVER_NAME"] = os.environ["BASE_DOMAIN"] @@ -52,6 +48,25 @@ app.teardown_request(redis_disconnect) +app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER", "localhost") +app.config["MAIL_PORT"] = int(os.environ.get("MAIL_PORT", 25)) +app.config["MAIL_USE_TLS"] = "MAIL_USE_TLS" in os.environ +app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME") +app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD") +app.config["MAIL_SUPPRESS_SEND"] = app.testing or bool(os.environ.get("NOMAIL")) +mail = Mail(app) + + +# Views/routes + +from newparp import views +from newparp.views import ( + account, admin, characters, chat, chat_api, chat_list, errors, guides, + search, search_characters, settings, +) +from newparp.views.admin import spamless, spamless2 + + class RegexConverter(BaseConverter): def __init__(self, url_map, *items): super(RegexConverter, self).__init__(url_map) @@ -94,6 +109,7 @@ def make_rules(subdomain, path, func, formats=False, paging=False): app.add_url_rule("/settings/theme", "settings_theme", settings.theme, methods=("POST",)) app.add_url_rule("/settings/log_in_details", "settings_log_in_details", settings.log_in_details, methods=("GET",)) app.add_url_rule("/settings/change_email", "settings_change_email", settings.change_email, methods=("POST",)) +app.add_url_rule("/settings/verify_email", "settings_verify_email", settings.verify_email, methods=("GET",)) app.add_url_rule("/settings/change_password", "settings_change_password", settings.change_password, methods=("POST",)) make_rules("settings", "/settings/blocks", settings.blocks, formats=True) app.add_url_rule("/settings/unblock", "settings_unblock", settings.unblock, methods=("POST",)) @@ -136,14 +152,6 @@ def make_rules(subdomain, path, func, formats=False, paging=False): app.add_url_rule("/search/save", "rp_search_save", search.search_save, methods=("POST",)) app.add_url_rule("/search", "rp_search", search.search_get, methods=("GET",)) app.add_url_rule("/search", "rp_search_post", search.search_post, methods=("POST",)) -app.add_url_rule("/search/continue", "rp_search_continue", search.search_continue, methods=("POST",)) -app.add_url_rule("/search/stop", "rp_search_stop", search.search_stop, methods=("POST",)) - -app.add_url_rule("/roulette/save", "rp_roulette_save", roulette.roulette_save, methods=("POST",)) -app.add_url_rule("/roulette", "rp_roulette", roulette.roulette_get, methods=("GET",)) -app.add_url_rule("/roulette", "rp_roulette_post", roulette.roulette_post, methods=("POST",)) -app.add_url_rule("/roulette/continue", "rp_roulette_continue", roulette.roulette_continue, methods=("POST",)) -app.add_url_rule("/roulette/stop", "rp_roulette_stop", roulette.roulette_stop, methods=("POST",)) # 6. Groups @@ -162,6 +170,7 @@ def make_rules(subdomain, path, func, formats=False, paging=False): make_rules("rp", "//log/--", chat.log_day, formats=True) make_rules("rp", "//users", chat.users, formats=True, paging=True) +app.add_url_rule("//users/reset_regexes", "rp_chat_reset_regexes", chat.reset_regexes, methods=("POST",)) make_rules("rp", "//invites", chat.invites, formats=True, paging=True) app.add_url_rule("//uninvite", "rp_chat_uninvite", chat.uninvite, methods=("POST",)) @@ -170,11 +179,10 @@ def make_rules(subdomain, path, func, formats=False, paging=False): app.add_url_rule("//subscribe", "rp_chat_subscribe", chat.subscribe, methods=("POST",)) app.add_url_rule("//unsubscribe", "rp_chat_unsubscribe", chat.unsubscribe, methods=("POST",)) -app.add_url_rule("/redirect", "redirect", views.redirect, methods=("GET",)) +app.add_url_rule("/redirect", "redirect", views.redirect_view, methods=("GET",)) # 8. Chat API -app.add_url_rule("/chat_api/messages", "messages", chat_api.messages, methods=("POST",)) app.add_url_rule("/chat_api/send", "send", chat_api.send, methods=("POST",)) app.add_url_rule("/chat_api/draft", "draft", chat_api.draft, methods=("POST",)) app.add_url_rule("/chat_api/block", "block", chat_api.block, methods=("POST",)) @@ -190,8 +198,6 @@ def make_rules(subdomain, path, func, formats=False, paging=False): app.add_url_rule("/chat_api/request_username", "request_username", chat_api.request_username, methods=("POST",)) app.add_url_rule("/chat_api/exchange_usernames", "exchange_usernames", chat_api.exchange_usernames, methods=("POST",)) app.add_url_rule("/chat_api/look_up_user", "look_up_user", chat_api.look_up_user, methods=("POST",)) -app.add_url_rule("/chat_api/ping", "ping", chat_api.ping, methods=("POST",)) -app.add_url_rule("/chat_api/quit", "quit", chat_api.quit, methods=("POST",)) # 9. Admin @@ -237,6 +243,10 @@ def make_rules(subdomain, path, func, formats=False, paging=False): app.add_url_rule("/admin/ip_bans/new", "admin_new_ip_ban", admin.new_ip_ban, methods=("POST",)) app.add_url_rule("/admin/ip_bans/delete", "admin_delete_ip_ban", admin.delete_ip_ban, methods=("POST",)) +make_rules("admin", "/admin/email_bans", admin.email_bans, formats=True) +app.add_url_rule("/admin/email_bans/new", "admin_new_email_ban", admin.new_email_ban, methods=("POST",)) +app.add_url_rule("/admin/email_bans/delete", "admin_delete_email_ban", admin.delete_email_ban, methods=("POST",)) + app.add_url_rule("/admin/worker_status", "admin_worker_status", admin.worker_status, methods=("GET",)) # 10. Guides diff --git a/newparp/helpers/auth.py b/newparp/helpers/auth.py index 88b88cdf..466959a5 100644 --- a/newparp/helpers/auth.py +++ b/newparp/helpers/auth.py @@ -16,13 +16,24 @@ def decorated_function(*args, **kwargs): return decorated_function +def activation_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user is None: + return render_template("account/log_in_required.html") + elif g.user.group == "new": + return render_template("account/user_new.html") + elif g.user.group == "deactivated": + return render_template("account/user_deactivated.html") + return f(*args, **kwargs) + return decorated_function + + def log_in_required(f): @wraps(f) def decorated_function(*args, **kwargs): if g.user is None: return render_template("account/log_in_required.html") - elif g.user.group == "guest": - return render_template("account/activation_required.html") return f(*args, **kwargs) return decorated_function diff --git a/newparp/helpers/chat.py b/newparp/helpers/chat.py index 507f4706..19f28735 100644 --- a/newparp/helpers/chat.py +++ b/newparp/helpers/chat.py @@ -27,46 +27,22 @@ class KickedException(Exception): pass -def group_chat_only(f): +def require_socket(f): + """Only allow this request if the user has a socket open.""" @wraps(f) def decorated_function(*args, **kwargs): - if g.chat.type != "group": - abort(404) + g.chat_id = int(request.form["chat_id"]) + if g.redis.scard("chat:%s:sockets:%s" % (g.chat_id, g.session_id)) == 0: + abort(403) return f(*args, **kwargs) return decorated_function -def mark_alive(f): +def group_chat_only(f): @wraps(f) def decorated_function(*args, **kwargs): - g.joining = False - g.chat_id = int(request.form["chat_id"]) - # Don't bother with any of this if we have a socket open, because the - # request is probably from that window. - if g.redis.scard("chat:%s:sockets:%s" % (g.chat_id, g.session_id)) != 0: - return f(*args, **kwargs) - session_online = g.redis.hexists("chat:%s:online" % g.chat_id, g.session_id) - if not session_online: - g.joining = True - # Make sure we're connected to the database. - db_connect() - # Get ChatUser if we haven't got it already. - if not hasattr(g, "chat_user"): - get_chat_user() - try: - authorize_joining(g.redis, g.db, g) - except (UnauthorizedException, BannedException, TooManyPeopleException): - abort(403) - try: - kick_check(g.redis, g) - except KickedException: - return jsonify({"exit": "kick"}) - join(g.redis, g.db, g) - g.redis.zadd( - "chats_alive", - time.time() + 60, - "%s/%s" % (g.chat_id, g.session_id), - ) + if g.chat.type != "group": + abort(404) return f(*args, **kwargs) return decorated_function diff --git a/newparp/helpers/email.py b/newparp/helpers/email.py new file mode 100644 index 00000000..c8297de8 --- /dev/null +++ b/newparp/helpers/email.py @@ -0,0 +1,42 @@ +from flask import g, render_template, url_for +from flask_mail import Message as EmailMessage +from uuid import uuid4 + +from newparp import mail + + +expiry_times = { + "welcome": 86400, + "verify": 86400, + "reset": 600, +} + +subjects = { + "welcome": "Welcome to MSPARP", + "verify": "Verify your e-mail address", + "reset": "Reset your password", +} + +keys = { + "welcome": "verify", + "verify": "verify", + "reset": "reset", +} + + +def send_email(action, email_address): + email_token = str(uuid4()) + g.redis.setex( + ":".join([keys[action], str(g.user.id), email_address]), + expiry_times[action], + email_token, + ) + message = EmailMessage( + subject=subjects[action], + sender="admin@msparp.com", + recipients=[email_address], + body=render_template("email/%s_plain.html" % action, user=g.user, email_address=email_address, email_token=email_token), + html=render_template("email/%s.html" % action, user=g.user, email_address=email_address, email_token=email_token), + ) + mail.send(message) + diff --git a/newparp/helpers/matchmaker.py b/newparp/helpers/matchmaker.py index 4c259883..262eae13 100644 --- a/newparp/helpers/matchmaker.py +++ b/newparp/helpers/matchmaker.py @@ -1,12 +1,72 @@ -import os -import json -import logging +from collections import namedtuple + +def validate_searcher_exists(redis, searcher_id): + """Check whether a searcher's mandatory keys are present.""" + return redis.eval("""local session_id = redis.call("get", "searcher:"..ARGV[1]..":session_id") or "" + return { + session_id, + redis.call("get", "session:"..session_id), + redis.call("get", "searcher:"..ARGV[1]..":search_character_id"), + redis.call("hlen", "searcher:"..ARGV[1]..":character"), + redis.call("get", "searcher:"..ARGV[1]..":style"), + redis.call("scard", "searcher:"..ARGV[1]..":levels"), + }""", 0, searcher_id) + + +def validate_searcher_is_searching(redis, searcher_id): + """Check whether a searcher's mandatory keys are present and they're in the searchers set.""" + return redis.eval("""local session_id = redis.call("get", "searcher:"..ARGV[1]..":session_id") or "" + return { + redis.call("sismember", "searchers", ARGV[1]), + session_id, + redis.call("get", "session:"..session_id), + redis.call("get", "searcher:"..ARGV[1]..":search_character_id"), + redis.call("hlen", "searcher:"..ARGV[1]..":character"), + redis.call("get", "searcher:"..ARGV[1]..":style"), + redis.call("scard", "searcher:"..ARGV[1]..":levels"), + }""", 0, searcher_id) + + +def refresh_searcher(redis, searcher_id): + """Reset the expiry times on a searcher's keys.""" + return redis.eval("""local session_id = redis.call("get", "searcher:"..ARGV[1]..":session_id") or "" + return { + redis.call("get", "session:"..session_id), + redis.call("sismember", "searchers", ARGV[1]), + redis.call("expire", "searcher:"..ARGV[1]..":session_id", 30), + redis.call("expire", "searcher:"..ARGV[1]..":search_character_id", 30), + redis.call("expire", "searcher:"..ARGV[1]..":character", 30), + redis.call("expire", "searcher:"..ARGV[1]..":style", 30), + redis.call("expire", "searcher:"..ARGV[1]..":levels", 30), + redis.call("expire", "searcher:"..ARGV[1]..":filters", 30), + redis.call("expire", "searcher:"..ARGV[1]..":choices", 30), + }""", 0, searcher_id) + + +searcher = namedtuple("searcher", ("id", "searching", "session_id", "user_id", "search_character_id", "character", "style", "levels", "filters", "choices")) + + +def fetch_searcher(redis, searcher_id): + """Fetch searcher keys for matching.""" + searcher_keys = redis.eval("""local session_id = redis.call("get", "searcher:"..ARGV[1]..":session_id") or "" + return { + redis.call("sismember", "searchers", ARGV[1]), + session_id, + redis.call("get", "session:"..session_id), + redis.call("get", "searcher:"..ARGV[1]..":search_character_id"), + redis.call("hgetall", "searcher:"..ARGV[1]..":character"), + redis.call("get", "searcher:"..ARGV[1]..":style"), + redis.call("smembers", "searcher:"..ARGV[1]..":levels"), + redis.call("lrange", "searcher:"..ARGV[1]..":filters", 0, -1), + redis.call("smembers", "searcher:"..ARGV[1]..":choices"), + }""", 0, searcher_id) + # Hashes and sets get returned as lists so we need to convert them manually. + if searcher_keys[4]: + searcher_keys[4] = {k: v for k, v in zip(*(iter(searcher_keys[4]),) * 2)} + searcher_keys[6] = set(searcher_keys[6]) + searcher_keys[8] = set(searcher_keys[8]) + return searcher(searcher_id, *searcher_keys) -from random import shuffle -from sqlalchemy import and_, func -from uuid import uuid4 - -from newparp.model import Block, ChatUser, Message, User option_messages = { "script": "This is a script style chat.", @@ -17,112 +77,3 @@ "roulette": "TT: There is a 98.413% chance that you have just connected to someone anonymously. It seems that you should probably say \"Hello\" now.", } - -def wake_unmatched_searchers(redis, searcher_prefix, searcher_ids): - for searcher in searcher_ids: - logging.debug("Waking unmatched searcher %s." % searcher) - redis.publish("%s:%s" % (searcher_prefix, searcher), "{ \"status\": \"unmatched\" }") - - -def run_matchmaker( - db, redis, lock_id, searchers_key, searcher_prefix, get_searcher_info, - check_compatibility, ChatClass, get_character_info -): - - root = logging.getLogger() - if 'DEBUG' in os.environ: - root.setLevel(logging.DEBUG) - - searcher_ids = redis.smembers(searchers_key) - - # Reset the searcher list for the next iteration. - redis.delete(searchers_key) - - logging.debug("Starting match loop.") - - # We can't do anything with less than 2 people, so don't bother. - if len(searcher_ids) < 2: - logging.debug("Not enough searchers, skipping.") - wake_unmatched_searchers(redis, searcher_prefix, searcher_ids) - redis.set( - "searching_users" if searchers_key == "searchers" else "rouletting_users", - len(searcher_ids), - ) - return - - searchers = get_searcher_info(redis, searcher_ids) - logging.debug("Searcher list: %s" % searchers) - - redis.set( - "searching_users" if searchers_key == "searchers" else "rouletting_users", - len({_["user_id"] for _ in searchers}), - ) - - shuffle(searchers) - - already_matched = set() - # Range hack so we don't check opposite pairs or against itself. - for n in range(len(searchers)): - s1 = searchers[n] - - for m in range(n + 1, len(searchers)): - s2 = searchers[m] - - if s1["id"] in already_matched or s2["id"] in already_matched: - continue - - logging.debug("Comparing %s and %s." % (s1["id"], s2["id"])) - - match, options = check_compatibility(redis, s1, s2) - if not match: - logging.debug("No match.") - continue - - blocked = ( - db.query(func.count("*")).select_from(Block).filter(and_( - Block.blocking_user_id == s1["user_id"], - Block.blocked_user_id == s2["user_id"] - )).scalar() != 0 - or db.query(func.count("*")).select_from(Block).filter(and_( - Block.blocking_user_id == s2["user_id"], - Block.blocked_user_id == s1["user_id"] - )).scalar() != 0 - ) - if blocked: - logging.debug("Blocked.") - continue - - new_url = str(uuid4()).replace("-", "") - logging.info( - "Matched %s and %s, sending to %s." - % (s1["id"], s2["id"], new_url) - ) - new_chat = ChatClass(url=new_url) - db.add(new_chat) - db.flush() - - s1_user = db.query(User).filter(User.id == s1["user_id"]).one() - s2_user = db.query(User).filter(User.id == s2["user_id"]).one() - db.add(ChatUser.from_user(s1_user, chat_id=new_chat.id, number=1, search_character_id=s1["search_character_id"], **get_character_info(db, s1))) - db.add(ChatUser.from_user(s2_user, chat_id=new_chat.id, number=2, search_character_id=s2["search_character_id"], **get_character_info(db, s2))) - - if options: - db.add(Message( - chat_id=new_chat.id, - type="search_info", - text=" ".join(option_messages[_] for _ in options), - )) - - db.commit() - - already_matched.add(s1["id"]) - already_matched.add(s2["id"]) - - match_message = json.dumps({ "status": "matched", "url": new_url }) - redis.publish("%s:%s" % (searcher_prefix, s1["id"]), match_message) - redis.publish("%s:%s" % (searcher_prefix, s2["id"]), match_message) - searcher_ids.remove(s1["id"]) - searcher_ids.remove(s2["id"]) - - wake_unmatched_searchers(redis, searcher_prefix, searcher_ids) - diff --git a/newparp/model/__init__.py b/newparp/model/__init__.py index 1ea65563..57cc601e 100644 --- a/newparp/model/__init__.py +++ b/newparp/model/__init__.py @@ -93,9 +93,10 @@ class User(Base): email_verified = Column(Boolean, nullable=False, default=False) group = Column(Enum( - "banned", - "guest", + "new", "active", + "deactivated", + "banned", name="users_group", ), nullable=False, default="guest") admin_tier_id = Column(Integer, ForeignKey("admin_tiers.id")) @@ -961,6 +962,27 @@ def to_dict(self): } +class EmailBan(Base): + __tablename__ = "email_bans" + id = Column(Integer, primary_key=True) + pattern = Column(Unicode(255), nullable=False, unique=True) + date = Column(DateTime(), nullable=False, default=now) + creator_id = Column(Integer, ForeignKey("users.id"), nullable=False) + reason = Column(Unicode(255), nullable=False) + + def __repr__(self): + return "" % self.address + + def to_dict(self): + return { + "id": self.id, + "pattern": self.pattern, + "date": time.mktime(self.date.timetuple()), + "creator": self.creator.to_dict(), + "reason": self.reason, + } + + class AdminTier(Base): __tablename__ = "admin_tiers" id = Column(Integer, primary_key=True) @@ -1208,6 +1230,7 @@ def to_dict(self): AdminLogEntry.chat = relation(Chat) IPBan.creator = relation(User) +EmailBan.creator = relation(User) AdminTier.admin_tier_permissions = relation(AdminTierPermission, backref="admin_tier") AdminTier.permissions = association_proxy( diff --git a/newparp/static/css/newparp.css b/newparp/static/css/newparp.css index 539efc0e..8a22396b 100644 --- a/newparp/static/css/newparp.css +++ b/newparp/static/css/newparp.css @@ -146,7 +146,7 @@ h1 img { display: inline-block; max-width: 100%; max-width:60vw; max-height:10v #global_navigation li a:before, #global_navigation button:before { content: "> "; visibility: hidden; } #global_navigation li a:focus, #global_navigation button:focus {outline:none;} #global_navigation li a:focus:before, #global_navigation button:focus:before, #global_navigation li a:hover:before, #global_navigation button:hover:before { visibility: visible; } -#global_navigation button { width: 100%; margin: 0; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } +#global_navigation button { width: 100%; margin: 0; padding-right: 20px; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } #global_navigation .user {z-index:1;font-variant:normal;font-family: Ricty, Inconsolata, "Helvetica Neue", Helvetica, Arial, sans-serif;font-size:14px;line-height:18px;} #global_navigation .user > a:before {content: '\22EE\00a0'; visibility:visible;} #global_navigation .user ul { margin: 0 0 0 -10px; font-variant:normal;} @@ -179,7 +179,7 @@ h1 img { display: inline-block; max-width: 100%; max-width:60vw; max-height:10v #global_navigation .user li a:before{ content: "> "; } } -@media (max-width: 800px) { +@media (max-width: 1000px) { /* Swap in short titles */ #global_navigation li a .long {display:none} #global_navigation li a .short {display:inline} @@ -2050,14 +2050,12 @@ body.chat { margin: 0; background-color: #535353; font-size: 12px; word-wrap: br text-decoration: underline; } -.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, #connection_method, .mobile_nav_wrap, .unum #action_list {font-family: Ricty, Inconsolata, "Helvetica Neue", Helvetica, Arial, sans-serif;} +.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, .mobile_nav_wrap, .unum #action_list {font-family: Ricty, Inconsolata, "Helvetica Neue", Helvetica, Arial, sans-serif;} .sidebar, #info_panel, #edit_info_panel {font-weight:normal;color:#555;} #chat_wrapper {background-color:#ccc} -#connection_method {position:absolute;top:0px;right:5px;color: #888} - #global_navigation.in_chat_nav .mobile_nav {display:none} .chat h2, .chat h3, .chat p { margin: 5px; } @@ -2578,7 +2576,6 @@ body:not(.no_forms) #settings .input input[type="radio"] + label {margin-left:-3 #conversation {top:37px} #topbar ~ #conversation, #topbar ~ .sidebar, body .sidebar, #info_panel, #edit_info_panel { top: 62px; } body:not(.disable_left_bar) #chat_logo {top:5px} - #connection_method {top:-3px;right:0px; font-size:80%} #sidebar_tabs, #topbar {top:37px} .log_top_nav {top:37px} #archive_conversation {top:64px} } @@ -2622,7 +2619,6 @@ body.disable_left_bar .sidebar, body.disable_left_bar #topbar ~ .sidebar {top:92 /* fix wider right bar */ @media (min-width: 1650px) { body.disable_left_bar.chatting > #chat_wrapper #chat_content #topbar, body.disable_left_bar.chatting > #chat_wrapper #chat_content #info_panel, body.disable_left_bar.chatting > #chat_wrapper #chat_content #edit_info_panel, body.disable_left_bar.chatting > #chat_wrapper #chat_content #conversation, body.disable_left_bar.chatting > #chat_wrapper #chat_content #send_form, body.disable_left_bar.chatting > #chat_wrapper #global_navigation.in_chat_nav { right: 260px;} - body.disable_left_bar #connection_method {top:-3px;right:0px; font-size:80%} body.disable_left_bar #chat_wrapper #sidebar_left_tabs {width:246px} .userlist_name {max-width:170px} } @@ -2683,7 +2679,7 @@ body.disable_left_bar .sidebar, body.disable_left_bar #topbar ~ .sidebar {top:92 #topbar .topic {display:inline;top:0px} body.disable_left_bar.chatting > #chat_wrapper #chat_content #topbar, .chatting > #chat_wrapper #chat_content #topbar { left:34px; right: 320px;} #topbar ~ #conversation, #conversation, #topbar ~ .sidebar, body .sidebar, #info_panel, #edit_info_panel, body.disable_left_bar #topbar ~ .sidebar, body.disable_left_bar .sidebar { top: 36px; } - #connection_method, #chat_logo, body.disable_left_bar #chat_logo {display:none} + #chat_logo, body.disable_left_bar #chat_logo {display:none} #sidebar_tabs, body.disable_left_bar #sidebar_tabs {top:11px} .log_top_nav {top:36px} #archive_conversation {top:63px} } @@ -2693,7 +2689,6 @@ body.disable_left_bar .sidebar, body.disable_left_bar #topbar ~ .sidebar {top:92 body #chat_logo, body.disable_left_bar #chat_logo {display:block;left:auto;right:-5px} body .sidebar, body #topbar ~ .sidebar {top:92px} body #sidebar_tabs, body.disable_left_bar #sidebar_tabs {top:67px} - #connection_method {top:-3px;right:0px; font-size:80%} } @media (min-width: 1270px) and (max-height:500px) { .chatting > #chat_wrapper #chat_content #topbar { left:274px; right: 320px;} body.disable_left_bar.chatting > #chat_wrapper #chat_content #topbar { left:34px; right: 320px;}} @@ -2794,7 +2789,7 @@ body.no_moving *, body.no_moving *:before, body.no_moving *:after {animation:non @media amzn-kf8, amzn-mobi { html {-webkit-text-size-adjust: none} #text_preview_container, body {font-family: "Courier", "Courier New", monospace;} - #topbar h1, #global_navigation.in_chat_nav li, #global_navigation.in_chat_nav label, #global_navigation.in_chat_nav label:before, .sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, #connection_method, .mobile_nav_wrap, .unum #action_list, .sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear, #global_navigation li, #global_navigation label, #global_navigation label:before, #global_navigation .user, #users_online p, main { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;} + #topbar h1, #global_navigation.in_chat_nav li, #global_navigation.in_chat_nav label, #global_navigation.in_chat_nav label:before, .sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, .mobile_nav_wrap, .unum #action_list, .sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear, #global_navigation li, #global_navigation label, #global_navigation label:before, #global_navigation .user, #users_online p, main { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;} #prompt-cursor{display:none} #chat_line_input input {font-size:16px;line-height:19px} /*try and prevent zoom for these devices since they disregard anything else */ } diff --git a/newparp/static/css/themes/darkskin.css b/newparp/static/css/themes/darkskin.css index fe9c05b1..1c08ee9d 100644 --- a/newparp/static/css/themes/darkskin.css +++ b/newparp/static/css/themes/darkskin.css @@ -630,8 +630,6 @@ body.chat { background-color: #000;} #chat_wrapper { background: #1d1f2c;} -#connection_method {color: #3a3d59} - body:not(.no_forms) .chat input, body:not(.no_forms) .chat textarea, body:not(.no_forms) .chat button, body:not(.no_forms) .chat select { background-color: #1a8005; } #chat_logo {background-image: url('/static/img/logo-dark.png');} @@ -805,7 +803,6 @@ body:not(.no_forms) .no_preview #chat_line_input {top:0px} #topbar {background:transparent;} #topbar h1 {background:transparent} #topbar, #topbar .topic {color: #939eb7} - #connection_method, #chat_logo {display:none} #global_navigation.in_chat_nav label:before { background-image: url('/static/img/menu-dark.png'); background-image: url('/static/img/menu-dark.svg'), none; @@ -856,4 +853,4 @@ body:not(.no_forms) .no_preview #chat_line_input {top:0px} .col_circle[style*="background-color: #000000"], .col_circle_inner[style*="background-color: #000000"] {background-color:#60626e !important} .col_circle[style*="background-color: #2B0057"], .col_circle_inner[style*="background-color: #2B0057"], .col_circle[style*="background-color: #2b0057"], .col_circle_inner[style*="background-color: #2b0057"] {background-color:#6d03de !important} .col_circle[style*="background-color: #000056"], .col_circle_inner[style*="background-color: #000056"] {background-color:#2323ed !important} -.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} \ No newline at end of file +.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} diff --git a/newparp/static/css/themes/darkskin_monochrome.css b/newparp/static/css/themes/darkskin_monochrome.css index 7e3c6ed6..c2cc4010 100644 --- a/newparp/static/css/themes/darkskin_monochrome.css +++ b/newparp/static/css/themes/darkskin_monochrome.css @@ -622,8 +622,6 @@ body.chat { background-color: #000;} #chat_wrapper { background: #1d1f2c;} -#connection_method {color: #3a3d59} - body:not(.no_forms) .chat input, body:not(.no_forms) .chat textarea, body:not(.no_forms) .chat button, body:not(.no_forms) .chat select { background-color: #1a8005; } #chat_logo {background-image: url('/static/img/logo-dark.png');} @@ -797,7 +795,6 @@ body:not(.no_forms) .no_preview #chat_line_input {top:0px} #topbar {background:transparent;} #topbar h1 {background:transparent} #topbar, #topbar .topic {color: #939eb7} - #connection_method, #chat_logo {display:none} #global_navigation.in_chat_nav label:before { background-image: url('/static/img/menu-dark.png'); background-image: url('/static/img/menu-dark.svg'), none; @@ -875,4 +872,4 @@ body:not(.no_forms) .no_preview #chat_line_input {top:0px} .col_circle[style*="background-color: #000000"], .col_circle_inner[style*="background-color: #000000"] {background-color:#666 !important} .col_circle[style*="background-color: #2B0057"], .col_circle_inner[style*="background-color: #2B0057"], .col_circle[style*="background-color: #2b0057"], .col_circle_inner[style*="background-color: #2b0057"] {background-color:#6d03de !important} .col_circle[style*="background-color: #000056"], .col_circle_inner[style*="background-color: #000056"] {background-color:#2323ed !important} -.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} \ No newline at end of file +.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} diff --git a/newparp/static/css/themes/felt.css b/newparp/static/css/themes/felt.css index 6c9b5bb1..ec7b3678 100644 --- a/newparp/static/css/themes/felt.css +++ b/newparp/static/css/themes/felt.css @@ -636,8 +636,6 @@ body.chat { background-color: #000;} #chat_wrapper { background: #082900 url('/static/img/felt_manor_background.png') no-repeat bottom right; background-attachment: fixed;} -#connection_method {color: #156704} - body:not(.no_forms) .chat input, body:not(.no_forms) .chat textarea, body:not(.no_forms) .chat button, body:not(.no_forms) .chat select { background-color: #1a8005; } body:not(.disable_left_bar) #chat_logo {top:13px;left:20px; background: transparent url('/static/img/logo-felt.png') no-repeat scroll 0px bottom; background-size:cover; width:150px;height:54px; background-size:150px 54px;image-rendering: auto; -ms-interpolation-mode: auto;} @@ -874,4 +872,4 @@ body.disable_left_bar #chat_logo {display:block;left:auto;right:90px; top:10px; .col_circle[style*="background-color: #000000"], .col_circle_inner[style*="background-color: #000000"] {background-color:#fff !important} .col_circle[style*="background-color: #2B0057"], .col_circle_inner[style*="background-color: #2B0057"], .col_circle[style*="background-color: #2b0057"], .col_circle_inner[style*="background-color: #2b0057"] {background-color:#6d03de !important} .col_circle[style*="background-color: #000056"], .col_circle_inner[style*="background-color: #000056"] {background-color:#2323ed !important} -.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} \ No newline at end of file +.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} diff --git a/newparp/static/css/themes/msparp_basic.css b/newparp/static/css/themes/msparp_basic.css index 829b8a0c..2ec5c2c1 100644 --- a/newparp/static/css/themes/msparp_basic.css +++ b/newparp/static/css/themes/msparp_basic.css @@ -83,7 +83,7 @@ h1 img { display: block; max-width: 100%; } #global_navigation a:hover, #global_navigation button:hover { color: #fff; } #global_navigation li a:before, #global_navigation button:before { content: "> "; visibility: hidden; } #global_navigation li a:hover:before, #global_navigation button:hover:before { visibility: visible; } -#global_navigation button { width: 100%; margin: 0; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } +#global_navigation button { width: 100%; margin: 0; padding-right: 20px; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } #global_navigation .user ul { margin: 0 0 0 -10px; } #global_navigation #unread_counter { float: right; padding: 7px 14px; background-color: #a10000; } @@ -282,7 +282,7 @@ body.chat #global_navigation {display:none} #chat_wrapper {left:0px !important; right:0px !important;top:0px !important;bottom:0px !important} -.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, #connection_method, .mobile_nav_wrap, .unum #action_list, #topbar, #topbar h1, #topbar .topic {font-family: "Courier", "Courier New", monospace; font-weight: bold;} +.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, .mobile_nav_wrap, .unum #action_list, #topbar, #topbar h1, #topbar .topic {font-family: "Courier", "Courier New", monospace; font-weight: bold;} .sidebar * {font-weight:bold !important} @@ -294,12 +294,10 @@ body:not(.no_forms) .sidebar .fromto input {width:50px;} /* revert sidebar wrap positioning with forms fallback to cover broken scrolling on ICS android vanilla browser */ body.no_forms .sidebar_wrap {position:static;padding-bottom:23px} -body.no_forms #connection_method {background:#eee;width:230px;} @media (max-height: 600px) { .sidebar_wrap {bottom:0px !important} body.no_forms .sidebar_wrap {padding-bottom:0px} - #connection_method {display:none} } @media (max-width: 780px), (max-height: 500px) { @@ -307,8 +305,6 @@ body.no_forms #connection_method {background:#eee;width:230px;} #topbar h1 {margin: -1px 2px 0 0; padding: 0 5px 1px 5px;} } - #connection_method, body.disable_left_bar #connection_method {bottom:5px;top:auto;z-index:5;font-size:12px !important} - #activity_spinner {display:none} #send_form_wrap {margin-left:2px} body.no_forms #send_form.no_preview input[name="text"] {margin-top:2px} diff --git a/newparp/static/css/themes/msparp_basic_dark.css b/newparp/static/css/themes/msparp_basic_dark.css index b7c48578..800100f9 100644 --- a/newparp/static/css/themes/msparp_basic_dark.css +++ b/newparp/static/css/themes/msparp_basic_dark.css @@ -83,7 +83,7 @@ h1 img { display: block; max-width: 100%; } #global_navigation a:hover, #global_navigation button:hover { color: #fff; } #global_navigation li a:before, #global_navigation button:before { content: "> "; visibility: hidden; } #global_navigation li a:hover:before, #global_navigation button:hover:before { visibility: visible; } -#global_navigation button { width: 100%; margin: 0; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } +#global_navigation button { width: 100%; margin: 0; padding-right: 20px; border: 0; background-color: transparent; text-align: left; text-transform: uppercase; } #global_navigation .user ul { margin: 0 0 0 -10px; } #global_navigation #unread_counter { float: right; padding: 7px 14px; background-color: #a10000; } @@ -282,7 +282,7 @@ body.chat #global_navigation {display:none} #chat_wrapper {left:0px !important; right:0px !important;top:0px !important;bottom:0px !important} -.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, #connection_method, .mobile_nav_wrap, .unum #action_list, #topbar, #topbar h1, #topbar .topic {font-family: "Courier", "Courier New", monospace; font-weight: bold;} +.sidebar, #topbar, #info_panel, #edit_info_panel, #sidebar_tabs, #sidebar_left_tabs, #button_wrap, .mobile_nav_wrap, .unum #action_list, #topbar, #topbar h1, #topbar .topic {font-family: "Courier", "Courier New", monospace; font-weight: bold;} .sidebar * {font-weight:bold !important} @@ -294,12 +294,10 @@ body:not(.no_forms) .sidebar .fromto input {width:50px;} /* revert sidebar wrap positioning with forms fallback to cover broken scrolling on ICS android vanilla browser */ body.no_forms .sidebar_wrap {position:static;padding-bottom:23px} -body.no_forms #connection_method {background:#000;width:230px;} @media (max-height: 600px) { .sidebar_wrap {bottom:0px !important} body.no_forms .sidebar_wrap {padding-bottom:0px} - #connection_method {display:none} } @media (max-width: 780px), (max-height: 500px) { @@ -307,8 +305,6 @@ body.no_forms #connection_method {background:#000;width:230px;} #topbar h1 {margin: -1px 2px 0 0; padding: 0 5px 1px 5px;} } - #connection_method, body.disable_left_bar #connection_method {bottom:5px;top:auto;z-index:5;font-size:12px !important} - #activity_spinner {display:none} #send_form_wrap {margin-left:2px} body.no_forms #send_form.no_preview input[name="text"] {margin-top:2px} @@ -600,4 +596,4 @@ body:not(.touch) .highlighted .unum:hover + p {background:#161823} .col_circle[style*="background-color: #000000"], .col_circle_inner[style*="background-color: #000000"] {background-color:#606262 !important} .col_circle[style*="background-color: #2B0057"], .col_circle_inner[style*="background-color: #2B0057"], .col_circle[style*="background-color: #2b0057"], .col_circle_inner[style*="background-color: #2b0057"] {background-color:#6d03de !important} .col_circle[style*="background-color: #000056"], .col_circle_inner[style*="background-color: #000056"] {background-color:#2323ed !important} -.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} \ No newline at end of file +.col_circle[style*="background-color: #0715CD"], .col_circle_inner[style*="background-color: #0715CD"], .col_circle[style*="background-color: #0715cd"], .col_circle_inner[style*="background-color: #0715cd"] {background-color:#172aff !important} diff --git a/newparp/static/js/newparp.js b/newparp/static/js/newparp.js index 1704964b..8093cc37 100644 --- a/newparp/static/js/newparp.js +++ b/newparp/static/js/newparp.js @@ -1,6 +1,7 @@ var msparp = (function() { var body = $(document.body); + var ws_protocol = (location.protocol=="https:") ? "wss://" : "ws://"; // Prevent breaking browsers and settings that don't like localStorage try { @@ -375,49 +376,6 @@ var msparp = (function() { $('#clear_regexes').click(clear_regexes_and_add); } - // Searching - var search_type = "search" - var searching = false; - var searcher_id; - - function start_search() { - if (!searching) { - searching = true; - body.addClass("searching"); - $.post("/" + search_type, {}, function(data) { - searcher_id = data.id; - continue_search(); - }).error(function() { - searching = false; - body.removeClass("searching").addClass("search_error"); - }); - } - } - function continue_search() { - if (searching) { - $.post("/" + search_type + "/continue", { "id": searcher_id }, function(data) { - if (data.status == "matched") { - searching = false; - window.location.href = "/" + data.url; - } else if (data.status == "quit") { - searching = false; - } else { - continue_search(); - } - }).error(function() { - window.setTimeout(function() { - searching = false; - start_search(); - }, 2000); - }); - } - } - function stop_search() { - searching = false; - $.ajax("/" + search_type + "/stop", { "type": "POST", data: { "id": searcher_id }, "async": false }); - body.removeClass("searching"); - } - // BBCode var tag_properties = {bgcolor: "background-color", color: "color", font: "font-family", bshadow: "box-shadow", tshadow: "text-shadow"} function bbencode(text, admin) { return raw_bbencode(Handlebars.escapeExpression(text), admin); } @@ -588,16 +546,36 @@ var msparp = (function() { }, // Character search "search": function(token) { + var ws, ws_interval; $.ajaxSetup({data: {"token": token}}); - $(window).unload(function () { if (searching) { stop_search(); }}); - start_search(); - }, - // Roulette - "roulette": function(token) { - $.ajaxSetup({data: {"token": token}}); - search_type = "roulette"; - $(window).unload(function () { if (searching) { stop_search(); }}); - start_search(); + $.post("/search", {}, function(data) { + matched = false; + body.addClass("searching"); + searcher_id = data.id; + ws = new WebSocket(ws_protocol + "live." + location.host + "/search/" + searcher_id); + ws.onopen = function() { + console.log("ready"); + ws_interval = window.setInterval(function() { console.log("ping"); ws.send("ping"); }, 10000) + } + ws.onmessage = function(e) { + var data = JSON.parse(e.data); + console.log(data); + if (data.status == "matched") { + matched = true; + window.location.href = "/" + data.url; + } else if (data.status == "quit") { + ws.close(); + } + } + ws.onclose = function() { + if (matched) { return; } + body.removeClass("searching").addClass("search_error"); + window.clearInterval(ws_interval); + } + }).error(function() { + searching = false; + body.removeClass("searching").addClass("search_error"); + }); }, // Character pages "character": function() { @@ -633,13 +611,18 @@ var msparp = (function() { var latest_date = user.meta.show_timestamps ? new Date(latest_time) : null; var new_messages = []; - // Websockets - var messages_method = typeof(WebSocket) != "undefined" ? "websocket" : "long_poll"; - var ws_protocol = (location.protocol=="https:") ? "wss://" : "ws://"; + // Connecting and disconnecting + var ws; var ws_works = false; var ws_connected_time = 0; - function launch_websocket() { + + function connect() { + if (typeof(WebSocket) == "undefined") { + status_bar.css("color", "#f00").html("Sorry, your browser doesn't appear to support websockets. Please use the latest version of Firefox or Chrome to participate in this chat."); + return; + } + // Don't create a new websocket unless the previous one is closed. // This prevents problems with eg. double clicking the join button. if (ws && ws.readyState != 3) { return; } @@ -652,56 +635,17 @@ var msparp = (function() { if (status == "connecting" || status == "chatting") { // Fall back to long polling if we've never managed to connect. if (!ws_works || (Date.now() - ws_connected_time) < 5000) { - messages_method = "long_poll"; - launch_long_poll(true); + status_bar.css("color", "#f00").html("Sorry, the connection to the server has been lost."); return; } // Otherwise try to reconnect. exit(); status_bar.css("color", "#f00").text("Sorry, the connection to the server has been lost. Attempting to reconnect..."); - window.setTimeout(launch_websocket, Math.random() * 10000); - } - } - } - - // Long polling - function launch_long_poll(joining) { - var data = { "chat_id": chat.id, "after": latest_message_id }; - if (joining) { enter(); data["joining"] = true; } - $.post("/chat_api/messages", data, receive_messages).complete(function(jqxhr, text_status) { - if (status == "chatting") { - if (jqxhr.status < 400 && text_status == "success") { - launch_long_poll(); - } else { - window.setTimeout(launch_long_poll, 2000); - // XXX display a message if it still doesn't work after several attempts. - } - } - }); - } - - // Ping loop - function ping() { - if (status == "chatting") { - if (messages_method == "websocket") { - if (ws.readyState == 1) { - ws.send("ping"); - window.setTimeout(ping, 10000); - } - } else { - $.post("/chat_api/ping", { "chat_id": chat.id }).complete(function() { window.setTimeout(ping, 10000); }); + window.setTimeout(connect, Math.random() * 10000); } } } - // Connecting and disconnecting - function connect() { - if (messages_method == "websocket") { - launch_websocket(); - } else { - launch_long_poll(true); - } - } function enter() { status = "chatting"; window.setTimeout(ping, 10000); @@ -725,14 +669,13 @@ var msparp = (function() { } status_bar.css("color", "").text(( // Show status bar if typing notifications are available and switched on. - (messages_method == "websocket" && user.meta.typing_notifications) + user.meta.typing_notifications // Also always show it in PM and roulette chats for online status. || chat.type == "pm" || chat.type == "roulette" ) ? " " : ""); text_input.keyup(); scroll_to_bottom(); abscond_button.text("Abscond"); - $("#messages_method").text(messages_method); } function exit() { status = "disconnected"; @@ -746,14 +689,18 @@ var msparp = (function() { status_bar.text(""); abscond_button.text(chat.type == "searched" || chat.type == "roulette" ? "Search again" : "Join"); if (chat.type == "searched" || chat.type == "roulette") { $("#send_form_wrap").addClass("abscond_again"); } - $("#connection_method").css("display", "none"); } function disconnect() { exit(); - if (messages_method == "websocket") { - ws.close(1000); receive_messages({}); - } else { - $.ajax("/chat_api/quit", { "type": "POST", data: { "chat_id": chat.id } }); + ws.close(1000); + receive_messages({}); + } + + // Ping loop + function ping() { + if (status == "chatting" && ws.readyState == 1) { + ws.send("ping"); + window.setTimeout(ping, 10000); } } @@ -767,9 +714,6 @@ var msparp = (function() { $(window).unload(function() { if (status == "chatting") { status = "disconnected"; - if (messages_method != "websocket") { - $.ajax("/chat_api/quit", { "type": "POST", data: { "chat_id": chat.id }, "async": false }); - } } }); @@ -1083,9 +1027,7 @@ var msparp = (function() { return (chat.type == "searched" || chat.type == "roulette") && their_number != user.meta.number; } function block(number) { - var reason = prompt("If you block this person, you will never encounter them in random chats. Optionally, you can also provide a reason below."); - if (reason == null) { return; } - $.post("/chat_api/block", { "chat_id": chat.id, "number": number, "reason": reason }); + text_input.val("/block " + number + " (reason)").focus(); } function can_set_group(new_group, current_group) { // Setting group only works in group chats. @@ -1363,10 +1305,8 @@ var msparp = (function() { edit_info_panel.show(); }); $(".set_topic_button").click(function() { - var topic = prompt("Please enter a new topic for the chat:"); - if (topic != null) { - $.post("/chat_api/set_topic", { "chat_id": chat.id, "topic": topic }); - } + info_panel.hide(); + text_input.val("/topic ").focus(); }); var edit_info_panel = $("#edit_info_panel"); $("#edit_info_form").submit(function() { @@ -1713,7 +1653,7 @@ var msparp = (function() { previous_status_message = null; } } else { - status_bar.text(messages_method == "websocket" && this.checked ? " " : ""); + status_bar.text(this.checked ? " " : ""); } } parse_variables(); @@ -1795,21 +1735,19 @@ var msparp = (function() { var text_input = $("input[name=text]").keydown(function() { changed_since_draft = true; - if (messages_method == "websocket") { - window.clearTimeout(typing_timeout); - if (!typing) { - typing = true; - ws.send("typing"); - $("#activity_spinner").addClass("active_self"); - $("#activity_spinner").attr("title", "You are typing..."); - } - typing_timeout = window.setTimeout(function() { - typing = false; - ws.send("stopped_typing"); - $("#activity_spinner").removeClass("active_self"); - $("#activity_spinner").attr("title", "No activity"); - }, 1000); - } + window.clearTimeout(typing_timeout); + if (!typing) { + typing = true; + ws.send("typing"); + $("#activity_spinner").addClass("active_self"); + $("#activity_spinner").attr("title", "You are typing..."); + } + typing_timeout = window.setTimeout(function() { + typing = false; + ws.send("stopped_typing"); + $("#activity_spinner").removeClass("active_self"); + $("#activity_spinner").attr("title", "No activity"); + }, 1000); }).keyup(function() { if (user.meta.show_preview) { text = this.value.trim() @@ -1856,7 +1794,8 @@ var msparp = (function() { var executed = execute_command(data.text); if (executed) { text_input.val(""); - if (messages_method == "websocket") { typing = false; ws.send("stopped_typing"); } + typing = false; + ws.send("stopped_typing"); return false; } // If the current temporary character matches, apply their quirks. @@ -2222,8 +2161,6 @@ var msparp = (function() { if (confirm("Are you sure you want to abscond?")) { disconnect(); } } else if (chat.type == "searched") { location.href = "/search"; - } else if (chat.type == "roulette") { - location.href = "/roulette"; } else { connect(); } diff --git a/newparp/tasks/__init__.py b/newparp/tasks/__init__.py index 15a26bb0..72a490b6 100644 --- a/newparp/tasks/__init__.py +++ b/newparp/tasks/__init__.py @@ -13,8 +13,8 @@ "newparp.tasks.background", "newparp.tasks.matchmaker", "newparp.tasks.reaper", - "newparp.tasks.roulette_matchmaker", "newparp.tasks.chat", + "newparp.tasks.test", ]) # Sentry exception logging if there is a sentry object. diff --git a/newparp/tasks/config.py b/newparp/tasks/config.py index cecc7a59..59650b84 100644 --- a/newparp/tasks/config.py +++ b/newparp/tasks/config.py @@ -4,7 +4,7 @@ from kombu import Exchange, Queue # Debug -if 'DEBUG' in os.environ: +if "DEBUG" in os.environ: CELERY_REDIRECT_STDOUTS_LEVEL = "DEBUG" # Broker and Result backends @@ -28,13 +28,16 @@ CELERY_DISABLE_RATE_LIMITS = True # Queue config -CELERY_DEFAULT_QUEUE = 'default' +CELERY_DEFAULT_QUEUE = "default" CELERY_QUEUES = ( # Default queue - Queue('default', Exchange('default'), routing_key='default'), + Queue("default", Exchange("default"), routing_key="default"), # Worker queue - Queue('worker', Exchange('worker'), routing_key='worker', delivery_mode=1), + Queue("worker", Exchange("worker"), routing_key="worker", delivery_mode=1), + + # Matchmaker queue + Queue("matchmaker", Exchange("matchmaker"), routing_key="matchmaker", delivery_mode=1), ) # Beats config @@ -55,12 +58,8 @@ "task": "newparp.tasks.background.update_user_meta", "schedule": timedelta(seconds=5), }, - "matchmaker": { - "task": "newparp.tasks.matchmaker.run", - "schedule": timedelta(seconds=10), - }, - "roulette_matchmaker": { - "task": "newparp.tasks.roulette_matchmaker.run", + "generate_searching_counter": { + "task": "newparp.tasks.matchmaker.generate_searching_counter", "schedule": timedelta(seconds=10), }, "ping_longpolls": { diff --git a/newparp/tasks/matchmaker.py b/newparp/tasks/matchmaker.py index 7f49b628..a348bfca 100644 --- a/newparp/tasks/matchmaker.py +++ b/newparp/tasks/matchmaker.py @@ -1,61 +1,96 @@ +from celery import chord from celery.utils.log import get_task_logger +from random import shuffle +from sqlalchemy import and_, func, or_ +from uuid import uuid4 -from newparp.helpers.matchmaker import run_matchmaker -from newparp.model import SearchedChat +from newparp.helpers.matchmaker import fetch_searcher, option_messages +from newparp.model import Block, ChatUser, Message, SearchedChat, User from newparp.tasks import celery, WorkerTask logger = get_task_logger(__name__) -def get_searcher_info(redis, searcher_ids): - searchers = [] - for searcher_id in searcher_ids: - session_id = redis.get("searcher:%s:session_id" % searcher_id) - # This will fail if they've logged out since sending the request. - try: - user_id = int(redis.get("session:%s" % session_id)) - search_character_id = int(redis.get("searcher:%s:search_character_id" % searcher_id)) - except (TypeError, ValueError): - continue - searchers.append({ - "id": searcher_id, - "user_id": user_id, - "search_character_id": search_character_id, - "character": redis.hgetall("searcher:%s:character" % searcher_id), - "style": redis.get("searcher:%s:style" % searcher_id), - "levels": redis.smembers("searcher:%s:levels" % searcher_id), - "filters": redis.lrange("searcher:%s:filters" % searcher_id, 0, -1), - "choices": {int(_) for _ in redis.smembers("searcher:%s:choices" % searcher_id)}, - }) - return searchers - - -def check_compatibility(redis, s1, s2): + +@celery.task(base=WorkerTask, queue="worker") +def generate_searching_counter(): + redis = generate_searching_counter.redis + + pipe = redis.pipeline() + for searcher_id in redis.smembers("searchers"): + pipe.get("searcher:%s:session_id" % searcher_id) + + for session_id in set(pipe.execute()): + if session_id: + pipe.get("session:%s" % session_id) + + redis.set("searching_users", len(set(pipe.execute()))) + + +@celery.task(base=WorkerTask, queue="matchmaker") +def new_searcher(searcher_id): + redis = new_searcher.redis + if redis.exists("lock:matchmaker"): + new_searcher.apply_async((searcher_id,), countdown=2) + return + + logger.debug("new searcher: %s") + searchers = redis.smembers("searchers") + + try: + searchers.remove(searcher_id) + except KeyError: + logger.debug("no longer searching") + return + if not searchers: + logger.debug("not enough searchers, skipping") + return + + chord( + (compare.s(searcher_id, _) for _ in searchers if _ != searcher_id), + comparison_callback.s(searcher_id), + ).delay() + + redis.setex("lock:matchmaker", 60, 1) + + +@celery.task(base=WorkerTask, queue="matchmaker") +def compare(searcher_id_1, searcher_id_2): + redis = compare.redis + logger.debug("comparing %s and %s" % (searcher_id_1, searcher_id_2)) + + s1 = fetch_searcher(redis, searcher_id_1) + s2 = fetch_searcher(redis, searcher_id_2) + + alive = True + for searcher in (s1, s2): + if not all(searcher[:-2]): + logger.debug("%s not alive" % searcher.id) + redis.srem("searchers", searcher.id) + alive = False + if not alive: + return None, None # Don't pair people with themselves. - if s1["user_id"] == s2["user_id"]: - return False, None + if s1.user_id == s2.user_id: + return None, None # Don't match if they've already been paired up recently. - match_key = "matched:%s:%s" % tuple(sorted([s1["user_id"], s2["user_id"]])) + match_key = "matched:%s:%s" % tuple(sorted([s1.user_id, s2.user_id])) if redis.exists(match_key): - return False, None + return None, None options = [] # Style options should be matched with themselves or "either". - if ( - s1["style"] != "either" - and s2["style"] != "either" - and s1["style"] != s2["style"] - ): - return False, None - if s1["style"] != "either": - options.append(s1["style"]) - elif s2["style"] != "either": - options.append(s2["style"]) + if s1.style != "either" and s2.style != "either" and s1.style != s2.style: + return None, None + if s1.style != "either": + options.append(s1.style) + elif s2.style != "either": + options.append(s2.style) # Levels have to overlap. - levels_in_common = s1["levels"] & s2["levels"] + levels_in_common = s1.levels & s2.levels logger.debug("Levels in common: %s" % levels_in_common) if levels_in_common: options.append( @@ -64,46 +99,99 @@ def check_compatibility(redis, s1, s2): else "sfw" ) else: - return False, None + return None, None # Check filters. - s1_name = s1["character"]["name"].lower().encode("utf8") - for search_filter in s2["filters"]: + s1_name = s1.character["name"].lower().encode("utf8") + for search_filter in s2.filters: search_filter = search_filter.encode("utf8") logger.debug("comparing %s and %s" % (s1_name, search_filter)) if search_filter in s1_name: logger.debug("FILTER %s MATCHED" % search_filter) - return False, None - s2_name = s2["character"]["name"].lower().encode("utf8") - for search_filter in s1["filters"]: + return None, None + s2_name = s2.character["name"].lower().encode("utf8") + for search_filter in s1.filters: search_filter = search_filter.encode("utf8") logger.debug("comparing %s and %s" % (s2_name, search_filter)) if search_filter in s2_name: logger.debug("FILTER %s MATCHED" % search_filter) - return False, None + return None, None if ( # Match if either person has wildcard, or if they're otherwise compatible. - (len(s2["choices"]) == 0 or s1["search_character_id"] in s2["choices"]) - and (len(s1["choices"]) == 0 or s2["search_character_id"] in s1["choices"]) + (len(s2.choices) == 0 or s1.search_character_id in s2.choices) + and (len(s1.choices) == 0 or s2.search_character_id in s1.choices) ): - redis.set(match_key, 1) - redis.expire(match_key, 1800) - return True, options - - return False, None - - -def get_character_info(db, searcher): - return searcher["character"] - -@celery.task(base=WorkerTask, queue="worker") -def run(): - db = run.db - redis = run.redis - - run_matchmaker( - db, redis, 2, "searchers", "searcher", get_searcher_info, - check_compatibility, SearchedChat, get_character_info, - ) + # don't do this until comparison_callback + return s2.id, options + + return None, None + + +@celery.task(base=WorkerTask, queue="matchmaker") +def comparison_callback(results, searcher_id_1): + redis = comparison_callback.redis + db = comparison_callback.db + + # Check if there's a match. + matched_searchers = [_ for _ in results if _[0] is not None] + if not matched_searchers: + logger.debug("no results") + redis.delete("lock:matchmaker") + return + logger.debug("results: %s" % matched_searchers) + shuffle(matched_searchers) + + # Fetch searcher 1. + s1 = fetch_searcher(redis, searcher_id_1) + if not all(s1[:-2]): + logger.debug("%s has expired" % searcher_id_1) + redis.delete("lock:matchmaker") + return + + # Pick a second searcher from the matches. + for searcher_id_2, options in matched_searchers: + s2 = fetch_searcher(redis, searcher_id_2) + if all(s2[:-2]) and db.query(func.count("*")).select_from(Block).filter(or_( + and_(Block.blocking_user_id == s1.user_id, Block.blocked_user_id == s2.user_id), + and_(Block.blocking_user_id == s2.user_id, Block.blocked_user_id == s1.user_id), + )).scalar() == 0: + logger.debug("matched %s" % searcher_id_2) + break + else: + logger.debug("all matches have expired") + redis.delete("lock:matchmaker") + return + + new_url = str(uuid4()).replace("-", "") + logger.info("matched %s and %s, sending to %s." % (s1.id, s2.id, new_url)) + new_chat = SearchedChat(url=new_url) + db.add(new_chat) + db.flush() + + s1_user = db.query(User).filter(User.id == s1.user_id).one() + s2_user = db.query(User).filter(User.id == s2.user_id).one() + db.add(ChatUser.from_user(s1_user, chat_id=new_chat.id, number=1, search_character_id=s1.search_character_id, **s1.character)) + if s1_user != s2_user: + db.add(ChatUser.from_user(s2_user, chat_id=new_chat.id, number=2, search_character_id=s2.search_character_id, **s2.character)) + + if options: + db.add(Message( + chat_id=new_chat.id, + type="search_info", + text=" ".join(option_messages[_] for _ in options), + )) + + db.commit() + + pipe = redis.pipeline() + match_key = "matched:%s:%s" % tuple(sorted([s1.user_id, s2.user_id])) + pipe.set(match_key, 1) + pipe.expire(match_key, 1800) + pipe.srem("searchers", s1.id, s2.id) + match_message = """{"status":"matched","url":"%s"}""" % new_url + pipe.publish("searcher:%s" % s1.id, match_message) + pipe.publish("searcher:%s" % s2.id, match_message) + pipe.delete("lock:matchmaker") + pipe.execute() diff --git a/newparp/tasks/roulette_matchmaker.py b/newparp/tasks/roulette_matchmaker.py deleted file mode 100644 index 62a56cd6..00000000 --- a/newparp/tasks/roulette_matchmaker.py +++ /dev/null @@ -1,87 +0,0 @@ -from sqlalchemy.orm.exc import NoResultFound - -from newparp.helpers.matchmaker import run_matchmaker -from newparp.model import Character, RouletteChat, SearchCharacter -from newparp.tasks import celery, WorkerTask - -def get_searcher_info(redis, searcher_ids): - searchers = [] - for searcher_id in searcher_ids: - session_id = redis.get("roulette:%s:session_id" % searcher_id) - # This will fail if they've logged out since sending the request. - try: - searcher = { - "id": searcher_id, - "user_id": int(redis.get("session:%s" % session_id)), - "search_character_id": int(redis.get("roulette:%s:search_character_id" % searcher_id)) - } - character_id = redis.get("roulette:%s:character_id" % searcher_id) - if character_id is not None: - searcher["character_id"] = int(character_id) - except (TypeError, ValueError): - continue - searchers.append(searcher) - return searchers - - -def check_compatibility(redis, s1, s2): - # Don't pair people with themselves. - if s1["user_id"] == s2["user_id"]: - return False, ("roulette",) - # Don't match if they've already been paired up recently. - match_key = "matched:%s:%s" % tuple(sorted([s1["user_id"], s2["user_id"]])) - if redis.exists(match_key): - return False, ("roulette",) - redis.set(match_key, 1) - redis.expire(match_key, 1800) - return True, ("roulette",) - - -def get_character_info(db, searcher): - # Use character if it exists. - if "character_id" in searcher: - try: - character = db.query(Character).filter( - Character.id == searcher["character_id"], - Character.user_id == searcher["user_id"], - ).one() - except NoResultFound: - return {} - return { - "name": character.name, - "acronym": character.acronym, - "color": character.color, - "quirk_prefix": character.quirk_prefix, - "quirk_suffix": character.quirk_suffix, - "case": character.case, - "replacements": character.replacements, - "regexes": character.regexes, - } - # Otherwise use search character. - try: - search_character = db.query(SearchCharacter).filter( - SearchCharacter.id == searcher["search_character_id"], - ).one() - except NoResultFound: - return {} - return { - "name": search_character.name, - "acronym": search_character.acronym, - "color": search_character.color, - "quirk_prefix": search_character.quirk_prefix, - "quirk_suffix": search_character.quirk_suffix, - "case": search_character.case, - "replacements": search_character.replacements, - "regexes": search_character.regexes, - } - -@celery.task(base=WorkerTask, queue="worker") -def run(): - db = run.db - redis = run.redis - - run_matchmaker( - db, redis, 3, "roulette_searchers", "roulette", get_searcher_info, - check_compatibility, RouletteChat, get_character_info, - ) - diff --git a/newparp/templates/account/activation_required.html b/newparp/templates/account/activation_required.html deleted file mode 100644 index b7dd4339..00000000 --- a/newparp/templates/account/activation_required.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} -{% import "account/forms.html" as forms %} -{% block content: %} -

Account not activated.

-

Thanks for registering for the MSPARP beta. Account activation seems to be unavailable at the moment. Please keep an eye on our blog to know when activation is back up.



If your account has been activated before, but is unavailable now, that means it has been locked due to suspicious activity. Please send in a ticket here to find out why your account has been locked.

-{% endblock %} diff --git a/newparp/templates/account/banned_email.html b/newparp/templates/account/banned_email.html new file mode 100644 index 00000000..398835ee --- /dev/null +++ b/newparp/templates/account/banned_email.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content: %} +

Something's Not Right

+

Thank you for registering for MSPARP. The information you have used to sign up with has flagged your account as a potential spam or throwaway account. To complete registration for this account, please contact an administrator through our support ticket system. Your account will be activated manually after you verify that you are not a spambot.

+{% endblock %} diff --git a/newparp/templates/account/user_deactivated.html b/newparp/templates/account/user_deactivated.html new file mode 100644 index 00000000..f2c8af36 --- /dev/null +++ b/newparp/templates/account/user_deactivated.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block content: %} +

Account deactivated.

+

Your account has been locked due to suspicious activity. Please send in a ticket here to find out why your account has been locked and how long the suspension will last. Please know that evasions of temporary bans may result in the permanent suspension of your account.

+{% endblock %} diff --git a/newparp/templates/account/user_new.html b/newparp/templates/account/user_new.html new file mode 100644 index 00000000..ef56b410 --- /dev/null +++ b/newparp/templates/account/user_new.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block content: %} +

Awaiting activation.

+

Thank you for signing up! Before you start using MSPARP, please complete this final step to complete your registration. We've sent you an e-mail to confirm your registration. Please click the link provided in the e-mail to activate your account. The e-mail may be in your spam folder so make sure to check there too.

+

If you have not received an e-mail within twenty four hours, contact us through our support ticket system.

+{% endblock %} diff --git a/newparp/templates/admin/email_bans.html b/newparp/templates/admin/email_bans.html new file mode 100644 index 00000000..aa149c5d --- /dev/null +++ b/newparp/templates/admin/email_bans.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% block title: %}Email bans - {% endblock %} +{% block content: %} +
+

+
+ +
+ +

+
+ + {% if request.args.email_ban_error == "already_banned" %} +

That pattern is already banned.

+ {% endif %} +

+

+

+
+
+ + {% if email_bans: %} + + + + + + + + + + + + {% for email_ban in email_bans: %} + + + + + + + + {% endfor %} + +
PatternBanned byDateReasonDelete
{{email_ban.pattern}}{{email_ban.creator.username}}{{g.user.localize_time(email_ban.date).strftime("%Y-%m-%d %H:%M:%S")}}{{email_ban.reason}}
+ + + +
+ {% else: %} +

No email bans. Thank god.

+ {% endif %} +
+
+ +{% endblock %} + diff --git a/newparp/templates/admin/home.html b/newparp/templates/admin/home.html index b2df8b1d..c5febd1d 100644 --- a/newparp/templates/admin/home.html +++ b/newparp/templates/admin/home.html @@ -32,6 +32,7 @@

{% endif %} {% if g.user.has_permission("ip_bans"): %}
  • IP bans
  • +
  • Email bans
  • {% endif %}
  • Worker status
  • diff --git a/newparp/templates/admin/log.html b/newparp/templates/admin/log.html index c8dbdf09..5f469d0e 100644 --- a/newparp/templates/admin/log.html +++ b/newparp/templates/admin/log.html @@ -7,7 +7,7 @@

    {{ pager() }} {% if entries: %} @@ -109,6 +114,9 @@

    {% elif entry.type == "ip_ban": %} IP ban {{entry.description}} + {% elif entry.type == "email_ban": %} + Email ban + {{entry.description}} {% endif %} {% endfor %} diff --git a/newparp/templates/admin/user.html b/newparp/templates/admin/user.html index 7eacd929..e6615f92 100644 --- a/newparp/templates/admin/user.html +++ b/newparp/templates/admin/user.html @@ -48,8 +48,13 @@