#!/usr/bin/env python3 import os import random import urllib import uuid import flask import flask_socketio import pony from pony.flask import Pony word_file = "/usr/share/dict/words" WORDS = open(word_file).read().splitlines() app = flask.Flask(__name__) socketio = flask_socketio.SocketIO(app) db = None # @TODO Change this into using redis or something else for storage across # threads etc. session_map = {} @app.route('/') @pony.orm.db_session def index(): r = flask.Response() uid, user = get_uid(r) negotiations = pony.orm.select(n for n in Negotiation if n.owner == user) suggestion = suggest_unused_name() r.data = render_page('index.html', { 'negotiations': list(negotiations), 'suggestion': suggestion, }) return r @pony.orm.db_session def get_uid(r, create = True): """ Returns a tuple (uid, created) where created is true if the user was new. r is the flask response, so that the cookie uid may be set if necessary. """ uid = flask.request.cookies.get('uid') if not uid: if create: uid = generate_uid() r.set_cookie('uid', uid) else: return None, None user = pony.orm.select(u for u in User if u.uid == uid).first() if not user: # Recreate the database user if it doesn't exist user = User(uid = uid) return uid, user @app.route('/negotiation/create', methods = ['POST']) @pony.orm.db_session def negotiation_create(): n = flask.request.form.get('negotiation') uid, user = get_uid(None, False) if not uid or not user: return flask.redirect('/') # @TODO Signal error to user. They don't have a uid cookie if len(n) > 128: return flask.redirect('/') # @TODO Signal error to user. Request too Large nego = get_negotiation(n) if nego: # @TODO Signal to the user that the negotiation already exists and they # are being sent to it instead. return flask.redirect('/negotiations/{}'.format(n)) nego = Negotiation(name = n, owner = user.id, taker_sway = 1, market_sway = 6, is_first_negotiation = 1) pony.orm.commit() return flask.redirect('/negotiations/{}'.format(n)) @app.route('/negotiations/') def negotiation_redir(): return flask.redirect('/') @app.route('/negotiations/') def negotiation(negotiation): r = flask.Response() uid, user = get_uid(r) nego = get_negotiation(negotiation) if not nego: return "Error!" c = { 'negotiation': nego, 'user': uid, 'room_owner': nego.owner, 'room_owner_display_name': nego.owner.display_name if nego.owner.display_name else nego.owner.uid, 'participant_count': len(get_room_participants(negotiation)), } r.data = render_page('negotiation.html', c) return r @socketio.on('join negotiation') def handle_join_negotiation(json): r = flask.Response() app.logger.info('Received join request: {}'.format(json)) uid, user = get_uid(r) flask_socketio.join_room(json['room']) session_map[flask.request.sid] = uid; app.logger.info('{} has joined room "{}" (sid: {})'.format(uid, json['room'], flask.request.sid)) participants = get_room_participants(json['room']) app.logger.info('{} has {} connections'.format( json['room'], len(participants))) flask_socketio.emit('participants changed', {'participants': participants}, room = json['room']) # Update the user's negotation state with what is stored on the server nego = get_negotiation(json['room']) return nego.to_dict() def get_room_participants(room): if '/' not in socketio.server.manager.get_namespaces(): return [] try: sessions = [s for s in socketio.server.manager.get_participants('/', room)] participants = [p for s,p in session_map.items() if s in sessions] except: return [] app.logger.debug(participants) return participants @socketio.on('update negotiation') @pony.orm.db_session def handle_update_negotiation(json): r = flask.Response() app.logger.info('Received update request: {}'.format(json)) uid, user = get_uid(r) nego = get_negotiation(json['room']) app.logger.info('Room owner is {}'.format(nego.owner.uid)) if nego.owner.uid != uid: # Refuse the update from non-owners app.logger.warning('Refusing update of {} from non-owner {}'.format(json['room'], uid)) return False del json['room'] # If a negotiation was set to manual, it may not be set back to automatic. if nego.manual_negotiation and 'manual_negotiation' in json: del json['manual_negotiation'] nego.set(**json) pony.orm.commit() flask_socketio.emit('negotiation updated', {**json}, room = nego.name) return True @socketio.on('leave negotiation') def handle_leave_disconnect(json): r = flask.Response() app.logger.info('Received join request: {}'.format(json)) uid, user = get_uid(r) del session_map[flask.request.sid] app.logger.info('{} has disconnected from {} (sid: {})'.format(uid, json['room'],flask.request.sid)) participants = get_room_participants(json['room']) flask_socketio.emit('participants changed', {'participants': participants}, room = json['room']) return True @pony.orm.db_session def get_negotiation(negotiation): return pony.orm.select(nego for nego in Negotiation if nego.name == negotiation).first() @pony.orm.db_session def generate_uid(): while True: uid = str(uuid.uuid4()) if pony.orm.exists(u for u in User if u.uid == uid): continue u = User(uid = uid) pony.orm.commit() return uid @pony.orm.db_session def suggest_unused_name(): tries = 0; while True: tries += 1 w = "{}{}{}{}".format( random.choice(WORDS).title(), random.choice(WORDS).title(), random.choice(WORDS).title(), random.choice(WORDS).title() ) if not w.isalpha(): continue if pony.orm.exists(n.name for n in Negotiation if n.name == w): continue app.logger.info('Took {} tries to generate a room suggestion'.format(tries)) return w def render_page(template, context): app_context = { 'LANGUAGE': flask.request.cookies.get('language') if flask.request.cookies.get('language') else app.config['DEFAULT_LANGUAGE'], } context = {**context, **app_context} return flask.render_template(template, **context) if __name__ == '__main__': port = os.environ.get('APP_PORT', 5000) appkey = os.environ.get('APP_KEY', 'test') bindaddr = os.environ.get('APP_BINDADDRESS', '127.0.0.1') app.secret_key = appkey app.config.update(dict( PONY = { 'provider': 'sqlite', 'filename': 'negotiation.db', 'create_db': True, }, DEFAULT_LANGUAGE = 'en', )) db = pony.orm.Database() class User(db.Entity): uid = pony.orm.Required(str) display_name = pony.orm.Optional(str) negotiations = pony.orm.Set('Negotiation') # Negotiations have a state. Maybe storing a state machine object (pickle?) # would be an idea. But maybe not. # # Let's start with understanding negotiations # 1. Prep Work: Before a negotiation begins, each take may do Prep Work # 2. The takers decide who their Lead Negotiator will be. Once set, this # cannot be changed. # 3. The Lead Negotiation makes a First Impression # This is a leadership check which has four effects: # 1. The number of rounds of negotiation: max(floor(black+ld/2), 5) # 2. The starting position (crit success +1 sway, crit failure: -1 sway) # 3. Whether or not the negotiation length is known to takers. T/F based # simple succes. # 4. Who leads the negotiations (starting round): takers on success, Market otherwise. # 4. Negotiation Round(s) # 1. Lead negotiator plays a negotiation tactic # 2. Other negotiator plays a negotiation tactic # 3. Check negotiation end condition # 4. If not ended, a non-negotiator taker may play scam # 5. Wrap-up # 1. Taker negotiator rolls Leadership. Success moves up to meet Market, # failure moves market down to meet takers. # 2a. Takers may accept the deal. # 2b. Takers may back out. Next negotiation starts on lowest tracker, has # no prep work, scams, or will. # 3. Competition undercuts. If able, the competition will undercut by one # spot on the way tracker. _Any_ taker may make a CHA check to stop this, # but on failure the takers must lower theire price to match the competition. # class Negotiation(db.Entity): name = pony.orm.PrimaryKey(str) owner = pony.orm.Required(User) client_name = pony.orm.Optional(str) negotiator_name = pony.orm.Optional(str) taker_crew_name = pony.orm.Optional(str) # Manual negotation manual_negotiation = pony.orm.Optional(bool) # Pre-negotiation details takers = pony.orm.Optional(int) lead_negotiator = pony.orm.Optional(str) is_first_negotiation = pony.orm.Optional(bool) first_impression_black = pony.orm.Optional(int) # One of critfail, fail, success, critsuccess first_impression_state = pony.orm.Optional(str) negotiation_round = pony.orm.Optional(int) # One of leadnego, secondnego, scam negotiation_phase = pony.orm.Optional(str) taker_sway = pony.orm.Optional(int) market_sway = pony.orm.Optional(int) # If negotiation_round = (calculed length) && negotiation_phase = scam then # the negotiation is no into the wrap up section # One of critfail, fail, success, critsuccess wrapup_state = pony.orm.Optional(int) job_accepted = pony.orm.Optional(bool) may_be_undercut = pony.orm.Optional(bool) undercut = pony.orm.Optional(bool) db.bind(**app.config['PONY']) db.generate_mapping(create_tables=True) Pony(app) socketio.run(app, host = bindaddr, port = port)