344 lines
14 KiB
Python
344 lines
14 KiB
Python
from flask import Flask
|
|
from flask import make_response
|
|
from flask import jsonify
|
|
from flask import request
|
|
from flask import g
|
|
from flask import redirect
|
|
|
|
from markupsafe import escape
|
|
|
|
import json
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
import random
|
|
|
|
MEETUP_INVITE_CLIENT_VERSION='v3.0.0'
|
|
DATABASE = '/.hmc-meetups/hmc-meetups.db'
|
|
AGREEMENT_SUCCESS_URL = 'https://handmadecities.com/meetups-success'
|
|
|
|
# NOTE(jmug): This was a large list of invite codes, request the "invite-codes"
|
|
# string array secret from secrets server, client at:
|
|
# https://git.handmadecities.com/HMC/shhh/src/commit/36af43779460dcfd818d1481abeded14fa45e1a4/pkg/secretsclient/cli.go#L12-L34
|
|
INVITE_CODES = ["<redacted>"]
|
|
|
|
# NOTE(jmug): This was a map of "cities" to a shared secret corresponding to each city, request the "city-secrets"
|
|
# string map secret from secrets server, client at:
|
|
# https://git.handmadecities.com/HMC/shhh/src/commit/36af43779460dcfd818d1481abeded14fa45e1a4/pkg/secretsclient/cli.go#L12-L34
|
|
shared_secrets = {
|
|
'<redacted>': '<redacted>',
|
|
}
|
|
|
|
# Flask config
|
|
|
|
def home():
|
|
if os.name == 'nt':
|
|
return os.getenv('HOMEPATH')
|
|
else:
|
|
return os.getenv('HOME')
|
|
|
|
app = Flask(__name__)
|
|
# NOTE(jmug): Flask uses this secret_key to validate cookies, among other things,
|
|
# we hsould not have a need for this in a Go port.
|
|
app.secret_key = '<redacted>'
|
|
|
|
start_time = datetime.datetime.now()
|
|
logname = start_time.strftime('flask-%Y-%m-%d-%Hh-%Mm-%Ss.log')
|
|
logging.basicConfig(filename=home() + '/.hmc-meetups/logs/' + logname, level=logging.INFO, format=f'%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s')
|
|
|
|
@app.before_request
|
|
def before_request():
|
|
g.db = sqlite3.connect(home() + DATABASE)
|
|
g.db.row_factory = sqlite3.Row
|
|
|
|
@app.teardown_request
|
|
def teardown_request(exception):
|
|
if hasattr(g, 'db'):
|
|
g.db.close()
|
|
|
|
def insert_subscriber(email, agent, city):
|
|
cur = g.db.cursor()
|
|
creation_date = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
query = '''
|
|
INSERT INTO subscribers (
|
|
email, creation_date, suppressed, suppressed_date, user_agent, city
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
'''
|
|
cur.execute(query, (email, creation_date, 0, None, agent, city))
|
|
g.db.commit() # @robustness @abner catch exceptions
|
|
|
|
@app.route('/v1/meetups/wishlist', methods=["POST"])
|
|
def meetups_wishlist():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
city = request.form['city'] # @robustness @abner
|
|
insert_subscriber(address, request.headers.get('User-Agent'), city);
|
|
app.logger.info("'" + address + "' has wishlisted '" + city + "'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/vienna', methods=["POST"])
|
|
def meetups_vienna():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Vienna');
|
|
app.logger.info("'" + address + "' has subscribed to 'Vienna'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/denmark', methods=["POST"])
|
|
def meetups_denmark():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Denmark');
|
|
app.logger.info("'" + address + "' has subscribed to 'Denmark'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/montreal', methods=["POST"])
|
|
def meetups_montreal():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Montreal');
|
|
app.logger.info("'" + address + "' has subscribed to 'Montreal'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/revolt', methods=["POST"])
|
|
def meetups_revolt():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Revolt');
|
|
app.logger.info("'" + address + "' has subscribed to 'Revolt'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/dc', methods=["POST"])
|
|
def meetups_dc():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'D.C.');
|
|
app.logger.info("'" + address + "' has subscribed to 'D.C.'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/vancouver', methods=["POST"])
|
|
def meetups_vancouver():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Vancouver');
|
|
app.logger.info("'" + address + "' has subscribed to 'Vancouver'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/cambridge', methods=["POST"])
|
|
def meetups_cambridge():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Cambridge');
|
|
app.logger.info("'" + address + "' has subscribed to 'Cambridge'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/atlanta', methods=["POST"])
|
|
def meetups_atlanta():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Atlanta');
|
|
app.logger.info("'" + address + "' has subscribed to 'Atlanta'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/australia', methods=["POST"])
|
|
def meetups_australia():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Australia');
|
|
app.logger.info("'" + address + "' has subscribed to 'Australia'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/tokyo', methods=["POST"])
|
|
def meetups_tokyo():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Tokyo');
|
|
app.logger.info("'" + address + "' has subscribed to 'Tokyo'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/berlin', methods=["POST"])
|
|
def meetups_berlin():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Berlin');
|
|
app.logger.info("'" + address + "' has subscribed to 'Berlin'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/termina', methods=["POST"])
|
|
def meetups_termina():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Termina');
|
|
app.logger.info("'" + address + "' has subscribed to 'Termina'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/london', methods=["POST"])
|
|
def meetups_london():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'London');
|
|
app.logger.info("'" + address + "' has subscribed to 'London'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/seattle', methods=["POST"])
|
|
def meetups_seattle():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Seattle');
|
|
app.logger.info("'" + address + "' has subscribed to 'Seattle'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/portland', methods=["POST"])
|
|
def meetups_portland():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Portland');
|
|
app.logger.info("'" + address + "' has subscribed to 'Portland'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/boston', methods=["POST"])
|
|
def meetups_boston():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Boston');
|
|
app.logger.info("'" + address + "' has subscribed to 'Boston'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/las_vegas', methods=["POST"])
|
|
def meetups_las_vegas():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Las Vegas');
|
|
app.logger.info("'" + address + "' has subscribed to 'Las Vegas'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/san_francisco', methods=["POST"])
|
|
def meetups_san_francisco():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'San Francisco');
|
|
app.logger.info("'" + address + "' has subscribed to 'San Francisco'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/new_york_city', methods=["POST"])
|
|
def meetups_new_york_city():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'New York City');
|
|
app.logger.info("'" + address + "' has subscribed to 'New York City'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/detroit', methods=["POST"])
|
|
def meetups_detroit():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Detroit');
|
|
app.logger.info("'" + address + "' has subscribed to 'Detroit'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/stockholm', methods=["POST"])
|
|
def meetups_stockholm():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Stockholm');
|
|
app.logger.info("'" + address + "' has subscribed to 'Stockholm'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/gothenburg', methods=["POST"])
|
|
def meetups_gothenburg():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Gothenburg');
|
|
app.logger.info("'" + address + "' has subscribed to 'Gothenburg'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/grenoble', methods=["POST"])
|
|
def meetups_grenoble():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Grenoble');
|
|
app.logger.info("'" + address + "' has subscribed to 'Grenoble'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/israel', methods=["POST"])
|
|
def meetups_israel():
|
|
address = request.form['email'] # @robustness @abner what if it's empty?
|
|
insert_subscriber(address, request.headers.get('User-Agent'), 'Israel');
|
|
app.logger.info("'" + address + "' has subscribed to 'Israel'")
|
|
return redirect(AGREEMENT_SUCCESS_URL);
|
|
|
|
@app.route('/v1/meetups/subscribers', methods=["POST"])
|
|
def meetups_subscribers():
|
|
# Verify headers
|
|
|
|
content_type = (
|
|
request.headers['Content-Type'] if 'Content-Type' in request.headers else ''
|
|
)
|
|
|
|
if 'application/json' in content_type:
|
|
payload = request.json
|
|
else:
|
|
resp = { 'error' : 'JSON payload expected'}
|
|
return make_response(jsonify(resp), 415)
|
|
|
|
# Confirm client version is compatible
|
|
|
|
if 'client' not in payload or payload['client'] != MEETUP_INVITE_CLIENT_VERSION:
|
|
resp = { 'error' : 'Outdated client. Download latest version: https://git.handmadecities.com/meetups/meetupinvite2000/releases' }
|
|
return make_response(jsonify(resp), 403)
|
|
|
|
# Extract city from POST request
|
|
|
|
if 'city' not in payload:
|
|
resp = { 'error' : 'city field missing' }
|
|
return make_response(jsonify(resp), 400)
|
|
|
|
city = payload['city']
|
|
|
|
app.logger.info('Received subscribers request with city \'' + city + '\'')
|
|
|
|
if city not in shared_secrets:
|
|
resp = { 'error' : 'There are no official Handmade meetups in ' + city }
|
|
return make_response(jsonify(resp), 400)
|
|
|
|
auth_header = ( request.headers['Authorization'] if 'Authorization' in request.headers else '')
|
|
|
|
if shared_secrets[city] not in auth_header:
|
|
resp = { 'error' : 'wrong shared secret for ' + city }
|
|
return make_response(jsonify(resp), 401)
|
|
|
|
# Query emails given the city
|
|
cur = g.db.cursor()
|
|
cur.execute('SELECT email FROM Subscribers WHERE city="' + city + '"')
|
|
rows = cur.fetchall()
|
|
|
|
emails = [row[0] for row in rows]
|
|
resp = { 'emails' : emails }
|
|
|
|
return make_response(jsonify(resp), 200)
|
|
|
|
@app.route('/v1/meetups/chat/invite', methods=["POST"])
|
|
def meetups_chat_invite():
|
|
content_type = (
|
|
request.headers['Content-Type'] if 'Content-Type' in request.headers else ''
|
|
)
|
|
|
|
if 'application/json' not in content_type:
|
|
resp = { 'error': 'JSON payload expected' }
|
|
return make_response(jsonify(resp), 415)
|
|
|
|
payload = request.json
|
|
|
|
if not payload or 'city' not in payload:
|
|
resp = { 'error': 'city field missing' }
|
|
return make_response(jsonify(resp), 400)
|
|
|
|
city = payload['city']
|
|
app.logger.info('Received chat invite request for city \'' + city + '\'')
|
|
|
|
if city not in shared_secrets:
|
|
resp = { 'error' : 'There are no official Handmade meetups in ' + city }
|
|
return make_response(jsonify(resp), 400)
|
|
|
|
auth_header = ( request.headers['Authorization'] if 'Authorization' in request.headers else '')
|
|
|
|
if shared_secrets[city] not in auth_header:
|
|
resp = { 'error' : 'wrong shared secret for ' + city }
|
|
return make_response(jsonify(resp), 401)
|
|
|
|
# Pick a random invite code and return it
|
|
|
|
code = random.choice(INVITE_CODES)
|
|
app.logger.info('Issued chat invite for ' + city + ': ' + code)
|
|
resp = {
|
|
'invite_code': code,
|
|
# TODO(jmug): Verify with Abner is this is an actual secret.
|
|
# NOTE(jmug): Request "revolt-invite-link" secret.
|
|
'invite_link': '<redacted>',
|
|
# NOTE(jmug): Request postmark-server-token secret.
|
|
'postmark_server_token': '<redacted>',
|
|
'postmark_template_id_register': 41923378,
|
|
'postmark_template_id_join': 41930444
|
|
}
|
|
|
|
return make_response(jsonify(resp), 200)
|
|
|