class TwitterOAuth(db.Model): id = db.Column(db.Integer, primary_key=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey(User.id)) screen_name = db.Column(db.String, unique=True) token = db.Column(db.String) token_secret = db.Column(db.String) # Twitter comes with a 15 requests / 15 minutes rate. To avoid # people DOS'ing our service we allow only one refresh per hour. last_friend_refresh = db.Column(db.DateTime) # List of Friend objects (see named tuple above this class) friends = db.Column(db.PickleType()) def seconds_to_next_refresh(self, utcnow=None): if utcnow is None: utcnow = datetime.datetime.utcnow() if self.last_friend_refresh is None: return 0 seconds_since_refresh = ( utcnow - self.last_friend_refresh ).total_seconds() - 3600 return abs(min(seconds_since_refresh, 0))
class PrintKey(db.Model): """ Keys uniquely identify a printer and include a secret so you can print to a little printer by just embedding a secret in an URL. """ id = db.Column(db.Integer, primary_key=True) secret = db.Column(db.String, default=generate_secret, unique=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) printer_id = db.Column(db.Integer, db.ForeignKey('printer.id')) printer = db.relationship('Printer', backref=db.backref('print_keys', lazy='dynamic')) number_of_uses = db.Column(db.Integer, default=0, nullable=False) senders = db.Column(postgresql.ARRAY(db.String), default=list, nullable=False) def record_usage(self, by_name): self.number_of_uses += 1 if by_name not in self.senders: self.senders.append(by_name) flag_modified(self, 'senders') parent_id = db.Column(db.Integer, db.ForeignKey('print_key.id')) parent = db.relationship('PrintKey', backref=db.backref('children', lazy='dynamic'), remote_side=[id]) def senders_formatted(self): return ', '.join((s or 'Anonymous') for s in self.senders) @property def url(self): if 'DEVICE_KEY_DOMAIN' in os.environ: return 'https://%s/%s' % (os.environ['DEVICE_KEY_DOMAIN'], self.secret) else: # if a DEVICE_KEY_DOMAIN is not defined, we need a request object # to get the full URL. return '%sprintkey/%s' % (request.url_root, self.secret)
class PrintKey(db.Model): """ Keys uniquely identify a printer and include a secret so you can print to a little printer by just embedding a secret in an URL. """ id = db.Column(db.Integer, primary_key=True) secret = db.Column(db.String, default=generate_secret, unique=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) printer_id = db.Column(db.Integer, db.ForeignKey('printer.id')) printer = db.relationship('Printer', backref=db.backref('print_keys', lazy='dynamic'))
class ClaimCode(db.Model): """Printer and claim codes are joined over the hardware xor. Claim codes are meant to have a temporary life time though we're not treating them like this for now. """ id = db.Column(db.Integer, primary_key=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) by_id = db.Column(db.ForeignKey('user.id')) by = db.relationship('User', backref=db.backref('claim_codes', lazy='dynamic')) hardware_xor = db.Column(db.Integer) claim_code = db.Column(db.String, unique=True) # Intended name of printer; used for display purposes. name = db.Column(db.String) def __repr__(self): return '<ClaimCode xor: {} code: {}>'.format(self.hardware_xor, self.claim_code)
class Printer(db.Model): """On reset printers generate a new, unique device address, so every reset will result in a new Printer row. Note that this model is only ever created by a printer calling home. Users create a ClaimCode row. """ id = db.Column(db.Integer, primary_key=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) device_address = db.Column(db.String) hardware_xor = db.Column(db.Integer) # Name of printer; used for display purposes. name = db.Column(db.String) # Update the following fields after we connected (i.e. joined over # hardware xor) a claim to a printer. The fields start out as # NULL. owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) owner = db.relationship('User', backref=db.backref('printers', lazy='dynamic')) used_claim_code = db.Column(db.String, nullable=True, unique=True) def __repr__(self): return 'Printer {}, xor: {}, owner: {}'.format(self.device_address, self.hardware_xor, self.owner_id) @classmethod def phone_home(cls, device_address): """This gets called every time the in-memory machinery thinks it has seen a printer for the first time round, e.g. after a re-connect of the websocket. """ printer = cls.query.filter_by(device_address=device_address).first() hardware_xor = bitshuffle.hardware_xor_from_device_address( device_address) if printer is not None: return printer = cls( device_address=device_address, hardware_xor=hardware_xor, owner_id=None, used_claim_code=None, ) db.session.add(printer) db.session.commit() # Connect hardware xor and printer if there is a claim code # waiting. # # Printers always generate the same XOR. I.e. we can have more # than one claim code with the same XOR. We always pick the # newest claim code. claim_code_query = ClaimCode.query.filter_by( hardware_xor=hardware_xor).order_by(desc('created'), desc('id')) claim_code = claim_code_query.first() if claim_code is None: return printer.owner_id = claim_code.by_id printer.name = claim_code.name printer.used_claim_code = claim_code.claim_code db.session.add(printer) db.session.commit() @classmethod def get_claim_code(cls, device_address): """Find and return a claim code.""" printer = cls.query.filter_by(device_address=device_address).first() if printer is None: return None db.session.commit() return printer.used_claim_code @property def is_online(self): from sirius.protocol import protocol_loop return protocol_loop.device_is_online(self.device_address) class OfflineError(Exception): pass def print_html(self, html, from_name, face='default'): from sirius.coding import image_encoding from sirius.coding import templating pixels = image_encoding.default_pipeline( templating.default_template(html, from_name=from_name)) self.print_pixels(pixels, from_name=from_name, face=face) def print_pixels(self, pixels, from_name, face='default'): from sirius.protocol import messages from sirius.protocol import protocol_loop from sirius import stats from sirius.models import messages as model_messages hardware_message = None if face == "noface": hardware_message = messages.SetDeliveryAndPrintNoFace( device_address=self.device_address, pixels=pixels, ) else: hardware_message = messages.SetDeliveryAndPrint( device_address=self.device_address, pixels=pixels, ) # If a printer is "offline" then we won't find the printer # connected and success will be false. success, next_print_id = protocol_loop.send_message( self.device_address, hardware_message) if success: stats.inc('printer.print.ok') else: stats.inc('printer.print.offline') # Store the same message in the database. png = io.BytesIO() pixels.save(png, "PNG") model_message = model_messages.Message( print_id=next_print_id, pixels=bytearray(png.getvalue()), sender_name=from_name, target_printer=self, ) # We know immediately if the printer wasn't online. if not success: model_message.failure_message = 'Printer offline' model_message.response_timestamp = datetime.datetime.utcnow() db.session.add(model_message) if not success: raise Printer.OfflineError()
class Message(db.Model): """Messages are printed by someone on ("sender") to a "target printer". """ id = db.Column(db.Integer, primary_key=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) pixels = db.Column(db.LargeBinary) print_id = db.Column(db.Integer, unique=True) sender_name = db.Column(db.String) target_printer_id = db.Column(db.Integer, db.ForeignKey('printer.id')) target_printer = db.relationship('Printer', backref=db.backref('messages', lazy='dynamic')) # The response is received by the protocol_loop. It's also # possible to not receive a response (e.g. printer got # disconnected) in which case we "time out" after TIMEOUT_SECONDS. response_timestamp = db.Column(db.DateTime, nullable=True) failure_message = db.Column(db.String, nullable=True) @classmethod def timeout_updates(cls, utcnow=None): """Update all messages that are timed-out with an error message. This is a destructive update. It should also be fairly cheap because of the index on created. We're doing these updates "eagerly" on every printer overview page load instead of through a cron job, but there's no reason why we couldn't do that. """ if utcnow is None: utcnow = datetime.datetime.utcnow() cutoff = utcnow - datetime.timedelta(seconds=TIMEOUT_SECONDS) cls.query.filter( cls.created <= cutoff, cls.response_timestamp == None ).update(dict( response_timestamp=utcnow, failure_message="Timed out", )) db.session.commit() def base64_pixels(self): return base64.b64encode(self.pixels) @classmethod def ack(cls, return_code, command_id, utcnow=None): if return_code != 0: # TODO map codes to error messages failure_message = 'Problem printing: {}'.format(return_code) else: failure_message = None if utcnow is None: utcnow = datetime.datetime.utcnow() message = cls.query.filter_by(print_id=command_id).first() if message is None: logger.error("Ack'ed unknown message %s.", command_id) return message.response_timestamp = utcnow message.failure_message = failure_message db.session.add(message) db.session.commit() @classmethod def get_next_command_id(cls): """Return the next command id to be used by the bridge. Command ids are used to ack messages so we need to make sure we don't have any collisions. The best way to do so is to pick the next-highest number after the ones we already used up. """ last = cls.query.order_by(desc('print_id')).first() if last is None: next_print_id = 1 else: next_print_id = last.print_id + 1 db.session.commit() return next_print_id
class Printer(db.Model): """On reset printers generate a new, unique device address, so every reset will result in a new Printer row. Note that this model is only ever created by a printer calling home. Users create a ClaimCode row. """ id = db.Column(db.Integer, primary_key=True) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) device_address = db.Column(db.String) hardware_xor = db.Column(db.Integer) # Name of printer; used for display purposes. name = db.Column(db.String) # Update the following fields after we connected (i.e. joined over # hardware xor) a claim to a printer. The fields start out as # NULL. owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) owner = db.relationship('User', backref=db.backref('printers', lazy='dynamic')) used_claim_code = db.Column(db.String, nullable=True, unique=True) def __repr__(self): return 'Printer {}, xor: {}, owner: {}'.format(self.device_address, self.hardware_xor, self.owner_id) @classmethod def phone_home(cls, device_address): """This gets called every time the in-memory machinery thinks it has seen a printer for the first time round, e.g. after a re-connect of the websocket. """ printer = cls.query.filter_by(device_address=device_address).first() hardware_xor = bitshuffle.hardware_xor_from_device_address( device_address) if printer is not None: return printer = cls( device_address=device_address, hardware_xor=hardware_xor, owner_id=None, used_claim_code=None, ) db.session.add(printer) db.session.commit() # Connect hardware xor and printer if there is a claim code # waiting. # # Printers always generate the same XOR. I.e. we can have more # than one claim code with the same XOR. We always pick the # newest claim code. claim_code_query = ClaimCode.query.filter_by( hardware_xor=hardware_xor).order_by(desc('created'), desc('id')) claim_code = claim_code_query.first() if claim_code is None: return printer.owner_id = claim_code.by_id printer.name = claim_code.name printer.used_claim_code = claim_code.claim_code db.session.add(printer) db.session.commit() @classmethod def get_claim_code(cls, device_address): """Find and return a claim code.""" printer = cls.query.filter_by(device_address=device_address).first() if printer is None: return None db.session.commit() return printer.used_claim_code