a lot of changes

This commit is contained in:
Izalia Mae 2021-01-22 12:26:20 -05:00
parent 6eb934307f
commit f4cb4486a4
49 changed files with 2734 additions and 1000 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "social/Lib/izzylib"]
path = social/Lib/izzylib
url = https://git.barkshark.xyz/izaliamae/izzylib.git

View file

@ -10,6 +10,8 @@ Note 2: It's still very much pre-alpha and not usable yet
## Installation
I recommend installing [https://github.com/pyenv/pyenv](pyenv) and running `pyenv install 3.8.4 && pyenv virtualenv 3.8.4 social`. Be sure to do `pip install -U pip` since pip will be out of date.
There is a submodule, so you'll have to run `git clone --recurse-submodules https://git.barkshark.xyz/izaliamae/izzylib.git` on the initial clone
I recommend installing [https://github.com/pyenv/pyenv](pyenv) and running `pyenv install 3.8.4 && pyenv virtualenv 3.8.4 social && pyenv local social`. Be sure to do `pip install -U pip` since pip will be out of date.
`pip3 install -r requirements.txt`

View file

@ -1,16 +1,26 @@
pygresql>=5.1
dbutils>=1.3
envbash>=1.1.2
pycryptodome>=3.9.0
beautifulsoup4>=4.9.0
colour>=0.1.5
dramatiq[redis]>=1.10.0
envbash>=1.1.2
hiredis>=1.1.0
jinja2>=2.10.1
markdown >=3.3.0
pillow>=8.1.0
pycryptodome>=3.9.0
pygresql>=5.1
python-magic>=0.4.18
sanic>=3.6.0
sanic-cors>=0.7.0
watchdog>=0.8.3
validators>=0.14.0
pyyaml>=5.1.2
celery[redis]>=4.4.2
sqlalchemy>=1.2.0
SQLAlchemy-Paginator>=0.2
tldextract>=2.2.2
python-magic>=0.4.18
pillow>=8.1.0
validators>=0.14.0
#git+https://git.barkshark.xyz/izaliamae/izzylib.git@0.2
## keeping here just in case
#celery[redis]>=4.4.2
## not sure if I need these
#pyyaml>=5.1.2
#watchdog>=0.8.3

1
social/Lib/izzylib Submodule

@ -0,0 +1 @@
Subproject commit 0e59542626fed943f5832c549f2ea6cea631a1ef

View file

@ -51,7 +51,7 @@ class Post(object):
def statuses_none(self, request, data, args):
data = DefaultDict(data)
user = request.ctx.token_user
print(data)
if not any([data.status, data.spoiler_text]):
return JsonResp('Missing status text or warning', 400)

View file

@ -7,6 +7,7 @@ from ..functions import ApDate
def mastodon_user(user):
with db.session() as s:
last_status = s.query(s.table.status).filter_by(userid=user.id).order_by(s.table.status.id.desc()).limit(1).one_or_none()
prefs = user.config if user.config else {}
data = {
'id': user.id,
@ -25,8 +26,8 @@ def mastodon_user(user):
'followers_count': 0,
'following_count': 0,
'statuses_count': s.count('status', userid=user.id),
'fields': [],
'last_status_at': None
'fields': [{'name': k, 'value': v, 'verified_at': None} for k,v in prefs.info_table.items()],
'last_status_at': ApDate(last_status.timestamp)
}
if user.table:

View file

@ -1,61 +0,0 @@
import io
from IzzyLib import logging
from IzzyLib.misc import Boolean, DotDict
from PIL import Image
from ..config import config
from ..database import db
from ..functions import mkhash
def get_profile(request):
return {}
def post_profile(request):
user = request.ctx.cookie_user
form = request.ctx.form
data = DotDict({k: v for k,v in form.items() if k in user.keys()})
data.config = DotDict(user.config if type(user.config) == dict else {})
avatar = request.ctx.files.get('avatar')
header = request.ctx.files.get('header')
if avatar and avatar.name:
path = config.data.join(f'media/avatar/{user.domain}/{user.handle[0]}/{user.handle}.png')
image = Image.open(io.BytesIO(avatar.body))
path.parent().mkdir()
image.save(path.str())
if header and header.name:
path = config.data.join(f'media/header/{user.domain}/{user.handle[0]}/{user.handle}.png')
image = Image.open(io.BytesIO(header.body))
path.parent().mkdir()
image.save(path.str())
if form.get('oldpassword'):
if not form.get('password1') or not form.get('password2'):
return {'error': 'Missing a password field'}
if form.password1 != form.password2:
return {'error': 'Passwords don\'t match'}
curr_hash = mkhash(form.oldpassword)
if curr_hash != user.password:
return {'error': 'Current password invalid'}
data.password = mkhash(form.password1)
data.config.private = Boolean(form.get('private'))
data.config = data.config.asDict()
if data.asDict():
with db.session() as s:
row = s.fetch('user', id=user.id)
row.update_session(s, data.asDict())
request.ctx.cookie_user = row
return {'message': 'Updated profile'}

View file

@ -3,7 +3,7 @@ import os, sys, sanic, socket
from os import environ as env
from IzzyLib import logging
from IzzyLib.misc import DotDict, Path, Boolean, GetIp, RandomGen
from IzzyLib.misc import DefaultDict, DotDict, Path, Boolean, GetIp, RandomGen
from IzzyLib.template import Template
from envbash import load_envbash
from jinja2 import Environment
@ -66,6 +66,12 @@ config = DotDict({
'max_chars': env.get('MAX_CHARS', 69420),
'posts': env.get('PROFILE_POSTS', 20)
}),
'proxy': DotDict({
'enabled': Boolean(env.get('PROXY_ENABLED', False)),
'ptype': env.get('PROXY_TYPE', 'https'),
'host': env.get('PROXY_HOST', None),
'port': env.get('PROXY_PORT', 443)
}),
'rd': DotDict({
'host': env.get('REDIS_HOST', 'localhost'),
'port': int(env.get('REDIS_PORT', 6379)),
@ -119,13 +125,19 @@ config.template = Template(
)
def Context(context, globals):
user = context.request.ctx.cookie_user
try:
user = context.request.ctx.cookie_user
except AttributeError:
user = None
with globals.db.session() as s:
settings = s.get.configs(cached=False)
return {
'cookie_user': user,
'menu_left': user.config.get('menu_left', False) if user else False,
'color_theme': context.request.cookies.get('theme', 'pink'),
'settings': globals.db.get().configs(),
'settings': DefaultDict(settings),
'var': {'base': 'https://'+config.web_domain}
}

View file

@ -29,8 +29,13 @@ class Row(DotDict):
super().__init__()
for attr in self._columns:
self[attr] = getattr(row, attr)
for k in self._columns:
v = getattr(row, k)
if type(v) == dict:
v = DotDict(v)
self[k] = v
self.__run__(row, db)
@ -49,7 +54,7 @@ class Row(DotDict):
return data
def _update(self, new_data={None}, **kwargs):
def _update(self, new_data={}, **kwargs):
kwargs.update(new_data)
for k,v in kwargs.items():
@ -73,7 +78,7 @@ class Row(DotDict):
def update(self, dict_data={}, **data):
self._update(data)
self._update(dict_data)
self._update(dict(dict_data))
data = self._filter_data()
@ -96,11 +101,14 @@ class User(Row):
domain = db.fetch('domain', id=self.domainid)
self.domain = domain.domain
self.avatar = 'static/one.png'
self.header = 'static/one.png'
self.avatar = 'static/img/one.png'
self.header = 'static/img/one.png'
avatar_path = f'media/avatar/{self.domain}/{self.handle[0]}/{self.handle}.png'
header_path = f'media/header/{self.domain}/{self.handle[0]}/{self.handle}.png'
avatar = row.config.get('avatar')
header = row.config.get('header')
avatar_path = f'media/avatar/{self.domain}/{self.handle[0]}/{avatar}.png'
header_path = f'media/header/{self.domain}/{self.handle[0]}/{header}.png'
if config.data.join(avatar_path).exists():
self.avatar = avatar_path
@ -165,7 +173,7 @@ class Session(object):
rows = self.s.query(self.table[table_name]).filter_by(**kwargs).all()
if single:
return RowClass(rows[0], db) if rows else None
return RowClass(rows[0], db) if len(rows) > 0 else None
return [RowClass(row, db) for row in rows]
@ -249,8 +257,7 @@ class DataBase(object):
session.rollback()
finally:
if session.transaction.is_active:
session.commit()
session.commit()
db = DataBase(f'sqlite:///{config.sqfile}', table)
@ -266,6 +273,12 @@ elif config_version < config.dbversion:
else:
with db.session() as s:
# not sure how many users is too many for sqlite, but 5 is still kinda low
# doesn't count system accounts btw
if config.dbtype == 'sqlite' and s.get.stats().user_count >= 5:
logging.warning('The sqlite backend is in use with 5 or more users which might be unstable')
logging.warning('It is highly recommended to convert the database to postgresql')
for k,v in s.get.configs(False).items():
db.cache.config[k] = v

View file

@ -1,9 +1,11 @@
from IzzyLib.misc import Boolean, DotDict, RandomGen
from datetime import datetime
from IzzyLib import logging
from IzzyLib.misc import Boolean, DefaultDict, DotDict, RandomGen
from datetime import datetime, timedelta
from sqlalchemy import func
from sqlalchemy_paginator import Paginator
from ..config import config
from ..functions import GenKey
from ..functions import Client, FetchActor, FetchApi, GenKey, mkhash
subtypes = {
@ -25,10 +27,18 @@ config_defaults = {
'emoji_size_limit': 100,
'secure': True,
'require_approval': True,
'closed': False,
'private_profiles': True,
'domain_approval': False,
'open': True,
'robotstxt': 'User-agent: *\nDisallow: /'
}
user_config_defaults = {
'public': False,
'locked': False,
'private': False,
'info_table': {},
'avatar': None,
'header': None
}
@ -39,6 +49,7 @@ def SetupFuncs(self, session):
self.db = session.db
self.cache = session.cache
self.count = session.count
self.fetch = session.fetch
self.insert = session.insert
self.table = session.table
@ -83,13 +94,12 @@ class Get():
def configs(self, cached=True):
if cached:
data = config_defaults.copy()
data.update(self.cache.config)
data = DotDict(config_defaults.copy())
if cached:
data.update(self.cache.config)
return DotDict(data)
data = DotDict({})
rows = self.fetch('config', single=False)
for row in rows:
@ -98,10 +108,20 @@ class Get():
return data
def domainid(self, domain):
domain = self.fetch('domain', domain=domain)
def domain(self, domain=None):
web_domain = domain if domain else config.web_domain
domain = domain if domain else config.domain
row = self.fetch('domain', web_domain=web_domain)
return domain.id if domain else None
if not row:
row = self.fetch('domain', domain=domain)
return row
def domainid(self, domain=None):
row = self.domain(domain)
return row.id if row else None
def emoji(self, shortcode=None, domain=None):
@ -128,8 +148,8 @@ class Get():
return
def user_count(self):
domain = self.fetch('domain', domain=config.domain)
def user_count(self, domain=None):
domain = self.fetch('domain', domain=config.domain if not domain else domain)
return self.query(self.table.user).filter_by(domainid=domain.id).count()
@ -137,11 +157,61 @@ class Get():
return self.query(self.table.domain).count()
def stats(self):
domainid = self.domainid(config.domain)
sys_accts = [row.id for row in self.fetch('user', permissions=0, single=False)]
user = self.table.user
status = self.table.status
today = datetime.today()
users = self.query(user).filter(user.domainid == domainid, user.permissions > 0)
statuses = self.query(status).filter(status.domainid == domainid, status.userid.notin_(sys_accts))
data = DefaultDict()
data.user_count = users.count()
data.users_month = users.filter(user.timestamp >= (datetime.today() - timedelta(days=30))).count()
data.users_halfyear = users.filter(user.timestamp >= (datetime.today() - timedelta(days=30*6))).count()
data.status_count = statuses.count()
return data
def status_count(self, domain=None):
domain = self.fetch('domain', domain=config.domain if not domain else domain)
domain = self.fetch('domain', domain=domain)
return self.query(self.table.status).filter_by(domainid=domain.id).count()
def statuses(self, handle=None, user=None, domain=None, count=20, desc=True):
if not any([handle, user]):
logging.error('db.get.statuses: Please specify a user or handle')
return
if handle and user:
logging.error('db.get.statuses: Only specify a handle OR user')
return
if handle:
domainid = self.domainid(domain)
user = self.fetch('user', handle=handle, domainid=domainid)
if not user and not handle:
logging.debug('db.get.statuses: Cannot find user')
return
if not user and handle:
logging.debug('db.get.status: Cannot find user:', handle)
return
table = self.table.status
rows = self.query(table).filter(table.userid == user.id)
if desc:
rows = rows.order_by(table.id.desc())
return Paginator(rows, count)
def token(self, token_header, table='token'):
response = {'user': None, 'token': None}
response['token'] = self.fetch(table, token=token_header)
@ -157,12 +227,12 @@ class Get():
def user(self, handle, domain=None, single=True):
domain = config.domain if not domain else domain
domainrow = self.fetch('domain', domain=domain)
domainid = self.domainid(domain)
if not domainrow:
if not domainid:
return
row = self.fetch('user', lower=handle.lower(), domainid=domainid)
row = self.query(self.table.user).filter(func.lower(self.table.user.handle) == func.lower(handle) and self.table.user.domainid == domainid)
return self.classes.User(row.one_or_none(), self.db)
@ -208,25 +278,31 @@ class Put():
row.update_session(s, data)
return row
self.insert('app', data)
self.insert('app', **data)
return self.fetch('app', client_id=data['client_id'])
def config(self, key, value, subtype='str'):
row = self.fetch('config', key=key)
typefunc = subtypes[row.type] if row else str
value = int(datetime.timestamp(value)) if subtype == 'datetime' else typefunc(value)
if subtype == 'bool':
new_value = 'True' if Boolean(value) else 'False'
else:
new_value = int(datetime.timestamp(value)) if subtype == 'datetime' else typefunc(value)
if row:
if row.value == value:
if row.value == new_value:
return
row.value = value
row.value = new_value
row.update_session(self.s)
else:
self.insert('config',
key=key,
value=value,
value=new_value,
type=subtype
)
@ -243,8 +319,7 @@ class Put():
logging.warning(f'db.put.configs: Missing key or value for config: key={key}, value={value}')
continue
self.insert('config', key=key, value=value, type=subtype)
self.cache.config.pop(key)
self.config(key, value, subtype)
def cookie(self, userid, address=None, agent=None, access=datetime.now(), token=RandomGen()):
@ -267,6 +342,25 @@ class Put():
return row
def domain(self, domain, web_domain):
api = FetchApi(web_domain)
description = api.description if api.description else api.short_description
admin = None
if api.contact_account:
admin = api.contact_account['acct']
self.insert('domain',
domain = domain,
web_domain = web_domain,
name = api.title,
admin = admin,
description = description,
short_description = api.short_description,
timestamp = datetime.now()
)
def emoji(self, filename, shortcode, domain=None, display=None, enabled=None):
domain = domain if domain else config.domain
domainrow = self.fetch('domain', domain=domain)
@ -305,37 +399,68 @@ class Put():
pass
def user(self, handle, domain=None, **data):
domain = config.domain if not domain else domain
domainrow = self.fetch('domain', domain=domain)
def user(self, handle, email, password, name=None, permissions=4):
domain = self.fetch('domain', domain=config.domain)
if not domainrow:
if not domain:
logging.error(f'session.put.user: Unknown domain: {domain}')
return
row = self.fetch('user', handle=handle, domainid=domainrow.id)
row = self.fetch('user', handle=handle, domainid=domain.id)
if row:
row.update_session(self.s, data)
return row
logging.warning('db.functions.put.user: User already exists:', handle)
return
if not data.get('privkey'):
keys = GenKey()
data['privkey'] = keys.PRIVKEY
data['pubkey'] = keys.PUBKEY
if not data.get('permissions'):
data['permissions'] = 4
if not data.get('timestamp'):
data['timestamp'] = datetime.now()
keys = GenKey()
actor = f'https://{config.web_domain}/actor/{handle}'
self.insert('user',
handle = handle,
lower = handle.lower(),
name = name if name else handle,
domainid = domainrow.id,
**data
email = email,
password = mkhash(password),
permissions = permissions,
pubkey = keys.PUBKEY,
privkey = keys.PRIVKEY,
actor = actor,
inbox = actor,
config = user_config_defaults.copy(),
timestamp = datetime.now()
)
def remote_user(self, actor: dict):
print(actor)
domain = self.fetch('domain', web_domain=actor.web_domain)
handle = actor.preferredUsername
if not domain:
logging.error(f'session.put.user: Unknown domain: {web_domain}')
return
row = self.fetch('user', handle=handle, domainid=domain.id)
if row:
logging.warning('db.functions.put.user: User already exists:', handle)
return
self.insert('user',
handle = handle,
lower = handle.lower(),
name = actor.get('name', handle),
domainid = domain.id,
permissions = 4 if actor.type == 'Person' else 0,
pubkey = actor.pubkey,
actor = actor.id,
inbox = actor.inbox,
config = user_config_defaults.copy(),
timestamp = datetime.now()
)
def version(self, ver):
self.config('version', ver, 'int')

View file

@ -25,7 +25,7 @@ def setup(db, settings):
s.insert('domain',
domain = config.domain,
name = None,
url = f'https://{config.web_domain}',
web_domain = config.web_domain,
description = None,
timestamp = datetime.now()
)
@ -34,18 +34,30 @@ def setup(db, settings):
domain = s.fetch('domain', domain=config.domain)
else:
domain.update_session(s, {'domain': config.domain, 'url': f'https://{config.web_domain}'})
domain.update_session(s, {'domain': config.domain, 'web_domain': config.web_domain})
s.put.user('system',
name = name,
bio = 'System account',
permissions = 0
permissions = 0,
actor = f'https://{config.web_domain}/actor/system',
inbox = f'https://{config.web_domain}/actor/system'
)
s.put.user('relay',
name = f'{name} Relay',
bio = 'System relay account',
permissions = 0
permissions = 0,
actor = f'https://{config.web_domain}/actor/relay',
inbox = f'https://{config.web_domain}/actor/relay'
)
s.put.user('relay',
name = f'{name} Local Relay',
bio = 'System local relay account',
permissions = 0,
actor = f'https://{config.web_domain}/actor/local',
inbox = f'https://{config.web_domain}/actor/local'
)

View file

@ -11,9 +11,11 @@ class Domain(Base):
id = Column('id', Integer, primary_key=True, autoincrement=True)
domain = Column('domain', String, nullable=False, unique=True)
url = Column('url', String, nullable=False, unique=True)
web_domain = Column('web_domain', String, nullable=False, unique=True)
name = Column('name', String)
admin = Column('admin', String)
description = Column('description', String)
short_description = Column('short_description', String)
timestamp = Column('timestamp', DateTime)
@ -26,25 +28,27 @@ class User(Base):
id = Column('id', Integer, primary_key=True, autoincrement=True)
handle = Column('handle', String, nullable=False)
domainid = Column('domainid', Integer, ForeignKey('domain.id'), nullable=False)
lower = Column('lower', String, nullable=False)
name = Column('name', String)
domainid = Column('domainid', Integer, ForeignKey('domain.id'), nullable=False)
email = Column('email', String)
password = Column('password', String)
bio = Column('bio', String)
signature = Column('signature', String)
table = Column('table', JSON, nullable=False, default={})
info_table = Column('info_table', JSON, nullable=False, default={})
permissions = Column('permissions', Integer, default=4)
privkey = Column('privkey', String)
pubkey = Column('pubkey', String)
enabled = Column('enabled', Boolean, default=True)
silenced = Column('silenced', Boolean, default=False)
suspended = Column('suspended', Boolean, default=False)
actor = Column('actor', String, nullable=False)
inbox = Column('inbox', String, nullable=False)
config = Column('config', JSON, default={'private': False})
timestamp = Column('timestamp', DateTime)
def __repr__(self):
return f'Domain(id={self.id}, handle="{self.handle}", name="{self.name}")'
return f'User(id={self.id}, handle="{self.handle}", name="{self.name}")'
class Status(Base):
@ -156,6 +160,38 @@ class Config(Base):
return f'Config(id={self.id}, key="{self.key}", value="{self.value}", type="{self.type}")'
class Bans(Base):
__tablename__ = 'bans'
id = Column('id', Integer, primary_key=True, autoincrement=True)
handle = Column('handle', String)
domain = Column('domain', String)
bantype = Column('bantype', String, default='silence')
reject_media = Column('reject_media', Boolean, default=False)
reason = Column('reason', String)
timestamp = Column('timestamp', DateTime)
def __repr__(self):
if self.handle:
return f'UserBan(id={self.id}, handle="{self.handle}", domain="{self.domain}", bantype="{self.bantype}", reason="{self.reason}")'
else:
return f'DomainBan(id={self.id}, domain="{self.domain}", bantype="{self.bantype}", reason="{self.reason}")'
class IPBlocks(Base):
__tablename__ = 'ipblocks'
id = Column('id', Integer, primary_key=True, autoincrement=True)
address = Column('address', String, nullable=False)
cidr = Column('cidr', Integer)
timestamp = Column('timestamp', DateTime)
def __repr__(self):
return f'IPBlock(id={self.id}, address="{self.address}", cidr="{self.cidr}")'
table.update({
'app': App,
'config': Config,
@ -165,5 +201,7 @@ table.update({
'object': Object,
'status': Status,
'token': Token,
'user': User
'user': User,
'bans': Bans,
'ipblocks': IPBlocks
})

View file

@ -2,21 +2,21 @@
%html
%head
%title << {{settings.name}}: {{page}}
%link rel='stylesheet' type='text/css' href='{{var.base}}/style-{{color_theme}}-{{CssTimestamp("style")}}.css'
%link rel='stylesheet' type='text/css' href='{{var.base}}/style-{{CssTimestamp("style")}}.css'
%link rel='manifest' href='{{var.base}}/manifest.json'
%meta charset='UTF-8'
%meta name='viewport' content='width=device-width, initial-scale=1'
%body
#body
#header.grid-container
#header.flex-container
-if menu_left
#btn.grid-item.section << Menu
.page-title.grid-item.section -> %a.title href='{{var.base}}/' << {{settings.name}}
#btn.section
.page-title.section -> %a.title href='{{var.base}}/' << {{settings.name}}
-else
.page-title.grid-item.section -> %a.title href='{{var.base}}/' << {{settings.name}}
#btn.grid-item.section << Menu
.page-title.section -> %a.title href='{{var.base}}/' << {{settings.name}}
#btn.section
-if message
#message.section << {{message}}
@ -25,44 +25,45 @@
#error.secion << {{error}}
#menu.section
.title-item.item << Menu
#items
.item -> %a href='{{var.base}}/' << Home
-if settings.rules
%a.item href='{{var.base}}/about/rules' << Rules
%a.item href='{{var.base}}/about' << About
-if cookie_user
.item.name -> %a href='{{var.base}}/@{{cookie_user.handle}}' << Profile
%details
%summary.item << Settings
.item -> %a href='{{var.base}}/settings/profile' << Profile
.item -> %a href='{{var.base}}/logout' << Logout
%a.item.name href='{{var.base}}/@{{cookie_user.handle}}' << Profile
%details open
%summary.item << Preferences
%a.item.sub-item href='{{var.base}}/preferences/profile' << Profile
-if cookie_user and cookie_user.permissions < 3
%details open
%summary.item << Admin
%a.item.sub-item href='{{var.base}}/admin/dashboard' << Dashboard
%a.item.sub-item href='{{var.base}}/admin/settings' << Settings
%a.item href='{{var.base}}/logout' << Logout
-else
.item -> %a href='{{var.base}}/login' << Login
.item -> %a href='{{var.base}}/register' << Register
.item -> %a href='{{var.base}}/about/rules' << Rules
.item -> %a href='{{var.base}}/about' << About
;#content.grid-container
;#menu.grid-item.section
; %ul.menu
; %li -> %a href='{{var.base}}/' << Home
; %li -> %a href='{{var.base}}/about/rules' << Rules
; %li -> %a href='{{var.base}}/about' << About
%a.item href='{{var.base}}/login' << Login
%a.item href='{{var.base}}/register' << Register
#content-body.section
-block content
#footer.grid-container.section
.avatar.grid-item
-if cookie_user
%img src='{{var.base}}/{{cookie_user.avatar}}'
-if cookie_user
.avatar
%img src='{{var.base}}/{{cookie_user.avatar}}?20'
.user.grid-item
-if cookie_user
=cookie_user.name
-else
.username << Guest
.user
%a href='{{base}}/@{{cookie_user.handle}}' -> =cookie_user.name
-else
.user
Guest
.source.grid-item
%span
%a href='https://git.barkshark.xyz/izaliamae/social' target='_new' << Barkshark Social/{{config.version}}
.source
%a href='https://git.barkshark.xyz/izaliamae/social' target='_new' << Barkshark Social/{{config.version}}
%script type='application/javascript' src='{{var.base}}/static/menu.js'
%script type='application/javascript' src='{{var.base}}/static/js/menu.js'

View file

@ -2,7 +2,7 @@
.header.grid-container
.grid-item.avatar
%a href='{{var.base}}/@{{status.user.handle}}'
%img src="{{var.base}}/{{status.user.avatar}}"
%img src="{{var.base}}/{{status.user.avatar}}?50"
.grid-item
.user-info
.user -> %a href='{{var.base}}/@{{status.user.handle}}' -> =status.user.name

View file

@ -0,0 +1,11 @@
-extends 'base.haml'
-set page = 'Admin: Panel'
-block content
#admin
.title << Admin Panel
.info
.flex-item << Local users: {{local_users}}
.flex-item << Remote users: {{remote_users}}
.flex-item << Local statuses: {{local_statuses}}
.flex-item << Remote statuses: {{remote_statuses}}
.flex-item << Database size: {{dbsize}}

View file

@ -0,0 +1,63 @@
-extends 'base.haml'
-set page = 'Admin: Instance Settings'
-block content
#admin.settings
.title << Instance Settings
%form#settings method='POST' enctype='multipart/form-data' autocomplete='off' action='{{var.base}}/admin/settings'
.grid-container
.grid-item.label
%lable for='name' << Instance Name
.grid-item.input
%input.tooltip id='name' name='name' placeholder='Barkshark Social' value='{{settings.name}}'
.grid-item.label
%lable for='admin' << Admin Contact
.grid-item.input
%select id='admin' name='admin' value='{{settings.admin}}'
%option value='none' << None
-for admin in admins
-if admin == settings.admin
%option value='{{admin}}' selected << {{admin}}
-else
%option value='{{admin}}' << {{admin}}
.grid-item.label
%label for='robotstxt' << Robots.txt
.grid-item.input
%textarea id='robotstxt' name='robotstxt' << {{settings.robotstxt if settings.robotstxt else ""}}
.grid-item.label
%label for='description' << Instance Description
.grid-item.input
%textarea id='description' name='description' << {{settings.description if settings.description else ""}}
.grid-item.label
%label for='short_description' << Short Description
.grid-item.input
%input id='short_description' name='short_description' placeholder='a short blurb about the instance' value="{{settings.short_description if settings.short_description else ''}}"
.grid-item.label
.grid-item.input
.bools
%input.tooltip id='open' name='open' type='checkbox' {{'checked' if settings.open}}
%label for='open' << Registration Enabled
%br
%input.tooltip id='require_approval' name='require_approval' type='checkbox' {{'checked' if settings.require_approval}}
%label for='require_approval' << Require Registration Approval
%br
%input.tooltip id='domain_approval' name='domain_approval' type='checkbox' {{'checked' if settings.domain_approval}}
%label for='domain_approval' << Require Instance Approval
%br
%input.tooltip id='secure' name='secure' type='checkbox' {{'checked' if settings.secure}}
%label for='secure' << Secure Mode
%center -> %input.submit type='submit' value='Submit'

View file

@ -1,4 +1,4 @@
-extends 'base.haml'
-set page = 'Home'
-block content
{{settings.get('description')}}
{{settings.description.replace('\n', '<br />\n')}}

View file

@ -15,4 +15,5 @@
%input type='password' name='password' placeholder='Password'
%input type='hidden' name='redir' value='{{redir.path}}'
%input type='hidden' name='redir_data' value='{{redir.data}}'
%input type='hidden' name='login_type' value='password'
%input type='submit' value='Login'

View file

@ -0,0 +1,21 @@
-extends 'base.haml'
-set page = 'Login'
-block content
.title << Login
%form action='{{var.base}}/login' method='post' id='logreg_form'
%center
.error.message
-if msg
{{msg}}
-else
%br
%input type='text' name='username' placeholder='Username'
%input type='password' name='password' placeholder='Password'
%input type='hidden' name='redir' value='{{redir.path}}'
%input type='hidden' name='redir_data' value='{{redir.data}}'
%input type='hidden' name='login_type' value='keypair'
%input#login_value type='hidden' name='login_value' value='{{rand}}'
%input type='hidden' name='encrypt_value' value='{{str(uuid4)}}'
%input type='submit' value='Login'

View file

@ -0,0 +1,7 @@
-extends 'base.haml'
-set page = 'Moderation: Instance Bans'
-block content
#mod.bans
.title << Instance Bans
heck

View file

@ -0,0 +1,59 @@
-extends 'base.haml'
-set page = 'Settings - Profile'
-block content
.settings
.title << Settings
%form#preferences method='POST' enctype='multipart/form-data' autocomplete='off' action='{{var.base}}/preferences/profile'
.grid-container
.grid-item.label.img
%label for='avatar' << Avatar
.grid-item.input
%input id='avatar' name='avatar' type='file'
%img src='{{var.base}}/{{cookie_user.avatar}}?100'
.grid-item.label.img
%label for='header_img' << Header
.grid-item.input
%input id='header_img' name='header' type='file'
%img src='{{var.base}}/{{cookie_user.header}}?height=100'
.grid-item.label
%label for='name' << Display Name
.grid-item.input
%input id='name' name='name' placeholder='Funny display name goes here' value='{{cookie_user.name if cookie_user.bio else ""}}'
.grid-item.label
%label for='email' << E-Mail
.grid-item.input
%input id='email' name='email' type='email' placeholder='user@domain.com' value='{{cookie_user.email if cookie_user.email else ""}}'
.grid-item.label
%label for='password' << Password
.grid-item.input
%input id='password' name='oldpassword' type='password' placeholder='Current password'
%input name='password1' type='password' placeholder='New password'
%input name='password2' type='password' placeholder='New password again'
.grid-item.label
%label for='bio' << Bio
.grid-item.input
%textarea id='bio' name='bio' << {{cookie_user.bio if cookie_user.bio else ""}}
.grid-item.label
%label for='theme' << Color Theme
.grid-item.input
%select id='theme' name='color_theme'
-for color in color_themes
-if (cookie_user and color == cookie_user.config.theme) or (color == 'pink' and (not cookie_user or not cookie_user.config.theme))
%option value='{{color}}' selected << {{color.capitalize()}}
-else
%option value='{{color}}' << {{color.capitalize()}}
.grid-item.label
.grid-item.input
.bool
%input id='private' name='private' type='checkbox' {{'checked' if cookie_user.config.get("private", False)}}
%label for='private' << Locked Account
%center -> %input.submit type='submit' value='Submit'

View file

@ -3,33 +3,39 @@
-block content
#profile
.header -> %img src='{{var.base}}/{{user.header}}'
.grid-container
.grid-item.icon -> %img src='{{var.base}}/{{user.avatar}}'
#user.grid-container
.grid-item.icon -> %img src='{{var.base}}/{{user.avatar}}?100'
.grid-item.info
.name -> =user.name
.handle -> @{{user.handle}}@{{user.domain}}
.bio -> -if user.bio -> =user.bio
#tablediv
%table#table
-if user.table
-for key, value in user.table.items()
-if user.info_table
#tablediv
%table#table
-for key, value in user.info_table.items()
%tr
%td.key << {{key}}
%td.value << {{value}}
-else
%tr
%td.key << empty
%td.value << table
%tr
%td.key << another
%td.value << row
#posts
.title << Posts
.pagination
-if page_data.has_previous()
%a.prev href='{{var.base}}/@{{user.handle}}?page={{page_data.previous_page_number}}' << Previous
-if page_data.has_next()
%a.next href='{{var.base}}/@{{user.handle}}?page={{page_data.next_page_number}}' << Next
-if not posts
%center -> No posts :/
-else
-for status in posts
-include 'component/status.haml'
.pagination
-if page_data.has_previous()
%a.prev href='{{var.base}}/@{{user.handle}}?page={{page_data.previous_page_number}}' << Previous
-if page_data.has_next()
%a.next href='{{var.base}}/@{{user.handle}}?page={{page_data.next_page_number}}' << Next

View file

@ -1,5 +1,9 @@
-extends 'base.haml'
-set page = 'Rules'
-block content
.title << Rules
{{text}}
#rules
.title << Rules
-if settings.rules
=settings.rules
-else
Not set

View file

@ -1,45 +0,0 @@
-extends 'base.haml'
-set page = 'Settings - Profile'
-block content
.title << Settings
%form#settings method='POST' enctype='multipart/form-data' autocomplete='off' action='{{var.base}}/settings/profile'
.grid-container
.grid-item.avatar
-if cookie_user.avatar == 'static/one.png'
%img src='{{var.base}}/static/missing_avatar.svg'
-else
%img src='{{var.base}}/{{cookie_user.avatar}}'
%label for='avatar' << Avatar
%input name='avatar' type='file'
.grid-item.header
-if cookie_user.header == 'static/one.png'
%img src='{{var.base}}/static/missing_header.svg'
-else
%img src='{{var.base}}/{{cookie_user.header}}'
%label for='header' << Header
%input name='header' type='file'
.grid-item.name
%label for='name' << Display Name
%input name='name' placeholder='Funny display name goes here' value='{{cookie_user.name if cookie_user.bio else ""}}'
.grid-item.email
%label for='email' << E-Mail
%input name='email' type='email' placeholder='user@domain.com' value='{{cookie_user.email if cookie_user.email else ""}}'
.grid-item.password
%label for='password' << Password
%input name='oldpassword' type='password' placeholder='Current password'
%input name='password1' type='password' placeholder='New password'
%input name='password2' type='password' placeholder='New password again'
.grid-item.bio
%label for='bio' << Bio
%textarea name='bio' << {{cookie_user.bio if cookie_user.bio else ""}}
.grid-item.private
%label for='private' << Locked Account
%input name='private' type='checkbox' {{'checked' if cookie_user.config.get("private", False)}}
%center -> %input.submit type='submit' value='Submit'

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="menu.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
id="svg8"
version="1.1"
viewBox="0 0 132.29167 79.375002"
height="300"
width="500">
<defs
id="defs2" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="36"
inkscape:window-x="36"
inkscape:window-height="990"
inkscape:window-width="1644"
units="px"
showgrid="true"
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:document-units="mm"
inkscape:cy="151.34478"
inkscape:cx="232.18877"
inkscape:zoom="1.4"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:snap-text-baseline="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
dotted="true"
id="grid1402"
type="xygrid"
originx="-7.9375001"
originy="-27.781234" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
id="layer2"
inkscape:groupmode="layer"
transform="translate(-7.9374999,-27.781233)">
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,67.468765 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590" />
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,35.718766 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590-7" />
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,99.218766 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590-7-8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,199 @@
const login_value = document.getElementById("login_value").value
const encodedMessage = enc.encode('hello');
const localStorage.setItem('privkey', privateKey);
const localStorage.setItem('pubkey', publicKey);
var privateKey;
var publicKey;
var iv;
function asciiToUint8Array(str) {
var chars = [];
for (var i = 0; i < str.length; ++i)
chars.push(str.charCodeAt(i));
return new Uint8Array(chars);
}
function RSAPSS_Sign(plainText) {
var cryptoObj = window.crypto || window.msCrypto;
if(!cryptoObj)
{
alert("Crypto API is not supported by the Browser");
return;
}
window.crypto.subtle.generateKey({
name: 'RSA-PSS',
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: 'SHA-256'},
},
true,
['sign', 'verify']
)
.then(function(key) {
publicKey = key.publicKey;
privateKey = key.privateKey;
// For Demo Purpos Only Exported in JWK format
window.crypto.subtle.exportKey('jwk', key.publicKey).then(
function(keydata) {
publicKeyhold = keydata;
publicKeyJson = JSON.stringify(publicKeyhold);
document.getElementById("rsapublic").value = publicKeyJson;
}
);
window.crypto.subtle.exportKey("jwk", key.privateKey).then(
function(keydata) {
privateKeyhold = keydata;
privateKeyJson = JSON.stringify(privateKeyhold);
document.getElementById("rsaprivate").value = privateKeyJson;
}
);
window.crypto.subtle.sign({
name: "RSA-PSS",
saltLength: 128, //the length of the salt
},
privateKey, //from generateKey or importKey above
asciiToUint8Array(plainText) //ArrayBuffer of data you want to sign
)
.then(function(signature) {
//returns an ArrayBuffer containing the signature
document.getElementById("cipherText").value = bytesToHexString(signature);
})
.catch(function(err) {
console.error(err);
});
})
.catch(function(err) {
console.error(err);
});
}
function RSAPSS_Verify() {
var cryptoObj = window.crypto || window.msCrypto;
if(!cryptoObj)
{
alert("Crypto API is not supported by the Browser");
return;
}
var cipherText = document.getElementById("cipherText").value;
var plainText = document.getElementById("plainText").value;
if(!publicKey)
{
alert("Generate RSA-PSS Keys First")
return;
}
window.crypto.subtle.verify({
name: "RSA-PSS",
saltLength: 128, //the length of the salt
},
publicKey, //from generateKey or importKey above
hexStringToUint8Array(cipherText), //ArrayBuffer of the data
asciiToUint8Array(plainText)
)
.then(function(decrypted) {
alert("Verified " + decrypted);
})
.catch(function(err) {
console.error(err);
});
}
function bytesToASCIIString(bytes) {
return String.fromCharCode.apply(null, new Uint8Array(bytes));
}
function bytesToHexString(bytes) {
if (!bytes)
return null;
bytes = new Uint8Array(bytes);
var hexBytes = [];
for (var i = 0; i < bytes.length; ++i) {
var byteString = bytes[i].toString(16);
if (byteString.length < 2)
byteString = "0" + byteString;
hexBytes.push(byteString);
}
return hexBytes.join("");
}
function hexStringToUint8Array(hexString) {
if (hexString.length % 2 != 0)
throw "Invalid hexString";
var arrayBuffer = new Uint8Array(hexString.length / 2);
for (var i = 0; i < hexString.length; i += 2) {
var byteValue = parseInt(hexString.substr(i, 2), 16);
if (byteValue == NaN)
throw "Invalid hexString";
arrayBuffer[i / 2] = byteValue;
}
return arrayBuffer;
}
function failAndLog(error) {
console.log(error);
alert(error);
}
function sign_uuid() {
var uuid = document.getElementById("plainText").value;
}
form.addEventListener('submit', function(e) {
e.preventDefault();
});
function generate_key() {
keypair = window.crypto.subtle.generateKey(
{
name: 'RSA-PSS',
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: 'SHA-256'},
},
true,
['sign', 'verify']
)
return keypair.then(function(key) {
publicKey = key.publicKey;
privateKey = key.privateKey;
privkey = window.crypto.subtle.exportKey('jwk', key.publicKey)
return privkey.then(
function(keydata) {
publicKeyhold = keydata;
publicKeyJson = JSON.stringify(publicKeyhold);
return publicKeyJson;
}
);
});
};
generate_key()

View file

@ -1,12 +1,11 @@
const sidebarBox = document.querySelector('#menu'),
sidebarBtn = document.querySelector('#btn'),
pageWrapper = document.querySelector('#body');
pageWrapper = document.querySelector('html');
header = document.querySelector('#header')
sidebarBtn.addEventListener('click', event => {
sidebarBtn.classList.toggle('active');
sidebarBox.classList.toggle('active');
header.classList.toggle('active');
});
pageWrapper.addEventListener('click', event => {
@ -18,7 +17,6 @@ pageWrapper.addEventListener('click', event => {
if (sidebarBox.classList.contains('active') && (indexId == -1 && indexClass == -1)) {
sidebarBtn.classList.remove('active');
sidebarBox.classList.remove('active');
header.classList.remove('active');
}
});
@ -27,6 +25,5 @@ window.addEventListener('keydown', event => {
if (sidebarBox.classList.contains('active') && event.keyCode === 27) {
sidebarBtn.classList.remove('active');
sidebarBox.classList.remove('active');
header.classList.remove('active');
}
});

View file

@ -1,11 +1,266 @@
{% set background = '#191919' %}
{% set primary = theme %}
{% set text = lighten(desaturate(primary, 0.25), 0.5) %}
{% set positive = '#ada' %}
{% set negative = '#daa' %}
{% set speed = 250 %}
:root {
--text: #eee;
--hover: {{lighten(desaturate(primary, 0.5), 0.5)}};
--primary: {{primary}};
--background: {{background}};
--ui: {{lighten(desaturate(primary, 0.25), 0.5)}};
--ui-background: {{lighten(background, 0.075)}};
--shadow-color: {{rgba('#000', 0.5)}};
--shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
--negative: {{negative}};
--negative-dark: {{darken(negative, 0.85)}};
--positive: {{positive}};
--positive-dark: {{darken(positive, 0.85)}};
--message: var(--positive);
--error: var(--negative);
--gap: 15px;
/* --easing: cubic-bezier(.6, .05, .28, .91); */
--trans-speed: {{speed}}ms;
}
body {
color: var(--text);
background-color: var(--background);
font-family: sans undertale;
font-size: 16px;
margin: 15px 0;
}
a, a:visited {
color: var(--primary);
text-decoration: none;
}
a:hover {
color: var(--hover);
text-decoration: underline;
}
input:not([type='checkbox']), select, textarea {
margin: 1px 0;
color: var(--text);
background-color: var(--background);
border: 1px solid var(--background);
box-shadow: 0 2px 2px 0 var(--shadow-color);
}
input:hover, select:hover, textarea:hover {
border-color: var(--hover);
}
input:focus, select:focus, textarea:focus {
outline: 0;
border-color: var(--primary);
}
details:focus, summary:focus {
outline: 0;
}
/* Classes */
.grid-container {
display: grid;
grid-template-columns: auto;
grid-gap: var(--gap);
}
.grid-item {
display: inline-grid;
}
.flex-container {
display: flex;
flex-wrap; wrap;
}
.menu {
list-style-type: none;
padding: 0;
margin: 0;
}
.menu li {
display: inline-block;
text-align: center;
min-width: 60px;
background-color: {{lighten(background, 0.2)}};
}
.menu li a {
display: block;
padding-left: 5px;
padding-right: 5px;
}
.menu li:hover {
background-color: {{desaturate(lighten(primary, 0.25), 0.25)}};
}
.menu li a:hover {
text-decoration: none;
color: {{desaturate(darken(primary, 0.90), 0.5)}};
}
.section {
padding: 8px;
background-color: var(--ui-background);
box-shadow: var(--shadow);
}
.shadow {
box-shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
}
.message {
line-height: 2em;
display: block;
}
/* # this is kinda hacky and needs to be replaced */
.tooltip:hover::after {
position: relative;
padding: 8px;
bottom: 35px;
border-radius: 5px;
white-space: nowrap;
border: 1px solid var(--text);
color: var(--text);
background-color: {{darken(desaturate(primary, 0.5), 0.75)}};
box-shadow: var(--shadow);
/*z-index: -1;*/
}
/* Nunito Sans */
/* ids */
#message, #error {
padding: 10px;
color: var(--background);
margin-bottom: var(--gap);
text-align: center;
}
#message {
background-color: var(--message);
}
#error {
background-color: var(--error);
}
#body {
width: 790px;
margin: 0 auto;
}
#header {
display: flex;
margin-bottom: var(--gap);
text-align: center;
font-size: 2em;
line-height: 40px;
font-weight: bold;
}
#header > div {
/*display: inline-block;*/
height: 40px;
}
#header .page-title {
text-align: {% if menu_left %}right{% else %}left{% endif %};
white-space: nowrap;
overflow: hidden;
width: 100%;
}
#content-body .title {
text-align: center;
font-size: 1.5em;
font-weight: bold;
color: var(--primary)
}
#footer {
margin-top: var(--gap);
display: flex;
grid-gap: 5px;
font-size: 0.80em;
line-height: 20px;
}
#footer > div {
height: 20px;
}
#footer .avatar img {
margin: 0 auto;
}
#footer .user {
white-space: nowrap;
overflow: hidden;
width: 100%;
}
#footer .source {
white-space: nowrap;
}
{% for file in cssfiles %}
{% include 'style/'+file.name %}
{% endfor %}
/* responsive design */
@media (max-width: 810px) {
body {
margin: 0;
}
#body {
width: auto;
}
#logreg_form input:not(input[submit]), #logreg_form textarea {
width: 75%;
}
}
@media (max-width: 610px) {
.settings .grid-container {
grid-template-columns: auto;
}
.settings .label {
text-align: center;
}
}
/*@media screen and (orientation:portrait) and (max-width: 610px) {
#menu {
width: 100%;
}
#menu .item {
text-align: center;
}
}*/
/* scrollbar */
body {scrollbar-width: 15px; scrollbar-color: var(--primary) {{darken(background, 0.1)}};}
::-webkit-scrollbar {width: 15px;}
::-webkit-scrollbar-track {background: {{darken(background, 0.1)}};}
/*::-webkit-scrollbar-button {background: var(--primary);}
::-webkit-scrollbar-button:hover {background: var(--text);}*/
::-webkit-scrollbar-thumb {background: var(--primary);}
::-webkit-scrollbar-thumb:hover {background: {{lighten(primary, 0.25)}};}
/* page font */
@font-face {
font-family: 'sans undertale';
src: local('Nunito Sans Bold'),
@ -41,221 +296,3 @@
font-weight: normal;
font-style: normal;
}
:root {
--text: {{text}};
--hover: {{lighten(desaturate(primary, 0.9), 0.5)}};
--primary: {{primary}};
--background: {{background}};
--ui-background: {{lighten(background, 0.075)}};
--shadow-color: {{rgba('#000', 0.5)}};
--message: {{positive}};
--error: {{negative}};
--gap: 15px;
--easing: cubic-bezier(.6, .05, .28, .91);
--trans-speed: 500ms;
}
body {
color: var(--text);
background-color: var(--background);
font-family: sans undertale;
font-size: 16px;
margin: 15px 0;
}
a, a:visited {
color: var(--primary);
text-decoration: none;
}
a:hover {
color: var(--hover);
text-decoration: underline;
}
input:not([type='checkbox']), textarea {
margin: 1px 0;
color: var(--text);
background-color: var(--background);
border: 1px solid var(--background);
box-shadow: 0 2px 2px 0 var(--shadow-color);
}
input:focus, textarea:focus {
/* margin: 1px; */
border: 1px solid var(--hover);
}
details:focus, summary:focus {
outline: 0;
}
/* Classes */
.grid-container {
display: grid;
grid-template-columns: auto;
grid-gap: var(--gap);
}
.grid-item {
display: inline-grid;
}
.menu {
list-style-type: none;
padding: 0;
margin: 0;
}
.menu li {
display: inline-block;
text-align: center;
min-width: 60px;
background-color: {{lighten(background, 0.2)}};
}
.menu li a {
display: block;
padding-left: 5px;
padding-right: 5px;
}
.menu li:hover {
background-color: {{desaturate(lighten(primary, 0.25), 0.25)}};
}
.menu li a:hover {
text-decoration: none;
color: {{desaturate(darken(primary, 0.90), 0.5)}};
}
.section {
padding: 8px;
background-color: var(--ui-background);
box-shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
}
.shadow {
box-shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color);
}
.message {
line-height: 2em;
display: block;
}
/* ids */
#message, #error {
padding: 10px;
color: var(--background);
margin-bottom: var(--gap);
text-align: center;
}
#message {
background-color: var(--message);
}
#error {
background-color: var(--error);
}
#body {
width: 790px;
margin: 0 auto;
}
#header {
grid-template-columns: {% if menu_left %}100px auto{% else %}auto 100px{% endif %};
margin-bottom: var(--gap);
text-align: center;
font-size: 2em;
line-height: 1.2em;
font-weight: bold;
}
#header .page-title {
text-align: {% if menu_left %}right{% else %}left{% endif %};
}
#header.active {
grid-template-columns: auto;
}
#footer {
grid-template-columns: 20px auto auto;
grid-gap: 5px;
font-size: 0.80em;
line-height: 20px;
}
#footer .source {
text-align: right;
}
#footer .avatar img {
max-height: 20px;
max-width: 20px;
margin: 0 auto;
}
#content {
grid-template-columns: 125px auto;
margin-bottom: var(--gap);
}
#content-body {
margin-bottom: var(--gap);
}
#content-body .title {
text-align: center;
font-size: 1.5em;
font-weight: bold;
color: var(--primary)
}
{% for file in cssfiles %}
{% include 'style/'+file.name %}
{% endfor %}
/* responsive design */
@media (max-width: 810px) {
body {
margin: 0;
}
#body {
width: auto;
}
#content{
grid-template-columns: auto;
}
#logreg_form input:not(input[submit]), #logreg_form textarea {
width: 75%;
}
}
@media (max-width: 610px) {
#settings .grid-container {
grid-template-columns: auto;
}
}
/*@media screen and (orientation:portrait) and (max-width: 610px) {
#menu {
width: 100%;
}
#menu .item {
text-align: center;
}
}*/

View file

@ -0,0 +1,11 @@
/* admin pages */
#admin .info {
display: flex;
flex-wrap: wrap;
}
#admin .info .flex-item {
width: 250px;
white-space: nowrap;
}

View file

@ -6,26 +6,33 @@
#btn {
transition: background-color var(--trans-speed);
width: 55px;
margin-left: var(--gap);
background-image: url('{{base}}/static/img/menu.svg');
background-size: 50px;
background-position: center center;
background-repeat: no-repeat;
}
#btn div {
transition: transform 500ms var(--easing), opacity 500ms, background-color var(--trans-speed);
transition: transform var(--trans-speed) ease, opacity var(--trans-speed), background-color var(--trans-speed);
}
#btn.active {
margin-left: 0;
position: fixed;
z-index: 5;
top: 12px;
{% if menu_left %}left: 12px{% else %}right: 12px;{% endif %};
background-color: {{positive}};
{% if menu_left %}right: calc(100% - 250px + 12px){% else %}right: 12px;{% endif %};
background-color: {{darken(primary, 0.75)}};
color: {{background}};
}
#btn.active div {
/*#btn.active div {
width: 35px;
height: 2px;
margin-bottom: 8px;
}
}*/
#btn.active:parent {
grid-template-columns: auto;
@ -41,33 +48,17 @@
padding: 20px 0px;
width: 250px;
height: 100%;
transition: all 350ms var(--easing);
transition: all var(--trans-speed) ease;
{% if menu_left %}left{% else %}right{% endif %}: -250px;
}
#menu.active {
{% if menu_left %}left{% else %}right{% endif %}: 0;
opacity: 1;
}
{% if menu_left %}
#menu {
left: -100%;
}
#menu.active {
left: 0px;
}
{% else %}
#menu {
right: -100%;
}
#menu.active {
right: 0px;
}
{% endif %}
#menu #items {
margin-top: 50px;
/*margin-top: 50px;*/
margin-bottom: 30px;
}
@ -75,48 +66,62 @@
text-decoration: none;
}
#menu {
font-weight: bold;
}
#menu .item {
display: block;
position: relative;
font-size: 2em;
transition: all var(--trans-speed);
padding-left: 20px;
}
#menu .item:not(summary) {
#menu .title-item {
color: var(--primary);
}
#items .sub-item {
padding-left: 40px;
}
#menu details .item:not(summary) {
padding-left: 55px;
#items .item:not(.title-item):hover {
padding-left: 40px;
}
#menu details {
padding-left: 5px;
#items .sub-item:hover {
padding-left: 60px !important;
}
#menu .item:not(summary):hover {
padding-left: 55px;
}
/*#menu details .item:hover {
padding-left: 60px;
}*/
#menu details .item:not(summary):hover {
padding-left: 70px;
}
#menu summary {
#items summary {
cursor: pointer;
color: var(--primary);
}
#menu summary:hover {
color: var(--text);
padding-left: 20px;
}
#menu details[open]>.item:not(details):not(summary) {
#items details[open]>.item:not(details) {
animation-name: fadeInDown;
animation-duration: var(--trans-speed);
}
#items summary::-webkit-details-marker {
display: none;
}
#items details summary:after {
content: " +";
}
#items details[open] summary:after {
content: " -";
}
#btn, #btn * {
will-change: transform;
}

View file

@ -1,5 +1,5 @@
/* Profile page */
#profile .grid-container {
#profile #user.grid-container {
grid-template-columns: 100px auto;
}
@ -48,3 +48,35 @@
#posts .title {
margin: var(--gap) 0;
}
#profile .pagination {
margin: var(--gap) 0;
text-align: right;
}
#profile .pagination a {
padding: 8px;
box-shadow: var(--shadow)
}
#profile .pagination a:hover {
text-decoration: none;
}
#profile .pagination .prev {
background-color: var(--negative-dark);
}
#profile .pagination .next {
background-color: var(--positive-dark);
}
#profile .pagination .prev:hover {
color: var(--background);
background-color: var(--negative);
}
#profile .pagination .next:hover {
color: var(--background);
background-color: var(--positive);
}

View file

@ -1,29 +1,50 @@
/* Settings pages */
#settings .grid-container {
grid-template-columns: auto auto;
grid-gap: var(--gap);
/* generic settings */
.settings .grid-container {
grid-template-columns: 150px auto;
}
#settings .avatar img {
margin: 0 auto;
height: 100px;
.settings .label {
text-align: right;
}
#settings .header img {
height: 100px;
margin: 0 auto;
}
#settings input {
.settings input {
padding: 5px;
}
#settings textarea {
.settings textarea {
height: 5em;
resize: vertical;
}
#settings .submit {
.settings .submit {
margin-top: var(--gap);
}
.settings .input img {
margin: 20px auto 0 auto;
}
/* admin settings page */
{% if cookie_user and cookie_user.config.get('enable_tooltips', True) %}
#settings #name:hover::after{
content: "Instance info displayed on the homepage";
}
#settings #open:hover::after {
content: "Allow new user registrations";
}
#require_approval:hover::after {
content: "Require an admin or mod to approve new accounts";
}
#domain_approval:hover::after {
content: "Require an admin or mod to approve an instance for federation";
/*left: -100px;*/
}
#secure:hover::after {
content: "Require other instances to sign GETs";
}
{% endif %}

View file

@ -1,5 +1,5 @@
.status .header {
grid-template-columns: 100px auto;
grid-template-columns: 50px auto;
margin-bottom: 15px;
}
@ -9,8 +9,8 @@
}
.status .avatar img {
max-width: 100px;
max-height: 100px;
max-width: 50;
/* max-height: 100px; */
}
.status .user-info {

View file

@ -1,19 +1,26 @@
import json, magic, os, re, sys, sanic, socket, yaml
import functools, json, magic, os, re, sys, sanic, socket, yaml
import tarfile, tempfile, zipfile
from IzzyLib import logging
from IzzyLib.http import Client
from IzzyLib.misc import DotDict
from sanic import response
from colour import Color
from datetime import datetime
from Crypto.Hash import SHA3_512, SHA3_256, BLAKE2b
from Crypto.Hash import SHA3_512, SHA3_256, SHA256, BLAKE2b
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from IzzyLib import logging, color
from IzzyLib.http import HttpClient, SetClient
from IzzyLib.misc import DefaultDict, DotDict
from base64 import b64decode, b64encode
from sanic import response
from datetime import datetime
from hashlib import md5
from markdown import markdown
from urllib.error import HTTPError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
from .config import config
cache = DefaultDict()
css_check = lambda css_file : int(os.path.getmtime(config.frontend.join(f'{css_file}.css').str()))
cssts = DotDict()
hashalgs = {
'blake': BLAKE2b,
@ -22,13 +29,161 @@ hashalgs = {
'md5': md5
}
paths = DotDict({
'anon_api': [
'/api/v1/apps',
'/api/v1/instance'
],
'ap': [
'/status',
'/actor',
'/inbox'
],
'cookie': [
'/@',
'/:',
'/welcome',
'/preferences',
'/admin',
'/oauth/authorize'
]
})
css_check = lambda css_file : int(os.path.getmtime(config.frontend.join(f'{css_file}.css').str()))
client = Client(useragent=config.agent, timeout=15)
cssts = DotDict()
themes = {
'blue': {'primary': '#55e'},
'green': {'primary': '#2e5'},
'orange': {'primary': '#e52'},
'red': {'primary': '#e24'},
'pink': {'primary': '#e29'},
'purple': {'primary': '#a2e'},
'yellow': {'primary': '#ed2'}
}
def CheckForwarded(header):
def mkhash(string, salt=config.salt, alg='blake'):
hashalg = hashalgs.get(alg.lower())
if not hashalg:
raise KeyError(f'Not a valid hash algorithm: {alg}')
string = string.encode('UTF-8') if type(string) != bytes else string
salt = salt.encode('UTF-8') if type(salt) != bytes else salt
newhash = hashalg() if alg == 'md5' else hashalg.new()
newhash.update(string)
newhash.update(salt)
return newhash.hexdigest()
def ApDate(dtobject):
return dtobject.strftime('%Y-%m-%dT%H:%M:%SZ')
def CssTheme(theme):
custpath = config.data.join(f'themes/{theme}.yml')
defpath = config.frontend.join(f'themes/{theme}.yml')
if custpath.isfile():
themefile = custpath.str()
elif defpath.isfile():
themefile = defpath.str()
else:
themefile = None
if cssfile:
data = yaml.load(open(themefile, 'r'), Loader=yaml.FullLoader)
else:
data = {}
colors = {
'background': data.get('background', '#111'),
'primary': data.get('primary', '#3bf'),
'secondary': data.get('secondary', '#3bf'),
}
return colors
def CssTimestamp(*args):
timestamp = 0
for name in args:
filename = config.frontend.join(f'{name}.css')
timestamp += int(filename.mtime())
return timestamp
#def timestamp(integer=True):
#ts = datetime.timestamp(datetime.now())
#return int(ts) if integer == True else ts
def Error(request, code, msg=None):
message = msg if msg else ''
if any(map(request.path.startswith, ['/status', '/actor'])) or JsonCheck(request):
return JsonResp({'error': message}, code)
else:
cont_type = 'text/html'
data = {
'login_token': request.cookies.get('login_token') if not isinstance(request, str) else '',
'code': str(code),
'msg': msg
}
return config.template.response(f'error.haml', request, {'data': data, 'msg': message}, status=code)
def FetchActor(url, user=None):
if user:
actor = Client.signed_request(user.privkey, f'https://{config.web_domain}/actor/{user.handle}', url).json()
else:
actor = Client.request(url).json()
actor.web_domain = urlparse(url).netloc
actor.shared_inbox = actor.inbox
actor.pubkey = None
actor.handle = actor.preferredUsername
if actor.get('endpoints'):
actor.shared_inbox = actor.endpoints.get('sharedInbox', actor.inbox)
if actor.get('publicKey'):
actor.pubkey = actor.publicKey.get('publicKeyPem')
return actor
def FetchApi(domain):
return Client.request(f'https://{domain}/api/v1/instance').json()
@functools.lru_cache(maxsize=2048)
def FetchWebfingerAcct(handle, domain):
data = DefaultDict({'actor': None})
webfinger = Client.request(f'https://{domain}/.well-known/webfinger?resource=acct:{handle}@{domain}')
if not webfinger.body:
return
data.handle, data.domain = webfinger.json().subject.replace('acct:', '').split('@')
for link in webfinger.json().links:
if link['rel'] == 'self' and link['type'] == 'application/activity+json':
data.actor = link['href']
return data
def ForwardCheck(header):
forwarded = DotDict(valid=False)
for pair in header.split(';'):
@ -45,36 +200,19 @@ def CheckForwarded(header):
return forwarded
def boolean(raw_val):
val = raw_val.lower() if raw_val not in [None, True, False, 0, 1] else raw_val
def GenKey():
privkey = RSA.generate(2048)
pubkey = privkey.publickey()
if val in [True, False]:
return val
elif val in ['true', 'yes', 'enable', 'enabled', '1', 1]:
return True
elif val in ['false', 'no', 'disable', 'disabled', '0', 0, '', None]:
return False
else:
print('WARNING: Returning false for:', val)
return False
return DotDict({
'pubkey': pubkey,
'privkey': privkey,
'PUBKEY': pubkey.exportKey('PEM').decode(),
'PRIVKEY': privkey.exportKey('PEM').decode()
})
def mkhash(string, salt=config.salt, alg='blake'):
data = str(string + salt).encode('UTF-8')
hashalg = hashalgs.get(alg.lower())
if not hashalg:
raise KeyError(f'Not a valid hash algorithm: {alg}')
hash = hashalg.new()
hash.update(data)
return hash.hexdigest()
def get_ip():
def GetIp():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
@ -91,84 +229,24 @@ def get_ip():
return ip
def GenKey():
privkey = RSA.generate(2048)
pubkey = privkey.publickey()
return DotDict({
'pubkey': pubkey,
'privkey': privkey,
'PUBKEY': pubkey.exportKey('PEM').decode(),
'PRIVKEY': privkey.exportKey('PEM').decode()
})
def todate(ts):
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
def ApDate(dtobject):
return dtobject.strftime('%Y-%m-%dT%H:%M:%SZ')
def themes():
theme_list = []
for theme in os.listdir(f'{config.path}/themes'):
theme_list.append(theme.replace('.yml', ''))
theme_list.sort()
return theme_list
def CssTimestamp(*args):
timestamp = 0
for name in args:
filename = config.frontend.join(f'{name}.css')
timestamp += int(filename.mtime())
return timestamp
def timestamp(integer=True):
ts = datetime.timestamp(datetime.now())
return int(ts) if integer == True else ts
# Generate css file for color styling
def cssTheme(theme):
custpath = config.data.join(f'themes/{theme}.yml')
defpath = config.frontend.join(f'themes/{theme}.yml')
if custpath.isfile():
cssfile = custpath.str()
elif defpath.isfile():
cssfile = defpath.str()
else:
cssfile = None
if cssfile:
data = yaml.load(open(cssfile, 'r'), Loader=yaml.FullLoader)
else:
data = {}
colors = {
'background': data.get('background', '#111'),
'primary': data.get('primary', '#3bf'),
'secondary': data.get('secondary', '#3bf')
def GetTheme(name):
defaults = {
'primary': '#e7a',
'secondary': '#e7a',
'background': '#191919',
'positive': '#ada',
'negative': '#daa'
}
return colors
defaults.update(themes.get(name, {}))
return defaults
def JsonCheck(headers):
accept = headers.get('Accept')
def JsonCheck(request):
if any(map(request.path.startswith, ['/actor', '/inbox', '/status'])) or request.path.endswith('.json'):
return True
accept = request.headers.get('Accept')
if not accept:
return
@ -181,23 +259,6 @@ def JsonCheck(headers):
return False
def Error(request, code, msg=None):
message = msg if msg else ''
if any(map(request.path.startswith, ['/status', '/actor'])) or JsonCheck(request.headers):
return JsonResp({'error': message}, code)
else:
cont_type = 'text/html'
data = {
'login_token': request.cookies.get('login_token') if not isinstance(request, str) else '',
'code': str(code),
'msg': msg
}
return config.template.response(f'error.haml', request, {'data': data, 'msg': message}, status=code)
def JsonResp(data, status=200, headers=None, activity=False):
params = {
'content_type': 'application/activity+json' if activity else 'application/json',
@ -211,10 +272,22 @@ def JsonResp(data, status=200, headers=None, activity=False):
def ParseRequest(request, db):
request.ctx.parsed = True
request.ctx.media = False
request.ctx.query = DotDict(dict(request.query_args))
request.ctx.form = DotDict({k:v[0] for k,v in request.form.items()})
request.ctx.files = DotDict({k:v[0] for k,v in request.files.items()})
if any(map(request.path.startswith, ['/static', '/media', '/favicon'])):
request.ctx.media = True
return
request.ctx.api_path = request.path.startswith('/api')
request.ctx.api_anon_path = any(map(request.path.startswith, paths.anon_api))
request.ctx.ap_path = any(map(request.path.startswith, paths.ap))
request.ctx.cookie_path = any(map(request.path.startswith, paths.cookie))
request.ctx.json_path = JsonCheck(request)
with db.session() as s:
request.ctx.token = None
request.ctx.token_user = None
@ -234,53 +307,223 @@ def ParseRequest(request, db):
request.ctx.cookie = login_db['token']
request.ctx.cookie_user = login_db['user']
def ParseSig(signature):
if not signature:
logging.verbose('Missing signature header')
return
class color:
def __init__(self):
self.check = lambda color: Color(f'#{str(color)}' if re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', color) else color)
split_sig = signature.split(',')
sig = DefaultDict({})
def multi(self, multiplier):
if multiplier >= 1:
return 1
for part in split_sig:
key, value = part.split('=', 1)
sig[key.lower()] = value.replace('"', '')
elif multiplier <= 0:
return 0
if not sig.headers:
logging.verbose('Missing headers section in signature')
return
return multiplier
sig.headers = sig.headers.split()
def lighten(self, color, multiplier):
col = self.check(color)
col.luminance += ((1 - col.luminance) * self.multi(multiplier))
return col.hex_l
def darken(self, color, multiplier):
col = self.check(color)
col.luminance -= (col.luminance * self.multi(multiplier))
return col.hex_l
return sig
def saturate(self, color, multiplier):
col = self.check(color)
col.saturation += ((1 - col.saturation) * self.multi(multiplier))
return col.hex_l
def ParseText(text, style='markdown'):
'insert code to process text here'
if style == 'markdown':
return markdown(text)
def desaturate(self, color, multiplier):
col = self.check(color)
col.saturation -= (col.saturation * self.multi(multiplier))
def PkcsHeaders(key, headers, sig=None):
if sig:
head_items = [f'{item}: {headers[item]}' for item in sig.headers]
return col.hex_l
else:
head_items = [f'{k.lower()}: {v}' for k,v in headers.items()]
head_string = '\n'.join(head_items)
head_bytes = head_string.encode('UTF-8')
KEY = RSA.importKey(key)
pkcs = PKCS1_v1_5.new(KEY)
h = SHA256.new(head_bytes)
if sig:
return pkcs.verify(h, b64decode(sig.signature))
else:
return pkcs.sign(h)
def rgba(self, color, transparency):
col = self.check(color)
def StrDate(ts):
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
red = col.red*255
green = col.green*255
blue = col.blue*255
trans = self.multi(transparency)
return f'rgba({red:0.2f}, {green:0.2f}, {blue:0.2f}, {trans:0.2f})'
def Themes():
theme_list = []
for theme in os.listdir(f'{config.path}/themes'):
theme_list.append(theme.replace('.yml', ''))
theme_list.sort()
return theme_list
#def VerifyString(string, enc_string, pubkey, alg='SHA256'):
#KEY = RSA.importKey(key)
#pkcs = PKCS1_v1_5.new(KEY)
#h = SHA256.new(string.encode('UTF-8'))
#return pkcs.verify(h, b64decode(enc_string))
#class HttpClient(object):
#def __init__(self, headers={}, useragent='IzzyLib/0.3', proxy_type='https', proxy_host=None, proxy_port=None):
#proxy_ports = {
#'http': 80,
#'https': 443
#}
#if proxy_type not in ['http', 'https']:
#raise ValueError(f'Not a valid proxy type: {proxy_type}')
#self.headers=headers
#self.agent=useragent
#self.proxy = DotDict({
#'enabled': True if proxy_host else False,
#'ptype': proxy_type,
#'host': proxy_host,
#'port': proxy_ports[proxy_type] if not proxy_port else proxy_port
#})
#def __sign_request(self, request, privkey, keyid):
#request.add_header('(request-target)', f'{request.method.lower()} {request.path}')
#request.add_header('host', request.host)
#request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'))
#if request.body:
#body_hash = b64encode(SHA256.new(request.body).digest()).decode("UTF-8")
#request.add_header('digest', f'SHA-256={body_hash}')
#request.add_header('content-length', len(request.body))
##head_items = [f'{k.lower()}: {v}' for k,v in request.headers.items()]
##head_string = '\n'.join(head_items)
##PRIVKEY = RSA.importKey(privkey)
##pkcs = PKCS1_v1_5.new(PRIVKEY)
##h = SHA256.new(head_string.encode('UTF-8'))
#sig = {
#'keyId': keyid,
#'algorithm': 'rsa-sha256',
#'headers': ' '.join([k.lower() for k in request.headers.keys()]),
#'signature': b64encode(PkcsHeaders(privkey, request.headers)).decode('UTF-8')
#}
#sig_items = [f'{k}="{v}"' for k,v in sig.items()]
#sig_string = ','.join(sig_items)
#request.add_header('signature', sig_string)
#request.remove_header('(request-target)')
#request.remove_header('host')
#def __build_request(self, url, data=None, headers={}, method='GET'):
#new_headers = self.headers.copy()
#new_headers.update(headers)
#parsed_headers = {k.lower(): v.lower() for k,v in new_headers.items()}
#if not parsed_headers.get('user-agent'):
#parsed_headers['user-agent'] = self.agent
#if isinstance(data, dict):
#data = json.dumps(data)
#if isinstance(data, str):
#data = data.encode('UTF-8')
#request = HttpRequest(url, data=data, headers=parsed_headers, method=method)
#if self.proxy.enabled:
#request.set_proxy(f'{self.proxy.host}:{self.proxy.host}', self.proxy.ptype)
#return request
#def request(self, *args, **kwargs):
#request = self.__build_request(*args, **kwargs)
#try:
#response = urlopen(request)
#except HTTPError as e:
#response = e.fp
#return HttpResponse(response)
#def json(self, *args, headers={}, activity=True, **kwargs):
#json_type = 'activity+json' if activity else 'json'
#headers.update({
#'accept': f'application/{json_type}'
#})
#return self.request(*args, headers=headers, **kwargs)
#def signed_request(self, privkey, keyid, *args, **kwargs):
#request = self.__build_request(*args, **kwargs)
#self.__sign_request(request, privkey, keyid)
#try:
#response = urlopen(request)
#except HTTPError as e:
#response = e
#return HttpResponse(response)
#class HttpRequest(Request):
#def __init__(self, *args, **kwargs):
#super().__init__(*args, **kwargs)
#parsed = urlparse(self.full_url)
#self.scheme = parsed.scheme
#self.host = parsed.netloc
#self.domain = parsed.hostname
#self.port = parsed.port
#self.path = parsed.path
#self.query = parsed.query
#self.body = self.data if self.data else b''
#class HttpResponse(object):
#def __init__(self, response):
#self.body = response.read()
#self.headers = DefaultDict({k.lower(): v.lower() for k,v in response.headers.items()})
#self.status = response.status
#self.url = response.url
#def text(self):
#return self.body.decode('UTF-8')
#def json(self):
#return DotDict(self.text())
#def json_pretty(self, indent=4):
#return json.dumps(self.json().asDict(), indent=indent)
proxy_config = {}
if config.proxy.enabled:
proxy_config = config.proxy.asDict()
del proxy_config['enabled']
Client = HttpClient(useragent=config.agent, **proxy_config)
SetClient(Client)

View file

@ -8,9 +8,10 @@ from IzzyLib.misc import RandomGen, Input, Path, Boolean, DotDict
from collections import Counter
from datetime import datetime
from . import messages
from .config import config
from .database import DataBase, db, migrate, schema
from .functions import mkhash
from .functions import Client, mkhash
parser = argparse.ArgumentParser()
@ -20,27 +21,137 @@ pargs = parser.parse_args()
command, args = pargs.command, pargs.options
def man_test(name='Barkshark Social'):
class Row(DotDict):
def __init__(self, row):
for attr in dir(row):
if not attr.startswith('_') and attr != 'metadata':
self[attr] = getattr(row, attr)
def man_test():
for user in db.fetch('user', single=False):
actor = 'https://social.barkshark.xyz/actor/'+user.handle
user.update({
'actor': actor,
'inbox': actor
})
class User(Row):
def __init__(self, user):
super().__init__(user)
def man_pushtest(objectid):
with db.session() as s:
status = s.fetch('status', id=int(objectid))
user = status.user
domain = db.fetch('domain', id=self.domainid)
self.domain = domain.domain
keyid = f'https://{config.web_domain}/actor/{user.handle}#main-key'
headers = {'Accept': 'application/activity+json'}
resp = Client.signed_request(user.privkey, keyid, 'https://barkshark.xyz/inbox',
headers = headers,
method = 'POST',
data = messages.Announce(messages.Note(status))
)
return resp.text()
row = db.get.user('izalia', config.domain)
user = User(row)
def man_httptest(url):
with db.session() as s:
user = s.fetch('user', handle='izalia')
for k,v in user.items():
print(f'{k}:\t{v}')
keyid = f'https://{config.web_domain}/actor/izalia#main-key'
headers = {'Accept': 'application/activity+json'}
args = [
user.privkey,
keyid,
url
##'https://barkshark.xyz/users/izalia/statuses/105572315304601364/activity'
]
kwargs = {
'headers': headers,
}
resp = Client.signed_request(*args, **kwargs)
try:
return resp.json_pretty()
except Exception as e:
print(f'{e.__class__name}: {e}')
return resp.text()
def man_allstar():
text = '''Somebody once told me the world is gonna roll me
I ain't the sharpest tool in the shed
She was looking kind of dumb with her finger and her thumb
In the shape of an "L" on her forehead
Well, the years start coming and they don't stop coming
Fed to the rules and I hit the ground running
Didn't make sense not to live for fun
Your brain gets smart but your head gets dumb
So much to do, so much to see
So what's wrong with taking the back streets?
You'll never know if you don't go
You'll never shine if you don't glow
Hey, now, you're an all-star, get your game on, go play
Hey, now, you're a rock star, get the show on, get paid
And all that glitters is gold
Only shooting stars break the mold
It's a cool place and they say it gets colder
You're bundled up now wait 'til you get older
But the meteor men beg to differ
Judging by the hole in the satellite picture
The ice we skate is getting pretty thin
The water's getting warm so you might as well swim
My world's on fire. How about yours?
That's the way I like it and I'll never get bored
Hey, now, you're an all-star, get your game on, go play
Hey, now, you're a rock star, get the show on, get paid
And all that glitters is gold
Only shooting stars break the mold
Go for the moon
Go for the moon
Go for the moon
Go for the moon
Hey, now, you're an all-star, get your game on, go play
Hey, now, you're a rock star, get the show on, get paid
And all that glitters is gold
Only shooting stars
Somebody once asked could I spare some change for gas
I need to get myself away from this place
I said yep, what a concept
I could use a little fuel myself
And we could all use a little change
Well, the years start coming and they don't stop coming
Fed to the rules and I hit the ground running
Didn't make sense not to live for fun
Your brain gets smart but your head gets dumb
So much to do, so much to see
So what's wrong with taking the back streets?
You'll never know if you don't go
You'll never shine if you don't glow
Hey, now, you're an all star, get your game on, go play
Hey, now, you're a rock star, get the show on, get paid
And all that glitters is gold
Only shooting stars break the mold
And all that glitters is gold
Only shooting stars break the mold'''
lines = text.splitlines()
lines.reverse()
with db.session() as s:
for index, line in enumerate(lines):
domainid = s.get.domainid(config.domain)
user = s.get.user('izalia')
time = datetime.now()
if not user:
return f'Invalid user: {name}'
s.insert('status',
userid = user.id,
domainid = domainid,
content = line,
warning = f'Smash Mouth - Allstar {index+1}-{len(lines)}',
timestamp = time
)
return 'You\'ve been Allstar-ed!'
def man_post(name, text, warning=None):
@ -402,6 +513,12 @@ REDIS_PASSWORD={config.rd.password}
REDIS_DATABASE={config.rd.database}
REDIS_PREFIX={config.rd.prefix}
REDIS_CONNECTIONS={config.rd.maxconnections}
# Http client proxy stuff
PROXY_ENABLED={config.proxy.enabled}
PROXY_TYPE={config.proxy.ptype}
PROXY_HOST={config.proxy.host}
PROXY_PORT={config.proxy.port}
'''
with Path(config.data).join(config.env+'.env').open('w') as fd:

View file

@ -1,29 +1,25 @@
from IzzyLib.misc import FormatUtc
from IzzyLib.misc import ApDate, DotDict
from uuid import uuid4
from .config import config
from .database import get
domain = get.settings('domain')
weburl = config.web_domain
from .functions import ParseText
from .database import db
###
# Messages
###
def Note(objectid):
post, user = get_post_data(objectid)
warning = post['warning']
content = post['content']
handle = user['handle']
date = formatUTC(post['timestamp'], ap=True)
def Note(status):
status = db.fetch('status', id=status) if type(status) in [int, str] else status
user = status.user
date = ApDate(status.timestamp)
short_date = date.split('T', 1)[0]
actor_url = f'https://{weburl}/actor/{handle}'
object_url = f'https://{weburl}/status/{objectid}'
actor_url = f'https://{config.web_domain}/actor/{user.handle}'
object_url = f'https://{config.web_domain}/status/{status.id}'
content = ParseText(status.content)
data = {
data = DotDict({
'@context': [
'https://www.w3.org/ns/activitystreams',
{
@ -36,58 +32,57 @@ def Note(objectid):
'votersCount': 'toot:votersCount'
}
],
'id': object_url,
'id': f'{object_url}',
'type': 'Note',
'summary': None,
'summary': status.warning,
'inReplyTo': None,
'published': date,
'url': f'https://{weburl}/:{objectid}',
'url': f'https://{config.web_domain}/:{status.id}',
'attributedTo': actor_url,
'sensitive': True if warning else False,
'sensitive': True if status.warning else False,
'atomUri': object_url,
'inReplyToAtomUri': None,
'conversation': f'tag:{domain},{short_date}:objectId={objectid}:objectType=Conversation',
'conversation': f'tag:{user.domain},{short_date}:objectId={status.id}:objectType=Conversation',
'content': content,
'contentMap': {
'en': content
},
'attachment': [],
'tag': [],
'replies': {
'id': f'{object_url}/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'next': f'{object_url}/replies?min_id=1&page=true',
'partOf': f'{object_url}/replies',
'items': [
object_url
]
}
}
}
#'replies': {
#'id': f'{object_url}/replies',
#'type': 'Collection',
#'first': {
#'type': 'CollectionPage',
#'next': f'{object_url}/replies?min_id=1&page=true',
#'partOf': f'{object_url}/replies',
#'items': [
#object_url
#]
#}
#}
})
return set_visibility(data, post, user)
return set_visibility(data, status, user)
def Actor(request, user):
if isinstance(user, str):
user = get.user(user)
user = db.fetch('user', handle=user) if type(user) == str else user
if not user:
logging.verbose('User not found when generating actor')
raise NotFound('User not found')
sysaccounts = ['instance', 'relay', 'local']
sysaccounts = ['system', 'relay', 'local']
handle = user['handle']
display = user['name']
bio = user['bio']
pubkey = user['pubkey']
handle = user.handle
display = user.name
bio = user.bio
pubkey = user.pubkey
actor_url = f'https://{weburl}/actor/{handle}'
actor_url = f'https://{config.web_domain}/actor/{handle}'
data = {
data = DotDict({
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
@ -115,54 +110,55 @@ def Actor(request, user):
'type': 'Application' if handle == 'instance' else 'Person',
'following': f'{actor_url}/following',
'followers': f'{actor_url}/followers',
'inbox': f'{actor_url}/inbox',
'inbox': f'{actor_url}',
'featured': f'{actor_url}/featured',
'preferredUsername': f'{handle}',
'url': f'https://{weburl}/about' if handle in sysaccounts else f'https://{weburl}/@{handle}',
'preferredUsername': f'{user.handle}',
'url': f'https://{config.web_domain}/about' if handle in sysaccounts else f'https://{config.web_domain}/@{user.handle}',
'manuallyApprovesFollowers': False,
'publicKey': {
'id': f'{actor_url}#main-key',
'owner': actor_url,
'publicKeyPem': f'{pubkey}'
'publicKeyPem': f'{user.pubkey}'
},
'endpoints': {
'sharedInbox': f'https://{weburl}/inbox'
'sharedInbox': f'https://{config.web_domain}/inbox'
},
}
})
if handle not in sysaccounts and request['valid']:
if handle not in sysaccounts and request.ctx.valid:
data.update({
'name': f'{display}',
'summary': f'{bio}',
'name': f'{user.name}',
'summary': ParseText(f'{user.bio}'),
'outbox': f'{actor_url}/outbox',
'attachment': [
{
'type': 'PropertyValue',
'name': 'Pronouns',
'value': 'She/Her, They/Them'
}
],
'attachment': [],
'icon': {
'type': 'Image',
'mediaType': 'image/png',
'url': '' #avatar
'url': f'https://{config.web_domain}/{user.avatar}'
},
'image': {
'type': 'Image',
'mediaType': 'image/png',
'url': '' #header
'url': f'https://{config.web_domain}/{user.header}'
}
})
for k,v in user.info_table.items():
data['attachment'].append({
'type': 'PropertyValue',
'name': k,
'value': ParseText(v)
})
return data
def Tombstone(url):
data = {
"id": objectid,
"type": "Tombstone",
"atomUri": objectid
}
def Tombstone(objectid):
data = DotDict({
'id': objectid,
'type': 'Tombstone',
'atomUri': objectid
})
return data
@ -170,61 +166,137 @@ def Tombstone(url):
###
# Actions
###
def Create(objectid):
user, post = get_post_data(objectid)
#def Create(url, row, user):
#data = {
#"@context": [
#"https://www.w3.org/ns/activitystreams",
#{
#"ostatus": "http://ostatus.org#",
#"atomUri": "ostatus:atomUri",
#"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
#"conversation": "ostatus:conversation",
#"sensitive": "as:sensitive",
#"toot": "http://joinmastodon.org/ns#",
#"votersCount": "toot:votersCount",
#"litepub": "http://litepub.social/ns#",
#"directMessage": "litepub:directMessage"
#}
#],
#"id": f"{url}/activity",
#"type": "Create",
#"actor": f"https://{config.web_domain}/actor/{user.handle}",
#"published": ApDate(row.timestamp),
##"to": [
##"https://barkshark.xyz/users/izalia/followers"
##],
##"cc": [
##"https://www.w3.org/ns/activitystreams#Public"
##],
#"object": url
#}
handle = user['handle']
actor_url = f'https://{weburl}/user/{handle}' if not user['url'] else user['url']
date = formatUTC(post['timestamp'], ap=True)
short_date = date.split('T', 1)[0]
#return data
#return set_visibility(data, row, user)
def Create(object):
actor = GetObjectActor(object)
if not actor:
logging.error('messages.Create: Cannot find actor for object:\n', json.dumps(object.asDict(), indent=4))
return
data = {
'id': f'{url}/activity',
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'{object.id}/activity',
'type': 'Create',
'actor': actor_url,
'published': data,
'to': None,
'cc': None,
'object': url,
'actor': actor,
'to': object.get('to', ['https://www.w3.org/ns/activitystreams#Public']),
'cc': object.get('cc', [f'{actor}/followers']),
'object': object.asDict(),
}
return data
def Delete(objectid):
objurl = objecturl(objectid)
data = {'@context': ['https://www.w3.org/ns/activitystreams'],
"id": f"{objectid}#delete",
"type": "Delete",
"actor": "https://barkshark.xyz/users/izalia",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": Tombstone(objectid)
}
def Delete(object):
actor = GetObjectActor(object)
return data
if not actor:
logging.error('messages.Delete: Cannot find actor for object:\n', json.dumps(object.asDict(), indent=4))
return
def Announce(url, cc=None):
data = {
"id": "https://barkshark.xyz/users/izalia/statuses/104246196676073858/activity",
"type": "Announce",
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'{object.id}#delete',
'type': 'Delete',
'actor': actor,
'to': object.get('to', ['https://www.w3.org/ns/activitystreams#Public']),
'cc': object.get('cc', [f'{actor}/followers']),
'object': Tombstone(objectid).asDict()
}
return data
def Announce(object):
actor = GetObjectActor(object)
if not actor:
logging.error('messages.Announce: Cannot find actor for object:\n', json.dumps(object.asDict(), indent=4))
return
data = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'https://{config.web_domain}/object/{uuid4()}',
'type': 'Announce',
'actor': actor,
'to': object.get('to', ['https://www.w3.org/ns/activitystreams#Public']),
'cc': object.get('cc', [f'{actor}/followers']),
'object': object.id
}
return data
def Follow(user, dest):
data = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": f"https://{config.web_domain}/object/{uuid4()}",
'to': dest,
"type": "Follow",
"actor": f"https://{config.web_domain}/actor/{user}",
"object": dest
}
return data
def Unfollow():
data = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://barkshark.xyz/users/izalia#follows/354/undo",
"type": "Undo",
"actor": "https://barkshark.xyz/users/izalia",
"published": "2020-05-28T12:58:45Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://snouts.online/users/jay",
"https://barkshark.xyz/users/izalia/followers"
],
"object": "https://snouts.online/users/jay/statuses/104246188200326735",
"atomUri": "https://barkshark.xyz/users/izalia/statuses/104246196676073858/activity"
"object": {
"id": "https://barkshark.xyz/a3b68a17-1a4e-465e-8460-a1f1c9317566",
"type": "Follow",
"actor": "https://barkshark.xyz/users/izalia",
"object": "https://social.barkshark.xyz/actor/izalia"
}
}
###
# Functions
###
def GetObjectActor(object):
if object.get('type') in ['Person', 'Application']:
return object['id']
return object.get('attributedTo', object.get('actor'))
def objecturl(objectid, objtype=None):
if objectid.startswith(f'https://'):
return objectid
@ -232,7 +304,7 @@ def objecturl(objectid, objtype=None):
if not objtype or objtype not in ['actor', 'status']:
return
return f'https://{weburl}/{objtype}/{objectid}'
return f'https://{config.web_domain}/{objtype}/{objectid}'
def get_post_data(objectid):
@ -249,21 +321,27 @@ def get_post_data(objectid):
return (post, user)
def set_visibility(data, post, user):
visibility = post['visibility']
handle = user['handle']
actor_url = f'https://{weburl}/user/{handle}'
def set_visibility(data, status, user):
visibility = status.visibility
handle = user.handle
actor_url = f'https://{config.web_domain}/user/{handle}'
if visibility == 'public':
if status.visibility == 'public':
data.update({
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [f'{actor_url}/followers']
})
elif visibility == 'unlisted':
elif status.visibility == 'unlisted':
data.update({
'to': [f'{actor_url}/followers'],
'cc': ['https://www.w3.org/ns/activitystreams#Public']
})
elif status.visibility == 'private':
data.update({
'to': [f'{actor_url}/followers'],
'cc': []
})
return data

View file

@ -1,13 +1,15 @@
import binascii, base64, json, hashlib, multiprocessing
import binascii, base64, json, hashlib, multiprocessing, traceback
import httpsig
from urllib.parse import urlparse, unquote, quote_plus
from urllib.parse import quote, urlparse, unquote
from IzzyLib import logging
from IzzyLib.cache import LRUCache
from IzzyLib.http import ValidateRequest, ParseSig
from IzzyLib.misc import DotDict
from IzzyLib.http import ParseSig, PkcsHeaders, VerifyRequest
from IzzyLib.misc import DefaultDict, DotDict
from Crypto.Hash import SHA, SHA256, SHA512
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from base64 import b64encode, b64decode
from sanic.response import raw, redirect, json as rjson
from sanic import exceptions as exc
from tldextract import extract
@ -15,7 +17,7 @@ from tldextract import extract
from .config import config
from .database import db
from .errors import Teapot
from .functions import CheckForwarded, JsonCheck, client, Error, JsonResp, ParseRequest
from .functions import Client, FetchActor, FetchWebfingerAcct, ForwardCheck, ParseRequest
blocked_agents = [
@ -48,34 +50,13 @@ domain_bans = [
blocked_domains = [extract(domain) for domain in domain_bans]
hash_cache = LRUCache(4096)
anon_api_paths = [
'/api/v1/apps',
'/api/v1/instance'
]
ap_paths = [
'/status',
'/actor',
]
cookie_paths = [
'/@',
'/:',
'/status',
'/welcome',
'/settings',
'/admin',
'/oauth/authorize'
]
log_ignore_paths = [
'/media',
'/static',
'/style',
'/manifest',
'/favicon'
]
'/media',
'/static',
'/style',
'/manifest',
'/favicon'
]
def CSP():
@ -123,100 +104,133 @@ def CSP():
csp_header = CSP()
async def http_bans(request):
async def http_domain_user_check(request):
ParseRequest(request, db)
request.ctx.actor = None
request.ctx.domain = None
request.ctx.web_domain = None
request.ctx.top_domain = None
request.ctx.valid = None
if request.ctx.media:
return
if not request:
logging.warning('Empty request')
raise exc.InvalidUsage('Empty request')
request.ctx.signature = ParseSig(request.headers.get('signature'))
if not request.ctx.signature:
return
keyid = request.ctx.signature.keyid
web_domain = urlparse(keyid).netloc
domain_extract = extract(keyid)
top_domain = f'{domain_extract.domain}.{domain_extract.suffix}'
domain_cache = hash_cache.get(top_domain)
request.ctx.actor_url = keyid.split('#', 1)[0]
request.ctx.valid = VerifyRequest(request)
request.ctx.top_domain = top_domain
request.ctx.web_domain = web_domain
actor = FetchActor(request.ctx.actor_url, db.fetch('user', handle='system'))
webfinger = FetchWebfingerAcct(actor.preferredUsername, web_domain)
if not webfinger:
return
request.ctx.domain = webfinger.domain
request.ctx.actor = actor
with db.session() as s:
domainrow = s.fetch('domain', domain=webfinger.domain)
if not domainrow:
s.put.domain(webfinger.domain, web_domain)
domainrow = s.fetch('domain', domain=webfinger.domain)
s.commit()
if not s.get.user(actor.preferredUsername, webfinger.domain):
s.put.remote_user(actor)
actor = s.get.user(webfinger.handle, webfinger.domain)
request.ctx.domainrow = domainrow
request.ctx.actorrow = actor
async def http_bans(request):
# Let the request go through if it's basic resources needed for the error page
if not any(map(request.path.startswith, ['/favicon', '/style', '/static'])):
if [agent for agent in blocked_agents if agent in request.headers.get('User-Agent', '').lower()]:
raise Teapot('This teapot kills fascists')
if any(map(request.path.startswith, ['/favicon', '/style', '/static'])):
return
signature = ParseSig(request.headers)
if [agent for agent in blocked_agents if agent in request.headers.get('User-Agent', '').lower()]:
raise Teapot('This teapot kills fascists')
if signature:
keyid = signature['keyid']
domain = extract(keyid)
domain_cache = hash_cache.get('domain')
if not request.ctx.top_domain:
return
if not domain_hash:
domain_string = f'{domain.domain}.{domain.suffix}'
domain_hash = hashlib.sha256(domain_string.encode("utf-8")).hexdigest()
domain_hash = hash_cache.fetch('domain')
for banned_domain in blocked_domains:
# untested, but it should work
if domain_hash == banned_domain:
raise Teapot('This teapot kills fascists')
if not domain_hash:
domain = request.ctx.top_domain
domain_hash = hashlib.sha256(domain.encode("utf-8")).hexdigest()
hash_cache.store(domain, domain_hash)
if domain_hash in blocked_domains:
raise Teapot('This teapot kills fascists')
async def http_auth(request):
if any(map(request.path.startswith, ['/media', '/static'])):
if request.ctx.media:
return
ParseRequest(request, db)
if not request.ctx.api_anon_path and request.ctx.api_path:
resp = handle_api_path(request)
token = request.ctx.token
token_user = request.ctx.token_user
cookie = request.ctx.cookie
cookie_user = request.ctx.cookie_user
if resp:
return resp
if request.path.startswith('/api/v1') and not any(map(request.path.startswith, anon_api_paths)):
if not token or not token_user:
logging.debug('middleware.http_auth: Invalid token:', token)
return JsonResp({'error': 'Invalid token'}, 401)
elif request.ctx.ap_path:
resp = handle_api_path(request)
login_token = request.cookies.get('login_token')
if resp:
return resp
if any(map(request.path.startswith, ap_paths)) and JsonCheck(request.headers):
request.ctx.valid = False
elif request.ctx.cookie_path:
resp = handle_api_path(request)
if not await ValidateRequest(request, client=client):
#logging.debug('middleware.httpauth: Signature verify failed')
raise exc.Unauthorized('Failed to verify signature')
else:
request.ctx.valid = True
elif any(map(request.path.startswith, cookie_paths)):
if not login_token:
if JsonCheck(request.headers):
raise exc.Unauthorized('No Token')
else:
return redirect(f'/login?redir={quote_plus(request.path)}&query={quote_plus(request.query_string)}')
elif not cookie:
return redirect(f'/login?redir={quote_plus(request.path)}&msg=InvalidToken')
elif any(map(request.path.startswith, ['/login', '/register'])) and cookie and login_token == cookie.token:
return redirect('/welcome')
if resp:
return resp
async def http_headers(request, response):
#always_cache = ['ico', 'css', 'svg', 'js', 'png', 'woff2']
#always_cache_ext = ['ico', 'jpg', 'png', 'woff2']
always_cache_ext = []
prod_cache_ext = ['css', 'svg', 'js']
prod_cache_ext = ['css', 'svg', 'js', 'ico', 'jpg', 'png', 'woff2', 'mp4', 'mov', 'webm']
prod_cache_path = ['/manifest.json']
is_prod = config.env == 'prod'
raw_ext = request.path.split('.')[-1:]
ext = raw_ext[0] if len(raw_ext) > 0 else None
if request.ctx.parsed and not request.ctx.media:
if request.ctx.ap_path:
response.headers['Content-Type'] = 'application/activity+json'
elif request.ctx.api_path or request.ctx.json_path:
response.headers['Content-Type'] = 'application/json'
if not response.headers.get('Cache-Control'):
compare = [
ext in always_cache_ext,
request.path.startswith('/media'),
]
prod_compare = [
ext in prod_cache_ext,
request.path.startswith('/media'),
request.path in prod_cache_path
]
if True in [*compare, *(prod_compare if is_prod else [])]:
if is_prod and any(compare):
response.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
else:
@ -228,15 +242,98 @@ async def http_headers(request, response):
async def http_access_log(request, response):
if any(map(request.path.startswith, log_ignore_paths)) or response.status == 302:
if config.env == 'dev' and (any(map(request.path.startswith, log_ignore_paths)) or response.status == 302):
return
uagent = request.headers.get('user-agent')
forwarded = CheckForwarded(request.headers.get('forwarded'))
forwarded = ForwardCheck(request.headers.get('forwarded'))
address = request.remote_addr if not forwarded.valid else forwarded.ip
logging.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"')
def handle_web_path(request):
with db.session() as s:
handle = request.match_info.get('user')
statusid = request.match_info.get('statusid')
if any(map(request.path.startswith, ['/@', '/:'])) and request.ctx.json_path:
return
if request.path.startswith('/@'):
domainid = s.get.domainid(config.domain)
user = s.fetch('user', handle=handle, domainid=domainid)
if user.config.get('public'):
return
if not request.ctx.cookie:
if request.ctx.json_path:
raise exc.Unauthorized('No Token')
else:
return redirect(f'/login?redir={quote(request.path)}&query={quote(request.query_string)}')
elif not request.ctx.cookie:
return redirect(f'/login?redir={quote(request.path)}&msg=InvalidToken')
def handle_api_path(request):
if not request.ctx.token or not request.ctx.token_user:
logging.debug('middleware.http_auth: Invalid token:', request.ctx.token)
return response.json({'error': 'Invalid token'}, status=401)
def handle_ap_path(request):
if not request.ctx.valid:
logging.debug('middleware.http_auth: Signature verify failed')
if request.method == 'GET' and request.path.startswith('/actor') and len(request.path.split('/')) == 3:
pass
else:
raise exc.Unauthorized('Failed to verify signature')
else:
request.ctx.valid = True
#def ValidateRequest(request):
#headers = {k.lower(): v for k,v in request.headers.items()}
#headers['(request-target)'] = f'{request.method.lower()} {request.path}'
#signature = request.ctx.signature
#actor = request.ctx.actor
#digest = headers.get('digest')
#if not signature:
#return False
### Make sure there is a digest header for inbox pushes
#if request.method.lower() == 'post' and (request.path.startswith('/actor') or request.path == '/inbox') and not request.body:
#return False
### Date and Host headers are required
#elif None in (headers.get('date'), headers.get('host')):
#return False
###
#elif request.body and digest and not ValidateBody(digest, request.body):
#return False
#try:
#resp = Client.json(actor)
#actor_data = resp.json()
#pubkey = actor_data.publicKey['publicKeyPem']
#except Exception as e:
#traceback.print_exc()
#logging.verbose(f'Failed to get public key for actor {signature.keyid}')
#return False
#request.ctx.valid = PkcsHeaders(pubkey, {k:v for k,v in headers.items() if k in signature.headers}, sig=signature)
#return request.ctx.valid
class InvalidAttr(object):
pass

142
social/settings.py Normal file
View file

@ -0,0 +1,142 @@
import io
from IzzyLib import logging
from IzzyLib.misc import Boolean, DotDict, DefaultDict
from PIL import Image
from .config import config
from .database import db
from .functions import mkhash, themes
bool_opts = [
'require_approval',
'open',
'secure',
'private_profiles'
]
def process_media(user, data, mtype):
filename = mkhash(data, user.get(mtype), 'md5')
oldpath = config.data.join(user.avatar)
mediapath = f'media/{mtype}/{user.domain}/{user.handle[0]}/{filename}.png'
path = config.data.join(mediapath)
path.parent().mkdir()
if path.exists():
path.delete()
image = Image.open(io.BytesIO(data))
path.parent().mkdir()
image.save(path.str())
user.config[mtype] = filename
user[mtype] = mediapath
if oldpath.exists():
oldpath.delete()
def get_user_profile(request):
data = {
'color_themes': list(themes.keys())
}
return data
def post_user_profile(request):
user = request.ctx.cookie_user
form = DefaultDict(request.ctx.form)
data = DotDict({k: v for k,v in form.items() if k in user.keys()})
avatar = request.ctx.files.get('avatar')
header = request.ctx.files.get('header')
if avatar and avatar.name:
process_media(user, avatar.body, 'avatar')
if header and header.name:
process_media(user, header.body, 'header')
if form.oldpassword:
if not form.password1 or not form.password2:
return {'error': 'Missing a password field'}
if form.password1 != form.password2:
return {'error': 'Passwords don\'t match'}
curr_hash = mkhash(form.oldpassword)
if curr_hash != user.password:
return {'error': 'Current password invalid'}
data.password = mkhash(form.password1)
user.config.private = Boolean(form.get('private'))
user.config.theme = form.get('color_theme')
print(form.get('color_theme', form))
user.update(data)
request.ctx.cookie_user = user
return {'message': 'Updated profile'}
def get_admin_dashboard(request):
with db.session() as s:
domainid = s.get.domainid(config.domain)
sys_accts = [row.id for row in s.fetch('user', permissions=0, single=False)]
user = s.table.user
status = s.table.status
data = {
'local_users': s.query(user).filter(user.domainid == domainid, user.permissions > 0).count(),
'remote_users': s.query(user).filter(user.domainid != domainid).count(),
'local_statuses': s.query(status).filter(status.domainid == domainid, status.userid.notin_(sys_accts)).count(),
'remote_statuses': s.query(status).filter(status.domainid != domainid).count(),
}
if config.dbtype == 'sqlite':
size = round(config.sqfile.size()/1024/1024, 2)
data['dbsize'] = f'{size} MiB'
else:
size = 'n/a'
return data
def get_admin_settings(request):
with db.session() as s:
rows = s.fetch('user', domainid=config.domainid, permissions=1, single=False)
data = {
'admins': [row.handle for row in rows]
}
return data
def post_admin_settings(request):
user = request.ctx.cookie_user
form = DefaultDict(request.ctx.form)
if user and user.permissions != 1:
return {'error': 'Not authorized'}
if not form.admin or form.admin == 'none':
form.admin = ''
for v in bool_opts:
if v not in form.keys():
form[v] = False
with db.session() as s:
for k,v in form.items():
if k in bool_opts:
s.put.config(k, Boolean(v), 'bool')
else:
s.put.config(k, v)
return {'message': 'Updated settings'}

View file

@ -2,5 +2,5 @@
#__all__ = ['activitypub', 'frontend', 'oauth', 'redirects', 'resources']
from . import api, frontend, redirects, resources
from . import activitypub, api, frontend, redirects, resources

273
social/views/activitypub.py Normal file
View file

@ -0,0 +1,273 @@
import json
from sanic.views import HTTPMethodView
from sanic import response
from .. import messages
from ..config import config
from ..functions import Error, JsonResp
from ..database import db
class Status(HTTPMethodView):
async def get(self, request, statusid, activity=None):
data = response.json(messages.Note(statusid))
if activity:
return messages.Create(data)
return data
class Replies(HTTPMethodView):
async def get(self, request, statusid=None):
data = {'msg': 'UvU'}
return response.json(data)
class Actor(HTTPMethodView):
async def get(self, request, user=None, extra=None):
user = db.get().user(user)
resp = {
'following': self._following,
'followers': self._followers,
'collections': self._collections,
'featured': self._featured,
'outbox': self._outbox
}
#print(user)
command = resp.get(extra)
if user == None:
return error(request, 404, 'That user doesn\'t exist.')
if not command:
if extra:
return error(request, 404, 'This user data doesn\'t exist.')
else:
data = self._actor_data(request, user)
else:
data = command(request, user)
return response.json(data)
async def post(self, request, user=None, extra=None):
with db.session() as s:
user = s.fetch('user', handle=user)
data = {}
print(json.dumps(request.json, indent=4))
return response.json(data, status=500)
def _actor_data(self, request, user):
return messages.Actor(request, user)
def _following(self, request, user):
data = {'msg': 'UvU'}
return data
def _followers(self, request, user):
data = {'msg': 'UvU'}
return data
def _collections(self, request, user):
data = {'msg': 'UvU'}
return data
def _featured(self, request, user):
data = {'msg': 'UvU'}
return data
def _outbox(self, request, user):
outbox = f'https://{config.web_domain}/actor/{user.handle}/outbox'
with db.session() as s:
status = s.get.statuses('izalia')
Status = s.classes.Status
if request.ctx.query.get('page'):
try:
page_num = int(request.ctx.query.get('page'))
except TypeError:
return {'error': 'not a valid number'}
page = status.page(page_num)
data = {
'@context': [
'https://www.w3.org/ns/activitystreams',
{
'ostatus': 'http://ostatus.org#',
'atomUri': 'ostatus:atomUri',
'inReplyToAtomUri': 'ostatus:inReplyToAtomUri',
'conversation': 'ostatus:conversation',
'sensitive': 'as:sensitive',
'toot': 'http://joinmastodon.org/ns#',
'votersCount': 'toot:votersCount',
'blurhash': 'toot:blurhash',
'focalPoint': {
'@container': '@list',
'@id': 'toot:focalPoint'
}
}
],
'id': f'{outbox}?page=true',
'type': 'OrderedCollectionPage',
'partOf': f'{outbox}'
}
if page.has_next():
data['next'] = f'{outbox}?page={page.next_page_number}'
if page.has_previous():
data['prev'] = f'{outbox}?page={page.previous_page_number}'
data['orderedItems'] = [messages.Note(row.id, Status(row, db)) for row in page.object_list]
else:
count = s.count('status', userid=user.id)
data = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': outbox,
'type': 'OrderedCollection',
'totalItems': count,
'first': f'{outbox}?page=1',
'last': f'{outbox}?page='
}
return data
class Inbox(HTTPMethodView):
async def post(self, request):
print(request.body)
return response.json({'msg': 'UvU'}, content_type='application/activity+json', status=202)
class Nodeinfo(HTTPMethodView):
async def get(self, request):
with db.session() as s:
settings = s.get.configs()
stats = s.get.stats()
data = {
'version': '2.0',
'usage': {
'users': {
'total': stats.user_count,
'activeMonth': stats.users_month,
'activeHalfyear': stats.users_halfyear
},
'localPosts': stats.status_count
},
'software': {
'name': 'bsocial',
'version': config.version
},
'protocols': ['activitypub'],
'openRegistrations': False,
'metadata': {
'staffAccounts': [],
'postFormats': ['text/plain', 'text/html', 'text/markdown'],
'nodeName': settings.name,
'nodeDescription': settings.description
}
}
return response.json(data)
class Wellknown(HTTPMethodView):
async def get(self, request, name):
endpoints = {
'nodeinfo': self.NodeInfo,
'host-meta': self.HostMeta,
'webfinger': self.WebFinger
}
endpoint = endpoints.get(name)
if not endpoint:
return error(request, 404, 'Invalid well-known endpoint')
ctype, data = endpoint(request)
if ctype.endswith('json'):
return response.json(data)
if name == 'webfinger' and not data:
return JsonResp({}, 404)
return response.text(data, content_type=ctype)
def NodeInfo(self, request):
data = {
'links': [
{
'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'href': f'https://{config.web_domain}/nodeinfo/2.0.json'
}
]
}
return ('application/json', data)
def HostMeta(self, request):
data = f'''<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml" template="https://{config.web_domain}/.well-known/webfinger?resource={{uri}}"/>
</XRD>
'''
return ('application/xrd+xml', data)
def WebFinger(self, request):
resource_query = request.ctx.query.get('resource')
if not resource_query:
return ('application/json', {})
resource = resource_query.split('@')
handle = resource[0].replace('acct:', '')
user = db.fetch('user', handle=handle)
if resource[1] == config.domain and user:
data = {
'subject': f'acct:{handle}@{config.domain}',
'aliases': [
f'https://{config.web_domain}/@{handle}',
f'https://{config.web_domain}/actor/{handle}'
],
'links': [
{
'rel': 'http://webfinger.net/rel/profile-page',
'type': 'text/html',
'href': f'https://{config.web_domain}/@{handle}'
},
{
'rel': 'self',
'type': 'application/activity+json',
'href': f'https://{config.web_domain}/actor/{handle}'
},
{
'rel': 'http://ostatus.org/schema/1.0/subscribe',
'template': f'https://{config.web_domain}/interact?url={{url}}'
}
]
}
return ('application/json', data)

View file

@ -74,8 +74,6 @@ def MastodonApiAction(request, method, path):
func = method.get(f'{base}_{name}')
print(f'MastodonAPI: {base}_{name}')
if not func:
logging.debug('Mastodon API: Invalid command:', f'{base}_{name}')
return JsonResp('Invalid command', 404)

View file

@ -1,17 +1,17 @@
import os, validators
from IzzyLib.http import VerifyString
from datetime import datetime
from sanic import response
from sanic.views import HTTPMethodView
from urllib.parse import unquote_plus
from urllib.parse import unquote
from ..api import settings
from .. import settings, messages
from ..config import config
from ..database import db, Row
from ..functions import GenKey, mkhash, Error
from ..database import db
from ..functions import Error, GenKey, mkhash
# home page (/)
class Home(HTTPMethodView):
async def get(self, request):
return config.template.response('pages/home.haml', request, {})
@ -19,8 +19,7 @@ class Home(HTTPMethodView):
class Rules(HTTPMethodView):
async def get(self, request):
data = {'text': config.frontend.join('beemovie.txt').read()[:10000].replace('\n', '<br>\n')}
return config.template.response(f'pages/rules.haml', request, data)
return config.template.response(f'pages/rules.haml', request, {})
class About(HTTPMethodView):
@ -36,6 +35,9 @@ class About(HTTPMethodView):
class User(HTTPMethodView):
async def get(self, request, user=None):
if request.ctx.json_path:
return response.json(messages.Actor(request, user))
if not user:
return Error(request, 404, 'User not specified')
@ -51,33 +53,49 @@ class User(HTTPMethodView):
with db.session() as s:
user = s.get.user(user, domain)
page_num = request.ctx.query.get('page', 1)
if not user:
return Error(request, 404, 'User not found')
post_rows = s.query(s.table.status).filter_by(userid=user.id).order_by(s.table.status.id.desc()).limit(20)
posts = [db.classes.Status(row, db) for row in post_rows.all()]
page_data = s.get.statuses(user=user)
table = {}
if not page_data:
posts = []
for k,v in user.table.items():
else:
page = page_data.page(page_num)
posts = [db.classes.Status(row, db) for row in page.object_list]
info_table = {}
for k,v in user.info_table.items():
if validators.url(v):
v = f"<a href='{v}'>{v}</a>"
table[k] = v
info_table[k] = v
user.table = table
user.info_table = info_table
user.bio = user.bio.replace('\n', '<br>\n') if user.bio else None
context = {'user': user, 'posts': posts}
context = {'user': user, 'posts': posts, 'page_data': page}
return config.template.response('pages/profile.haml', request, context)
class Status(HTTPMethodView):
def get(self, request, statusid=None):
if request.ctx.json_path:
if not request.ctx.valid:
return response.json({'error': 'Request not signed or signature not valid'}, status=401)
return response.json(messages.Note(statusid))
with db.session() as s:
status = s.fetch('status', id=statusid)
if not status:
return Error(request, 404, 'Status not found')
return config.template.response('pages/status.haml', request, {'status': status})
@ -127,10 +145,12 @@ class Register(HTTPMethodView):
# login page (/login)
# I wanna add keypair login, but apparently string signing via client-side js is a fuck
class Login(HTTPMethodView):
async def get(self, request, msg=None):
query = request.ctx.query
response = config.template.response('pages/login.haml', request, {'msg': msg, 'redir': {'path': query.get('redir'), 'data': query.get('query')}})
page = 'login_keypair' if query.get('login_type') == 'keypair' else 'login'
response = config.template.response(f'pages/{page}.haml', request, {'msg': msg, 'redir': {'path': query.get('redir'), 'data': query.get('query')}})
if not request.ctx.cookie_user and request.cookies.get('login_token'):
del response.cookies['login_token']
@ -143,36 +163,57 @@ class Login(HTTPMethodView):
headers = request.headers
data = request.ctx.form
login_type = data.get('login_type')
username = data.get('username')
password = data.get('password')
address = headers.get('X-Real-Ip')
agent = headers.get('User-Agent')
redir = data.get('redir')
redir_data = data.get('redir_data')
if None in [username, password]:
return await self.get(request, 'Username or password is missing')
if username in ['system', 'relay']:
return await self.get(request, 'Cannot login to a system account')
with db.session() as s:
user = s.get.user(username)
if username:
user = s.get.user(username)
if user and user.password == mkhash(password):
login_token = s.put.cookie(user.id, request.headers.get('X-Real-Ip'), request.headers.get('user-agent'))
resp = response.redirect(f'{redir}?{unquote_plus(redir_data)}' if redir and redir != 'None' else '/')
if not user:
return await self.get(request, 'User not found')
request.ctx.cookie = login_token
request.ctx.cookie_user = user
if data.get('login_type') == 'password':
password = data.get('password')
address = headers.get('X-Real-Ip')
agent = headers.get('User-Agent')
redir = data.get('redir')
redir_data = data.get('redir_data')
# Send login token. Lasts for 2 weeks (60 sec * 60 min * 24 hour * 14 day)
resp.cookies['login_token'] = login_token.token
resp.cookies['login_token']['max-age'] = 60*60*24*14
if None in [username, password]:
return await self.get(request, 'Username or password is missing')
return resp
if username in ['system', 'relay']:
return await self.get(request, 'Cannot login to a system account')
return await self.get(request, 'Wrong username or password')
if user.password != mkhash(password):
return await self.get(request, 'Password is invalid')
elif login_type == 'keypair':
login_value = data.get('login_value')
encrypt_value = data.get('encrypt_value')
pubkey = user.config.get('pubkey')
if not pubkey:
return await self.get(request, 'No saved pubkey')
if not VerifyString(login_value, encrypt_value, pubkey):
return await self.get(request, 'Failed to verify login value')
else:
return await self.get(request, f'Invalid login type: {login_type}')
login_token = s.put.cookie(user.id, request.headers.get('X-Real-Ip'), request.headers.get('user-agent'))
resp = response.redirect(f'{redir}?{unquote(redir_data)}' if redir and redir != 'None' else '/')
request.ctx.cookie = login_token
request.ctx.cookie_user = user
# Send login token. Lasts for 2 weeks (60 sec * 60 min * 24 hour * 14 day)
resp.cookies['login_token'] = login_token.token
resp.cookies['login_token']['max-age'] = 60*60*24*14
return resp
class Logout(HTTPMethodView):
@ -190,12 +231,12 @@ class Logout(HTTPMethodView):
return resp
class Settings(HTTPMethodView):
class Preferences(HTTPMethodView):
async def get(self, request, name=None, message=None, error=None):
if not name:
return response.redirect('/settings/profile')
func = getattr(settings, f'get_{name}')
func = getattr(settings, f'get_user_{name}')
data = func(request)
data['message'] = message
data['error'] = error
@ -203,14 +244,49 @@ class Settings(HTTPMethodView):
if type(data) != dict:
return data
return config.template.response(f'pages/settings/{name}.haml', request, data)
return config.template.response(f'pages/preferences/{name}.haml', request, data)
async def post(self, request, name=None):
if not name:
return Error(request, 404, 'Not found')
func = getattr(settings, f'post_{name}')
func = getattr(settings, f'post_user_{name}')
data = func(request)
message = None
error = None
if type(data) == dict:
error = data.get('error')
message = data.get('message')
if data and type(data) not in [str, dict]:
return data
return await self.get(request, name=name, message=message, error=error)
class Admin(HTTPMethodView):
async def get(self, request, name=None, message=None, error=None):
if not name:
return response.redirect('/admin/dashboard')
func = getattr(settings, f'get_admin_{name}')
data = func(request)
data['message'] = message
data['error'] = error
if type(data) != dict:
return data
return config.template.response(f'pages/admin/{name}.haml', request, data)
async def post(self, request, name=None):
if not name:
return Error(request, 404, 'Not found')
func = getattr(settings, f'post_admin_{name}')
data = func(request)
message = None
error = None

View file

@ -1,33 +1,34 @@
from IzzyLib.misc import Path
from PIL import Image
from datetime import datetime
from magic import Magic
from io import BytesIO
from sanic import response
from sanic.views import HTTPMethodView
from ..config import config
from ..database import db
themes = {
'pink': '#e7a',
'blue': '#77e',
'red': '#e77'
}
from ..functions import Error, GetTheme
# home page (/)
class Style(HTTPMethodView):
async def get(self, request, theme=None, timestamp=None):
async def get(self, request, timestamp=None):
user_config = request.ctx.cookie_user.config if request.ctx.cookie_user else {}
theme = user_config.get('theme', 'pink')
path = config.frontend.join('style')
data = {
'theme': themes.get(theme, 'pink'),
'cssfiles': [css for css in path.listdir() if css.str().endswith('css')]
}
data.update(GetTheme(theme))
return config.template.response('style.css', request, data, ctype='text/css')
class Favicon(HTTPMethodView):
async def get(self, request):
filename = config.frontend.join('static/icon-64.png').str()
filename = config.frontend.join('static/img/icon-64.png').str()
return await response.file(filename)
@ -40,12 +41,12 @@ class Manifest(HTTPMethodView):
'description': s.get.config('description'), #use description_short
'icons': [
{
'src': '/static/icon-512.png',
'src': '/static/img/icon-512.png',
'sizes': '512x512',
'type': 'image/png'
},
{
'src': '/static/icon-64.png',
'src': '/static/img/icon-64.png',
'sizes': '64x64',
'type': 'image/png'
}
@ -72,6 +73,41 @@ class Manifest(HTTPMethodView):
return response.json(data)
class Media(HTTPMethodView):
async def get(self, request, path=None):
path = Path(config.data.join('media').join(path))
if not path.isfile():
return Error(request, 404, 'Media not found')
raw_image = path.open('rb').read()
width = int(request.ctx.query.get('width', 0))
height = int(request.ctx.query.get('height', 0))
if not width and not height:
try:
width = height = int(request.query_string)
except ValueError:
width = height = None
if not width and not height:
mime = Magic(mime=True).from_buffer(raw_image)
return response.raw(raw_image, content_type=mime)
if not width:
width = 2000
if not height:
height = 2000
byte = BytesIO()
image = Image.open(BytesIO(raw_image))
image.thumbnail([width, height])
image.save(byte, format='PNG')
return response.raw(byte.getvalue(), content_type='image/png')
class Robots(HTTPMethodView):
async def get(self, request):
with db.session() as s:

View file

@ -8,24 +8,22 @@ from sanic import exceptions as exc
from sanic_cors import CORS
from jinja2.exceptions import TemplateNotFound
from sqlalchemy.orm.exc import DetachedInstanceError
from uuid import uuid4
from . import views, errors, middleware
from .database import db
from .config import config
from .functions import color, todate, themes, CssTimestamp
from .functions import CssTimestamp, StrDate, Themes
from .errors import Teapot
config.template.updateEnv({
#'name': db.get.settings('name'),
#'domain': db.config['web_domain'],
#'settings': db.get.settings('all'),
#'get_cookie': db.get.login_cookie,
'config': config,
'db': db,
'uuid': uuid4,
'CssTimestamp': CssTimestamp,
'todate': todate,
'themes': themes,
'StrDate': StrDate,
'Themes': Themes,
'json': json
})
@ -64,20 +62,26 @@ def setupRoutes():
for handler in error_handlers:
web.error_handler.add(*handler)
middlewares = [
(middleware.http_domain_user_check, 'request'),
(middleware.http_bans, 'request'),
(middleware.http_headers, 'response'),
(middleware.http_access_log, 'response')
]
web.register_middleware(middleware.http_bans)
web.register_middleware(middleware.http_auth)
web.register_middleware(middleware.http_headers, attach_to='response')
web.register_middleware(middleware.http_access_log, attach_to='response')
for m, a in middlewares:
web.register_middleware(m, attach_to=a)
web_routes = [
(views.api.Oauth, '/oauth/<name>'),
(views.api.MastodonBase, '/api/v1/<path:[A-z0-9\/]+>'),
#(views.activitypub.Actor, '/actor/<user>/<extra>'),
(views.activitypub.Actor, '/actor/<user>'),
(views.activitypub.Actor, '/actor/<user>/<extra>'),
(views.activitypub.Status, '/status/<statusid>'),
#(views.activitypub.Actor, '/actor/<user>'),
#(views.activitypub.Inbox, '/inbox'),
#(views.activitypub.Nodeinfo, '/nodeinfo/2.0.json'),
#(views.activitypub.Wellknown, '/.well-known/<name>'),
(views.activitypub.Nodeinfo, '/nodeinfo/2.0.json'),
(views.activitypub.Wellknown, '/.well-known/<name>'),
#(views.activitypub.Status, '/status/<status>'),
(views.frontend.Home, '/'),
(views.frontend.About, '/about'),
@ -86,18 +90,18 @@ def setupRoutes():
(views.frontend.Login, '/login'),
(views.frontend.Logout, '/logout'),
(views.frontend.User, '/@<user>'),
(views.frontend.User, '/user/<user>'),
(views.frontend.Status, '/:<statusid>'),
(views.frontend.Status, '/status/<statusid>'),
(views.frontend.Status, '/:<statusid:int>'),
#(views.frontend.Welcome, '/welcome'),
(views.frontend.Settings, '/settings'),
(views.frontend.Settings, '/settings/<name>'),
#(views.frontend.Admin, '/admin'),
(views.frontend.Preferences, '/preferences'),
(views.frontend.Preferences, '/preferences/<name>'),
(views.frontend.Admin, '/admin'),
(views.frontend.Admin, '/admin/<name>'),
#(views.frontend.Post, '/post'),
(views.redirects.About, '/about/more'),
(views.redirects.Admin, '/about/admin'),
(views.resources.Style, '/style-<theme>-<timestamp>.css'),
(views.resources.Style, '/style-<timestamp:int>.css'),
(views.resources.Manifest, '/manifest.json'),
(views.resources.Media, '/media/<path:[A-z0-9.\/?]+>'),
(views.resources.Favicon, '/favicon.ico'),
(views.resources.Robots, '/robots.txt')
]
@ -109,7 +113,7 @@ def setupRoutes():
# Media
web.static('/static', config.frontend.join('static').str())
web.static('/media', config.data.join('media').str())
#web.static('/media', config.data.join('media').str())
# Shitpost