major rewrite
|
@ -1 +1 @@
|
|||
3.8.0
|
||||
social
|
||||
|
|
|
@ -7,3 +7,9 @@ Here's a list of all the ideas I plan on implementing: https://git.barkshark.xyz
|
|||
Note: 'social' is a placeholder name and will be changed in the future
|
||||
|
||||
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.
|
||||
|
||||
`pip3 install -r requirements.txt`
|
||||
|
|
1
aptpkg-dev.txt
Normal file
|
@ -0,0 +1 @@
|
|||
libpq-dev
|
2
aptpkg.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
libpq5
|
||||
libmagic1
|
|
@ -1,5 +1,5 @@
|
|||
## Config file for Process Reloader (https://git.barkshark.xyz/barkshark/reload)
|
||||
exec = env PYENV=dev python3 -m social
|
||||
exec = python3 -m social
|
||||
watch_ext = py, env
|
||||
ignore_dirs = webapp/js, bin, dist, misc, test
|
||||
ignore_files = reload.py, test.py
|
||||
|
|
|
@ -11,4 +11,6 @@ validators>=0.14.0
|
|||
pyyaml>=5.1.2
|
||||
celery[redis]>=4.4.2
|
||||
tldextract>=2.2.2
|
||||
git+https://git.barkshark.xyz/izaliamae/izzylib.git@0.2
|
||||
python-magic>=0.4.18
|
||||
pillow>=8.1.0
|
||||
#git+https://git.barkshark.xyz/izaliamae/izzylib.git@0.2
|
||||
|
|
|
@ -3,3 +3,12 @@
|
|||
# by Zoey Mae - https://git.barkshark.xyz/izaliamae/social
|
||||
###
|
||||
__version__ = '0.1+pre-alpha'
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
syspath = Path(__file__).parent.joinpath('Lib')
|
||||
|
||||
for path in syspath.iterdir():
|
||||
sys.path.insert(0, str(path))
|
||||
|
|
|
@ -1,28 +1,19 @@
|
|||
#!/usr/bin/env python3
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from os import environ as ENV
|
||||
import sys, tracemalloc
|
||||
|
||||
from IzzyLib import logging
|
||||
|
||||
import tracemalloc
|
||||
tracemalloc.start()
|
||||
|
||||
#from webapp import precompile
|
||||
from social.web_server import main
|
||||
|
||||
|
||||
#class MyHandler(FileSystemEventHandler):
|
||||
# def on_modified(self, event):
|
||||
# if event.event_type == 'modified' and event.src_path[-3:] == '.py' and event.src_path.startswith('webapp/js/__target__') == False:
|
||||
# logging.info(event.src_path)
|
||||
# precompile()
|
||||
from .web_server import start
|
||||
from .config import config
|
||||
from .database import db
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if config.env == 'dev':
|
||||
tracemalloc.start()
|
||||
|
||||
#if ENV.get('PYENV', 'default').lower() in ['dev', 'default']:
|
||||
# logging.info('Stopping javascript watcher')
|
||||
# observer.stop()
|
||||
# observer.join()
|
||||
if db.get.version() == 0:
|
||||
logging.error('Database is not setup. Run "python3 -m social.manage setup" to configure the server.')
|
||||
sys.exit()
|
||||
|
||||
start()
|
||||
|
|
3
social/api/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from . import mastodon, oauth
|
||||
|
||||
__all__ = ['mastodon', 'oauth']
|
|
@ -1,60 +1,127 @@
|
|||
import json, sys, os, asyncio
|
||||
import asyncio, json, os, sys, validators
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import DotDict
|
||||
from urllib.parse import urlparse
|
||||
from json.decoder import *
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import json as rjson
|
||||
|
||||
from .misc import sanitize
|
||||
|
||||
from ..config import config
|
||||
from ..database import *
|
||||
from ..web_functions import error, jresp
|
||||
from ..views import oauth
|
||||
from ..database import db
|
||||
from ..functions import Error, JsonResp
|
||||
|
||||
|
||||
class post_cmd:
|
||||
def apps(data):
|
||||
class BasePost:
|
||||
def apps(request, data):
|
||||
data = DotDict(data)
|
||||
print(data)
|
||||
fields = ['redirect_uris', 'scopes', 'client_name', 'website']
|
||||
|
||||
for line in fields:
|
||||
if line not in data:
|
||||
data[line] = None
|
||||
|
||||
retdata = oauth.create.app(data['redirect_uris'], data['scopes'], data['client_name'], data['website'])
|
||||
if None in [data.scopes, data.client_name]:
|
||||
logging.debug('Missing scope or name for app')
|
||||
logging.debug(f'scope: {data.scope}, name: {data.client_name}')
|
||||
return 'MissingData'
|
||||
|
||||
if type(retdata) == str:
|
||||
return error(request, 400, 'Something fucked up')
|
||||
#scopes = scope_check(data.scopes)
|
||||
|
||||
return retdata
|
||||
if data.scopes == None:
|
||||
logging.debug(f'Invalid scopes: {data.scope}')
|
||||
return 'InvalidScope'
|
||||
|
||||
#if not validators.url(data.redirect_uris):
|
||||
#logging.debug(f'Invalid app URL: {data.redirect_uris}')
|
||||
#return 'InvalidURL'
|
||||
|
||||
row = db.put.app(data.redirect_uris, data.scopes, data.client_name, data.website)
|
||||
|
||||
return {'client_id': row.client_id, 'client_secret': row.client_secret, 'redirect_uris': row.redirect_uri, 'scopes': row.scope}
|
||||
|
||||
|
||||
class get_cmd:
|
||||
def instance():
|
||||
stats = get.server_stats()
|
||||
settings = get.settings
|
||||
class BaseGet:
|
||||
def instance(request, data):
|
||||
#stats = db.get.server_stats()
|
||||
stats = {}
|
||||
settings = db.get.configs()
|
||||
|
||||
data = {
|
||||
'version': f'2.9.0 (compatible; Social {config["version"]})',
|
||||
'uri': config['domain'],
|
||||
'title': settings('name'),
|
||||
'description': settings('description'),
|
||||
'uri': config.web_domain,
|
||||
'title': settings.name,
|
||||
'short_description': settings.description,
|
||||
'description': '',
|
||||
'email': None,
|
||||
'version': f'2.7.0 (compatible; BarksharkSocial {config.version})',
|
||||
'urls': {
|
||||
'streaming_api': f'wss://{config["web_domain"]}'
|
||||
'streaming_api': 'wss://'+config.web_domain
|
||||
},
|
||||
'stats': {
|
||||
'user_count': stats['user_count'],
|
||||
'status_count': stats['status_count'],
|
||||
'domain_count': stats['domain_count']
|
||||
'user_count': db.get.user_count(),
|
||||
"status_count": db.get.status_count(),
|
||||
"domain_count": db.get.domain_count()
|
||||
},
|
||||
'max_toot_chars': settings('char_limit'),
|
||||
'contact_account': {
|
||||
'id': '',
|
||||
'username': '',
|
||||
'acct': '',
|
||||
'display_name': '',
|
||||
'created_at': '',
|
||||
'url': '',
|
||||
'avatar': ''
|
||||
}
|
||||
'thumbnail': None,
|
||||
'max_toot_chars': settings.char_limit,
|
||||
"poll_limits": {
|
||||
"max_options": 5,
|
||||
"max_option_chars": 100,
|
||||
"min_expiration": 300,
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"languages": [
|
||||
"en"
|
||||
],
|
||||
"registrations": False,
|
||||
"approval_required": False,
|
||||
"invites_enabled": False,
|
||||
"contact_account": None
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def custom_emojis(request, data):
|
||||
emojis = []
|
||||
|
||||
with db.session() as s:
|
||||
for row in s.query(db.table.emoji).filter_by(domainid=config.domainid, enabled=True).all():
|
||||
url = f'https://{config.domain}/media/emojis/{row.domainid}/{row.shortcode[0]}/{row.shortcode}.png'
|
||||
|
||||
emojis.append({
|
||||
'shortcode': row.shortcode,
|
||||
'url': url,
|
||||
'static_url': url,
|
||||
'visible_in_picker': row.display
|
||||
})
|
||||
|
||||
return emojis
|
||||
|
||||
|
||||
class AcctGet(object):
|
||||
def verify_credentials(request, data):
|
||||
user = request.ctx.token_user
|
||||
data = {
|
||||
'id': user.id,
|
||||
'username': user.handle,
|
||||
'acct': user.handle,
|
||||
'display_name': user.name,
|
||||
'locked': False,
|
||||
'bot': False,
|
||||
'created_at': '2016-11-24T10:02:12.085Z',
|
||||
'note': user.bio,
|
||||
'url': f'https://{config.web_domain}/@{user.handle}',
|
||||
'avatar': None,
|
||||
'avatar_static': None,
|
||||
'header': None,
|
||||
'header_static': None,
|
||||
'followers_count': 0,
|
||||
'following_count': 0,
|
||||
'statuses_count': db.count('status', userid=user.id),
|
||||
'last_status_at': '2019-11-24T15:49:42.251Z',
|
||||
}
|
||||
|
||||
return data
|
||||
|
@ -66,41 +133,3 @@ class stream_cmd:
|
|||
|
||||
def gay():
|
||||
return 'im gay'
|
||||
|
||||
|
||||
class handle(HTTPMethodView):
|
||||
async def post(request):
|
||||
command = request.match_info['name']
|
||||
|
||||
try:
|
||||
post_data = await request.json()
|
||||
|
||||
except JSONDecodeError:
|
||||
post_data = await request.post()
|
||||
|
||||
data = sanitize(post_data, command)
|
||||
|
||||
if callable(getattr(post_cmd, command, True)):
|
||||
msg = eval('post_cmd.'+command+'(data)')
|
||||
|
||||
else:
|
||||
error(request, 404, 'InvalidCommand')
|
||||
|
||||
return jresp(msg)
|
||||
|
||||
|
||||
async def get(request):
|
||||
command = request.match_info['name']
|
||||
|
||||
if callable(getattr(get_cmd, command, True)):
|
||||
msg = eval('get_cmd.'+command+'()')
|
||||
|
||||
else:
|
||||
error(request, 404, 'InvalidCommand')
|
||||
|
||||
return jresp(msg)
|
||||
|
||||
|
||||
class stream(HTTPMethodView):
|
||||
async def get(request):
|
||||
return jres({'error': 'not yet implemented'})
|
||||
|
|
|
@ -13,16 +13,19 @@ def sanitize(data, endpoint):
|
|||
|
||||
target = {}
|
||||
|
||||
for line in data:
|
||||
if line == 'name':
|
||||
target[line] = regex_clean(data[line], True)
|
||||
for k,v in data.items():
|
||||
if k == 'name':
|
||||
target[k] = regex_clean(data[v], True)
|
||||
|
||||
if line in ['scope', 'scopes']:
|
||||
target[line] = []
|
||||
for scope in data[line].split(' '):
|
||||
target[line].append(regex_clean(scope, False))
|
||||
if k == 'scope':
|
||||
k = 'scopes'
|
||||
|
||||
#if k == 'scopes':
|
||||
#target[k] = []
|
||||
#for scope in v.split(' '):
|
||||
#target[k].append(regex_clean(scope, False))
|
||||
|
||||
else:
|
||||
target[line] = regex_clean(data[line], False)
|
||||
target[k] = regex_clean(v, False)
|
||||
|
||||
return target
|
||||
|
|
|
@ -8,7 +8,7 @@ from urllib.parse import urlparse
|
|||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import json as rjson
|
||||
|
||||
from ..database import newtrans, get, put, update, delete
|
||||
from ..database import get, put, update, delete
|
||||
from ..web_functions import error, jresp
|
||||
from ..config import config, logging
|
||||
|
||||
|
@ -80,7 +80,7 @@ class post_cmd:
|
|||
return post_data
|
||||
# End post checking
|
||||
|
||||
token_data = newtrans(get.api_token(data['token']))
|
||||
token_data = get.api_token(data['token'])
|
||||
|
||||
if token_data == None:
|
||||
return error(request, 401, 'InvalidToken')
|
||||
|
|
|
@ -1,97 +1,154 @@
|
|||
import json, sys, os, asyncio
|
||||
import asyncio, json, os, sys, validators
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from json.decoder import *
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import RandomGen
|
||||
#from json.decoder import *
|
||||
from sanic import response
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.exceptions import Unauthorized, Forbidden
|
||||
from sanic.response import json as rjson
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .misc import sanitize
|
||||
|
||||
from ..config import config
|
||||
from ..database import *
|
||||
from ..web_functions import error
|
||||
from .. import oauth
|
||||
from ..database import db
|
||||
from ..functions import Error, JsonResp
|
||||
|
||||
|
||||
class post_cmd:
|
||||
def token(request, data):
|
||||
print(data)
|
||||
if data.get('grant_type') == 'password':
|
||||
pass
|
||||
def scope_check(scopes):
|
||||
read_write = ['follows', 'accounts', 'lists', 'blocks', 'mutes', 'bookmarks', 'notifications', 'favourites', 'search', 'filters', 'statuses']
|
||||
admin = ['read', 'write']
|
||||
admin_secc = ['accounts', 'reports']
|
||||
new_scopes = []
|
||||
|
||||
elif data.get('grant_type') in [None, 'authorization_code']:
|
||||
put.oath.auth_token(client_id, client_secret)
|
||||
for line in scopes:
|
||||
scope = line.split(':')
|
||||
|
||||
if len(scope) < 2:
|
||||
new_scopes.append(line)
|
||||
continue
|
||||
|
||||
if (scope[0] in ['read', 'write'] and scope[1] in read_write) or scope[0] in ['follow', 'push'] or (scope[0] == 'admin' and scope[1] in admin and scope[2]):
|
||||
new_scopes.append(line)
|
||||
|
||||
else:
|
||||
raise Unauthorized(f'Invalid grant_type: {data.get("grant_type")}')
|
||||
logging.warning(f'Invalid scope: {line}')
|
||||
|
||||
return 'UvU'
|
||||
if len(new_scopes) < 1:
|
||||
return
|
||||
|
||||
else:
|
||||
return new_scopes
|
||||
|
||||
|
||||
class get_cmd:
|
||||
class create:
|
||||
def app(redirect_uri, scope, name, url):
|
||||
if None in [scope, name]:
|
||||
logging.debug('Missing scope or name for app')
|
||||
logging.debug(f'scope: {scope}, name: {name}')
|
||||
return 'MissingData'
|
||||
|
||||
#scopes = scope_check(scope)
|
||||
scopes = scope
|
||||
|
||||
if scopes == None:
|
||||
logging.debug(f'Invalid scopes: {scope}')
|
||||
return 'InvalidScope'
|
||||
|
||||
if not validators.url(url):
|
||||
logging.debug(f'Invalid app URL: {url}')
|
||||
return 'InvalidURL'
|
||||
|
||||
row = db.put.app(redirect_uri, scopes, name, url)
|
||||
|
||||
return {'client_id': row.client_id, 'client_secret': row.client_secret, 'redirect_uris': redirect_uri, 'scopes': scope}
|
||||
|
||||
|
||||
def authorize(app, userid):
|
||||
auth_code = RandomGen(40)
|
||||
|
||||
with db.session() as s:
|
||||
if not s.query(db.table.user).filter_by(id=userid):
|
||||
return
|
||||
|
||||
s.add(db.table.token(
|
||||
userid = userid,
|
||||
appid = app.id,
|
||||
code = auth_code
|
||||
))
|
||||
|
||||
return auth_code
|
||||
|
||||
|
||||
def auth_code(client_id, login_token):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Get:
|
||||
def authorize(request, data):
|
||||
login_token = request.cookies.get('login_token')
|
||||
client_id = data.get('client_id')
|
||||
redirect_uri = data.get('redirect_uri')
|
||||
|
||||
print(data)
|
||||
|
||||
if None in [client_id, redirect_uri, login_token]:
|
||||
raise InvalidUsage('Missing client_id or redirect_uri')
|
||||
|
||||
login_data = get.login_cookie(login_token)
|
||||
app = db.fetch('app', client_id=client_id)
|
||||
|
||||
if login_data == None:
|
||||
if not app:
|
||||
raise InvalidUsage('Invalid app')
|
||||
|
||||
if not request.ctx.cookie:
|
||||
raise InvalidUsage('Invalid login cookie')
|
||||
|
||||
userid = login_data['userid']
|
||||
|
||||
if data.get('response_type') == 'code':
|
||||
auth_code = oauth.create.authorize(client_id, userid)
|
||||
auth_code = create.authorize(app, request.ctx.cookie.userid)
|
||||
|
||||
if not auth_code:
|
||||
return error(request, 401, f'Failed to create auth code')
|
||||
return Error(request, 401, f'Failed to create auth code')
|
||||
|
||||
if data.get('redirect_uri') == 'urn:ietf:wg:oauth:2.0:oob':
|
||||
return 'code to display the auth code here'
|
||||
return config.template.response('auth.haml', code=auth_code)
|
||||
|
||||
else:
|
||||
return response.redirect(f'{redirect_uri}?code={auth_code}')
|
||||
return config.template.response('redir.haml', request, {'redir': f'{redirect_uri}?code={auth_code}'})
|
||||
|
||||
|
||||
class handle(HTTPMethodView):
|
||||
async def post(request):
|
||||
command = request.match_info['name']
|
||||
def token(request, data):
|
||||
if not data.get('code'):
|
||||
return JsonResp({'error': 'missing_code'}, status=400)
|
||||
|
||||
try:
|
||||
post_data = await request.json()
|
||||
with db.session() as s:
|
||||
token = s.query(db.table.token).filter_by(code=data['code']).one_or_none()
|
||||
|
||||
except JSONDecodeError:
|
||||
post_data = await request.post()
|
||||
if not token:
|
||||
return JsonResp({'error': 'invalid_code'}, status=401)
|
||||
|
||||
data = sanitize(post_data, command)
|
||||
app = s.query(db.table.app).filter_by(id=token.appid).one_or_none()
|
||||
|
||||
if callable(getattr(post_cmd, command, True)):
|
||||
msg = eval('post_cmd.'+command+'(data)')
|
||||
if not app:
|
||||
return JsonResp({'error': 'invalid_app'}, status=401)
|
||||
|
||||
else:
|
||||
error(request, 404, 'InvalidCommand')
|
||||
token.code = None
|
||||
token.token = RandomGen(40)
|
||||
|
||||
return rjson(msg, status=200, content_type="text/html") if isinstance(msg, dict) else msg
|
||||
s.commit()
|
||||
|
||||
return {
|
||||
'access_token': token.token,
|
||||
'token_type': 'Bearer',
|
||||
'scope': app.scope,
|
||||
'created_at': int(app.timestamp.timestamp())
|
||||
}
|
||||
|
||||
|
||||
async def get(request):
|
||||
command = request.match_info['name']
|
||||
post_data = request.query
|
||||
print(post_data)
|
||||
class Post:
|
||||
token = Get.token
|
||||
|
||||
data = sanitize(post_data, command)
|
||||
|
||||
if callable(getattr(get_cmd, command, True)):
|
||||
msg = eval('get_cmd.'+command+'(request, data)')
|
||||
|
||||
else:
|
||||
error(request, 404, 'InvalidCommand')
|
||||
|
||||
return rjson(msg, status=200, content_type="text/html") if isinstance(msg, dict) else msg
|
||||
class InvalidUsage(Exception):
|
||||
pass
|
||||
|
|
33
social/api/settings.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import io
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import Boolean, DotDict
|
||||
from PIL import Image
|
||||
|
||||
from ..config import config
|
||||
from ..database import db
|
||||
|
||||
|
||||
def get_profile(request):
|
||||
return {}
|
||||
|
||||
|
||||
def post_profile(request):
|
||||
user = request.ctx.cookie_user
|
||||
data = DotDict({k: v for k,v in request.ctx.form.items() if k in user.keys()})
|
||||
avatar = request.ctx.files.get('avatar')
|
||||
|
||||
if 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 data.asDict():
|
||||
with db.session() as s:
|
||||
row = s.query(db.table.user).filter_by(id=user.id)
|
||||
row.update(data.asDict())
|
||||
request.ctx.cookie_user.update(data)
|
||||
|
||||
return 'Updated profile'
|
158
social/config.py
|
@ -1,96 +1,103 @@
|
|||
import os
|
||||
import sys
|
||||
import sanic
|
||||
import socket
|
||||
import os, sys, sanic, socket
|
||||
|
||||
from os import environ as env
|
||||
from os.path import abspath, dirname, isfile
|
||||
from collections import namedtuple
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import DotDict, Path, Boolean, GetIp, RandomGen
|
||||
from IzzyLib.template import Template
|
||||
from envbash import load_envbash
|
||||
from jinja2 import Environment
|
||||
from hamlish_jinja import HamlishExtension
|
||||
|
||||
from . import __version__ as VERSION
|
||||
from .functions import boolean
|
||||
|
||||
|
||||
dbtypes = ['sqlite', 'postgresql', 'mysql', 'mssql']
|
||||
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
script_path = dirname(abspath(sys.executable))
|
||||
script_path = Path(sys.executable).parent()
|
||||
|
||||
else:
|
||||
script_path = dirname(abspath(__file__))
|
||||
script_path = Path(__file__).parent()
|
||||
|
||||
if isfile(script_path + '/../datacfg.txt') and env.get('HOME'):
|
||||
stor_path = env.get('HOME') + '/.config/barkshark/social'
|
||||
|
||||
else:
|
||||
stor_path = script_path+'/../data'
|
||||
|
||||
os.makedirs(stor_path+'/media', exist_ok=True)
|
||||
pyenv = env.get('PYENV', 'default').lower()
|
||||
store_path = script_path.parent(True).join('data')
|
||||
pyenv = env.get('PYENV', 'dev').lower()
|
||||
pyenv_path = store_path.join(f'{pyenv}.env')
|
||||
|
||||
try:
|
||||
if pyenv == 'prod':
|
||||
load_envbash(stor_path+'/prod.env')
|
||||
|
||||
elif pyenv in ['dev', 'default']:
|
||||
load_envbash(stor_path+'/dev.env')
|
||||
|
||||
else:
|
||||
print('ERROR: Invalid "PYENV" value. Please use "prod" or "dev"')
|
||||
if pyenv not in ['prod', 'dev']:
|
||||
logging.error('Invalid "PYENV" value. Please use "prod" or "dev"')
|
||||
sys.exit()
|
||||
|
||||
load_envbash(pyenv_path.str())
|
||||
|
||||
|
||||
except FileNotFoundError:
|
||||
print('ERROR: Bash environment file not found. Exiting...')
|
||||
sys.exit()
|
||||
logging.warning('Bash environment file not found:', pyenv_path.str())
|
||||
|
||||
|
||||
Database = namedtuple('Database', 'host port user password database connections')
|
||||
|
||||
try:
|
||||
ip_address = socket.gethostbyname(socket.gethostname())
|
||||
except Exception as e:
|
||||
print(e)
|
||||
ip_address = '127.0.0.1'
|
||||
|
||||
listen = env.get('LISTEN', ip_address)
|
||||
listen = env.get('LISTEN', GetIp())
|
||||
port = int(env.get('PORT', 8020))
|
||||
web_domain = env.get('WEB_DOMAIN', env.get('DOMAIN', f'{listen}:{port}'))
|
||||
connections = int(env.get('DB_CONNECTIONS', 25))
|
||||
|
||||
config = {
|
||||
config = DotDict({
|
||||
'env': pyenv,
|
||||
'path': script_path,
|
||||
'data': store_path,
|
||||
'frontend': script_path.join('frontend', True),
|
||||
'version': VERSION,
|
||||
'dbversion': 20210103,
|
||||
'listen': listen,
|
||||
'port': port,
|
||||
'name': env.get('NAME', 'Barkshark Social'),
|
||||
'web_domain': web_domain,
|
||||
'domain': env.get('DOMAIN', web_domain),
|
||||
'log_level': env.get('LOG_LEVEL', 'INFO').upper(),
|
||||
'log_errors': boolean(env.get('LOG_ERRORS')),
|
||||
'log_date': boolean(env.get('LOG_DATE', True)),
|
||||
'salt': env.get('PASS_SALT'),
|
||||
'vars': {
|
||||
'salt': env.get('PASS_SALT', RandomGen(40)),
|
||||
'secret': env.get('FORWARDED_SECRET', RandomGen(20)),
|
||||
'agent': f'sanic/{sanic.__version__} (Barkshark-Social/{VERSION}; +https://{web_domain}/)',
|
||||
'dbtype': env.get('DB_TYPE', 'sqlite'),
|
||||
'log': DotDict({
|
||||
'level': env.get('LOG_LEVEL', 'INFO').upper(),
|
||||
'errors': Boolean(env.get('LOG_ERRORS')),
|
||||
'date': Boolean(env.get('LOG_DATE', True))
|
||||
}),
|
||||
'vars': DotDict({
|
||||
'max_chars': env.get('MAX_CHARS', 69420),
|
||||
'posts': env.get('PROFILE_POSTS', 20),
|
||||
},
|
||||
'posts': env.get('PROFILE_POSTS', 20)
|
||||
}),
|
||||
'rd': DotDict({
|
||||
'host': env.get('REDIS_HOST', 'localhost'),
|
||||
'port': int(env.get('REDIS_PORT', 6379)),
|
||||
'user': env.get('REDIS_USER', env.get('USER', 'social')),
|
||||
'password': env.get('REDIS_PASSWORD'),
|
||||
'database': int(env.get('REDIS_DATABASE', 0)),
|
||||
'prefix': env.get('REDIS_PREFIX', 'social'),
|
||||
'maxconnections': int(env.get('REDIS_CONNECTIONS', 25))
|
||||
}),
|
||||
'db': DotDict({
|
||||
'host': env.get('DB_HOST', '/var/run/postgresql'),
|
||||
'port': int(env.get('DB_PORT', 5432)),
|
||||
'user': env.get('DB_USER', env.get('USER', 'social')),
|
||||
'password': env.get('DB_PASS'),
|
||||
'database': env.get('DB_DATABASE', 'social'),
|
||||
'maxconnections': int(env.get('DB_CONNECTIONS', 25))
|
||||
}),
|
||||
'sqfile': store_path.join(env.get('SQ_DATABASE', 'database.sqlite3'))
|
||||
})
|
||||
|
||||
'redis': Database(
|
||||
host = env.get('REDIS_HOST', 'localhost'),
|
||||
port = int(env.get('REDIS_PORT', 6379)),
|
||||
user = env.get('REDIS_USER', env.get('USER', 'social')),
|
||||
password = env.get('REDIS_PASSWORD', None),
|
||||
database = env.get('REDIS_DATABASE', 0),
|
||||
connections = None
|
||||
),
|
||||
|
||||
'pg': Database(
|
||||
host = env.get('DB_HOST', None),
|
||||
port = int(env.get('DB_PORT', 5432)),
|
||||
user = env.get('DB_USER', env.get('USER', 'social')),
|
||||
password = env.get('DB_PASSWORD', None),
|
||||
database = env.get('DB_DATABASE', 'social'),
|
||||
connections = int(env.get('DB_CONNECTIONS', 5))
|
||||
)
|
||||
}
|
||||
if config.dbtype not in dbtypes:
|
||||
dbtypes_str = ', '.join(dbtypes)
|
||||
logging.error(f'Invalid dbtype : {config.dbtype}')
|
||||
os.exit()
|
||||
|
||||
# Delete me later
|
||||
config.update({
|
||||
'log_level': config.log.level,
|
||||
'log_errors': config.log.errors,
|
||||
'log_date': config.log.date
|
||||
})
|
||||
|
||||
|
||||
if not config['log_date']:
|
||||
|
@ -99,21 +106,14 @@ if not config['log_date']:
|
|||
else:
|
||||
log_date = '[%(asctime)s] '
|
||||
|
||||
logging.setConfig({'level': config.log.level})
|
||||
config.data.join('media', True).mkdir()
|
||||
config.data.join('template', True).mkdir()
|
||||
|
||||
header_string = f'sanic/{sanic.__version__} (Barkshark-Social/{config["version"]}; +https://{config["web_domain"]}/)'
|
||||
|
||||
|
||||
if pyenv in ['prod', 'default']:
|
||||
if pyenv == 'default':
|
||||
logging.warning('No environment specified. Assuming development')
|
||||
logging.warning('Set "PYENV" to "prod" or "dev" to disable this warning')
|
||||
|
||||
logging.debug('Starting in production mode')
|
||||
|
||||
elif pyenv == 'dev':
|
||||
logging.debug('Starting in development mode')
|
||||
|
||||
|
||||
if config['salt'] == None:
|
||||
logging.error('Pass salt is empty. Generate one with "setup.py uuid" and set PASS_SALT to that value.')
|
||||
sys.exit()
|
||||
config.template = Template(
|
||||
search=[
|
||||
config.data.join('template', True),
|
||||
config.frontend.str()
|
||||
],
|
||||
autoescape=False
|
||||
)
|
||||
|
|
|
@ -1,99 +1,129 @@
|
|||
import pg
|
||||
import uuid
|
||||
import sys
|
||||
import json
|
||||
import sqlite3, sys, traceback
|
||||
|
||||
from DBUtils.PooledPg import PooledPg
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import DotDict
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from ..config import config, script_path, logging
|
||||
from ..functions import genkey
|
||||
from .schema import table
|
||||
from .migrate import upgrade
|
||||
|
||||
from . import functions
|
||||
from ..config import config
|
||||
|
||||
|
||||
DB = config['pg']
|
||||
class Row(DotDict):
|
||||
def __init__(self, row):
|
||||
super().__init__()
|
||||
|
||||
if not row:
|
||||
return
|
||||
|
||||
for attr in dir(row):
|
||||
if not attr.startswith('_') and attr != 'metadata':
|
||||
self[attr] = getattr(row, attr)
|
||||
|
||||
|
||||
def dbconn(database, pooled=True):
|
||||
options = {
|
||||
'dbname': DB.database,
|
||||
'user': DB.user,
|
||||
'passwd': DB.password
|
||||
}
|
||||
class User(DotDict):
|
||||
def __init__(self, user):
|
||||
super().__init__(user)
|
||||
|
||||
if DB.host:
|
||||
options.update({
|
||||
'host': DB.host,
|
||||
'port': DB.port
|
||||
})
|
||||
domain = db.fetch('domain', id=self.domainid)
|
||||
self.domain = domain.domain
|
||||
self.avatar = f'https://{config.domain}/'
|
||||
self.avatar_path = f'media/avatar/{self.domain}/{self.handle[0]}/{self.handle}.png'
|
||||
|
||||
if pooled:
|
||||
cached = 5 if DB.connections >= 5 else 0
|
||||
dbsetup = PooledPg(maxconnections=DB.connections, mincached=cached, maxusage=1, **options)
|
||||
|
||||
else:
|
||||
dbsetup = pg.DB(**options)
|
||||
|
||||
return dbsetup.connection() if pooled == True else dbsetup
|
||||
if config.data.join(self.avatar_path).exists():
|
||||
self.avatar += self.avatar_path
|
||||
else:
|
||||
self.avatar += 'static/missing_pfp.svg'
|
||||
|
||||
|
||||
def db_check():
|
||||
database = DB.database
|
||||
pre_db = dbconn('postgres', pooled=False)
|
||||
|
||||
if '--dropdb' in sys.argv:
|
||||
pre_db.query(f'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = \'{database}\';')
|
||||
pre_db.query(f'DROP DATABASE {database};')
|
||||
|
||||
if database not in pre_db.get_databases():
|
||||
logging.info('Database doesn\'t exist. Creating it now...')
|
||||
|
||||
pre_db.query(f'CREATE DATABASE {database} WITH TEMPLATE = template0;')
|
||||
|
||||
db_setup = dbconn(database, pooled=False)
|
||||
database = open(script_path+'/dist/database.sql').read().replace('\t', '').replace('\n', '')
|
||||
db_setup.query(database)
|
||||
db_setup.close()
|
||||
|
||||
pre_db.close()
|
||||
class DataBase(object):
|
||||
Row = Row
|
||||
cache = DotDict()
|
||||
classes = DotDict()
|
||||
classes.User = User
|
||||
|
||||
|
||||
if '--skipdbcheck' not in sys.argv:
|
||||
db_check()
|
||||
def __init__(self, sqfile=None, tables=None):
|
||||
self.db = create_engine(sqfile)
|
||||
self.table = tables
|
||||
|
||||
db = dbconn(DB.database)
|
||||
for table in tables:
|
||||
self.cache[table] = DotDict()
|
||||
|
||||
self.get = functions.Get(self)
|
||||
self.put = functions.Put(self)
|
||||
self.delete = functions.Del(self)
|
||||
|
||||
|
||||
def newtrans(funct):
|
||||
db.begin()
|
||||
result = funct
|
||||
db.end()
|
||||
|
||||
return result
|
||||
def execute(self, string, values=[]):
|
||||
with self.session() as s:
|
||||
data = s.execute(string, values)
|
||||
return data.fetchall()
|
||||
|
||||
|
||||
def first_setup():
|
||||
dbcheck = db.query('SELECT * FROM settings WHERE setting = \'setup\'').dictresult()
|
||||
def fetch(self, table_name, single=True, **kwargs):
|
||||
with self.session() as s:
|
||||
q = s.query(self.table[table_name]).filter_by(**kwargs)
|
||||
rows = q.all()
|
||||
|
||||
if dbcheck == []:
|
||||
keys = genkey()
|
||||
if single:
|
||||
return Row(rows[0]) if rows else None
|
||||
|
||||
settings = {
|
||||
'setup': True,
|
||||
'pubkey': keys['pubkey'],
|
||||
'privkey': keys['privkey'],
|
||||
'char_limit': 4096,
|
||||
'table_limit': 8,
|
||||
'name': 'Barkshark Social',
|
||||
'description': 'UwU',
|
||||
'theme': 'blue',
|
||||
'domain': config['domain']
|
||||
}
|
||||
return [Row(row) for row in rows]
|
||||
|
||||
for key in settings:
|
||||
db.insert('settings', setting=key, val=settings[key])
|
||||
|
||||
logging.info('Database setup finished :3')
|
||||
def count(self, table_name, **kwargs):
|
||||
with self.session() as s:
|
||||
q = s.query(self.table[table_name]).filter_by(**kwargs)
|
||||
return q.count()
|
||||
|
||||
newtrans(first_setup())
|
||||
|
||||
__all__ = ['get', 'put', 'update', 'delete', 'newtrans']
|
||||
@contextmanager
|
||||
def session(self):
|
||||
session = sessionmaker(bind=self.db)()
|
||||
|
||||
try:
|
||||
yield session
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
session.rollback()
|
||||
|
||||
finally:
|
||||
if session.transaction.is_active:
|
||||
session.commit()
|
||||
|
||||
|
||||
def get_tables(self):
|
||||
query = db.execute("SELECT name FROM sqlite_master WHERE type IN ('table','view') and name NOT LIKE 'sqlite_%'")
|
||||
return [row[0] for row in query]
|
||||
|
||||
|
||||
def drop_tables(self):
|
||||
tables = self.get_tables()
|
||||
|
||||
with self.session() as s:
|
||||
for table in tables:
|
||||
s.execute(f'DROP TABLE {table}')
|
||||
|
||||
|
||||
db = DataBase(f'sqlite:///{config.sqfile}', table)
|
||||
config_version = db.get.version()
|
||||
|
||||
if config_version in [0, None]:
|
||||
pass
|
||||
|
||||
elif config_version < config.dbversion:
|
||||
logging.error('Run "python3 -m social.manage migrate" to upgrade the database schema')
|
||||
sys.exit()
|
||||
|
||||
else:
|
||||
for k,v in db.get.configs(False).items():
|
||||
db.get.cache.config[k] = v
|
||||
|
||||
config.domainid = db.get.domainid(config.domain)
|
||||
|
|
42
social/database/__main__.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from datetime import datetime
|
||||
|
||||
from . import db, table, Base
|
||||
|
||||
from ..config import config
|
||||
|
||||
|
||||
Base.metadata.create_all(db.db)
|
||||
with db.session() as session:
|
||||
print(session.transaction.is_active)
|
||||
|
||||
#initdomain = table.domain(
|
||||
#domain = config.domain,
|
||||
#url = f'https://{config.web_domain}',
|
||||
#name = config.name,
|
||||
#description = 'im gay',
|
||||
#timestamp = datetime.now()
|
||||
#)
|
||||
|
||||
#session.add(initdomain)
|
||||
#domain = session.query(table.domain).filter_by(domain=config.domain).first()
|
||||
|
||||
#inituser = table.user(
|
||||
#handle = 'izalia',
|
||||
#name = 'Izzy Wizzy',
|
||||
#domainid = domain.id,
|
||||
#email = 'izalia@barkshark.xyz',
|
||||
#password = 'FuckThisNeedsToBeHashed',
|
||||
#bio = 'im gay',
|
||||
#signature = 'MERP!',
|
||||
#data = {'im': 'gay'},
|
||||
#permissions = 0,
|
||||
#pubkey = 'NotAPubKeyButWhatever',
|
||||
#privkey = None,
|
||||
#timestamp = datetime.now()
|
||||
#)
|
||||
|
||||
#session.add(inituser)
|
||||
#session.commit()
|
||||
|
||||
print(session.query(table.domain).all())
|
||||
print(session.query(table.user).all())
|
|
@ -1,19 +0,0 @@
|
|||
from . import db, get, logging
|
||||
|
||||
|
||||
def login_cookie(cid, cookie):
|
||||
db.delete('login_cookies', id=cid)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def post(postid, post_data, login_token):
|
||||
token_data = get.login_cookie(login_token)
|
||||
|
||||
if int(postid) != int(post_data['post']['id']) or token_data == None:
|
||||
return
|
||||
|
||||
if token_data['userid'] != post_data['post']['userid']:
|
||||
return
|
||||
|
||||
print(db.delete('statuses', id=post_data['post']['id']))
|
375
social/database/functions.py
Normal file
|
@ -0,0 +1,375 @@
|
|||
from IzzyLib.misc import Boolean, DotDict, RandomGen
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..config import config
|
||||
from ..functions import GenKey
|
||||
|
||||
|
||||
subtypes = {
|
||||
'str': str,
|
||||
'int': int,
|
||||
'bool': Boolean,
|
||||
'datetime': int
|
||||
}
|
||||
|
||||
|
||||
config_defaults = {
|
||||
'name': 'Barkshark Social',
|
||||
'description': 'Barkshark Social is an upcoming Activity Pub server with a focus on fredom of association, low resource usage, and cusomizability.',
|
||||
'description_short': None,
|
||||
'char_limit': 5000,
|
||||
'bio_limit': 1000,
|
||||
'emoji_size_limit': 100,
|
||||
'secure': True,
|
||||
'require_approval': True,
|
||||
'closed': False,
|
||||
'private_profiles': True,
|
||||
'robotstxt': 'User-agent: *\nDisallow: /'
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Get():
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.session = db.session
|
||||
self.table = db.table
|
||||
self.fetch = db.fetch
|
||||
self.cache = db.cache
|
||||
|
||||
|
||||
def __parse_config(self, row):
|
||||
typefunc = subtypes[row.type] if row.type else str
|
||||
value = typefunc(row.value)
|
||||
|
||||
if row.key == 'emoji_size_limit':
|
||||
value = value * 1024
|
||||
|
||||
return datetime.fromtimestamp(value) if row.type == 'datetime' else value
|
||||
|
||||
|
||||
def config(self, key, default=None):
|
||||
cached = self.cache.config.get(key)
|
||||
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
row = self.fetch('config', key=key)
|
||||
|
||||
if row:
|
||||
var = self.__parse_config(row)
|
||||
self.cache.config[key] = var
|
||||
return var
|
||||
|
||||
if not default:
|
||||
value = config_defaults.get(key)
|
||||
return value * 1000 if key == 'emoji_size_limit' else value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def configs(self, cached=True):
|
||||
if cached:
|
||||
data = config_defaults.copy()
|
||||
data.update(self.cache.config)
|
||||
|
||||
return DotDict(data)
|
||||
|
||||
data = DotDict({})
|
||||
rows = self.fetch('config', single=False)
|
||||
|
||||
for row in rows:
|
||||
data[row.key] = self.__parse_config(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def domainid(self, domain):
|
||||
domain = self.fetch('domain', domain=domain)
|
||||
|
||||
return domain.id if domain else None
|
||||
|
||||
|
||||
def emoji(self, shortcode=None, domain=None):
|
||||
domain = domain if domain else config.domain
|
||||
domainid = self.domainid(domain)
|
||||
cachekey = domain+shortcode
|
||||
cache = self.cache.emoji.get(cachekey)
|
||||
|
||||
if cache:
|
||||
return cache
|
||||
|
||||
if not domainid:
|
||||
logging.error('get.emoji: Cannot find domain:', domain)
|
||||
return
|
||||
|
||||
row = self.fetch('emoji', shortcode=shortcode, domainid=domainid)
|
||||
|
||||
if row:
|
||||
row.path = f'media/emojis/{domainid}/{row.shortcode[0]}/{row.shortcode}.png'
|
||||
self.cache.emoji[cachekey] = row
|
||||
|
||||
return self.cache.emoji[cachekey]
|
||||
|
||||
return
|
||||
|
||||
|
||||
def user_count(self):
|
||||
domain = self.fetch('domain', domain=config.domain)
|
||||
|
||||
with self.session() as s:
|
||||
return s.query(self.table.user).filter_by(domainid=domain.id).count()
|
||||
|
||||
|
||||
def domain_count(self):
|
||||
with self.session() as s:
|
||||
return s.query(self.table.domain).count()
|
||||
|
||||
|
||||
def status_count(self, domain=None):
|
||||
domain = self.fetch('domain', domain=config.domain if not domain else domain)
|
||||
|
||||
with self.session() as s:
|
||||
return s.query(self.table.status).filter_by(domainid=domain.id).count()
|
||||
|
||||
|
||||
def token(self, token_header, table='token'):
|
||||
table = self.table[table]
|
||||
response = {'user': None, 'token': None}
|
||||
|
||||
with self.session() as s:
|
||||
token = s.query(table).filter_by(token=token_header)
|
||||
response['token'] = self.db.Row(token.one_or_none())
|
||||
|
||||
if table == 'cookie':
|
||||
print(response['token'])
|
||||
|
||||
if response['token']:
|
||||
row = self.db.Row(s.query(self.table.user).filter_by(id=response['token'].userid).one_or_none())
|
||||
response['user'] = self.db.classes.User(row.asDict())
|
||||
|
||||
if not response['user'] and response['token']:
|
||||
response['token'] = None
|
||||
token.delete()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def user(self, handle, domain=None, single=True):
|
||||
domain = config.domain if not domain else domain
|
||||
|
||||
with self.session() as s:
|
||||
domainrow = s.query(self.table.domain).filter_by(domain=domain)
|
||||
|
||||
if not domainrow:
|
||||
return
|
||||
|
||||
row = s.query(self.table.user).filter(func.lower(self.table.user.handle) == func.lower(handle) and self.table.user.domainid == domainid)
|
||||
return self.db.Row(row.one_or_none())
|
||||
|
||||
|
||||
def version(self):
|
||||
with self.session() as s:
|
||||
try:
|
||||
rows = s.execute("SELECT name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'").fetchall()
|
||||
tables = [row[0] for row in rows]
|
||||
|
||||
if 'config' not in tables:
|
||||
return 0
|
||||
|
||||
row = self.config('version')
|
||||
|
||||
if not row:
|
||||
return 0
|
||||
|
||||
return row
|
||||
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
|
||||
class Put():
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.session = db.session
|
||||
self.table = db.table
|
||||
self.fetch = db.fetch
|
||||
self.cache = db.cache
|
||||
|
||||
|
||||
def app(self, redirect_uri, scope, name, url, client_id=RandomGen(40, '-_'), client_secret=RandomGen(40, '-_')):
|
||||
data = {
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'redirect_uri': redirect_uri,
|
||||
'scope': scope,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
|
||||
with self.session() as s:
|
||||
row = s.query(self.table.app).filter_by(client_id=client_id, client_secret=client_secret)
|
||||
|
||||
if row.one_or_none():
|
||||
row.update(data)
|
||||
return row.one_or_none()
|
||||
|
||||
s.add(self.table.app(**data))
|
||||
return row.one_or_none()
|
||||
|
||||
|
||||
def config(self, key, value, subtype='str'):
|
||||
row = self.fetch('config', key=key)
|
||||
|
||||
with self.session() as s:
|
||||
typefunc = subtypes[row.type] if row else str
|
||||
value = int(datetime.timestamp(value)) if subtype == 'datetime' else typefunc(value)
|
||||
|
||||
if row:
|
||||
if row.value == value:
|
||||
return
|
||||
|
||||
row.value = value
|
||||
|
||||
else:
|
||||
s.add(self.table.config(
|
||||
key=key,
|
||||
value=value,
|
||||
type=subtype
|
||||
))
|
||||
|
||||
self.cache.config[key] = value
|
||||
return True
|
||||
|
||||
|
||||
def configs(self, *args):
|
||||
with self.session() as s:
|
||||
for config in args:
|
||||
key = config.get('key')
|
||||
value = config.get('value')
|
||||
subtype = config.get('subtype')
|
||||
|
||||
row = s.query(self.table.config).filter_by(key=key)
|
||||
|
||||
if row:
|
||||
row.value = value
|
||||
|
||||
else:
|
||||
s.add(self.table.config(
|
||||
key = key,
|
||||
value = value,
|
||||
subtype = subtype
|
||||
))
|
||||
|
||||
self.cache.config.pop(key)
|
||||
|
||||
|
||||
def cookie(self, userid, address=None, agent=None, access=datetime.now(), token=RandomGen()):
|
||||
row = self.fetch('cookie', token=token, userid=userid)
|
||||
|
||||
with self.session() as s:
|
||||
if row:
|
||||
row.access = access
|
||||
|
||||
else:
|
||||
s.add(self.table.cookie(
|
||||
token = token,
|
||||
userid = userid,
|
||||
address = address,
|
||||
agent = agent,
|
||||
access = access
|
||||
))
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def emoji(self, filename, shortcode, domain=None, display=True, enabled=True):
|
||||
domain = domain if domain else config.domain
|
||||
domainid = self.db.get.domainid(domain)
|
||||
cachekey = domain+shortcode
|
||||
|
||||
if self.db.get.emoji(shortcode, domain):
|
||||
return False
|
||||
|
||||
with self.session() as s:
|
||||
s.add(self.table.emoji(
|
||||
shortcode = shortcode,
|
||||
domainid = domainid,
|
||||
display = display,
|
||||
enabled = enabled,
|
||||
filename = filename,
|
||||
timestamp = datetime.now()
|
||||
))
|
||||
|
||||
self.cache.emoji.pop(cachekey, None)
|
||||
return True
|
||||
|
||||
|
||||
def user(self, handle, domain=None, **data):
|
||||
if not domain:
|
||||
domain = config.domain
|
||||
|
||||
domainid = self.db.get.domainid(domain)
|
||||
|
||||
with self.session() as s:
|
||||
row = s.query(self.table.user).filter_by(handle=handle, domainid=domainid)
|
||||
|
||||
if row.one_or_none():
|
||||
row.update(data)
|
||||
return
|
||||
|
||||
if not data.get('privkey'):
|
||||
keys = GenKey()
|
||||
|
||||
data['privkey'] = keys.PRIVKEY
|
||||
data['pubkey'] = keys.PUBKEY
|
||||
|
||||
if not data.get('timestamp'):
|
||||
data['timestamp'] = datetime.now()
|
||||
|
||||
s.add(self.table.user(
|
||||
handle = handle,
|
||||
domainid = domainid,
|
||||
**data
|
||||
))
|
||||
|
||||
|
||||
def version(self, ver):
|
||||
self.config('version', ver, 'int')
|
||||
|
||||
|
||||
class Del():
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.session = db.session
|
||||
self.table = db.table
|
||||
self.fetch = db.fetch
|
||||
self.cache = db.cache
|
||||
|
||||
|
||||
def config(self, key):
|
||||
with self.session() as s:
|
||||
row = s.query(self.table.config).filter_by(key=key)
|
||||
|
||||
if row.one_or_none():
|
||||
row.delete()
|
||||
|
||||
if config.get(key):
|
||||
del config[key]
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def cookie(self, token):
|
||||
with self.session() as s:
|
||||
row = s.query(self.table.cookie).filter_by(token=token)
|
||||
|
||||
if row.one_or_none():
|
||||
row.delete()
|
||||
return True
|
||||
|
||||
return False
|
|
@ -1,203 +0,0 @@
|
|||
import json
|
||||
|
||||
from . import db, logging, config
|
||||
from ..functions import timestamp
|
||||
|
||||
|
||||
def handle_to_userid(handle):
|
||||
user = db.query(f'SELECT * FROM users WHERE handle = \'{handle}\'').dictresult()
|
||||
|
||||
if user == []:
|
||||
return None
|
||||
|
||||
else:
|
||||
userid = user[0]['id']
|
||||
|
||||
return userid
|
||||
|
||||
|
||||
def api_token(token):
|
||||
raw_token = db.query(f'SELECT * FROM auth_tokens WHERE token = \'{token}\'').dictresult()
|
||||
|
||||
if raw_token == []:
|
||||
return
|
||||
|
||||
token_data = raw_token[0]
|
||||
|
||||
return token_data
|
||||
|
||||
|
||||
def login_cookie(cookie):
|
||||
raw_cookie = db.query(f'SELECT * FROM login_cookies WHERE cookie = \'{cookie}\'').dictresult()
|
||||
|
||||
if raw_cookie == []:
|
||||
return None
|
||||
|
||||
else:
|
||||
cookie_data = raw_cookie[0]
|
||||
|
||||
return cookie_data
|
||||
|
||||
|
||||
def app(client_id):
|
||||
raw_client = db.query(f'SELECT * FROM auth_apps WHERE client_id = \'{client_id}\'').dictresult()
|
||||
|
||||
if raw_client == []:
|
||||
return
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def auth_code(code):
|
||||
raw_code = db.query(f'SELECT * FROM auth_code WHERE code = \'{code}\'').dictresult()
|
||||
|
||||
if len(raw_code) == 0:
|
||||
return
|
||||
|
||||
return raw_code[0]
|
||||
|
||||
|
||||
def post(postid):
|
||||
try:
|
||||
int(postid)
|
||||
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
raw_post = db.query(f'SELECT * FROM statuses WHERE id = \'{postid}\'').dictresult()
|
||||
|
||||
if raw_post == []:
|
||||
return None
|
||||
|
||||
else:
|
||||
post_data = raw_post[0]
|
||||
|
||||
return post_data
|
||||
|
||||
|
||||
def posts(username, postid, newtrans=False):
|
||||
if postid != None:
|
||||
page = f'and id < {postid}'
|
||||
|
||||
else:
|
||||
page = ''
|
||||
|
||||
user_data = user(username)
|
||||
|
||||
if user_data == None:
|
||||
return None
|
||||
|
||||
postlimit = config['vars']['posts']
|
||||
|
||||
query = f'''SELECT id, userid, content, visibility, mentions, timestamp, warning
|
||||
FROM statuses
|
||||
WHERE userid = {user_data['id']} {page}
|
||||
ORDER BY id DESC LIMIT {postlimit};'''
|
||||
|
||||
raw_posts = db.query(query).dictresult()
|
||||
|
||||
posts = []
|
||||
|
||||
for post in raw_posts:
|
||||
post['user'] = user(post['userid'])
|
||||
posts.append(post)
|
||||
|
||||
return posts
|
||||
|
||||
|
||||
def user(user, filters=None):
|
||||
if user == None:
|
||||
return
|
||||
|
||||
if isinstance(user, str):
|
||||
userid = handle_to_userid(user.lower())
|
||||
|
||||
else:
|
||||
userid = user
|
||||
|
||||
if userid == None:
|
||||
return
|
||||
|
||||
def filter_data(data, fields):
|
||||
if not data:
|
||||
logging.warning('Missing data for filtering.')
|
||||
return None
|
||||
|
||||
if fields:
|
||||
new_data = {}
|
||||
|
||||
for item in data:
|
||||
if item not in fields.split(','):
|
||||
new_data[item] = data[item]
|
||||
|
||||
return new_data
|
||||
|
||||
else:
|
||||
return data
|
||||
|
||||
raw_user_data = db.query(f'SELECT * FROM users WHERE id = \'{userid}\'').dictresult()
|
||||
|
||||
if raw_user_data == []:
|
||||
return None
|
||||
|
||||
else:
|
||||
user_data = raw_user_data[0]
|
||||
table = user_data['info_table']
|
||||
|
||||
if table:
|
||||
user_data['info_table'] = json.loads(table)
|
||||
|
||||
user_data = filter_data(user_data, filters)
|
||||
|
||||
return user_data
|
||||
|
||||
|
||||
def profile(handle, postid=None):
|
||||
raw_user_data = user(handle)
|
||||
|
||||
if raw_user_data == None:
|
||||
return None
|
||||
|
||||
user_data = {'profile': raw_user_data.copy()}
|
||||
userid = user_data['profile']['id']
|
||||
|
||||
post_count = db.query(f'SELECT COUNT(*) FROM statuses WHERE userid={userid};').dictresult()
|
||||
|
||||
user_data['post'] = posts(handle, postid)
|
||||
user_data.update(post_count[0])
|
||||
|
||||
return user_data
|
||||
|
||||
|
||||
def server_stats():
|
||||
ts = timestamp()
|
||||
stats = {
|
||||
'user_count': db.query('SELECT COUNT(*) FROM users WHERE domain_id is NULL;').dictresult()[0]['count'],
|
||||
'status_count': db.query('SELECT COUNT(*) FROM statuses WHERE id is not NULL;').dictresult()[0]['count'],
|
||||
'domain_count': db.query('SELECT COUNT(*) FROM domains WHERE id is not NULL;').dictresult()[0]['count'],
|
||||
'timestamp': ts
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def settings(name):
|
||||
if type(name) != str:
|
||||
logging.error('get.settings only accepts a string, dingus!')
|
||||
|
||||
if name != 'all':
|
||||
setresults = db.query(f'SELECT * FROM settings WHERE setting = \'{name}\'').dictresult()
|
||||
|
||||
if setresults == []:
|
||||
logging.warning('Can\'t find settings in the database')
|
||||
return
|
||||
|
||||
return setresults[0]['val']
|
||||
|
||||
setresults = db.query(f'SELECT * FROM settings').dictresult()
|
||||
setret = {}
|
||||
|
||||
for line in setresults:
|
||||
setret.update({line['setting']: line['val']})
|
||||
|
||||
return setret
|
49
social/database/migrate.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from IzzyLib import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .schema import table, Base
|
||||
from ..config import config
|
||||
|
||||
|
||||
def setup(db, settings):
|
||||
Base.metadata.create_all(db.db)
|
||||
|
||||
db.put.version(config.dbversion)
|
||||
db.put.config('name', settings.get('name', 'Barkshark Social'))
|
||||
db.put.config('domain', settings.get('domain', 'example.com'))
|
||||
db.put.config('host', settings.get('example.com', 'example.com'))
|
||||
db.put.config('description', settings.get('description', 'Barkshark Social is an upcoming Activity Pub server with a focus on fredom of association, low resource usage, and cusomizability.'))
|
||||
db.put.config('char_limit', settings.get('char_limit', 5000), 'int')
|
||||
db.put.config('bio_limit', settings.get('bio_limit', 1000), 'int')
|
||||
db.put.config('secure', settings.get('secure', True), 'bool')
|
||||
|
||||
with db.session() as s:
|
||||
domain = s.query(db.table.domain).filter_by(domain=config.domain).one_or_none()
|
||||
|
||||
if not domain:
|
||||
s.add(db.table.domain(
|
||||
domain = config.domain,
|
||||
name = None,
|
||||
url = f'https://{config.web_domain}',
|
||||
description = None,
|
||||
timestamp = datetime.now()
|
||||
))
|
||||
|
||||
domain = s.query(db.table.domain).filter_by(domain=config.domain).one_or_none()
|
||||
|
||||
domain.domain = config.domain
|
||||
domain.url = f'https://{config.web_domain}'
|
||||
|
||||
db.put.user('system',
|
||||
name = settings.get('name', f'Barkshark Social @ {config.domain}'),
|
||||
bio = 'System account',
|
||||
permissions = 0
|
||||
)
|
||||
|
||||
|
||||
def upgrade(dbversion, db):
|
||||
if dbversion == 0:
|
||||
setup(db)
|
||||
return
|
||||
|
||||
logging.info('Migrated database to version', config.dbversion)
|
|
@ -1,147 +0,0 @@
|
|||
import pg
|
||||
import time
|
||||
import json
|
||||
import secrets
|
||||
|
||||
from . import db, logging, config
|
||||
from ..functions import mkhash, genkey, timestamp
|
||||
|
||||
|
||||
# Why the fuck did I think validating users via token on the db level was a good idea!?
|
||||
def user(handle, email, password, display, bio, table, sig):
|
||||
keys = genkey()
|
||||
|
||||
ts = timestamp()
|
||||
token_string = str(ts) + email
|
||||
pass_hash = mkhash(password+config['salt'])
|
||||
token = mkhash(token_string)
|
||||
|
||||
try:
|
||||
user = db.insert('users',
|
||||
handle=handle.lower(), name=display, bio=bio,
|
||||
info_table=json.dumps(table), email=email, password=pass_hash,
|
||||
permissions=4, timestamp=ts, sig=sig,
|
||||
pubkey=keys['pubkey'], privkey=keys['privkey']
|
||||
)
|
||||
|
||||
except pg.IntegrityError:
|
||||
db.end()
|
||||
return False
|
||||
|
||||
if user['id'] == 1:
|
||||
db.update('users', {'id': 1}, perms=0)
|
||||
|
||||
db.insert('auth_tokens', userid=user['id'], appid=0, token=token, timestamp=ts)
|
||||
|
||||
return {'id': user['id'], 'token': token, 'password': pass_hash, 'username': user['handle'], 'name': user['name'], 'timestamp': ts}
|
||||
|
||||
|
||||
def login_cookie(userid, password, address, agent):
|
||||
ts = timestamp()
|
||||
cookie = mkhash(password+config['salt']+str(ts))
|
||||
|
||||
db.insert('login_cookies',
|
||||
userid=userid, cookie=cookie, timestamp=ts,
|
||||
address=address, agent=agent
|
||||
)
|
||||
|
||||
return cookie
|
||||
|
||||
|
||||
def api_token(username, password):
|
||||
pass_hash = mkhash(password)
|
||||
|
||||
|
||||
def local_post(userid, data):
|
||||
post_data = {
|
||||
'text': data.get('text'),
|
||||
'warning': data.get('warning'),
|
||||
'privacy': data.get('privacy', 'public'),
|
||||
'token': data.get('token'),
|
||||
'media_id': data.get('media_id'),
|
||||
'reply_id': data.get('reply_id')
|
||||
}
|
||||
|
||||
ts = timestamp(integer=False)
|
||||
post_hash = int(mkhash(str(ts) + post_data['text'], alg='md5'), 16)
|
||||
|
||||
if post_data['warning'] == '':
|
||||
post_data['warning'] = None
|
||||
|
||||
db.begin()
|
||||
|
||||
post = db.insert('statuses',
|
||||
hash=post_hash, userid=userid, timestamp=ts,
|
||||
content=post_data['text'], warning=post_data['warning'], visibility=post_data['privacy']
|
||||
)
|
||||
|
||||
db.end()
|
||||
|
||||
return post
|
||||
|
||||
|
||||
def local_post2(userid, posts):
|
||||
db.begin()
|
||||
|
||||
for post in posts:
|
||||
ts = timestamp(integer=False)
|
||||
post_hash = int(mkhash(str(ts) + post['text'], alg='sha256'), 16)
|
||||
|
||||
try:
|
||||
db.insert('statuses',
|
||||
hash=post_hash, userid=userid, timestamp=ts,
|
||||
content=post['text'], warning=post['warning'], visibility=post['privacy']
|
||||
)
|
||||
|
||||
except pg.IntegrityError as e:
|
||||
logging.error(f'Failed to insert post: {e}')
|
||||
|
||||
db.end()
|
||||
|
||||
return 'Done'
|
||||
|
||||
|
||||
def auth_token(code):
|
||||
auth_code = get.auth_token(auth_code)
|
||||
|
||||
if not auth_code:
|
||||
return
|
||||
|
||||
db.insert('auth_tokens', userid='heck', timestamp=timestamp(integer=False))
|
||||
|
||||
|
||||
def status(data):
|
||||
new_data = {
|
||||
'heck': 'heck'
|
||||
}
|
||||
|
||||
|
||||
def instance(data):
|
||||
db.insert('domains')
|
||||
|
||||
|
||||
class oauth:
|
||||
def app(client_id, client_secret, redirect_uri, scope, name, url):
|
||||
if None in [client_id, client_secret, redirect_uri, scope, name]:
|
||||
return
|
||||
|
||||
db.insert('auth_apps',
|
||||
client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri,
|
||||
scope=scope, name=name, url=url
|
||||
)
|
||||
|
||||
def auth_code(userid, appid):
|
||||
user = get.user(userid)
|
||||
app = get.app(appid)
|
||||
|
||||
auth_code = secrets.token_hex(20)
|
||||
db_auth_code = get.auth_token(auth_code)
|
||||
|
||||
while auth_code != db_auth_code[code]:
|
||||
try:
|
||||
newtrans(db.insert('auth_codes', userid=user[id], appid=app[id], code=code, timestamp=timestamp(integer=False)))
|
||||
|
||||
except pg.IntegrityError as e:
|
||||
logging.error(f'Failed to insert post: {e}')
|
||||
|
||||
return auth_code
|
169
social/database/schema.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
from IzzyLib.misc import DotDict
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Boolean
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
table = DotDict()
|
||||
|
||||
class Domain(Base):
|
||||
__tablename__ = 'domain'
|
||||
|
||||
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)
|
||||
name = Column('name', String)
|
||||
description = Column('description', String)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Domain(id={self.id}, domain="{self.domain}", name="{self.name}")'
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
handle = Column('handle', String, nullable=False)
|
||||
domainid = Column('domainid', Integer, ForeignKey('domain.id'), nullable=False)
|
||||
name = Column('name', String)
|
||||
email = Column('email', String)
|
||||
password = Column('password', String)
|
||||
bio = Column('bio', String)
|
||||
signature = Column('signature', String)
|
||||
table = Column('table', JSON)
|
||||
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)
|
||||
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}")'
|
||||
|
||||
|
||||
class Status(Base):
|
||||
__tablename__ = 'status'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
userid = Column('userid', Integer, ForeignKey('user.id'))
|
||||
domainid = Column('domainid', Integer, ForeignKey('domain.id'))
|
||||
hash = Column('hash', String, unique=True)
|
||||
content = Column('content', String, nullable=False)
|
||||
warning = Column('warning', String)
|
||||
visibility = Column('visibility', Integer, default=0)
|
||||
mentions = Column('mentions', String)
|
||||
replies = Column('replies', String)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Status(id={self.id}, userid={self.userid}, hash="{self.hash}")'
|
||||
|
||||
|
||||
class App(Base):
|
||||
__tablename__ = 'app'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
name = Column('name', String)
|
||||
url = Column('url', String)
|
||||
redirect_uri = Column('redirect_uri', String)
|
||||
client_id = Column('appid', String, nullable=False, unique=False)
|
||||
client_secret = Column('secret', String, nullable = False, unique=True)
|
||||
scope = Column('scope', JSON, nullable = False)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'App(id={self.id}, name="{self.name}", url="{self.url}")'
|
||||
|
||||
|
||||
class Token(Base):
|
||||
__tablename__ = 'token'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
token = Column('token', String, unique=True)
|
||||
userid = Column('userid', Integer, ForeignKey('user.id'))
|
||||
appid = Column('appid', Integer, ForeignKey('app.id'))
|
||||
code = Column('code', String, unique=True)
|
||||
access = Column('access', DateTime)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Code(id={self.id}, token="{self.token}")'
|
||||
|
||||
|
||||
class Cookie(Base):
|
||||
__tablename__ = 'cookie'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
token = Column('token', String, nullable=False, unique=True)
|
||||
userid = Column('userid', Integer, ForeignKey('user.id'))
|
||||
address = Column('address', String)
|
||||
agent = Column('agent', String)
|
||||
access = Column('access', DateTime)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Cookie(id={self.id}, cookie="{self.cookie}")'
|
||||
|
||||
|
||||
class Emoji(Base):
|
||||
__tablename__ = 'emoji'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
shortcode = Column('shortcode', String, nullable=False)
|
||||
domainid = Column('domainid', Integer, nullable=False)
|
||||
display = Column('display', Boolean, default=True)
|
||||
enabled = Column('enabled', Boolean, default=True)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Emoji(id={self.id}, shortcode="{self.shortcode}")'
|
||||
|
||||
|
||||
class Object(Base):
|
||||
__tablename__ = 'object'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
uuid = Column('uuid', String)
|
||||
data = Column('data', JSON)
|
||||
timestamp = Column('timestamp', DateTime)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Object(id={self.id}, uuid="{self.uuid}")'
|
||||
|
||||
|
||||
class Config(Base):
|
||||
__tablename__ = 'config'
|
||||
|
||||
id = Column('id', Integer, primary_key=True, autoincrement=True)
|
||||
key = Column('key', String, unique=True)
|
||||
value = Column('value', String)
|
||||
type = Column('type', String, default='str')
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'Config(id={self.id}, key="{self.key}", value="{self.value}", type="{self.type}")'
|
||||
|
||||
|
||||
table.update({
|
||||
'app': App,
|
||||
'config': Config,
|
||||
'cookie': Cookie,
|
||||
'emoji': Emoji,
|
||||
'domain': Domain,
|
||||
'object': Object,
|
||||
'status': Status,
|
||||
'token': Token,
|
||||
'user': User
|
||||
})
|
|
@ -1,54 +0,0 @@
|
|||
from . import db, get, logging, config
|
||||
from ..functions import mkhash
|
||||
|
||||
|
||||
def profile(handle, data):
|
||||
user = get.user(handle)
|
||||
new_data = {}
|
||||
|
||||
if user == None:
|
||||
return None
|
||||
|
||||
if data.get('type') == 'profile':
|
||||
if data.get('display'):
|
||||
new_data['name'] = data['display']
|
||||
|
||||
if data.get('bio'):
|
||||
new_data['bio'] = data['bio']
|
||||
|
||||
if data.get('sig'):
|
||||
new_data['sig'] = data['sig']
|
||||
|
||||
update = db.update('users', {'id': user['id']}, **new_data)
|
||||
|
||||
elif data.get('type') == 'password':
|
||||
curpass_hash = mkhash(data['curpassword']+config['salt'])
|
||||
|
||||
if data['newpassword1'] != data['newpassword2'] or curpass_hash != get.user(data['handle'])['password']:
|
||||
logging.warning('fuck')
|
||||
return None
|
||||
|
||||
pass_hash = mkhash(data['newpassword1']+config['salt'])
|
||||
update = db.update('users', {'id': user['id']}, password=pass_hash)
|
||||
|
||||
elif data.get('type') == 'email':
|
||||
update = db.update('users', {'id': user['id']}, email=data['email'])
|
||||
|
||||
else:
|
||||
logging.warning('Invalid action')
|
||||
return None
|
||||
|
||||
logging.debug(update)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def tables(handle, bio):
|
||||
user_id = check_token(token)
|
||||
|
||||
if user_id == False:
|
||||
return False
|
||||
|
||||
msg = db.update('users', {'id': user_id}, info_table=json.dumps(bio))
|
||||
|
||||
return msg
|
|
@ -4,15 +4,14 @@ from sanic import response
|
|||
from sanic import exceptions as exc
|
||||
from IzzyLib import logging
|
||||
|
||||
from .web_functions import error
|
||||
from .database import db
|
||||
from .functions import Error, ParseRequest
|
||||
|
||||
|
||||
@exc.add_status_code(418)
|
||||
class Teapot(exc.SanicException):
|
||||
pass
|
||||
|
||||
|
||||
### Insert named tuple or dict mapping error codes to exceptions here
|
||||
Exc = {
|
||||
400: exc.InvalidUsage,
|
||||
401: exc.Unauthorized,
|
||||
|
@ -27,18 +26,29 @@ Exc = {
|
|||
|
||||
|
||||
def generic(request, exception):
|
||||
ParseRequest(request, db)
|
||||
try:
|
||||
status = exception.status_code
|
||||
except:
|
||||
status = 500
|
||||
|
||||
return error(request, status, str(exception))
|
||||
if status == 400:
|
||||
print(exception)
|
||||
|
||||
return Error(request, status, str(exception))
|
||||
|
||||
|
||||
def server_error(request, exception):
|
||||
ParseRequest(request, db)
|
||||
traceback.print_exc()
|
||||
msg = 'OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!'
|
||||
return error(request, 500, msg)
|
||||
return Error(request, 500, msg)
|
||||
|
||||
|
||||
def orm_detached_instance(request, exception):
|
||||
msg = 'Sqlalchemy is fucking up again :/'
|
||||
#return response.text(msg, status=500)
|
||||
return Error(request, 500, msg)
|
||||
|
||||
|
||||
def missing_template(request, exception):
|
||||
|
|
51
social/frontend/base.haml
Normal file
|
@ -0,0 +1,51 @@
|
|||
-set base = 'https://' + config.web_domain
|
||||
-set color_theme = request.cookies.get('theme', 'pink')
|
||||
-set cookie_user = request.ctx.cookie_user
|
||||
-set instname = db.get.config('name')
|
||||
|
||||
<!DOCTYPE html>
|
||||
%html
|
||||
%head
|
||||
%title << {{instname}}: {{page}}
|
||||
%link rel='stylesheet' type='text/css' href='{{base}}/style-{{color_theme}}-{{CssTimestamp("style")}}.css'
|
||||
%link rel='manifest' href='{{base}}/manifest.json'
|
||||
%meta charset='UTF-8'
|
||||
%meta name='viewport' content='width=device-width, initial-scale=1'
|
||||
|
||||
%body
|
||||
#body
|
||||
#header.section
|
||||
%a.title href='{{base}}/' << {{instname}}
|
||||
|
||||
-if message
|
||||
#message.section << {{message}}
|
||||
|
||||
-if error
|
||||
#error.secion << {{error}}
|
||||
|
||||
#content.grid-container
|
||||
#menu.grid-item.section
|
||||
%ul.menu
|
||||
%li -> %a href='{{base}}/' << Home
|
||||
%li -> %a href='{{base}}/rules' << Rules
|
||||
%li -> %a href='{{base}}/about' << About
|
||||
|
||||
#content-body.grid-item.section
|
||||
.title << {{page}}
|
||||
|
||||
-block content
|
||||
|
||||
#footer.grid-container.section
|
||||
.user.grid-item
|
||||
%ul.menu
|
||||
-if cookie_user
|
||||
%li.name -> %a href='{{base}}/@{{cookie_user.handle}}' -> =cookie_user.name
|
||||
%li -> %a href='{{base}}/settings' << Settings
|
||||
%li -> %a href='{{base}}/logout' << Logout
|
||||
-else
|
||||
%li.name << Guest
|
||||
%li -> %a href='{{base}}/login' << Login
|
||||
%li -> %a href='{{base}}/register' << Register
|
||||
|
||||
.source.grid-item
|
||||
%a href='https://git.barkshark.xyz/izaliamae/social' target='_new' << Barkshark Social/{{config.version}}
|
8
social/frontend/error.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Error'
|
||||
-block content
|
||||
%center
|
||||
%font size='8'
|
||||
HTTP {{data.code}}
|
||||
%br
|
||||
=data.msg
|
4
social/frontend/pages/about.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'About'
|
||||
-block content
|
||||
{{text}}
|
4
social/frontend/pages/home.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Home'
|
||||
-block content
|
||||
{{db.get.config('description')}}
|
17
social/frontend/pages/login.haml
Normal file
|
@ -0,0 +1,17 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Login'
|
||||
-block content
|
||||
%form action='https://{{config.domain}}/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='submit' value='Login'
|
34
social/frontend/pages/profile.haml
Normal file
|
@ -0,0 +1,34 @@
|
|||
-extends 'base.haml'
|
||||
-set page = user.name
|
||||
-set posts = db.fetch('status', userid=user.id)
|
||||
-block content
|
||||
#profile.grid-container
|
||||
.grid-item.icon -> %img src='{{user.avatar}}'
|
||||
|
||||
.grid-item
|
||||
.handle -> @{{user.handle}}@{{user_domain}}
|
||||
.bio
|
||||
-if user.bio -> =user.bio
|
||||
|
||||
%table#table
|
||||
-if user.table
|
||||
-for key, value in user.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
|
||||
-if not posts
|
||||
%center -> No posts :/
|
||||
-else
|
||||
-for post in posts
|
||||
=post.content
|
18
social/frontend/pages/register.haml
Normal file
|
@ -0,0 +1,18 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Register'
|
||||
-block content
|
||||
%form action="/register" method="post" id="logreg_form" autocomplete="new-password"
|
||||
%center
|
||||
.error.message
|
||||
-if msg
|
||||
{{msg}}
|
||||
|
||||
-else
|
||||
%br
|
||||
|
||||
%input type="text" name="username" placeholder="Username"
|
||||
%input type="text" name="name" placeholder="Display Name"
|
||||
%input type="password" name="newpassword1" placeholder="New password" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters" autocomplete="new-password"
|
||||
%input type="password" name="newpassword2" placeholder="Repeat new password" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters" autocomplete="new-password"
|
||||
%input type="email" name="email" placeholder="E-mail"
|
||||
%input type="submit" value="Register"
|
4
social/frontend/pages/rules.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Rules'
|
||||
-block content
|
||||
{{text}}
|
24
social/frontend/pages/settings/profile.haml
Normal file
|
@ -0,0 +1,24 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Settings - Profile'
|
||||
-set user = request.ctx.cookie_user
|
||||
-block content
|
||||
%form#settings method='POST' enctype='multipart/form-data' action='https://{{config.domain}}/settings/profile'
|
||||
.grid-container
|
||||
.grid-item.avatar
|
||||
%img src='{{user.avatar}}'
|
||||
Avatar
|
||||
%input name='avatar' type='file'
|
||||
|
||||
.grid-item.name
|
||||
Display Name
|
||||
%input name='name' value='{{user.name if user.bio else ""}}'
|
||||
|
||||
.grid-item.bio
|
||||
Bio
|
||||
%textarea name='bio' << {{user.bio if user.bio else ""}}
|
||||
|
||||
.grid-item.signature
|
||||
Post Signature
|
||||
%textarea name='signature' << {{user.signature if user.signature else ""}}
|
||||
|
||||
%center -> %input.submit type='submit' value='Submit'
|
5
social/frontend/redir.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
-extends 'base.haml'
|
||||
-set page = 'Redirect'
|
||||
-block content
|
||||
%script
|
||||
window.location.replace(redir)
|
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 |
Before Width: | Height: | Size: 236 B After Width: | Height: | Size: 236 B |
157
social/frontend/static/missing_pfp.svg
Normal file
|
@ -0,0 +1,157 @@
|
|||
<?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"
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 67.733333 67.733333"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
sodipodi:docname="missing_pfp.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="10.583333"
|
||||
y="11.90625"
|
||||
width="47.625"
|
||||
height="38.364583"
|
||||
id="rect908" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.99"
|
||||
inkscape:cx="101.08211"
|
||||
inkscape:cy="131.6607"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer3"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:window-width="1226"
|
||||
inkscape:window-height="957"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid833" />
|
||||
</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 />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="head"
|
||||
style="display:inline">
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.79324;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers fill stroke;stroke-opacity:1"
|
||||
id="path850"
|
||||
cx="33.734375"
|
||||
cy="42.259773"
|
||||
r="22.380424" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="ears">
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.32292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="M 13.229168,35.718749 11.90625,9.2604167 23.812501,22.489583 Z"
|
||||
id="path853"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.32292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="M 54.239584,35.718749 55.5625,9.2604167 43.65625,22.489583 Z"
|
||||
id="path853-3"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="head 1"
|
||||
style="display:inline">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text906"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect908);fill:#000000;fill-opacity:1;stroke:none;" />
|
||||
<g
|
||||
aria-label="Missing
|
||||
Avatar"
|
||||
id="text914"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583">
|
||||
<path
|
||||
d="m 14.918419,28.03003 h 1.555455 l 1.968866,5.250309 1.979201,-5.250309 h 1.555456 v 7.715266 h -1.018023 v -6.774758 l -1.989536,5.29165 H 17.92081 l -1.989537,-5.29165 v 6.774758 h -1.012854 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path939" />
|
||||
<path
|
||||
d="m 24.008275,29.957554 h 0.950843 v 5.787742 h -0.950843 z m 0,-2.253085 h 0.950843 v 1.204057 h -0.950843 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path941" />
|
||||
<path
|
||||
d="m 30.63834,30.128086 v 0.899167 q -0.403075,-0.206705 -0.837155,-0.310057 -0.434081,-0.103353 -0.899167,-0.103353 -0.707965,0 -1.064531,0.21704 -0.351399,0.217041 -0.351399,0.651121 0,0.330729 0.253214,0.521931 0.253213,0.186034 1.018022,0.356566 l 0.325561,0.07235 q 1.012854,0.21704 1.4366,0.614947 0.428913,0.39274 0.428913,1.100705 0,0.80615 -0.640786,1.276404 -0.635618,0.470254 -1.751825,0.470254 -0.465087,0 -0.971514,-0.09302 -0.50126,-0.08785 -1.059364,-0.268716 v -0.981849 q 0.527098,0.273884 1.038693,0.41341 0.511595,0.134358 1.012855,0.134358 0.671792,0 1.033525,-0.227375 0.361734,-0.232544 0.361734,-0.651121 0,-0.387572 -0.263549,-0.594277 -0.258381,-0.206706 -1.142045,-0.397908 l -0.330728,-0.07751 q -0.883664,-0.186035 -1.276404,-0.568439 -0.39274,-0.387572 -0.39274,-1.059364 0,-0.816485 0.578775,-1.260901 0.578774,-0.444415 1.643305,-0.444415 0.527098,0 0.992184,0.07751 0.465087,0.07751 0.857826,0.232543 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path943" />
|
||||
<path
|
||||
d="m 36.152197,30.128086 v 0.899167 q -0.403075,-0.206705 -0.837155,-0.310057 -0.434081,-0.103353 -0.899167,-0.103353 -0.707965,0 -1.064532,0.21704 -0.351398,0.217041 -0.351398,0.651121 0,0.330729 0.253213,0.521931 0.253214,0.186034 1.018023,0.356566 l 0.32556,0.07235 q 1.012855,0.21704 1.436601,0.614947 0.428913,0.39274 0.428913,1.100705 0,0.80615 -0.640786,1.276404 -0.635618,0.470254 -1.751825,0.470254 -0.465087,0 -0.971514,-0.09302 -0.50126,-0.08785 -1.059364,-0.268716 v -0.981849 q 0.527098,0.273884 1.038693,0.41341 0.511595,0.134358 1.012855,0.134358 0.671792,0 1.033525,-0.227375 0.361734,-0.232544 0.361734,-0.651121 0,-0.387572 -0.263549,-0.594277 -0.258381,-0.206706 -1.142045,-0.397908 l -0.330728,-0.07751 q -0.883665,-0.186035 -1.276404,-0.568439 -0.39274,-0.387572 -0.39274,-1.059364 0,-0.816485 0.578774,-1.260901 0.578775,-0.444415 1.643306,-0.444415 0.527098,0 0.992184,0.07751 0.465086,0.07751 0.857826,0.232543 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path945" />
|
||||
<path
|
||||
d="m 37.97637,29.957554 h 0.950844 v 5.787742 H 37.97637 Z m 0,-2.253085 h 0.950844 v 1.204057 H 37.97637 Z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path947" />
|
||||
<path
|
||||
d="m 45.727811,32.251981 v 3.493315 h -0.950843 v -3.462309 q 0,-0.821653 -0.320393,-1.229896 -0.320393,-0.408242 -0.961179,-0.408242 -0.769976,0 -1.214392,0.490924 -0.444416,0.490925 -0.444416,1.338416 v 3.271107 h -0.956011 v -5.787742 h 0.956011 v 0.899168 q 0.341063,-0.521931 0.800982,-0.780312 0.465087,-0.258381 1.069699,-0.258381 0.997352,0 1.508947,0.620115 0.511595,0.614947 0.511595,1.813837 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path949" />
|
||||
<path
|
||||
d="m 51.432872,32.784246 q 0,-1.033525 -0.428913,-1.601964 -0.423745,-0.568439 -1.193722,-0.568439 -0.764809,0 -1.193722,0.568439 -0.423745,0.568439 -0.423745,1.601964 0,1.028358 0.423745,1.596797 0.428913,0.568439 1.193722,0.568439 0.769977,0 1.193722,-0.568439 0.428913,-0.568439 0.428913,-1.596797 z m 0.950843,2.24275 q 0,1.477942 -0.656288,2.196242 -0.656289,0.723467 -2.010207,0.723467 -0.50126,0 -0.945676,-0.07751 -0.444416,-0.07235 -0.862993,-0.227375 V 36.71681 q 0.418577,0.227376 0.82682,0.335896 0.408242,0.10852 0.831988,0.10852 0.93534,0 1.400427,-0.490924 0.465086,-0.485757 0.465086,-1.472774 v -0.470254 q -0.294555,0.511595 -0.754474,0.764809 -0.459918,0.253213 -1.100704,0.253213 -1.064531,0 -1.715652,-0.811317 -0.651121,-0.811317 -0.651121,-2.149733 0,-1.343583 0.651121,-2.1549 0.651121,-0.811317 1.715652,-0.811317 0.640786,0 1.100704,0.253213 0.459919,0.253214 0.754474,0.764809 v -0.878497 h 0.950843 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path951" />
|
||||
<path
|
||||
d="m 20.052456,42.287512 -1.41593,3.839547 h 2.837027 z m -0.58911,-1.028358 h 1.183387 l 2.94038,7.715267 H 22.501911 L 21.799114,46.99522 h -3.477813 l -0.702797,1.979201 h -1.100705 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path953" />
|
||||
<path
|
||||
d="m 23.370072,43.186679 h 1.007688 l 1.808669,4.857569 1.808669,-4.857569 h 1.007688 l -2.170404,5.787742 h -1.291906 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path955" />
|
||||
<path
|
||||
d="m 32.945685,46.065047 q -1.152381,0 -1.596797,0.263549 -0.444416,0.263549 -0.444416,0.899167 0,0.506428 0.330728,0.80615 0.335896,0.294555 0.909503,0.294555 0.790646,0 1.266068,-0.558104 0.480589,-0.563271 0.480589,-1.493444 v -0.211873 z m 1.896519,-0.392739 v 3.302113 H 33.89136 v -0.878497 q -0.32556,0.527098 -0.811317,0.780312 -0.485757,0.248046 -1.188554,0.248046 -0.888832,0 -1.41593,-0.496092 -0.52193,-0.50126 -0.52193,-1.338415 0,-0.976682 0.651121,-1.472774 0.656288,-0.496092 1.953363,-0.496092 h 1.333247 v -0.09302 q 0,-0.656289 -0.43408,-1.012855 -0.428913,-0.361734 -1.209225,-0.361734 -0.496092,0 -0.966346,0.118855 -0.470254,0.118856 -0.904335,0.356567 v -0.878497 q 0.52193,-0.201537 1.012855,-0.299722 0.490925,-0.103353 0.956011,-0.103353 1.255733,0 1.875848,0.651121 0.620116,0.651121 0.620116,1.974034 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path957" />
|
||||
<path
|
||||
d="m 37.741242,41.543374 v 1.643305 h 1.958531 v 0.738971 h -1.958531 v 3.141917 q 0,0.707965 0.191202,0.909502 0.19637,0.201538 0.790647,0.201538 h 0.976682 v 0.795814 h -0.976682 q -1.100704,0 -1.519282,-0.408242 -0.418578,-0.413411 -0.418578,-1.498612 V 43.92565 h -0.697629 v -0.738971 h 0.697629 v -1.643305 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path959" />
|
||||
<path
|
||||
d="m 43.580661,46.065047 q -1.152381,0 -1.596797,0.263549 -0.444416,0.263549 -0.444416,0.899167 0,0.506428 0.330729,0.80615 0.335895,0.294555 0.909502,0.294555 0.790647,0 1.266068,-0.558104 0.48059,-0.563271 0.48059,-1.493444 v -0.211873 z m 1.896519,-0.392739 v 3.302113 h -0.950843 v -0.878497 q -0.325561,0.527098 -0.811318,0.780312 -0.485757,0.248046 -1.188554,0.248046 -0.888832,0 -1.41593,-0.496092 -0.52193,-0.50126 -0.52193,-1.338415 0,-0.976682 0.651121,-1.472774 0.656289,-0.496092 1.953363,-0.496092 h 1.333248 v -0.09302 q 0,-0.656289 -0.434081,-1.012855 -0.428913,-0.361734 -1.209225,-0.361734 -0.496092,0 -0.966346,0.118855 -0.470254,0.118856 -0.904334,0.356567 v -0.878497 q 0.52193,-0.201537 1.012854,-0.299722 0.490925,-0.103353 0.956011,-0.103353 1.255734,0 1.875849,0.651121 0.620115,0.651121 0.620115,1.974034 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path961" />
|
||||
<path
|
||||
d="m 50.789499,44.075511 q -0.160196,-0.09302 -0.351398,-0.134358 -0.186035,-0.04651 -0.41341,-0.04651 -0.80615,0 -1.240231,0.527098 -0.428913,0.52193 -0.428913,1.503779 v 3.0489 h -0.956011 v -5.787742 h 0.956011 v 0.899167 q 0.299723,-0.527098 0.780312,-0.780312 0.480589,-0.258381 1.167884,-0.258381 0.09818,0 0.21704,0.0155 0.118855,0.01034 0.263549,0.03617 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
id="path963" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
338
social/frontend/style.css
Normal file
|
@ -0,0 +1,338 @@
|
|||
/* IDs */
|
||||
{% set background = '#191919' %}
|
||||
{% set primary = theme %}
|
||||
{% set text = lighten(desaturate(primary, 0.25), 0.5) %}
|
||||
|
||||
|
||||
/* Nunito Sans */
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold'),
|
||||
url('/static/fonts/nunito/NunitoSans-SemiBold.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-SemiBold.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light Italic'),
|
||||
url('/static/fonts/nunito/NunitoSans-ExtraLightItalic.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-ExtraLightItalic.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold Italic'),
|
||||
url('/static/fonts/nunito/NunitoSans-Italic.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-Italic.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light'),
|
||||
url('/static/fonts/nunito/NunitoSans-Light.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-Light.ttf') format('ttf');
|
||||
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: #ada;
|
||||
--error: #daa;
|
||||
--gap: 15px;
|
||||
}
|
||||
|
||||
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, textarea {
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--background);
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 2px 2px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border: 1px solid var(--hover);
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#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 {
|
||||
text-align: center;
|
||||
margin-bottom: var(--gap);
|
||||
}
|
||||
|
||||
#header .title {
|
||||
font-size: 2.5em;
|
||||
line-height: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer {
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 5px;
|
||||
line-height: 2em;
|
||||
font-size: 0.80em;
|
||||
}
|
||||
|
||||
#footer .source {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#footer .menu li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#menu .menu li {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
line-height: 2em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#menu li:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#footer .menu li:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
grid-template-columns: 125px auto;
|
||||
grid-gap: var(--gap);
|
||||
margin-bottom: var(--gap);
|
||||
}
|
||||
|
||||
#content-body .title {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: var(--primary)
|
||||
}
|
||||
|
||||
#logreg_form textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#logreg_form input, #logreg_form textarea {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
line-height: 1.2em;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#logreg_form input:not(input[type="submit"]), #logreg_form textarea {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#logreg_form input[type="submit"] {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#profile {
|
||||
grid-template-columns: 100px auto;
|
||||
grid-gap: 15px;
|
||||
}
|
||||
|
||||
#profile .icon {
|
||||
/* height: 100px; */
|
||||
/* border: 1px solid var(--primary); */
|
||||
background-color: {{darken(primary, 0.9)}};
|
||||
}
|
||||
|
||||
#profile .icon img {
|
||||
height: 100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 2px 2px;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#table td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#table .key {
|
||||
min-width: 100px;
|
||||
background-color: {{darken(primary, 0.9)}};
|
||||
}
|
||||
|
||||
#table .value {
|
||||
background-color: {{desaturate(darken(primary, 0.9), 0.85)}};
|
||||
}
|
||||
|
||||
#posts {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#settings .grid-container {
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 15px;
|
||||
}
|
||||
|
||||
#settings .avatar img {
|
||||
margin: 0 auto;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#settings .submit {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Classes */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-gap: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
/* responsive design */
|
||||
@media (max-width: 810px) {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#body {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#content{
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
#menu {
|
||||
float: top;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#menu .menu li {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
min-width: 65px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#menu .menu {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#logreg_form input:not(input[submit]), #logreg_form textarea {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
|
@ -1,36 +1,50 @@
|
|||
import yaml
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json, magic, os, re, sys, sanic, socket, yaml
|
||||
import tarfile, tempfile, zipfile
|
||||
|
||||
import ujson as json
|
||||
|
||||
from IzzyLib.template import sendResponse
|
||||
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 as SHA512, SHA3_256 as SHA256
|
||||
from Crypto.Hash import SHA3_512, SHA3_256, BLAKE2b
|
||||
from Crypto.PublicKey import RSA
|
||||
from hashlib import md5
|
||||
|
||||
from .config import config
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
script_path = os.path.dirname(abspath(sys.executable))
|
||||
|
||||
else:
|
||||
script_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
css_check = lambda css_file : int(os.path.getmtime(f'{script_path}/templates/{css_file}.css'))
|
||||
dumps = lambda *args, **kwargs : json.dumps(*args, escape_forward_slashes=False, **kwargs)
|
||||
|
||||
cssts = {
|
||||
'color': css_check('color'),
|
||||
'layout': css_check('layout')
|
||||
hashalgs = {
|
||||
'blake': BLAKE2b,
|
||||
'sha256': SHA3_256,
|
||||
'sha512': SHA3_512,
|
||||
'md5': md5
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def CheckForwarded(header):
|
||||
forwarded = DotDict(valid=False)
|
||||
|
||||
for pair in header.split(';'):
|
||||
k,v = pair.split('=', 1)
|
||||
|
||||
if k == 'for':
|
||||
k = 'ip'
|
||||
|
||||
forwarded[k] = v
|
||||
|
||||
if forwarded.secret == config.secret:
|
||||
forwarded.valid = True
|
||||
|
||||
return forwarded
|
||||
|
||||
|
||||
def boolean(raw_val):
|
||||
val = raw_val.lower() if raw_val not in [None, True, False, 0, 1] else raw_val
|
||||
|
||||
|
@ -48,38 +62,45 @@ def boolean(raw_val):
|
|||
return False
|
||||
|
||||
|
||||
def css_ts():
|
||||
color = css_check('color')
|
||||
layout = css_check('layout')
|
||||
def mkhash(string, salt=config.salt, alg='blake'):
|
||||
data = str(string + salt).encode('UTF-8')
|
||||
hashalg = hashalgs.get(alg.lower())
|
||||
|
||||
if cssts['color'] != color or cssts['layout'] != layout:
|
||||
cssts.update({
|
||||
'color': color,
|
||||
'layout': layout
|
||||
})
|
||||
if not hashalg:
|
||||
raise KeyError(f'Not a valid hash algorithm: {alg}')
|
||||
|
||||
return cssts['color'] + cssts['layout']
|
||||
hash = hashalg.new()
|
||||
hash.update(data)
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
def mkhash(string, alg='sha512'):
|
||||
if alg == 'sha512':
|
||||
return SHA512.new(string.encode('UTF-8')).hexdigest()
|
||||
def get_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
elif alg == 'sha256':
|
||||
return SHA256.new(string.encode('UTF-8')).hexdigest()
|
||||
try:
|
||||
s.connect(('10.255.255.255', 1))
|
||||
data = s.getsockname()
|
||||
ip = data[0]
|
||||
|
||||
elif alg == 'md5':
|
||||
md = md5()
|
||||
md.update(string.encode('UTF-8'))
|
||||
return md.hexdigest()
|
||||
except Exception:
|
||||
ip = '127.0.0.1'
|
||||
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
return ip
|
||||
|
||||
|
||||
def genkey():
|
||||
key = RSA.generate(2048)
|
||||
privkey = key.export_key()
|
||||
pubkey = key.publickey().export_key()
|
||||
def GenKey():
|
||||
privkey = RSA.generate(2048)
|
||||
pubkey = privkey.publickey()
|
||||
|
||||
return {'pubkey': pubkey, 'privkey': privkey}
|
||||
return DotDict({
|
||||
'pubkey': pubkey,
|
||||
'privkey': privkey,
|
||||
'PUBKEY': pubkey.exportKey('PEM').decode(),
|
||||
'PRIVKEY': privkey.exportKey('PEM').decode()
|
||||
})
|
||||
|
||||
|
||||
def todate(ts):
|
||||
|
@ -93,7 +114,7 @@ def ap_date(ts):
|
|||
def themes():
|
||||
theme_list = []
|
||||
|
||||
for theme in os.listdir(f'{script_path}/themes'):
|
||||
for theme in os.listdir(f'{config.path}/themes'):
|
||||
theme_list.append(theme.replace('.yml', ''))
|
||||
|
||||
theme_list.sort()
|
||||
|
@ -101,6 +122,16 @@ def themes():
|
|||
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())
|
||||
|
||||
|
@ -108,11 +139,23 @@ def timestamp(integer=True):
|
|||
|
||||
|
||||
# Generate css file for color styling
|
||||
def color_css(theme):
|
||||
try:
|
||||
data = yaml.load(open(f'{os.path.dirname(__file__)}/themes/'+theme+'.yml', 'r'), Loader=yaml.FullLoader)
|
||||
def cssTheme(theme):
|
||||
custpath = config.data.join(f'themes/{theme}.yml')
|
||||
defpath = config.frontend.join(f'themes/{theme}.yml')
|
||||
|
||||
except FileNotFoundError:
|
||||
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 = {
|
||||
|
@ -124,6 +167,74 @@ def color_css(theme):
|
|||
return colors
|
||||
|
||||
|
||||
def JsonCheck(headers):
|
||||
accept = headers.get('Accept')
|
||||
|
||||
if not accept:
|
||||
return
|
||||
|
||||
mimes = accept.split(',')
|
||||
|
||||
if any(mime in ['application/json', 'application/activity+json'] for mime in mimes):
|
||||
return True
|
||||
|
||||
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',
|
||||
'status': status
|
||||
}
|
||||
|
||||
if headers:
|
||||
params['headers'] = headers
|
||||
|
||||
return sanic.response.text(json.dumps(data), **params)
|
||||
|
||||
|
||||
def ParseRequest(request, db):
|
||||
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()})
|
||||
|
||||
with db.session() as s:
|
||||
request.ctx.token = None
|
||||
request.ctx.token_user = None
|
||||
request.ctx.cookie = None
|
||||
request.ctx.cookie_user = None
|
||||
|
||||
api_token = request.headers.get('authorization', '').replace('Bearer ', '')
|
||||
login_token = request.cookies.get('login_token')
|
||||
|
||||
if api_token:
|
||||
api_db = db.get.token(api_token)
|
||||
request.ctx.token = api_db['token']
|
||||
request.ctx.token_user = api_db['user']
|
||||
|
||||
if login_token:
|
||||
login_db = db.get.token(login_token, 'cookie')
|
||||
request.ctx.cookie = login_db['token']
|
||||
request.ctx.cookie_user = login_db['user']
|
||||
|
||||
|
||||
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)
|
||||
|
|
357
social/manage.py
|
@ -1,26 +1,307 @@
|
|||
import json, os
|
||||
from sys import argv, exit
|
||||
import argparse, json, magic, os, shutil, sys, tarfile, zipfile
|
||||
from os import environ as env
|
||||
from socket import gethostname, getfqdn
|
||||
from subprocess import Popen
|
||||
from pathlib import PurePath
|
||||
#from subprocess import Popen
|
||||
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.misc import getBin, Try
|
||||
from IzzyLib.misc import RandomGen, Input, Path, Boolean, DotDict
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
|
||||
from .config import script_path
|
||||
from .config import config
|
||||
from .database import DataBase, db, migrate, schema
|
||||
from .functions import mkhash
|
||||
|
||||
|
||||
#print(getfqdn(), gethostname())
|
||||
#exit()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('command', help='Management command to run')
|
||||
parser.add_argument('options', nargs=argparse.REMAINDER, help='Options passed to the management command')
|
||||
pargs = parser.parse_args()
|
||||
command, args = pargs.command, pargs.options
|
||||
|
||||
|
||||
def setup():
|
||||
print('')
|
||||
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 ConvertFonts():
|
||||
class User(Row):
|
||||
def __init__(self, user):
|
||||
super().__init__(user)
|
||||
|
||||
domain = db.fetch('domain', id=self.domainid)
|
||||
self.domain = domain.domain
|
||||
|
||||
|
||||
row = db.get.user('izalia', config.domain)
|
||||
user = User(row)
|
||||
|
||||
for k,v in user.items():
|
||||
print(f'{k}:\t{v}')
|
||||
|
||||
|
||||
def man_setup():
|
||||
global db
|
||||
user = env.get('USER', 'social')
|
||||
env_exists = config.data.join(f'{config.env}.env').exists()
|
||||
|
||||
if not env_exists or Input('Config file exists. Reconfigure?', False, Boolean, options=['true', 'false']):
|
||||
config.domain = Input('Domain name used for federation', config.domain)
|
||||
config.web_domain = Input('Domain used to access the instance from a web browser', config.web_domain)
|
||||
config.listen = Input('IP address for the server to listen on', config.listen)
|
||||
config.port = Input('Port number the server listens on', config.port, int)
|
||||
config.salt = Input('String to use when hashing a password', config.salt)
|
||||
config.secret = Input('String to user in a reverse proxy server to make sure it is valid', config.secret)
|
||||
|
||||
config.dbtype = Input('The type of database to use for the backend', config.dbtype, options=['sqlite', 'postgresql'])
|
||||
|
||||
if config.dbtype == 'sqlite':
|
||||
config.sq = DotDict()
|
||||
config.sqfile = Input('Name of the sqlite3 database file. Path is relative to the data directory', config.sqfile, Path)
|
||||
|
||||
if config.dbtype == 'postgresql':
|
||||
config.db = DotDict()
|
||||
config.db.host = Input('Database host or socket location', config.db.host)
|
||||
config.db.port = Input('Database port', config.db.port, int)
|
||||
config.db.user = Input('Database user', config.db.user)
|
||||
config.db.password = Input('Database password', config.db.password)
|
||||
config.db.database = Input('Database name', config.db.database)
|
||||
config.db.maxconnections = Input('Max connections to the database', config.db.maxconnections, int)
|
||||
|
||||
config.rd.host = Input('Redis host', config.rd.host)
|
||||
config.rd.port = Input('Redis port', config.rd.port, int)
|
||||
config.rd.user = Input('Redis user', config.rd.user)
|
||||
config.rd.password = Input('Redis password', config.rd.password, password=True)
|
||||
config.rd.database = Input('Redis database number', config.rd.database, int)
|
||||
config.rd.prefix = Input('Redis namespace prefix', config.rd.prefix)
|
||||
config.rd.maxconnections = Input('Max connections to Redis', config.rd.maxconnections, int)
|
||||
|
||||
setup_env_file()
|
||||
|
||||
dbconf = config.sq if config.dbtype == 'sqlite' else config.db
|
||||
db = DataBase(**dbconf, tables=schema.table)
|
||||
|
||||
if Counter(db.table.keys()) != Counter(db.get_tables()):
|
||||
if Input('Database exists. Drop tables first?', False, Boolean, options=['true', 'false']):
|
||||
db.drop_tables()
|
||||
|
||||
settings = DotDict()
|
||||
|
||||
if Input('Setup optional settings now? They can be changed later in the server settings', False, Boolean):
|
||||
settings.name = Input('Instance name', 'Barkshark Social')
|
||||
settings.description = Input('Instance description', 'UwU')
|
||||
settings.char_limit = Input('Character limit for posts', 5000, int)
|
||||
settings.bio_limit = Input('character limit for bios', 1000, int)
|
||||
settings.secure = Input('Should fetches from other instances require a signature?', True, Boolean)
|
||||
|
||||
newset = [{'key': k, 'value': v, 'subtype': type(v).__name__} for k, v in settings.items()]
|
||||
db.put.configs(newset)
|
||||
|
||||
migrate.setup(db, settings)
|
||||
|
||||
if Input('Create an admin account now?', True, Boolean):
|
||||
man_adduser()
|
||||
logging.info('Created new admin account')
|
||||
|
||||
return 'Instance all setup! Run it with "python3 -m social" :3'
|
||||
|
||||
|
||||
def man_adduser(handle=None, email=None, name=None, admin=True, password=None):
|
||||
domainid = db.get.domain(config.domain)
|
||||
|
||||
user = DotDict()
|
||||
user.handle = Input('Name which will be used to identify yourself on the fediverse') if not handle else handle
|
||||
|
||||
if domainid and db.get.user(handle=handle, domainid=domainid):
|
||||
return f'Error: User already exists: {handle}@{config.domain}'
|
||||
|
||||
user.password = Input('Password', password=True) if not password else password
|
||||
|
||||
if user.password != Input('Password again', password=True):
|
||||
logging.error('Passwords don\'t match')
|
||||
return
|
||||
|
||||
user.email = Input('E-Mail address') if not email else email
|
||||
user.name = Input('Display name', user.handle) if not email else name
|
||||
user.admin = Input('Make user an admin?', True, Boolean)
|
||||
|
||||
db.put.user(user.handle,
|
||||
name = user.name,
|
||||
password = mkhash(user.password),
|
||||
email = user.email,
|
||||
permissions = 1 if Boolean(user.admin) else 4
|
||||
)
|
||||
|
||||
return f'Created new user: {user.handle}'
|
||||
|
||||
|
||||
def man_deluser(handle=None, domain=None):
|
||||
if not handle:
|
||||
return 'Error: Please specify a user handle to delete'
|
||||
|
||||
if not domain:
|
||||
domain = config.domain
|
||||
domainid = db.get.domainid(config.domain)
|
||||
|
||||
if not domainid:
|
||||
return f'Error: Cannot find domain: {domain}'
|
||||
|
||||
with db.session() as s:
|
||||
row = s.query(db.table.user).filter_by(handle=handle, domainid=domainid)
|
||||
|
||||
if row.one_or_none():
|
||||
row.delete()
|
||||
return f'Deleted user: {handle}@{domain}'
|
||||
|
||||
return f'Error: User does not exist: {handle}@{domain}'
|
||||
|
||||
|
||||
def man_migrate():
|
||||
config_version = db.get.version()
|
||||
|
||||
if config_version < config.dbversion:
|
||||
migrate.upgrade(config_version, db)
|
||||
|
||||
|
||||
def man_getemoji(shortcode, domain=None):
|
||||
row = db.get.emoji(shortcode, domain)
|
||||
|
||||
if not row:
|
||||
return f'Error: Cannot find {shortcode}'
|
||||
|
||||
string = str(row)
|
||||
|
||||
return string[:-1] + f', path="{row.path}")'
|
||||
|
||||
|
||||
def man_importemoji(filename=None):
|
||||
if not filename:
|
||||
return 'Error: Please specify an archive to import'
|
||||
|
||||
success = 0
|
||||
fail = 0
|
||||
exist = 0
|
||||
m = magic.Magic(mime=True, uncompress=True)
|
||||
mime = m.from_file(filename)
|
||||
|
||||
if not mime.endswith('x-tar'):
|
||||
return f'Error: Unsupported archive mimetype: {mime}'
|
||||
|
||||
with tarfile.open(filename) as a:
|
||||
em = a.getmembers()[0]
|
||||
|
||||
with db.session() as s:
|
||||
for emoji in a.getmembers():
|
||||
if emoji.isfile() and emoji.name.endswith('png') and emoji.path == emoji.name:
|
||||
if emoji.size > db.get.config('emoji_size_limit'):
|
||||
logging.warning(f'Emoji too big: {emoji.name}')
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
#[use regex to make sure emojis are only alphanumeric with underscores here]
|
||||
|
||||
shortcode = emoji.name.split('.')[0]
|
||||
domain = s.query(db.table.domain).filter_by(domain=config.domain).one_or_none()
|
||||
path = config.data.join(f'media/emojis/{domain.id}/{emoji.name[0]}')
|
||||
emfile = path.join(emoji.name)
|
||||
exists = s.query(db.table.emoji).filter_by(shortcode=shortcode, domainid=domain.id).one_or_none()
|
||||
relpath = path.str().replace(config.data.str() + '/', '')
|
||||
path.mkdir()
|
||||
|
||||
if not exists:
|
||||
s.add(db.table.emoji(
|
||||
shortcode = shortcode,
|
||||
domainid = domain.id,
|
||||
display = True,
|
||||
enabled = True,
|
||||
timestamp = datetime.now()
|
||||
))
|
||||
|
||||
exists = True
|
||||
|
||||
if exists and not emfile.exists():
|
||||
#logging.info(f'Extracting {emoji.name} to {emfile}')
|
||||
a.extract(emoji, path=path.str(), set_attrs=False)
|
||||
success += 1
|
||||
|
||||
else:
|
||||
exist += 1
|
||||
|
||||
ret = f'Imported emojis: {success}'
|
||||
|
||||
if exist:
|
||||
ret += f'\nExisting emojis not imported: {exist}'
|
||||
|
||||
if fail:
|
||||
ret += f'\nFailed imports: {fail}'
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def man_config(key=None, value=None):
|
||||
if not key:
|
||||
for k,v in db.get.configs().items():
|
||||
print(f'{k}: {v}')
|
||||
|
||||
return ''
|
||||
|
||||
if not value:
|
||||
return f'{key}: {db.get.config(key)}'
|
||||
|
||||
if db.put.config(key, value):
|
||||
return f'Updated config: {key}'
|
||||
|
||||
else:
|
||||
return f'Failed to update config: {key}'
|
||||
|
||||
|
||||
def man_setup2():
|
||||
database = DB.database
|
||||
|
||||
with trans(dbconn('postgres', pooled=False)) as db:
|
||||
if '--dropdb' in sys.argv:
|
||||
db.prepare('terminate', "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$1';")
|
||||
db.prepare('drop', 'DROP DATABASE $1')
|
||||
db.prepare('create', 'CREATE DATABASE $1 WITH TEMPLATE = template0;')
|
||||
|
||||
db.query_prepared('terminate', [database])
|
||||
db.query_prepared('drop', [database])
|
||||
|
||||
if database not in db.get_databases():
|
||||
logging.info('Database doesn\'t exist. Creating it now...')
|
||||
db.query_prepared('create', [database])
|
||||
|
||||
with trans(dbconn(database, pooled=False)) as db:
|
||||
dbsql = open(script_path+'/dist/database.sql').read().replace('\t', '').replace('\n', '')
|
||||
db.query(dbsql)
|
||||
|
||||
dbcheck = db.query('SELECT * FROM settings WHERE setting = \'setup\'').dictresult()
|
||||
|
||||
if dbcheck == []:
|
||||
keys = genkey()
|
||||
|
||||
settings = {
|
||||
'setup': True,
|
||||
'pubkey': keys['pubkey'],
|
||||
'privkey': keys['privkey'],
|
||||
'char_limit': 4096,
|
||||
'table_limit': 8,
|
||||
'name': 'Barkshark Social',
|
||||
'description': 'UwU',
|
||||
'theme': 'blue',
|
||||
'domain': config['domain']
|
||||
}
|
||||
|
||||
for key in settings:
|
||||
db.insert('settings', setting=key, val=settings[key])
|
||||
|
||||
logging.info('Database setup finished :3')
|
||||
|
||||
|
||||
def man_convertfonts():
|
||||
font_path = script_path + '/static/fonts'
|
||||
|
||||
woffpath = Try(getBin, 'woff2_compress')
|
||||
|
@ -40,6 +321,54 @@ def ConvertFonts():
|
|||
cmd = f'{woffpath.result} {font.path}'
|
||||
proc = Popen(cmd.split())
|
||||
|
||||
exit()
|
||||
|
||||
ConvertFonts()
|
||||
def setup_env_file():
|
||||
data = f'''# General stuff
|
||||
WEB_DOMAIN={config.web_domain}
|
||||
LISTEN={config.listen}
|
||||
PORT={config.port}
|
||||
PASS_SALT={config.salt}
|
||||
LOG_LEVEL=info
|
||||
LOG_ERRORS=yes
|
||||
|
||||
# Federation stuff
|
||||
DOMAIN={config.domain}
|
||||
|
||||
# Random string used to verify proxy headers
|
||||
FORWARDED_SECRET={config.secret}
|
||||
|
||||
DB_TYPE={config.dbtype}
|
||||
SQ_DATABASE={config.sqfile}
|
||||
|
||||
# Postgresql stuff
|
||||
DB_HOST={config.db.host}
|
||||
DB_PORT={config.db.port}
|
||||
DB_USER={config.db.user}
|
||||
DB_PASSWORD={config.db.password}
|
||||
DB_DATABASE={config.db.database}
|
||||
DB_CONNECTIONS={config.db.maxconnections}
|
||||
|
||||
# Redis stuff
|
||||
REDIS_HOST={config.rd.host}
|
||||
REDIS_PORT={config.rd.port}
|
||||
REDIS_USER={config.rd.user}
|
||||
REDIS_PASSWORD={config.rd.password}
|
||||
REDIS_DATABASE={config.rd.database}
|
||||
REDIS_PREFIX={config.rd.prefix}
|
||||
REDIS_CONNECTIONS={config.rd.maxconnections}
|
||||
'''
|
||||
|
||||
with Path(config.data).join(config.env+'.env').open('w') as fd:
|
||||
fd.write(data)
|
||||
|
||||
logging.info(f'Environment file "{config.env}.env" saved')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = globals().get('man_'+command)
|
||||
|
||||
if not cmd:
|
||||
logging.error('Invalid command:', command)
|
||||
sys.exit()
|
||||
|
||||
print(cmd(*args))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from IzzyLib.misc import formatUTC
|
||||
from IzzyLib.misc import FormatUtc
|
||||
|
||||
from .config import config
|
||||
from .database import get
|
||||
|
||||
|
||||
domain = get.settings('domain')
|
||||
weburl = config['web_domain']
|
||||
weburl = config.web_domain
|
||||
|
||||
|
||||
###
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import binascii
|
||||
import base64
|
||||
import json
|
||||
import multiprocessing
|
||||
import binascii, base64, json, multiprocessing
|
||||
|
||||
import httpsig
|
||||
|
||||
from urllib.parse import urlparse, quote_plus
|
||||
from urllib.parse import urlparse, unquote, quote_plus
|
||||
|
||||
from IzzyLib import logging
|
||||
from IzzyLib.http import ValidateRequest, ParseSig
|
||||
from IzzyLib.misc import DotDict
|
||||
from sanic.response import raw, redirect, json as rjson
|
||||
from sanic import exceptions as exc
|
||||
from tldextract import extract
|
||||
from IzzyLib.http import ValidateRequest, ParseSig
|
||||
from IzzyLib import logging
|
||||
|
||||
from .config import config, pyenv
|
||||
from .web_functions import json_check, agent, client
|
||||
from .database import get
|
||||
from .config import config
|
||||
from .database import db
|
||||
from .errors import Teapot
|
||||
from .functions import CheckForwarded, JsonCheck, client, Error, JsonResp, ParseRequest
|
||||
|
||||
|
||||
blocked_agents = [
|
||||
'gabsocial',
|
||||
'soapbox',
|
||||
'spinster',
|
||||
'kiwifarms',
|
||||
'uncia'
|
||||
'kiwifarms'
|
||||
]
|
||||
|
||||
domain_bans = [
|
||||
|
@ -35,6 +33,27 @@ domain_bans = [
|
|||
blocked_domains = [extract(domain) for domain in domain_bans]
|
||||
|
||||
|
||||
anon_api_paths = [
|
||||
'/api/v1/apps',
|
||||
'/api/v1/instance'
|
||||
]
|
||||
|
||||
ap_paths = [
|
||||
'/status',
|
||||
'/actor',
|
||||
]
|
||||
|
||||
cookie_paths = [
|
||||
'/@',
|
||||
'/:',
|
||||
'/status',
|
||||
'/welcome',
|
||||
'/settings',
|
||||
'/admin',
|
||||
'/oauth/authorize'
|
||||
]
|
||||
|
||||
|
||||
def CSP():
|
||||
host = config['web_domain']
|
||||
data = ''
|
||||
|
@ -98,109 +117,87 @@ async def http_bans(request):
|
|||
|
||||
|
||||
async def http_auth(request):
|
||||
api_paths = [
|
||||
'/api/native/register',
|
||||
'/api/native/token',
|
||||
'/api/v1/apps'
|
||||
]
|
||||
if any(map(request.path.startswith, ['/media', '/static'])):
|
||||
return
|
||||
|
||||
ap_paths = [
|
||||
'/status',
|
||||
'/actor',
|
||||
]
|
||||
ParseRequest(request, db)
|
||||
|
||||
cookie_paths = [
|
||||
'/@',
|
||||
'/:',
|
||||
'/status',
|
||||
'/welcome',
|
||||
'/settings',
|
||||
'/admin',
|
||||
'/oauth/authorize'
|
||||
]
|
||||
token = request.ctx.token
|
||||
token_user = request.ctx.token_user
|
||||
cookie = request.ctx.cookie
|
||||
cookie_user = request.ctx.cookie_user
|
||||
|
||||
if request.path.startswith('/api') and request.path not in ['/api/v1/instance', '/api/instance']:
|
||||
token = request.headers.get('Token')
|
||||
|
||||
if token is None:
|
||||
raise exc.Unauthorized('MissingTokenHeader')
|
||||
|
||||
#if pass_hash() != token:
|
||||
db_token = get.api_token(token)
|
||||
|
||||
if db_token == False:
|
||||
raise exc.Unauthorized('InvalidToken')
|
||||
|
||||
else:
|
||||
pass
|
||||
if request.path.startswith('/api/v1') and not any(map(request.path.startswith, anon_api_paths)):
|
||||
if not token or not token_user:
|
||||
print('Invalid token')
|
||||
return JsonResp({'error': 'Invalid token'}, 401)
|
||||
|
||||
login_token = request.cookies.get('login_token')
|
||||
login_token_val = get.login_cookie(login_token)
|
||||
|
||||
if any(map(request.path.startswith, ap_paths)) or json_check(request.headers):
|
||||
if not ValidateRequest(request, client=client):
|
||||
request['valid'] = False
|
||||
|
||||
if not request.path.startswith('/actor'):
|
||||
raise exc.Unauthorized('Failed to verify signature')
|
||||
if any(map(request.path.startswith, ap_paths)) and JsonCheck(request.headers):
|
||||
request.ctx.valid = False
|
||||
if not await ValidateRequest(request, client=client):
|
||||
raise exc.Unauthorized('Failed to verify signature')
|
||||
|
||||
else:
|
||||
request['valid'] = True
|
||||
request.ctx.valid = True
|
||||
|
||||
elif any(map(request.path.startswith, cookie_paths)):
|
||||
if login_token == None:
|
||||
if json_check(request.headers):
|
||||
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 login_token_val == None:
|
||||
elif not cookie:
|
||||
return redirect(f'/login?redir={quote_plus(request.path)}&msg=InvalidToken')
|
||||
|
||||
elif any(map(request.path.startswith, ['/login', '/register'])) and login_token_val != None:
|
||||
if login_token == login_token_val['cookie']:
|
||||
return redirect('/welcome')
|
||||
|
||||
|
||||
async def http_filter(request):
|
||||
request['query'] = {}
|
||||
request['form'] = {}
|
||||
|
||||
for k, v in request.query_args:
|
||||
request['query'].update({k: v})
|
||||
|
||||
for k, v in request.form.items():
|
||||
request['form'].update({k: v[0]})
|
||||
elif any(map(request.path.startswith, ['/login', '/register'])) and cookie and login_token == cookie.token:
|
||||
return redirect('/welcome')
|
||||
|
||||
|
||||
async def http_headers(request, response):
|
||||
version = config['version']
|
||||
always_cache = ['ico', 'css', 'svg', 'js', 'png', 'woff2']
|
||||
#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_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 not response.headers.get('Cache-Control'):
|
||||
compare = [
|
||||
ext in always_cache,
|
||||
ext in always_cache_ext,
|
||||
request.path.startswith('/media'),
|
||||
]
|
||||
|
||||
ext = ['.woff2', '.css']
|
||||
prod_compare = [
|
||||
ext in prod_cache_ext,
|
||||
request.path in prod_cache_path
|
||||
]
|
||||
|
||||
if (True in compare and pyenv not in ['dev', 'default']) or any(map(request.path.endswith, ext)):
|
||||
if True in [*compare, *(prod_compare if is_prod else [])]:
|
||||
response.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
|
||||
|
||||
else:
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
|
||||
response.headers['Content-Security-Policy'] = csp_header
|
||||
response.headers['Server'] = f'BarksharkSocial/{version}'
|
||||
response.headers['Server'] = f'BarksharkSocial/{config.version}'
|
||||
response.headers['Trans'] = 'Rights'
|
||||
|
||||
|
||||
async def http_access_log(request, response):
|
||||
uagent = request.headers.get('user-agent')
|
||||
address = request.headers.get('X-Real-Ip', request.remote_addr)
|
||||
forwarded = CheckForwarded(request.headers.get('forwarded'))
|
||||
address = request.remote_addr if not forwarded.valid else forwarded.ip
|
||||
|
||||
logging.info(f'({multiprocessing.current_process().pid}) {address} {request.method} {request.path} {response.status} "{uagent}"')
|
||||
logging.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"')
|
||||
|
||||
|
||||
class InvalidAttr(object):
|
||||
pass
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import secrets
|
||||
import validators
|
||||
|
||||
from .database import *
|
||||
from .config import logging
|
||||
|
||||
|
||||
def scope_check(scopes):
|
||||
read_write = ['follows', 'accounts', 'lists', 'blocks', 'mutes', 'bookmarks', 'notifications', 'favourites', 'search', 'filters', 'statuses']
|
||||
admin = ['read', 'write']
|
||||
admin_secc = ['accounts', 'reports']
|
||||
new_scopes = []
|
||||
|
||||
for line in scopes:
|
||||
scope = line.split(':')
|
||||
if len(scope) < 2:
|
||||
scope[1] == None
|
||||
|
||||
if len(scope) < 3:
|
||||
scope[2] == None
|
||||
|
||||
if (scope[0] in ['read', 'write'] and scope[1] in read_write) or scope[0] in ['follow', 'push'] or (scope[0] == 'admin' and scope[1] in admin and scope[2]):
|
||||
new_scopes.append(line)
|
||||
|
||||
else:
|
||||
logging.warning(f'Invalid scope: {line}')
|
||||
|
||||
if len(new_scopes) < 1:
|
||||
return
|
||||
|
||||
else:
|
||||
return new_scopes
|
||||
|
||||
|
||||
class create:
|
||||
def app(redirect_uri, scope, name, url):
|
||||
if None in [scope, name]:
|
||||
logging.debug('Missing scope or name for app')
|
||||
logging.debug(f'scope: {scope}, name: {name}')
|
||||
return 'MissingData'
|
||||
|
||||
scopes = scope_check(scope)
|
||||
|
||||
if scopes == None:
|
||||
logging.debug(f'Invalid scopes: {scope}')
|
||||
return 'InvalidScope'
|
||||
|
||||
if not validators.url(redirect_uri):
|
||||
logging.debug(f'Invalid redirect URL: {redirect_uri}')
|
||||
redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
if not validators.url(url):
|
||||
logging.debug(f'Invalid app URL: {url}')
|
||||
return 'InvalidURL'
|
||||
|
||||
client_id = secrets.token_hex(20)
|
||||
client_secret = secrets.token_hex(20)
|
||||
|
||||
put.oauth.app(client_id, client_secret, redirect_uri, scopes, name, url)
|
||||
|
||||
return {'client_id': client_id, 'client_secret': client_secret, 'redirect_uris': redirect_uri, 'scopes': scopes}
|
||||
|
||||
|
||||
def authorize(client_id, client_secret, redirect_uri, *args):
|
||||
if None in [client_id, client_secret]:
|
||||
logging.debug(f'Invalid secrets: {client_id}, {client_secret}')
|
||||
return 'InvalidCredentials'
|
||||
|
||||
return
|
||||
|
||||
def auth_code(client_id, login_token):
|
||||
pass
|
|
@ -1,53 +0,0 @@
|
|||
#from webapp import webapp, modules
|
||||
|
||||
from .views import *
|
||||
from .api import native, mastodon, oauth
|
||||
from .web_server import web, cors
|
||||
from .config import script_path, stor_path
|
||||
|
||||
|
||||
web_routes = [
|
||||
(oauth.handle, '/oauth/<name>'),
|
||||
(native.handle, '/api/native/<name>'),
|
||||
(mastodon.handle, '/api/v1/<name>'),
|
||||
(mastodon.stream, '/api/v1/streaming/<name>'),
|
||||
(activitypub.actor, '/actor/<user>/<extra>'),
|
||||
(activitypub.actor, '/actor/<user>'),
|
||||
(activitypub.inbox, '/inbox'),
|
||||
(activitypub.nodeinfo, '/nodeinfo/2.0.json'),
|
||||
(activitypub.wellknown, '/.well-known/<name>'),
|
||||
(activitypub.status, '/status/<status>'),
|
||||
(frontend.home, '/'),
|
||||
(frontend.register, '/register'),
|
||||
(frontend.login, '/login'),
|
||||
(frontend.logout, '/logout'),
|
||||
(frontend.user, '/@<user>'),
|
||||
(frontend.user, '/user/<user>'),
|
||||
(frontend.status, '/:<status>'),
|
||||
(frontend.welcome, '/welcome'),
|
||||
(frontend.settings, '/settings'),
|
||||
(frontend.admin, '/admin'),
|
||||
(frontend.post, '/post'),
|
||||
(resources.style, '/style-<timestamp>.css'),
|
||||
(resources.manifest, '/manifest.json'),
|
||||
(resources.favicon, '/favicon.ico'),
|
||||
(resources.robots, '/robots.txt')
|
||||
]
|
||||
|
||||
|
||||
for view, route in web_routes:
|
||||
web.add_route(view.as_view(), route)
|
||||
|
||||
|
||||
# Media
|
||||
web.static('/static', script_path + '/static')
|
||||
web.static('/media', script_path + '/media')
|
||||
|
||||
|
||||
# Shitpost
|
||||
web.add_route(redirects.headpats.as_view(), '/headpats')
|
||||
web.add_route(redirects.socks.as_view(), '/socks')
|
||||
|
||||
# WebUI
|
||||
#web.add_subapp('/web', webapp)
|
||||
#web.add_route('/{module}.py', modules)
|
|
@ -1,66 +0,0 @@
|
|||
{% if request.cookies.login_token %}
|
||||
{% set cookie = newtrans(get_cookie(request.cookies.login_token)) %}
|
||||
{% if cookie != None %}
|
||||
{% set user = newtrans(get_user(cookie.userid, filters='pubkey,privkey,password')) %}
|
||||
{% else %}
|
||||
{% set user = None %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set user = None %}
|
||||
{% endif %}
|
||||
|
||||
{% if not request.cookies.get('theme') %}
|
||||
{% set theme = blue %}
|
||||
{% else %}
|
||||
{% set theme = request.cookies.theme %}
|
||||
{% endif %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{name}}: {{page}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://{{domain}}/style-{{theme}}-{{css_ts()}}.css">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script type="text/javascript">
|
||||
function reload_css() {
|
||||
var links = document.getElementsByTagName("link");
|
||||
for (var cl in links) {
|
||||
var link = links[cl];
|
||||
if (link.rel === "stylesheet")
|
||||
link.href += "";
|
||||
}
|
||||
}
|
||||
|
||||
function theme(name) {
|
||||
document.cookie = "theme=" + name + ";path=/";
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
function delete_cookie(name) {
|
||||
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{% include "components/menu.html" %}
|
||||
<div id="content" class="shadow">
|
||||
<div id="header">
|
||||
<h1 class="title"><a href="https://{{domain}}">{{name}}</a></h1>
|
||||
</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
<div id="footer">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="col1">UvU</td>
|
||||
<td class="col2">
|
||||
<a href="https://git.barkshark.xyz/izaliamae/social">source</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,137 +0,0 @@
|
|||
{% include "layout.css" %}
|
||||
|
||||
/* Variables */
|
||||
:root {
|
||||
--white: {{lighten(desaturate(primary, 0.95), 0.9)}};
|
||||
--error: {{desaturate(darken('red', 0.1), 0.25)}};
|
||||
--valid: {{desaturate(darken('green', 0.1), 0.25)}};;
|
||||
--shadow-color: {{rgba('#000', 0.5)}};
|
||||
--background: {{darken(desaturate(primary, 0.75), 0.95)}};
|
||||
--primary-ui: {{desaturate(primary, 0.4)}};
|
||||
--primary-ui-background: {{desaturate(darken(primary, 0.85), 0.85)}};
|
||||
--primary-ui-hover: {{desaturate(primary, 0.3)}};
|
||||
--primary-ui-lighter: {{desaturate(primary, 0.8)}};
|
||||
--primary-ui-element-background: var(--backgrouknd);
|
||||
--primary-ui-disabled: {{darken(desaturate(primary, 0.9), 0.2)}};
|
||||
--primary-ui-disabled-background: {{darken(desaturate(primary, 0.85), 0.85)}};
|
||||
}
|
||||
|
||||
/* Base elements */
|
||||
body {
|
||||
background-color: {{darken(desaturate(primary, 0.75), 0.95)}};
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-ui);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-ui-hover);
|
||||
border-color: var(--primary-ui-hover);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
/* background-color: {{lighten(background, 0.05)}}; */
|
||||
background-color:var(--primary-ui-element-background);
|
||||
color: var(--primary-ui-lighter);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
input:hover, textarea:hover {
|
||||
border-color: var(--primary-ui);
|
||||
color: var(--primary-ui);
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: var(--primary-ui-hover);
|
||||
color: var(--primary-ui-hover);
|
||||
}
|
||||
|
||||
input:disabled, textarea:disabled {
|
||||
color: var(--primary-ui-disabled);
|
||||
background-color: var(--primary-ui-disabled-background);
|
||||
}
|
||||
|
||||
input:disabled:hover, textarea:disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
input:invalid {
|
||||
border-color: var(--error)
|
||||
}
|
||||
|
||||
|
||||
/* base */
|
||||
#content {
|
||||
background: {{background}};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
#header {
|
||||
border-bottom: 1px solid {{desaturate(primary, 0.1)}};
|
||||
}
|
||||
|
||||
#footer {
|
||||
border-top: 1px solid {{desaturate(primary, 0.1)}};
|
||||
}
|
||||
|
||||
|
||||
/* styling classes */
|
||||
.shadow {
|
||||
box-shadow: 0 4px 8px 0 var(--shadow-color), 0 6px 20px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: var(--primary-ui-background);
|
||||
}
|
||||
|
||||
|
||||
/* Dropdown menus */
|
||||
#user_panel {
|
||||
background-color: var(--primary-ui-background);
|
||||
}
|
||||
|
||||
.submenu details[open] {
|
||||
background-color: {{desaturate(darken(primary, 0.92), 0.70)}};
|
||||
}
|
||||
|
||||
.menu summary {
|
||||
color: {{desaturate(primary, 0.6)}};
|
||||
}
|
||||
|
||||
|
||||
/* Profile & Posts */
|
||||
#user_info table .col1 {
|
||||
background-color: {{desaturate(darken(primary, 0.85), 0.70)}};
|
||||
}
|
||||
|
||||
#user_info table .col2 {
|
||||
background-color: {{desaturate(darken(primary, 0.85), 0.80)}};
|
||||
}
|
||||
|
||||
.post {
|
||||
border-bottom: 1px solid {{desaturate(darken(primary, 0.8), 0.8)}};
|
||||
}
|
||||
|
||||
/*.post:hover {
|
||||
background-color: {{desaturate(darken(primary, 0.90), 0.5)}};
|
||||
border-color: {{desaturate(primary, 0.4)}}
|
||||
}*/
|
||||
|
||||
.action-button {
|
||||
stroke: var(--primary-ui-lighter);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
stroke: var(--primary-ui-hover);
|
||||
}
|
||||
|
||||
/* Settings Page */
|
||||
#nav {
|
||||
background-color: {{desaturate(darken(primary, 0.90), 0.85)}};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<div class="submenu">
|
||||
<details>
|
||||
<summary class="item"><a>Colors</a></summary>
|
||||
{% for theme in themes() %}
|
||||
<div class="item"><a href="javascript:void(0);" onclick="javascript:theme('{{theme}}');" class="theme_link">{{theme}}</a></div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="25" height="25" class="action-button" onclick="document.getElementById('delete-{{post.id}}').submit();">
|
||||
<path d="M5 5 L20 20" style="stroke-width:5px;stroke-linecap:round;"/>
|
||||
<path d="M5 20 L20 5" style="stroke-width:5px;stroke-linecap:round;" />
|
||||
</svg>
|
Before Width: | Height: | Size: 269 B |
|
@ -1,40 +0,0 @@
|
|||
<div id="user_panel" class="menu menu-right shadow">
|
||||
{% if user != None %}
|
||||
<details>
|
||||
<summary id="menu_title"><a class="text">{{user.name}}</a></summary>
|
||||
<div class="item"><a href="https://{{domain}}/">Home</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/@{{user.handle}}">Profile</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/welcome">User Panel</a></div>
|
||||
<div class="submenu">
|
||||
<details>
|
||||
<summary class="item"><a class="text">Settings</a></summary>
|
||||
<div class="item"><a href="https://{{domain}}/settings#profile">Profile</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/settings#account">Account</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/settings#options">Options</a></div>
|
||||
{% if user.permissions < 2 %}<div class="item"><a href="https://{{domain}}/admin">Admin</a></div>{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
{% include "components/colors.html" %}
|
||||
<div class="submenu" id="toot_panel">
|
||||
<details>
|
||||
<summary class="item"><a class="text">Toot!</a></summary>
|
||||
<form action="/post" method="post">
|
||||
<input type="text" name="warning" placeholder="Content Warning"><br>
|
||||
<textarea id="toot_box" name="text" placeholder="*notices your post* OwO what's this?" maxlength="{{settings.char_limit}}"></textarea><br>
|
||||
<input type="submit" value="YEET!">
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="item"><a href="https://{{domain}}/logout">Logout</a></div>
|
||||
</details>
|
||||
{% else %}
|
||||
<details>
|
||||
<summary id="menu_title">Guest</summary>
|
||||
<div class="item"><a href="https://{{domain}}/">Home</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/login">Login</a></div>
|
||||
<div class="item"><a href="https://{{domain}}/register">Register</a></div>
|
||||
{% include "components/colors.html" %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -1,28 +0,0 @@
|
|||
{% set delete_svg %}
|
||||
{% include 'components/delete.svg' %}
|
||||
{% endset %}
|
||||
|
||||
|
||||
<div class="post">
|
||||
<form action="https://{{domain}}/:{{post.id}}" method="post" id="delete-{{post.id}}">
|
||||
<input name="post_data" value="{{json.dumps({'post': post, 'user': user})}}" hidden>
|
||||
</form>
|
||||
<div class="displayname"><a href="https://{{domain}}/@{{post.user.handle}}">{{post.user.name}}</a>
|
||||
<div class="post_date"><a href="https://{{domain}}/:{{post.id}}">{{todate(post.timestamp)}}</a></div>
|
||||
</div>
|
||||
<div class="grid-container">
|
||||
<div class="grid-item post_text">
|
||||
{% if post.get('warning') != None %}
|
||||
<details>
|
||||
<summary>CW: {{post.warning}}</summary>
|
||||
<p class="cw_text">{{post.content}}</p>
|
||||
</details>
|
||||
{% else %}
|
||||
<p>{{post.content}}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-item post-actions">
|
||||
{{delete_svg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,12 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block page %}data.code{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<center>
|
||||
<br><br><br><br><br><br>
|
||||
<font size=8>HTTP {{data.code}}</font><br>
|
||||
{{data.msg}}
|
||||
<br><br><br><br><br><br>
|
||||
</center>
|
||||
{% endblock %}
|
|
@ -1,326 +0,0 @@
|
|||
:root {
|
||||
--page-width: 980px;
|
||||
}
|
||||
|
||||
* {
|
||||
transition-property: color, background-color, border-color, border-style, border, stroke;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.25s;
|
||||
}
|
||||
|
||||
|
||||
/* Nunito Sans */
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold'),
|
||||
url('/static/fonts/nunito/NunitoSans-SemiBold.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-SemiBold.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light Italic'),
|
||||
url('/static/fonts/nunito/NunitoSans-ExtraLightItalic.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-ExtraLightItalic.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Bold Italic'),
|
||||
url('/static/fonts/nunito/NunitoSans-Italic.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-Italic.ttf') format('ttf');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'sans undertale';
|
||||
src: local('Nunito Sans Light'),
|
||||
url('/static/fonts/nunito/NunitoSans-Light.woff2') format('woff2'),
|
||||
url('/static/fonts/nunito/NunitoSans-Light.ttf') format('ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
/* Basic elements */
|
||||
body {
|
||||
margin: 0px;
|
||||
font-family: 'sans undertale';
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
margin: 15px 0;
|
||||
font-size: 14pt;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
summary:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr:first-child .col1 {
|
||||
border-radius: 5px 0 0 0;
|
||||
}
|
||||
|
||||
tr:first-child .col2 {
|
||||
border-radius: 0 5px 0 0;
|
||||
}
|
||||
|
||||
tr:last-child .col1 {
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
||||
|
||||
tr:last-child .col2 {
|
||||
border-radius: 0 0 5px 0;
|
||||
}
|
||||
|
||||
|
||||
/* Main page sections */
|
||||
#header {
|
||||
margin: 0 auto;
|
||||
width: var(--page-width);
|
||||
}
|
||||
|
||||
#header .title {
|
||||
margin: 0px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 0 10px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 10px;
|
||||
width: var(--page-width);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* Custom page elements */
|
||||
.section {
|
||||
margin: 20px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.section:not(#posts), .post {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.section .title {
|
||||
font-size: 36pt;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: 50% auto;
|
||||
grid-gap: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: inline-grid;
|
||||
}
|
||||
|
||||
|
||||
/* Post elements */
|
||||
.post_text summary {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.post .grid-container {
|
||||
grid-template-columns: auto 50px;;
|
||||
}
|
||||
|
||||
.post .action-button {
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.post .action-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.post .post-actions {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.post_date {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.action-button path {
|
||||
width: 5px;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
#footer {
|
||||
clear: both;
|
||||
padding: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#footer .col2 {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
/* Dropdown menu */
|
||||
#user_panel {
|
||||
position: fixed;
|
||||
display: block;
|
||||
right: 0;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
}
|
||||
|
||||
.menu #menu_title {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#user_panel .item {
|
||||
padding: 5px 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
#summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-right details[open] summary ~ * {
|
||||
animation: sweep-left .5s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
/* Toot panel */
|
||||
#toot_panel input, #toot_panel textarea {
|
||||
margin: 10px 0;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
#toot_panel textarea {
|
||||
height: 8em;
|
||||
resize: none;
|
||||
|
||||
}
|
||||
|
||||
#toot_panel details[open] {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#toot_panel form {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* Login/Register forms */
|
||||
#logreg_form .title {
|
||||
font-size: 36pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#logreg_form input, #logreg_form textarea {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#logreg_form textarea, #profile textarea {
|
||||
height: 4em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#logreg_form .error {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* Settings Page */
|
||||
#settings_page textarea {
|
||||
width: 90%;
|
||||
height: 8em;
|
||||
}
|
||||
|
||||
#settings_page input {
|
||||
width: 90%
|
||||
}
|
||||
|
||||
#settings_page input[type=submit] {
|
||||
width: 44%
|
||||
}
|
||||
|
||||
|
||||
/* responsive design */
|
||||
@media (max-width : 1000px) {
|
||||
#content, #header {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#content {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#settings_page .column {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
#settings_page input[type=submit] {
|
||||
min-width: 200px;
|
||||
width: 22%
|
||||
}
|
||||
.grid-container {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Horizontal swipe animations */
|
||||
@keyframes sweep-right {
|
||||
0% {opacity: 0; margin-left: -10px}
|
||||
100% {opacity: 1; margin-left: 0px}
|
||||
}
|
||||
|
||||
@keyframes sweep-left {
|
||||
0% {opacity: 0; margin-right: -10px}
|
||||
100% {opacity: 1; margin-right: 0px}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = 'Home' %}
|
||||
|
||||
{% block content %}
|
||||
<center>
|
||||
<font size=2>I don't know what I'm doing tbh</font><br><br><br><br><br><br><br>
|
||||
<font style="font-size: 8px">merp</font><br><br><br><br><br><br><br>
|
||||
</center>
|
||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% set page = 'Login' %}
|
||||
|
||||
{% block content %}
|
||||
<br>
|
||||
<center><form action="/login" method="post" id="logreg_form" onsubmit="javascript:delete_cookie('login_token')">
|
||||
<div class="title">{{page}}</div>
|
||||
{% if msg != None %}
|
||||
<div class="error message">{{msg}}</div>
|
||||
{% else %}
|
||||
<br>
|
||||
{% endif %}
|
||||
<input type="text" name="username" placeholder="Username"><br>
|
||||
<input type="password" name="password" placeholder="Password"></br>
|
||||
<input type="hidden" name="redir" value="{{redir.path}}">
|
||||
<input type="hidden" name="redir_data" value="{{redir.data}}">
|
||||
<td><input type="submit" value="Login">
|
||||
</form></center>
|
||||
{% endblock %}
|
|
@ -1,15 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{{name}}: Missing Template</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://{{domain}}/layout.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://{{domain}}/color.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<center><h1>{{name}}<br><font size=1>(working name)</font></h1>
|
||||
<br>The developer made an oopsie poopsie!<br>
|
||||
<font size=1>Error: Can't find the template file</font>
|
||||
</center>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% set page = '{} - {}'.format(post.user.name, post.id) %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="section single-post" id="posts">
|
||||
{% include "components/post.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,36 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = profile.name %}
|
||||
|
||||
{% block content %}
|
||||
<div id="bio" class="section">
|
||||
<a href="https://{{domain}}/@{{profile.handle}}">{{profile.name}}</a><br>
|
||||
{{profile.handle}}@{{domain}}<br>
|
||||
{{count}} toots<br>
|
||||
<br>
|
||||
{% if profile.bio != None %}
|
||||
{% for line in profile.bio %}
|
||||
{{line}}<br>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if profile.info_table != None %}
|
||||
<div id="user_info" class="section">
|
||||
<table>
|
||||
{% for line in profile.info_table %}
|
||||
<tr><td class="col1">{{line}}</td><td class="col2">{{profile.info_table[line]}}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="posts" class="section">
|
||||
{% if post != [] %}
|
||||
{% for post in post %}
|
||||
{% include "components/post.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if last_id != None %}
|
||||
<div id="new_posts"><center>{{last_id}}<a href="https://{{domain}}/@{{profile.handle}}?id={{last_id}}">[More Posts]</a></center></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,34 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = 'Register' %}
|
||||
|
||||
{% block content %}
|
||||
<br>
|
||||
<center><form action="/register" method="post" id="logreg_form" autocomplete="new-password">
|
||||
<div class="title">{{page}}</div>
|
||||
{% if msg != None %}
|
||||
<div class="error message">{{msg}}</div>
|
||||
{% else %}
|
||||
<br>
|
||||
{% endif %}
|
||||
<input type="text" name="username" placeholder="Username"><br>
|
||||
<input
|
||||
type="password"
|
||||
name="newpassword1"
|
||||
placeholder="New password"
|
||||
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||
title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters"
|
||||
autocomplete="new-password"><br>
|
||||
<input
|
||||
type="password"
|
||||
name="newpassword2"
|
||||
placeholder="Repeat new password"
|
||||
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||
title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters"
|
||||
autocomplete="new-password"><br>
|
||||
<input type="email" name="email" placeholder="E-mail"></br>
|
||||
<input type="text" name="name" placeholder="Display Name"></br>
|
||||
<textarea name="bio" placeholder="Bio"></textarea></br>
|
||||
<td><input type="submit" value="Register">
|
||||
</form></center>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = 'Admin' %}
|
||||
|
||||
{% block content %}
|
||||
<br><br><br>
|
||||
<center><h2>OwO</h2></center>
|
||||
{% endblock %}
|
|
@ -1,75 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = 'Settings' %}
|
||||
|
||||
{% block content %}
|
||||
<div id="settings_page">
|
||||
<div id="profile" class="section">
|
||||
<div class="title">Profile</div>
|
||||
<form action="/settings" method="post">
|
||||
<div class="grid-container">
|
||||
<div class="grid-item"><center>
|
||||
<input type="text" name="display" placeholder="Display name" value="{{user.name}}"><br>
|
||||
<input type="text" name="username" placeholder="Username" value="{{user.handle}}" disabled>
|
||||
</center></div>
|
||||
|
||||
<div class="grid-item"><center>
|
||||
<textarea name="bio" placeholder="Bio" maxlength="{{settings.char_limit}}">{% if user.bio %}{{user.bio}}{% endif %}</textarea>
|
||||
<textarea name="sig" placeholder="Post signature" maxlength="{{settings.char_limit}}">{% if user.sig %}{{user.sig}}{% endif %}</textarea>
|
||||
</center></div>
|
||||
</div>
|
||||
<input type="text" name="handle" placeholder="Username" value="{{user.handle}}" hidden>
|
||||
<input type="hidden" name="type" value="profile">
|
||||
<center><input type="submit" value="Update profile info" class="center_input"></center>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="security" class="section">
|
||||
<div class="title">Account</div>
|
||||
<div class="grid-container">
|
||||
<div class="grid-item"><center>
|
||||
<form action="/settings" method="post">
|
||||
<input type="password" name="curpassword" placeholder="Current password"><br>
|
||||
<input
|
||||
type="password"
|
||||
name="newpassword1"
|
||||
placeholder="New password"
|
||||
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||
title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters"><br>
|
||||
<input
|
||||
type="password"
|
||||
name="newpassword2"
|
||||
placeholder="Repeat new password"
|
||||
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||
title="Must contain at least one number, one uppercase & lowercase letter, and at least 8 or more characters"><br>
|
||||
<input type="text" name="handle" placeholder="Username" value="{{user.handle}}" hidden>
|
||||
<input type="hidden" name="type" value="password">
|
||||
<input type="submit" value="Change password">
|
||||
</form>
|
||||
</center></div>
|
||||
|
||||
<div class="grid-item"><center>
|
||||
<form action="/settings" method="post">
|
||||
<input type="email" name="email" placeholder="E-mail Address" value="{{user.email}}"><br>
|
||||
<input type="text" name="handle" placeholder="Username" value="{{user.handle}}" hidden>
|
||||
<input type="hidden" name="type" value="email">
|
||||
<input type="submit" value="Update email">
|
||||
</form>
|
||||
</center></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="options" class="section">
|
||||
<div class="title">Options</div>
|
||||
<form action="/settings" method="post">
|
||||
<div class="grid-container">
|
||||
<div class="grid-item">
|
||||
Colors
|
||||
<input type="radio" name="color" value="{{color}}" id="color-{{color}}"><label for="color-{{color}}">{{color}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% set page = 'Weclome' %}
|
||||
|
||||
{% block content %}
|
||||
<br><br><br>
|
||||
<center><h2>HEWWO!!</h2><br />ur gay</center>
|
||||
<br />
|
||||
{% endblock %}
|
|
@ -1,3 +1,6 @@
|
|||
from . import activitypub, frontend, oauth, resources
|
||||
#from . import activitypub, frontend, oauth, redirects, resources
|
||||
|
||||
#__all__ = ['activitypub', 'frontend', 'oauth', 'redirects', 'resources']
|
||||
|
||||
from . import api, frontend, redirects, resources
|
||||
|
||||
__all__ = ['activitypub', 'frontend', 'oauth', 'redirects', 'resources']
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
import ujson as json
|
||||
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic import response
|
||||
from IzzyLib.misc import formatUTC
|
||||
|
||||
from ..config import config
|
||||
from ..functions import dumps
|
||||
from ..web_functions import error, jresp
|
||||
from ..database import newtrans, get, db
|
||||
from ..messages import Note, Actor
|
||||
|
||||
|
||||
class status(HTTPMethodView):
|
||||
async def get(self, request, status):
|
||||
return jresp(Note(status), activity=True)
|
||||
|
||||
|
||||
class replies(HTTPMethodView):
|
||||
async def get(self, request, status=None):
|
||||
data = {'msg': 'UvU'}
|
||||
return jresp(data, activity=True)
|
||||
|
||||
|
||||
class actor(HTTPMethodView):
|
||||
async def get(self, request, user=None, extra=None):
|
||||
user = 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 jresp(data, activity=True)
|
||||
|
||||
|
||||
async def post(self, request, user=None, extra=None):
|
||||
user = get.user(user)
|
||||
data = {'msg': 'UvU'}
|
||||
print(dumps(request.body, indent=4))
|
||||
return jresp(data, 202, headers={'x-merp': 'heck'}, activity=True)
|
||||
|
||||
|
||||
def _actor_data(self, request, user):
|
||||
return 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):
|
||||
userid = user['id']
|
||||
count = db.query(f'SELECT COUNT(*) FROM statuses WHERE userid={userid};').dictresult()
|
||||
outbox = f'https://{config["web_domain"]}/actor/{user["handle"]}/outbox'
|
||||
|
||||
data = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': outbox,
|
||||
'type': 'OrderedCollection',
|
||||
'totalItems': count[0].get('count'),
|
||||
'first': f'{outbox}?page=true',
|
||||
'last': f'{outbox}?min_id=0&page=true'
|
||||
}
|
||||
|
||||
if request['query'].get('page') == 'true':
|
||||
data.update ({
|
||||
'@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',
|
||||
'next': f'{outbox}?max_id=104226311723994872&page=true',
|
||||
'prev': f'{outbox}?min_id=104255993430915469&page=true',
|
||||
'orderedItems': []
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class inbox(HTTPMethodView):
|
||||
async def post(self, request):
|
||||
print(request.body)
|
||||
return response.text('UvU', content_type='application/activity+json', status=202)
|
||||
|
||||
|
||||
class nodeinfo(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
settings = get.settings
|
||||
stats = get.server_stats()
|
||||
|
||||
data = {
|
||||
'version': '2.0',
|
||||
'usage': {
|
||||
'users': {'total': stats['user_count']},
|
||||
'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.text(json.dumps(data, escape_forward_slashes=False), content_type='application/json')
|
||||
|
||||
|
||||
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'):
|
||||
data = dumps(data)
|
||||
|
||||
return response.text(data, content_type=ctype)
|
||||
|
||||
|
||||
def NodeInfo(self, request):
|
||||
data = {
|
||||
'links': [
|
||||
{
|
||||
'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
'href': 'https://{}/nodeinfo/2.0.json'.format(config['web_domain'])
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return ('application/json', data)
|
||||
|
||||
|
||||
def HostMeta(self, request):
|
||||
data = '''<?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://{}/.well-known/webfinger?resource={{uri}}"/>
|
||||
</XRD>
|
||||
'''.format(config['web_domain'])
|
||||
|
||||
return ('application/xrd+xml', data)
|
||||
|
||||
|
||||
def WebFinger(self, request):
|
||||
query = request['query']
|
||||
settings = get.settings
|
||||
|
||||
if query.get('resource') != None:
|
||||
resource = query['resource'].split('@')
|
||||
user = resource[0].replace('acct:', '')
|
||||
web_domain = config['web_domain']
|
||||
domain = get.settings('domain')
|
||||
|
||||
if resource[1] == settings('domain') and newtrans(get.user(user)):
|
||||
data = {
|
||||
'subject': f'acct:{user}@{domain}',
|
||||
'aliases': [
|
||||
f'https://{web_domain}/@{user}',
|
||||
f'https://{web_domain}/actor/{user}'
|
||||
],
|
||||
'links': [
|
||||
{
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': f'https://{web_domain}/@{user}'
|
||||
},
|
||||
{
|
||||
'rel': 'self',
|
||||
'type': 'application/activity+json',
|
||||
'href': f'https://{web_domain}/actor/{user}'
|
||||
},
|
||||
{
|
||||
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template': f'https://{web_domain}/interact?url={{url}}'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
else:
|
||||
data = {}
|
||||
|
||||
return ('application/json', data)
|
||||
|
89
social/views/api.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import json
|
||||
|
||||
from sanic import exceptions
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic_openapi import doc
|
||||
|
||||
from ..api import oauth, mastodon, misc
|
||||
from ..config import config
|
||||
from ..functions import JsonResp, Error
|
||||
|
||||
|
||||
class Oauth(HTTPMethodView):
|
||||
async def get(self, request, name=None):
|
||||
return ApiAction(request, oauth.Get, name)
|
||||
|
||||
|
||||
async def post(self, request, name=None):
|
||||
return ApiAction(request, oauth.Post, name)
|
||||
|
||||
|
||||
class MastodonBase(HTTPMethodView):
|
||||
async def get(self, request, name=None):
|
||||
return ApiAction(request, mastodon.BaseGet, name)
|
||||
|
||||
|
||||
async def post(self, request, name=None):
|
||||
return ApiAction(request, mastodon.BasePost, name)
|
||||
|
||||
|
||||
class MastodonAcct(HTTPMethodView):
|
||||
async def get(self, request, name=None):
|
||||
return ApiAction(request, mastodon.AcctGet, name)
|
||||
|
||||
|
||||
async def post(self, request, name=None):
|
||||
return ApiAction(request, mastodon.AcctPost, name)
|
||||
|
||||
|
||||
class Streaming(HTTPMethodView):
|
||||
async def get(self, request, name=None):
|
||||
async def home(response):
|
||||
run = True
|
||||
print('Streaming started for', request.ctx.user.handle)
|
||||
|
||||
while run:
|
||||
body = await self.request.stream.read()
|
||||
data = body.decode()
|
||||
|
||||
await response.write('UvU')
|
||||
|
||||
if data == 'exit':
|
||||
run = False
|
||||
|
||||
print('Streaming stopped for', request.ctx.user.handle)
|
||||
|
||||
endpoint = {
|
||||
'home': home
|
||||
}
|
||||
|
||||
self.request = request
|
||||
return stream(self.endpoint.get(name))
|
||||
|
||||
|
||||
def ApiAction(request, modclass, name=None):
|
||||
if not name:
|
||||
return JsonResp('Missing command', 400)
|
||||
|
||||
data = PostData(request)
|
||||
cmd = getattr(modclass, name, False)
|
||||
|
||||
if not cmd:
|
||||
return JsonResp('Invalid command', 404)
|
||||
|
||||
msg = cmd(request, data)
|
||||
|
||||
return JsonResp(msg) if type(msg) in [dict, list] else msg
|
||||
|
||||
|
||||
def PostData(request, name=None):
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
except exceptions.InvalidUsage:
|
||||
data = request.ctx.form
|
||||
|
||||
data = data if data else {}
|
||||
data.update(request.ctx.query)
|
||||
|
||||
return misc.sanitize(data, name) if name else data
|
|
@ -1,56 +1,74 @@
|
|||
import ujson as json
|
||||
import os, validators
|
||||
|
||||
from urllib.parse import unquote_plus
|
||||
from IzzyLib.template import sendResponse
|
||||
from datetime import datetime
|
||||
from sanic import response
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic import response, exceptions
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
from . import activitypub as ap
|
||||
|
||||
from ..config import config, logging
|
||||
from ..functions import color_css, mkhash, timestamp
|
||||
from ..web_functions import json_check, error, dumps
|
||||
from ..database import newtrans, get, update, put, delete
|
||||
from ..api import settings
|
||||
from ..config import config
|
||||
from ..database import db
|
||||
from ..functions import GenKey, mkhash, Error
|
||||
|
||||
|
||||
# home page (/)
|
||||
class home(HTTPMethodView):
|
||||
class Home(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return sendResponse('pages/home.html', request, {})
|
||||
return config.template.response('pages/home.haml', request, {})
|
||||
|
||||
|
||||
class register(HTTPMethodView):
|
||||
class Rules(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
query = request['query']
|
||||
print(query)
|
||||
data = {'text': config.frontend.join('beemovie.txt').read()[:10000].replace('\n', '<br>\n')}
|
||||
return config.template.response(f'pages/rules.haml', request, data)
|
||||
|
||||
# This can probably be optimized
|
||||
if 'msg' in query:
|
||||
message = query['msg']
|
||||
|
||||
if message == 'MissingData':
|
||||
msg = 'Missing username or password. Try again.'
|
||||
class About(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
data = {'text': config.frontend.join('beemovie.txt').read()[:10000].replace('\n', '<br>\n')}
|
||||
return config.template.response('pages/about.haml', request, data)
|
||||
|
||||
elif message == 'LoggedOut':
|
||||
msg = 'You\'ve been logged out.'
|
||||
|
||||
elif message == 'UserExists':
|
||||
msg = 'User already exists'
|
||||
class User(HTTPMethodView):
|
||||
async def get(self, request, user=None):
|
||||
if not user:
|
||||
return Error('User not specified', 404)
|
||||
|
||||
else:
|
||||
msg = message
|
||||
if '@' in user:
|
||||
handle, domain = user.split('@', 1)
|
||||
|
||||
else:
|
||||
msg = None
|
||||
handle = user
|
||||
domain = config.domain
|
||||
|
||||
return sendResponse('pages/register.html', request, {'msg': msg})
|
||||
row = db.get.user(user, domain)
|
||||
|
||||
if not row:
|
||||
return Error('User not found', 404)
|
||||
|
||||
data = db.classes.User(dict(row))
|
||||
table = {}
|
||||
|
||||
for k,v in data.table.items():
|
||||
if validators.url(v):
|
||||
v = f"<a href='{v}'>{v}</a>"
|
||||
|
||||
table[k] = v
|
||||
|
||||
data.table = table
|
||||
data.bio = data.bio.replace('\n', '<br>\n')
|
||||
|
||||
context = {'user': data, 'user_domain': domain}
|
||||
return config.template.response('pages/profile.haml', request, context)
|
||||
|
||||
|
||||
async def post(self, request, **kwargs):
|
||||
headers = request.headers
|
||||
data = request['form']
|
||||
class Register(HTTPMethodView):
|
||||
async def get(self, request, msg=None):
|
||||
return config.template.response('pages/register.haml', request, {'msg': msg})
|
||||
|
||||
print(data)
|
||||
|
||||
async def post(self, request):
|
||||
data = request.ctx.form
|
||||
|
||||
pass1 = data.get('newpassword1')
|
||||
pass2 = data.get('newpassword2')
|
||||
|
@ -59,25 +77,43 @@ class register(HTTPMethodView):
|
|||
password = pass1 if None not in [pass1, pass2] and pass1 == pass2 else None
|
||||
email = data.get('email')
|
||||
name = data.get('name')
|
||||
bio = data.get('bio')
|
||||
sig = data.get('sig')
|
||||
info_table = data.get('table')
|
||||
address = headers.get('X-Real-Ip')
|
||||
agent = headers.get('User-Agent')
|
||||
#sig = data.get('sig')
|
||||
#info_table = data.get('table')
|
||||
address = request.headers.get('X-Real-Ip')
|
||||
agent = request.headers.get('User-Agent')
|
||||
|
||||
user = db.query('user', handle=username)
|
||||
|
||||
if user:
|
||||
return await self.get(request, 'User already exists')
|
||||
|
||||
if None in [username, password]:
|
||||
return response.redirect('/register?msg=MissingData')
|
||||
return await self.get(request, 'Missing username or password. Try again')
|
||||
|
||||
user_check = get.user(get.handle_to_userid(username))
|
||||
keys = GenKey()
|
||||
domain = db.query('domain', domain=config.domain)
|
||||
|
||||
if user_check != None:
|
||||
return response.redirect('/register?msg=UserExists')
|
||||
if not domain:
|
||||
logging.error('Domain row in db not setup')
|
||||
return
|
||||
|
||||
user = put.user(username, email, password, name, bio, info_table, sig)
|
||||
with db.session() as s:
|
||||
s.add(db.table.user(
|
||||
handle = username,
|
||||
domainid = domain.id,
|
||||
name = name,
|
||||
email = email,
|
||||
password = mkhash(pass1),
|
||||
permissions = 4,
|
||||
privkey = keys.PRIVKEY,
|
||||
pubkey = keys.PUBKEY,
|
||||
timestamp = datetime.now()
|
||||
))
|
||||
|
||||
token = put.login_cookie(user['id'], user['password'], address, agent)
|
||||
user = db.query('user', handle=username)
|
||||
token = db.put.cookie(user.id, address, agent)
|
||||
|
||||
resp = response.redirect('/login')
|
||||
resp = response.redirect('/')
|
||||
resp['login_token'] = token
|
||||
resp['login_token']['max-age'] = 60*60*24*14
|
||||
|
||||
|
@ -85,29 +121,16 @@ class register(HTTPMethodView):
|
|||
|
||||
|
||||
# login page (/login)
|
||||
class login(HTTPMethodView):
|
||||
class Login(HTTPMethodView):
|
||||
async def get(self, request, msg=None):
|
||||
query = request['query']
|
||||
|
||||
if 'msg' in query and not msg:
|
||||
message = query['msg']
|
||||
|
||||
if message == 'InvalidToken':
|
||||
msg = 'Invalid login token. Try logging in again.'
|
||||
|
||||
elif message == 'LoggedOut':
|
||||
msg = 'You\'ve been logged out.'
|
||||
|
||||
else:
|
||||
msg = message
|
||||
|
||||
return sendResponse('pages/login.html', request, {'msg': msg, 'redir': {'path': query.get('redir'), 'data': query.get('query')}})
|
||||
query = request.ctx.query
|
||||
return config.template.response('pages/login.haml', request, {'msg': msg, 'redir': {'path': query.get('redir'), 'data': query.get('query')}})
|
||||
|
||||
|
||||
async def post(self, request):
|
||||
query = request['query']
|
||||
query = request.ctx.query
|
||||
headers = request.headers
|
||||
data = request['form']
|
||||
data = request.ctx.form
|
||||
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
@ -119,168 +142,55 @@ class login(HTTPMethodView):
|
|||
if None in [username, password]:
|
||||
return await self.get(request, 'Username or password is missing')
|
||||
|
||||
user = newtrans(get.user(username.lower()))
|
||||
user = db.get.user(username)
|
||||
|
||||
pass_hash = mkhash(password+config['salt'])
|
||||
if user and user.password == mkhash(password):
|
||||
login_token = db.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 user != None:
|
||||
if user['password'] == pass_hash:
|
||||
login_token = newtrans(put.login_cookie(user['id'], password, address, agent))
|
||||
# Send login token. Lasts for 2 weeks (60 sec * 60 min * 24 hour * 14 day)
|
||||
resp.cookies['login_token'] = login_token
|
||||
resp.cookies['login_token']['max-age'] = 60*60*24*14
|
||||
|
||||
if redir != 'None':
|
||||
resp = response.redirect(f'{redir}?{unquote_plus(redir_data)}')
|
||||
|
||||
else:
|
||||
resp = response.redirect('/welcome')
|
||||
|
||||
# Send login token. Lasts for 2 weeks (60 sec * 60 min * 24 hour * 14 day)
|
||||
resp.cookies['login_token'] = login_token
|
||||
resp.cookies['login_token']['max-age'] = 60*60*24*14
|
||||
|
||||
return resp
|
||||
return resp
|
||||
|
||||
return await self.get(request, 'Wrong username or password')
|
||||
|
||||
|
||||
class logout(HTTPMethodView):
|
||||
class Logout(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
token = request.cookies.get('login_token')
|
||||
|
||||
resp = await login().get(request, msg='Logged out')
|
||||
|
||||
if token != None:
|
||||
cookie = get.login_cookie(token)
|
||||
|
||||
if cookie != None:
|
||||
newtrans(delete.login_cookie(cookie['id'], token))
|
||||
token_del = db.delete.cookie(token)
|
||||
resp = await Login().get(request, msg='Logged out' if token_del else None)
|
||||
|
||||
del resp.cookies['login_token']
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
# user profiles
|
||||
class user(HTTPMethodView):
|
||||
async def get(self, request, user=None):
|
||||
postid = request['query'].get('id')
|
||||
user_data = newtrans(get.profile(user, postid=postid))
|
||||
class Settings(HTTPMethodView):
|
||||
async def get(self, request, name=None, message=None):
|
||||
if not name:
|
||||
return response.redirect('/settings/profile')
|
||||
|
||||
if not user_data:
|
||||
return error(request, 404, msg='That user doesn\'t exist.')
|
||||
func = getattr(settings, f'get_{name}')
|
||||
data = func(request)
|
||||
data['message'] = message
|
||||
|
||||
if json_check(request.headers):
|
||||
return response.redirect(f'/actor/{user}')
|
||||
if type(data) != dict:
|
||||
return data
|
||||
|
||||
try:
|
||||
posts = user_data['post']
|
||||
|
||||
if len(posts) < config['vars']['posts']:
|
||||
last_id = None
|
||||
|
||||
else:
|
||||
last_id = posts[-1]['id']
|
||||
|
||||
except IndexError:
|
||||
last_id = None
|
||||
|
||||
user_data['last_id'] = last_id
|
||||
bio = []
|
||||
|
||||
for line in user_data['profile']['bio'].split('\n'):
|
||||
bio.append(line)
|
||||
|
||||
user_data['profile']['bio'] = bio
|
||||
|
||||
return sendResponse('pages/public/profile.html', request, user_data)
|
||||
return config.template.response(f'pages/settings/{name}.haml', request, data)
|
||||
|
||||
|
||||
# single post
|
||||
class status(HTTPMethodView):
|
||||
async def get(self, request, status=None):
|
||||
post = get.post(status)
|
||||
async def post(self, request, name=None):
|
||||
if not name:
|
||||
return Error('Not found', 404)
|
||||
|
||||
if post == None:
|
||||
return error(request, 404, msg='That post doesn\'t exist.')
|
||||
func = getattr(settings, f'post_{name}')
|
||||
data = func(request)
|
||||
|
||||
if json_check(request.headers):
|
||||
return response.redirect(f'/status/{status}')
|
||||
if type(data) == Error:
|
||||
return data
|
||||
|
||||
post_data = {'post': post}
|
||||
|
||||
post_data['post']['user'] = get.user(post_data['post']['userid'])
|
||||
|
||||
return sendResponse('pages/public/post.html', request, post_data)
|
||||
|
||||
|
||||
async def post(self, request, status=None):
|
||||
data = request['form']
|
||||
login_token = request.cookies.get('login_token')
|
||||
json_data = data.get('post_data')
|
||||
|
||||
if None in ['json_data', 'login_token']:
|
||||
return redirect(f'/:{status}')
|
||||
|
||||
post_data = json.loads(json_data)
|
||||
username = post_data['user']['handle']
|
||||
|
||||
newtrans(delete.post(status, post_data, login_token))
|
||||
|
||||
return response.redirect(f'/@{username}')
|
||||
|
||||
|
||||
# user home page (/welcome)
|
||||
class welcome(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return sendResponse('pages/user/welcome.html', request, {})
|
||||
|
||||
|
||||
class settings(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
login_token = request.cookies.get('login_token')
|
||||
settings = get.settings('all')
|
||||
|
||||
token = newtrans(get.login_cookie(login_token))
|
||||
handle = newtrans(get.user(token['userid']))['id']
|
||||
|
||||
if token == None:
|
||||
response.redirect('/login?msg=InvalidToken')
|
||||
|
||||
return sendResponse('pages/user/settings.html', request, {'settings': settings})
|
||||
|
||||
|
||||
async def post(self, request):
|
||||
headers = request.headers
|
||||
data = request['form']
|
||||
|
||||
new_data = {}
|
||||
for item in data:
|
||||
if data[item] != '' or item != 'handle':
|
||||
new_data[item] = data[item]
|
||||
|
||||
newtrans(update.profile(data['handle'], new_data))
|
||||
|
||||
return response.redirect('/settings')
|
||||
|
||||
|
||||
class admin(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return sendResponse('pages/user/admin.html', request, {})
|
||||
|
||||
|
||||
async def post(request):
|
||||
pass
|
||||
|
||||
|
||||
class post(HTTPMethodView):
|
||||
async def post(self, request):
|
||||
headers = request.headers
|
||||
data = request['form']
|
||||
|
||||
token_data = get.login_cookie(request.cookies.get('login_token'))
|
||||
|
||||
if token_data == None:
|
||||
return error(request, 401, msg='Invalid token')
|
||||
|
||||
post = put.local_post(token_data['userid'], data)
|
||||
|
||||
return response.redirect(f'/:{post["id"]}')
|
||||
return await self.get(request, name=name, message=data)
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import secrets
|
||||
import validators
|
||||
|
||||
from IzzyLib.cache import TTLCache, LRUCache
|
||||
|
||||
from ..database import *
|
||||
from ..config import logging
|
||||
|
||||
|
||||
authcodes = TTLCache(maxsize=4096, ttl='15m')
|
||||
|
||||
|
||||
def scope_check(scopes):
|
||||
read_write = ['follows', 'accounts', 'lists', 'blocks', 'mutes', 'bookmarks', 'notifications', 'favourites', 'search', 'filters', 'statuses', None]
|
||||
admin = ['read', 'write', None]
|
||||
admin_secc = ['accounts', 'reports']
|
||||
new_scopes = []
|
||||
|
||||
for line in scopes:
|
||||
scope = line.split(':')
|
||||
|
||||
print(type(scope), scope)
|
||||
|
||||
while len(scope) < 3:
|
||||
scope.append(None)
|
||||
|
||||
if (scope[0] in ['read', 'write'] and scope[1] in read_write) or scope[0] in ['follow', 'push'] or (scope[0] == 'admin' and scope[1] in admin and scope[2]):
|
||||
new_scopes.append(line)
|
||||
|
||||
else:
|
||||
logging.warning(f'Invalid scope: {line}')
|
||||
|
||||
if len(new_scopes) < 1:
|
||||
return
|
||||
|
||||
else:
|
||||
return new_scopes
|
||||
|
||||
|
||||
class create:
|
||||
def app(redirect_uri, scope, name, url):
|
||||
if None in [scope, name]:
|
||||
logging.debug('Missing scope or name for app')
|
||||
logging.debug(f'scope: {scope}, name: {name}')
|
||||
return 'MissingData'
|
||||
|
||||
scopes = scope_check(scope)
|
||||
|
||||
if scopes == None:
|
||||
logging.debug(f'Invalid scopes: {scope}')
|
||||
return 'InvalidScope'
|
||||
|
||||
if not validators.url(redirect_uri):
|
||||
logging.debug(f'Invalid redirect URL: {redirect_uri}')
|
||||
redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
if not validators.url(url):
|
||||
logging.debug(f'Invalid app URL: {url}')
|
||||
return 'InvalidURL'
|
||||
|
||||
client_id = secrets.token_hex(20)
|
||||
client_secret = secrets.token_hex(20)
|
||||
|
||||
put.oauth.app(client_id, client_secret, redirect_uri, scopes, name, url)
|
||||
|
||||
return {'client_id': client_id, 'client_secret': client_secret, 'redirect_uris': redirect_uri, 'scopes': scopes}
|
||||
|
||||
|
||||
def authorize(client_id, user_id, *args):
|
||||
if None in [client_id, user_id]:
|
||||
logging.debug(f'Invalid secrets: {client_id}, {user_id}')
|
||||
return 'InvalidCredentials'
|
||||
|
||||
return put.auth_code(client_id, user_id)
|
||||
|
||||
def token(client_id, login_token):
|
||||
pass
|
|
@ -1,36 +1,37 @@
|
|||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import redirect
|
||||
|
||||
from ..web_functions import json_check
|
||||
from ..functions import JsonCheck
|
||||
|
||||
|
||||
# Redirect /user/[user] to /@[user]
|
||||
class user(HTTPMethodView):
|
||||
async def get(request):
|
||||
user = request.match_info['user']
|
||||
|
||||
class User(HTTPMethodView):
|
||||
async def get(self, request, user):
|
||||
return redirect(f'/@{user}')
|
||||
|
||||
|
||||
# Redirect /status/[status] to /:[status]
|
||||
class post(HTTPMethodView):
|
||||
async def get(request):
|
||||
status = request.match_info['status']
|
||||
|
||||
if json_check(request.headers):
|
||||
class Post(HTTPMethodView):
|
||||
async def get(self, request, status):
|
||||
if JsonCheck(request.headers):
|
||||
return json_user(request, status)
|
||||
|
||||
return redirect(f'/:{status}')
|
||||
|
||||
|
||||
class About(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return redirect('/about')
|
||||
|
||||
|
||||
# PATPAT
|
||||
class headpats(HTTPMethodView):
|
||||
class HeadPats(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return redirect('https://static.barkshark.xyz/mastodon/main/custom_emojis/images/000/000/797/original/blobpatpat.png')
|
||||
|
||||
|
||||
#socks
|
||||
class socks(HTTPMethodView):
|
||||
class Socks(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return redirect('https://barkshark.xyz/@izalia/103155447990974282')
|
||||
|
||||
|
|
|
@ -1,36 +1,40 @@
|
|||
import ujson as json
|
||||
|
||||
from os.path import getmtime
|
||||
from datetime import datetime
|
||||
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic import response
|
||||
from IzzyLib.template import sendResponse, renderTemplate
|
||||
from IzzyLib import logging
|
||||
from sanic.views import HTTPMethodView
|
||||
|
||||
from ..config import config, script_path
|
||||
from ..functions import color_css, css_ts
|
||||
from ..database import get
|
||||
from ..config import config
|
||||
from ..database import db
|
||||
|
||||
|
||||
# color.css
|
||||
class style(HTTPMethodView):
|
||||
async def get(self, request, timestamp=None):
|
||||
theme = request.cookies.get('theme')
|
||||
|
||||
if theme == None:
|
||||
theme = get.settings('theme')
|
||||
|
||||
css = renderTemplate('color.css', color_css(theme), request)
|
||||
return response.text(css, content_type='text/css', headers={'Last-Modified': datetime.utcfromtimestamp(css_ts()).strftime('%a, %d %b %Y %H:%M:%S GMT')})
|
||||
themes = {
|
||||
'pink': '#e7a',
|
||||
'blue': '#77e',
|
||||
'red': '#e77'
|
||||
}
|
||||
|
||||
|
||||
class manifest(HTTPMethodView):
|
||||
# home page (/)
|
||||
class Style(HTTPMethodView):
|
||||
async def get(self, request, theme=None, timestamp=None):
|
||||
data = {
|
||||
'theme': themes.get(theme, 'pink')
|
||||
}
|
||||
|
||||
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()
|
||||
return await response.file(filename)
|
||||
|
||||
|
||||
class Manifest(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
data = {
|
||||
'name': get.settings('name'),
|
||||
'short_name': 'BarkShark',
|
||||
'description': '',
|
||||
'name': db.get.config('name'),
|
||||
'short_name': db.get.config('name'),
|
||||
'description': db.get.config('description'), #use description_short
|
||||
'icons': [
|
||||
{
|
||||
'src': '/static/icon-512.png',
|
||||
|
@ -43,22 +47,29 @@ class manifest(HTTPMethodView):
|
|||
'type': 'image/png'
|
||||
}
|
||||
],
|
||||
"theme_color": "#33bbff",
|
||||
"background_color": "#11111",
|
||||
"display":"standalone",
|
||||
"start_url":"/",
|
||||
"scope": f"https://{config.get('web_domain')}",
|
||||
'theme_color': '#33bbff',
|
||||
'background_color': '#222222',
|
||||
'display': 'standalone',
|
||||
'start_url': '/',
|
||||
'scope': f'https://{config.web_domain}',
|
||||
'share_target': {
|
||||
'url_template': 'share?title={title}&text={text}&url={url}',
|
||||
'action': 'share',
|
||||
'method': 'GET',
|
||||
'enctype': 'application/x-www-form-urlencoded',
|
||||
'params': {
|
||||
'title': 'title',
|
||||
'text': 'text',
|
||||
'url': 'url'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return response.json(data)
|
||||
|
||||
|
||||
class favicon(HTTPMethodView):
|
||||
class Robots(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return await response.file(f'{script_path}/static/icon-64.png')
|
||||
|
||||
|
||||
# turn away crawlers that respect robots.txt
|
||||
class robots(HTTPMethodView):
|
||||
async def get(self, request):
|
||||
return response.text('User-agent: *\nDisallow: /')
|
||||
data = db.get.config('robots')
|
||||
return response.text(data)
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
import sanic
|
||||
|
||||
from IzzyLib.http import httpClient
|
||||
from IzzyLib.template import sendResponse
|
||||
|
||||
from . import __version__ as VERSION
|
||||
from .functions import dumps
|
||||
from .config import config
|
||||
from .database import get
|
||||
|
||||
|
||||
key = get.settings('privkey')
|
||||
host = config['web_domain']
|
||||
agent = f'sanic/{sanic.__version__} (Barkshark-Social/{VERSION}; +https://{host}/)'
|
||||
|
||||
client = httpClient(agent=agent, timeout=5)
|
||||
|
||||
|
||||
def json_check(headers):
|
||||
accept = headers.get('Accept')
|
||||
|
||||
if not accept:
|
||||
return
|
||||
|
||||
mimes = accept.split(',')
|
||||
|
||||
if any(mime in ['application/json', 'application/activity+json'] for mime in mimes):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def error(request, code, msg=None):
|
||||
message = msg if msg else ''
|
||||
|
||||
if not isinstance(code, int):
|
||||
'heck'
|
||||
|
||||
if any(map(request.path.startswith, ['/status', '/actor'])) or json_check(request.headers):
|
||||
return jresp({'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 sendResponse(f'error.html', request, {'data': data, 'msg': message}, status=code)
|
||||
|
||||
|
||||
def jresp(data, status=200, headers=None, activity=False):
|
||||
params = {
|
||||
'content_type': 'application/activity+json' if activity else 'application/json',
|
||||
'status': status
|
||||
}
|
||||
|
||||
if headers:
|
||||
params['headers'] = headers
|
||||
|
||||
return sanic.response.text(dumps(data), **params)
|