a lot of changes
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "social/Lib/izzylib"]
|
||||
path = social/Lib/izzylib
|
||||
url = https://git.barkshark.xyz/izaliamae/izzylib.git
|
|
@ -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`
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'}
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
11
social/frontend/pages/admin/dashboard.haml
Normal 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}}
|
63
social/frontend/pages/admin/settings.haml
Normal 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'
|
|
@ -1,4 +1,4 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Home'
|
||||
-block content
|
||||
{{settings.get('description')}}
|
||||
{{settings.description.replace('\n', '<br />\n')}}
|
||||
|
|
|
@ -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'
|
||||
|
|
21
social/frontend/pages/login_keypair.haml
Normal 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'
|
7
social/frontend/pages/moderation/blocks.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Moderation: Instance Bans'
|
||||
-block content
|
||||
#mod.bans
|
||||
.title << Instance Bans
|
||||
|
||||
heck
|
59
social/frontend/pages/preferences/profile.haml
Normal 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'
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
84
social/frontend/static/img/menu.svg
Normal 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 |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
199
social/frontend/static/js/login.js
Normal 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()
|
|
@ -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');
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}*/
|
||||
|
|
11
social/frontend/style/admin.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* admin pages */
|
||||
#admin .info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#admin .info .flex-item {
|
||||
width: 250px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
149
social/manage.py
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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'}
|
|
@ -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
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|