major rewrite

This commit is contained in:
Izalia Mae 2021-01-12 08:43:46 -05:00
parent a977bb204c
commit 795aff4c07
101 changed files with 2746 additions and 2532 deletions

View file

@ -1 +1 @@
3.8.0
social

View file

@ -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
View file

@ -0,0 +1 @@
libpq-dev

2
aptpkg.txt Normal file
View file

@ -0,0 +1,2 @@
libpq5
libmagic1

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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
View file

@ -0,0 +1,3 @@
from . import mastodon, oauth
__all__ = ['mastodon', 'oauth']

View file

@ -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'})

View file

@ -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

View file

@ -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')

View file

@ -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
View 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'

View file

@ -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
)

View file

@ -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)

View 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())

View file

@ -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']))

View 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

View file

@ -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

View 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)

View file

@ -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
View 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
})

View file

@ -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

View file

@ -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
View 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}}

View file

@ -0,0 +1,8 @@
-extends 'base.haml'
-set page = 'Error'
-block content
%center
%font size='8'
HTTP {{data.code}}
%br
=data.msg

View file

@ -0,0 +1,4 @@
-extends 'base.haml'
-set page = 'About'
-block content
{{text}}

View file

@ -0,0 +1,4 @@
-extends 'base.haml'
-set page = 'Home'
-block content
{{db.get.config('description')}}

View 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'

View 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

View 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"

View file

@ -0,0 +1,4 @@
-extends 'base.haml'
-set page = 'Rules'
-block content
{{text}}

View 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'

View file

@ -0,0 +1,5 @@
-extends 'base.haml'
-set page = 'Redirect'
-block content
%script
window.location.replace(redir)

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

View 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
View 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%;
}
}

View file

@ -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)

View file

@ -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))

View file

@ -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
###

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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>

View file

@ -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)}};
}

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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}
}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% set page = 'Admin' %}
{% block content %}
<br><br><br>
<center><h2>OwO</h2></center>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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']

View file

@ -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
View 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

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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)

Some files were not shown because too many files have changed in this diff Show more