Пример #1
0
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))
Пример #2
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)
Пример #3
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'))
Пример #4
0
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)
Пример #5
0
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()
Пример #6
0
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
Пример #7
0
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