class Group(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), unique=True, nullable=False) owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) owner = db.relationship('User', backref=db.backref('groups', order_by=id, lazy='joined')) def __init__(self, name): self.name = name def __repr__(self): return '<Group({name!r})>'.format(name=self.name) @classmethod def get_or_create(cls, name): name = name.lower() g = cls.query.filter_by(name=name).first() if not g: g = Group(name=name) return g
class AuthToken(db.Model): """ Service authentication tokens, such as those used for Github's OAuth. """ id = db.Column(db.Integer, primary_key=True) created = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) name = db.Column(db.String(50), nullable=False) token = db.Column(db.String(512), nullable=False) owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) owner = db.relationship('User', backref=db.backref('tokens', order_by=id, lazy='dynamic', cascade='all, delete-orphan')) @classmethod def new(cls, token, name): c = cls() c.token = token c.name = name return c
class BotEvent(db.Model): id = db.Column(db.Integer, primary_key=True) created = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) channel = db.Column(db.String(80)) host = db.Column(db.String(255), nullable=False) port = db.Column(db.Integer, default=6667) ssl = db.Column(db.Boolean, default=False) message = db.Column(db.Text()) status = db.Column(db.String(30)) event = db.Column(db.String(255)) @classmethod def new(cls, host, port, ssl, message, status, event, channel=None): c = cls() c.host = host c.port = port c.ssl = ssl c.message = message c.status = status c.event = event c.channel = channel return c
class Hook(db.Model): id = db.Column(db.Integer, primary_key=True) created = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) key = db.Column(db.String(255), nullable=False) service_id = db.Column(db.Integer) config = db.Column(db.PickleType) project_id = db.Column(db.Integer, db.ForeignKey('project.id')) project = db.relationship('Project', backref=db.backref('hooks', order_by=id, lazy='dynamic', cascade='all, delete-orphan')) message_count = db.Column(db.Integer, default=0) @classmethod def new(cls, service_id, config=None): p = cls() p.service_id = service_id p.key = cls._new_key() p.config = config return p @staticmethod def _new_key(): return base64.urlsafe_b64encode(os.urandom(24))[:24] @classmethod def by_service_and_project(cls, service_id, project_id): return cls.query.filter_by(service_id=service_id, project_id=project_id).first() @property def hook(self): return HookService.services[self.service_id] def absolute_url(self): hook = self.hook try: hook_url = hook.absolute_url(self) return hook_url except NotImplementedError: return None
class Project(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) created = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) public = db.Column(db.Boolean, default=True) website = db.Column(db.String(1024)) owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) owner = db.relationship('User', backref=db.backref('projects', order_by=id, lazy='dynamic', cascade='all, delete-orphan')) full_name = db.Column(db.String(101), nullable=False, unique=True) message_count = db.Column(db.Integer, default=0) @classmethod def new(cls, name, public=True, website=None): c = cls() c.name = name.strip() c.public = public c.website = website.strip() if website else None return c @hybrid_property def name_i(self): return self.name.lower() @name_i.comparator def name_i(cls): return CaseInsensitiveComparator(cls.name) @classmethod def by_name(cls, name): return cls.query.filter_by(name_i=name).first() @classmethod def by_name_and_owner(cls, name, owner): q = cls.query.filter(cls.owner_id == owner.id) q = q.filter(cls.name_i == name) return q.first() @classmethod def visible(cls, q, user=None): """ Modifies the sqlalchemy query `q` to only show projects accessible to `user`. If `user` is ``None``, only shows public projects. """ if user and user.in_group('admin'): # We don't do any filtering for admins, # who should have full visibility. pass elif user: # We only show the projects that are either public, # or are owned by `user`. q = q.filter( or_(Project.owner_id == user.id, Project.public == True)) else: q = q.filter(Project.public == True) return q def is_owner(self, user): """ Returns ``True`` if `user` is the owner of this project. """ return user and user.id == self.owner.id def can_see(self, user): if self.public: # Public projects are always visible. return True if user and user.in_group('admin'): # Admins can always see projects. return True elif self.is_owner(user): # The owner of the project can always see it. return True return False def can_modify(self, user): """ Returns ``True`` if `user` can modify this project. """ if user and user.in_group('admin'): # Admins can always modify projects. return True elif self.is_owner(user): return True return False
class Channel(db.Model): id = db.Column(db.Integer, primary_key=True) created = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) channel = db.Column(db.String(80), nullable=False) host = db.Column(db.String(255), nullable=False) port = db.Column(db.Integer, default=6667) ssl = db.Column(db.Boolean, default=False) public = db.Column(db.Boolean, default=False) project_id = db.Column(db.Integer, db.ForeignKey('project.id')) project = db.relationship('Project', backref=db.backref('channels', order_by=id, lazy='dynamic', cascade='all, delete-orphan')) @classmethod def new(cls, channel, host, port=6667, ssl=False, public=False): c = cls() c.channel = channel c.host = host c.port = port c.ssl = ssl c.public = public return c @classmethod def channel_count_by_network(cls): q = (db.session.query( Channel.host, func.count(Channel.channel).label('count')).filter_by( public=True).group_by(Channel.host).order_by('-count')) for network, channel_count in q: yield network, channel_count def last_event(self): """ Returns the latest BotEvent to occur for this channel. """ return BotEvent.query.filter_by(host=self.host, port=self.port, ssl=self.ssl, channel=self.channel).order_by( BotEvent.created.desc()).first() @classmethod def visible(cls, q, user=None): """ Modifies the sqlalchemy query `q` to only show channels accessible to `user`. If `user` is ``None``, only shows public channels in public projects. """ from notifico.models import Project if user and user.in_group('admin'): # We don't do any filtering for admins, # who should have full visibility. pass else: q = q.join(Channel.project).filter(Project.public == True, Channel.public == True) return q
class User(db.Model): id = db.Column(db.Integer, primary_key=True) # --- # Required Fields # --- username = db.Column(db.String(50), unique=True, nullable=False) email = db.Column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False) salt = db.Column(db.String(8), nullable=False) joined = db.Column(db.TIMESTAMP(), default=datetime.datetime.utcnow) # --- # Public Profile Fields # --- company = db.Column(db.String(255)) website = db.Column(db.String(255)) location = db.Column(db.String(255)) @classmethod def new(cls, username, email, password): u = cls() u.email = email.lower().strip() u.salt = cls._create_salt() u.password = cls._hash_password(password, u.salt) u.username = username.strip() return u @staticmethod def _create_salt(): """ Returns a new base64 salt. """ return base64.b64encode(os.urandom(8))[:8] @staticmethod def _hash_password(password, salt): """ Returns a hashed password from `password` and `salt`. """ return hashlib.sha256(salt + password.strip()).hexdigest() def set_password(self, new_password): self.salt = self._create_salt() self.password = self._hash_password(new_password, self.salt) @classmethod def by_email(cls, email): return cls.query.filter_by(email=email.lower().strip()).first() @classmethod def by_username(cls, username): return cls.query.filter_by(username_i=username).first() @classmethod def email_exists(cls, email): return cls.query.filter_by(email=email.lower().strip()).count() >= 1 @classmethod def username_exists(cls, username): return cls.query.filter_by(username_i=username).count() >= 1 @classmethod def login(cls, username, password): """ Returns a `User` object for which `username` and `password` are correct, otherwise ``None``. """ u = cls.by_username(username) if u and u.password == cls._hash_password(password, u.salt): return u return None @hybrid_property def username_i(self): return self.username.lower() @username_i.comparator def username_i(cls): return CaseInsensitiveComparator(cls.username) def active_projects(self, limit=5): """ Return this users most active projets (by descending message count). """ q = self.projects.order_by(False).order_by('-message_count') q = q.limit(limit) return q def in_group(self, name): """ Returns ``True`` if this user is in the group `name`, otherwise ``False``. """ return any(g.name == name.lower() for g in self.groups) def add_group(self, name): """ Adds this user to the group `name` if not already in it. The group will be created if needed. """ if self.in_group(name): # We're already in this group. return self.groups.append(Group.get_or_create(name=name)) def export(self): """ Exports the user, his projects, and his hooks for use in a private-ly hosted Notifico instance. """ j = { 'user': { 'username': self.username, 'email': self.email, 'joined': self.joined.isoformat(), 'company': self.company, 'website': self.website, 'location': self.location }, 'projects': [{ 'name': p.name, 'created': p.created.isoformat(), 'public': p.public, 'website': p.website, 'message_count': p.message_count, 'channels': [{ 'created': c.created.isoformat(), 'channel': c.channel, 'host': c.host, 'port': c.port, 'ssl': c.ssl, 'public': c.public } for c in p.channels], 'hooks': [{ 'created': h.created.isoformat(), 'key': h.key, 'service_id': h.service_id, 'message_count': h.message_count, 'config': h.config } for h in p.hooks] } for p in self.projects] } return j