a whole bunch of changes

This commit is contained in:
Izalia Mae 2019-11-22 22:23:00 -05:00
parent d4c1d320b0
commit 1fd048c986
93 changed files with 14676 additions and 697 deletions

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.8.0

View file

@ -2,7 +2,7 @@
Lightweight ActivityPub server written in Python
Here's a list of all the ideas I plan on implementing: https://git.barkshark.tk/izaliamae/social/wiki/Ideas
Here's a list of all the ideas I plan on implementing: https://git.barkshark.xyz/izaliamae/social/wiki/Ideas
Note: 'social' is a placeholder name and will be changed in the future

21
dist/database.sql vendored
View file

@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS users (
settings TEXT,
privkey TEXT,
pubkey TEXT NOT NULL,
domain_id INT,
timestamp INT NOT NULL
);
@ -21,9 +22,19 @@ CREATE TABLE IF NOT EXISTS statuses (
warning TEXT,
visibility TEXT NOT NULL,
mentions TEXT,
replies TEXT,
domain_id INT,
timestamp FLOAT NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_codes (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL,
userid INT NOT NULL,
appid INT NOT NULL,
timestamp INT NOT NULL
)
CREATE TABLE IF NOT EXISTS auth_tokens (
id SERIAL PRIMARY KEY,
userid INT NOT NULL,
@ -33,6 +44,16 @@ CREATE TABLE IF NOT EXISTS auth_tokens (
access INT
);
CREATE TABLE IF NOT EXISTS auth_apps (
id SERIAL PRIMARY KEY,
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS login_cookies (
id SERIAL PRIMARY KEY,
userid INT NOT NULL,

View file

@ -1,25 +0,0 @@
import aiohttp, aiohttp_jinja2
# Redirect /user/[user] to /@[user]
async def user_get(request):
user = request.match_info['user']
return aiohttp.web.HTTPFound(f'/@{user}')
# Redirect /status/[status] to /:[status]
async def post_get(request):
status = request.match_info['status']
return aiohttp.web.HTTPFound(f'/:{post}')
# PATPAT
async def headpats(request):
return aiohttp.web.HTTPFound('https://static.barkshark.tk/mastodon/main/custom_emojis/images/000/000/590/original/blobpatpat.png')
#socks
async def socks(request):
return aiohttp.web.HTTPFound('https://glaceon.social/@monorail/102496098954435127')

View file

@ -1,42 +0,0 @@
{% extends "base.html" %}
{% set page = profile.name %}
{% block content %}
<div id="user_header">
<div id="bio">
<a href="https://{{domain}}/@{{profile.handle}}">{{profile.name}}</a><br>
{{profile.handle}}@{{domain}}<br>
{{profile.count}} toots<br>
<br>
{% if profile.bio != None %}
{% for line in profile.bio %}
{{line}}<br>
{% endfor %}
{% else %}
This bitch empty YEET
{% endif %}
</div>
{% if profile.info_table != None %}
<div id="user_info">
<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>
<div id="user_content">
<div id="posts">
{% if post != [] %}
{% for post in post %}
{% include "components/post.html" %}
{% endfor %}
{% endif %}
</div>
{% if last_id != None %}
<div id="new_posts"><center>{{profile.last_id}}<a href="https://{{domain}}/@{{profile.handle}}?id={{last_id}}">[More Posts]</a></center></div>
{% endif %}
</div>
{% endblock %}

6
reload.cfg Normal file
View file

@ -0,0 +1,6 @@
## Config file for Process Reloader (https://git.barkshark.xyz/barkshark/reload)
exec = ./server.py
watch_ext = py, env
ignore_dirs = webapp/js, bin, dist, misc, test
ignore_files = reload.py, test.py
log_level = INFO

View file

@ -1,74 +0,0 @@
#!/usr/bin/env python3
import time
import os
import re
import subprocess
import signal
import sys
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
os.environ['PYENV'] = 'dev'
from config import logging
pid = ''
def run(restart=False):
global pid
if restart == True and pid != '':
logging.info('Terminating process...')
os.kill(pid, signal.SIGTERM)
logging.info('Starting process...')
proc = subprocess.Popen(['nohup', './server.py'], preexec_fn=os.setpgrp)
pid = proc.pid
logging.info(f'Process PID: {pid}')
class MyHandler(FileSystemEventHandler):
def on_modified(self, event):
filename, ext = os.path.splitext(os.path.relpath(event.src_path))
if event.event_type == 'modified' and ext == '.py' and re.search(ignore_paths, filename) == None and filename not in ignore_filenames:
logging.info('Restarting server...')
run(restart=True)
def safe_stop():
logging.info('Stopping process watcher')
observer.stop()
observer.join()
if pid != '':
logging.info(f'Killing process: {pid}')
os.kill(pid, signal.SIGTERM)
sys.exit()
if __name__ == "__main__":
run()
path = os.path.dirname(os.path.realpath(__file__))
observer = Observer()
observer.schedule(MyHandler(), path, recursive=True)
ignore_paths = 'webapp/js|bin|dist|misc|data'
ignore_filenames = ['reload']
logging.info('Starting process watcher')
observer.start()
try:
while True:
time.sleep(100)
except KeyboardInterrupt:
pass
safe_stop()

View file

@ -1,9 +1,16 @@
psycopg2==2.8.3
pygresql==5.1
dbutils==1.3
envbash==1.1.2
pycryptodome==3.8.2
pycryptodome==3.9.0
colour==0.1.5
jinja2==2.10.1
aiohttp==3.6.0
aiohttp-cors==0.7.0
git+https://git.barkshark.tk/izaliamae/aiohttp-jinja2.git
git+https://git.barkshark.xyz/izaliamae/aiohttp-jinja2.git
dramatiq[redis, watch]==1.7.0
tinydb==3.15.0
tinyrecord==0.1.5
tinydb_smartcache==1.0.2
watchdog==0.8.3
validators==0.14.0
pyyaml==5.1.2

156
routes.py
View file

@ -1,156 +0,0 @@
import asyncio
import aiohttp
import jinja2
import aiohttp_jinja2
import json
import signal
import sys
from jinja2 import FileSystemLoader, select_autoescape
from config import config, logging
from backend import mastodon_api, api, middleware, wellknown
from backend.database import SETTINGS, newtrans, get, update_timestamps
from frontend import views, resources, user, redirects
from functions import color, todate, themes
from frontend.template_loader import CustomLoader
async def glob_vars(request):
return {
'name': SETTINGS['name'],
'domain': config['web_domain'],
'settings': SETTINGS,
'get_cookie': get.login_cookie,
'get_user': get.user,
'newtrans': newtrans,
'lighten': color().lighten,
'darken': color().darken,
'saturate': color().saturate,
'desaturate': color().desaturate,
'rgba': color().rgba,
'todate': todate,
'themes': themes,
'json': json
}
web = aiohttp.web.Application(middlewares=[
#middleware.http_signatures,
middleware.http_auth,
middleware.http_filter,
middleware.http_trailing_slash
])
aiohttp_jinja2.setup(web,
loader=CustomLoader('frontend/templates'),
autoescape=select_autoescape(['html', 'xml', 'css']),
context_processors=[glob_vars],
lstrip_blocks=True,
trim_blocks=True
)
env = aiohttp_jinja2.get_env(web)
# Public pages
web.router.add_get('/', views.home_get)
web.router.add_get('/register', views.register_get)
web.router.add_get('/login', views.login_get)
web.router.add_get('/logout', views.logout_get)
web.router.add_post('/register', views.register_post)
web.router.add_post('/login', views.login_post)
# Semi-public pages
web.router.add_get('/@{user}', views.user_get)
web.router.add_get('/:{status}', views.post_get)
web.router.add_get('/user/{user}', redirects.user_get)
web.router.add_get('/status/{status}', redirects.post_get)
web.router.add_post('/:{status}', user.post_delete_post)
# Frontend
web.router.add_get('/welcome', user.welcome_get)
web.router.add_get('/settings', user.settings_get)
web.router.add_get('/admin', user.admin_get)
web.router.add_post('/settings', user.settings_post)
web.router.add_post('/admin', user.admin_post)
web.router.add_post('/poast', user.poast_post)
# CSS, JS, etc
#web.router.add_get('/layout.css', resources.layout_get)
web.router.add_get('/style.css', resources.color_get)
web.router.add_get('/manifest.json', resources.manifest_get)
web.router.add_get('/favicon.ico', resources.favicon_get)
web.router.add_get('/robots.txt', resources.robots_txt)
# Media
web.router.add_static('/static', path='frontend/static', name='static')
web.router.add_static('/media', path='data/media', name='media')
# Native API
web.router.add_get('/api/native/{name}', api.handle_get)
web.router.add_post('/api/native/{name}', api.handle_post)
# Mastodon API
#web.router.add_post('/api/v1/{name}', mastodon_api.handle_post)
#web.router.add_get('/api/v1/{name}', mastodon_api.handle_get)
#web.router.add_get('/api/v1/streaming/{name}', mastodon_api.streaming_get)
# Various info endpoints
#web.router.add_get('/nodeinfo/2.0.json', wellknown.nodeinfo_json)
#web.router.add_get('/.well-known/nodeinfo', wellknown.nodeinfo_get)
#web.router.add_get('/.well-known/host-meta', wellknown.hostmeta_get)
#web.router.add_get('/.well-known/webfinger', wellknown.webfinger_get)
# Shitpost
web.router.add_get('/headpats', redirects.headpats)
web.router.add_get('/socks', redirects.socks)
async def start_web():
runner = aiohttp.web.AppRunner(web, access_log_format='%{X-Real-Ip}i "%r" %s %b "%{User-Agent}i"')
await runner.setup()
listen = config['listen']
port = config['port']
logging.info('Starting web server at {listen}:{port}'.format(listen=listen,port=port))
site = aiohttp.web.TCPSite(runner, listen, port)
await site.start()
async def save_tokens():
while True:
logging.debug('Saving updated timestamps for login and auth tokens to the database')
update_timestamps()
await asyncio.sleep(600)
def safe_stop(*args):
logging.debug('Saving updated timestamps')
update_timestamps()
logging.info('Bye')
sys.exit()
def main():
signal.signal(signal.SIGHUP, safe_stop)
signal.signal(signal.SIGINT, safe_stop)
signal.signal(signal.SIGQUIT, safe_stop)
signal.signal(signal.SIGTERM, safe_stop)
try:
loop = asyncio.get_event_loop()
asyncio.ensure_future(start_web())
asyncio.ensure_future(save_tokens())
loop.run_forever()
except KeyboardInterrupt:
pass
safe_stop()

View file

@ -3,10 +3,9 @@ from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from os import environ as ENV
import routes
from config import logging
from webapp import precompile
from social.config import logging
from social.web_server import main
class MyHandler(FileSystemEventHandler):
@ -17,15 +16,7 @@ class MyHandler(FileSystemEventHandler):
if __name__ == "__main__":
if ENV.get('PYENV', 'default').lower() in ['dev', 'default']:
path = 'webapp/js'
observer = Observer()
observer.schedule(MyHandler(), path, recursive=True)
logging.info('Starting javascript watcher')
observer.start()
routes.main()
main()
if ENV.get('PYENV', 'default').lower() in ['dev', 'default']:
logging.info('Stopping javascript watcher')

View file

@ -7,7 +7,7 @@ import re
from datetime import datetime
from collections import OrderedDict
from config import logging
from social.config import logging
def parse_ttl(ttl):
@ -46,52 +46,46 @@ class TTLCache:
self.maxsize = maxsize
def __contains__(self, key):
if key in self.items:
timestamp = int(datetime.timestamp(datetime.now()))
if timestamp >= self.items[key]['timestamp']:
del self.items[key]
else:
self.items[key]['timestamp'] = timestamp + self.ttl
self.items.move_to_end(key)
return True
return False
def invalidate(self, key):
if key in self.items:
del self.items[key]
return True
return False
def store(self, key, value):
timestamp = int(datetime.timestamp(datetime.now()))
item = self.items.get(key)
while len(self.items) >= self.maxsize and self.maxsize != 0:
timestamp = int(datetime.timestamp(datetime.now()))
self.items.popitem(last=False)
if (key in self.items) == False:
data = {'data': value, 'timestamp': timestamp}
if item == None:
logging.debug(f'adding to cache during store {key}')
data = {'data': value}
self.items[key] = data
if self.items[key]['timestamp'] + self.ttl < timestamp:
elif self.items[key]['timestamp'] + self.ttl < timestamp:
logging.debug(f'deleting from cache during store {key}')
del self.items[key]
self.items[key]['timestamp'] = timestamp
self.items[key]['timestamp'] = timestamp + self.ttl
self.items.move_to_end(key)
def fetch(self, key):
if key in self:
return self.items[key]['data']
item = self.items.get(key)
return None
if item != None:
timestamp = int(datetime.timestamp(datetime.now()))
if timestamp >= self.items[key]['timestamp']:
logging.debug(f'removing from cache during fetch {key}')
del self.items[key]
else:
logging.debug(f'updating timestamp during fetch {key}')
self.items[key]['timestamp'] = timestamp + self.ttl
self.items.move_to_end(key)
return self.items[key]['data']
class LRUCache:
@ -123,4 +117,4 @@ class LRUCache:
if key in self.items:
return self.items[key]
return None
return {}

View file

@ -1,28 +1,49 @@
import aiohttp, json, re, sys, os, asyncio
import aiohttp, json, sys, os, asyncio
from urllib.parse import urlparse
from config import config
from json.decoder import *
from .database import *
from functions import json_error
from .misc import sanitize
from ..config import config
from ..database import *
from ..functions import json_error
from .. import oauth
class post_cmd:
def apps():
return 'heck'
def apps(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 type(retdata) == str:
return json_error(400, 'Something fucked up')
return retdata
class get_cmd:
def instance():
stats = get.server_stats()
settings = get.settings
data = {
'version': '2.9.0 (compatible; Social {})'.format(config['version']),
'version': f'2.9.0 (compatible; Social {config["version"]})',
'uri': config['domain'],
'title': config['name'],
'title': settings('name'),
'description': 'OwO',
'urls': {
'streaming_api': 'wss://'+config['domain']
'streaming_api': f'wss://{config["web_domain"]}'
},
'max_toot_chars': config['vars']['max_chars'],
'stats': {
'user_count': stats['user_count'],
'status_count': stats['status_count'],
'domain_count': stats['domain_count']
},
'max_toot_chars': settings('char_limit'),
'contact_account': {
'id': '',
'username': '',
@ -45,32 +66,19 @@ class stream_cmd:
return 'im gay'
def sanitize(data, endpoint):
def regex_clean(target_string, spaces):
if spaces == True:
return re.sub(r'([^a-zA-Z@_.\-\d\s]+)', '', target_string).strip()
else:
return re.sub(r'([^a-zA-Z@_.\-\d]+)', '', target_string).strip()
target = []
for line in data:
if data[0] == 'name':
target.append(regex_clean(line, True))
else:
target.append(regex_clean(line, False))
return target
async def handle_post(request):
command = request.match_info['name']
data = sanitize(await request.json(), command)
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)):
return_msg = eval('post_cmd.'+command+'(data)')
msg = eval('post_cmd.'+command+'(data)')
else:
json_error(404, 'InvalidCommand')

28
social/api/misc.py Normal file
View file

@ -0,0 +1,28 @@
import re
def sanitize(data, endpoint):
def regex_clean(target_string, spaces):
if spaces == True:
space = '\s'
else:
space = ''
return re.sub(r'([^a-zA-Z@_.,\-\d/\[\]\'\:{}]+)'.format(space), '', target_string).strip()
target = {}
for line in data:
if line == 'name':
target[line] = regex_clean(data[line], True)
if line in ['scope', 'scopes']:
target[line] = []
for scope in data[line].split(' '):
target[line].append(regex_clean(scope, False))
else:
target[line] = regex_clean(data[line], False)
return target

View file

@ -7,9 +7,9 @@ import re
from urllib.parse import urlparse
from .database import cache, newtrans, get, put, update, delete
from functions import json_error
from config import config, logging
from ..database import newtrans, get, put, update, delete
from ..functions import json_error
from ..config import config, logging
class post_cmd:
@ -121,8 +121,6 @@ class post_cmd:
def getuser(data):
user = get_profile(data['user'])
print(json.dumps(user))
if user == False:
return {'err': 'UserNotExist'}
@ -159,12 +157,6 @@ class post_cmd:
else:
return {'msg': 'TablesUpdated'}
def cache(data):
#return {'tokens': str(cache.token.items), 'cookies': str(cache.cookie.items)}
return {'cookies': cache.cookie.items, 'tokens': cache.token.items, 'users': cache.user.items}
#-----------------------
class get_cmd:

88
social/api/oauth.py Normal file
View file

@ -0,0 +1,88 @@
import aiohttp, json, sys, os, asyncio
from urllib.parse import urlparse
from json.decoder import *
from .misc import sanitize
from ..config import config
from ..database import *
from ..functions import json_error, http_error
from .. import oauth
class post_cmd:
def token(data):
if data.get('grant_type') == 'password':
pass
elif data.get('grant_type') == 'auth':
pass
else:
print(data.get('grant_type'))
return json_error(400, f'Invalid grant_type: {data.get("grant_type")}')
class get_cmd:
def authorize(data):
login_token = data['request'].cookies.get('login_token')
client_id = data.get('client_id')
redirect_uri = data.get('redirect_uri')
if None in [client_id, redirect_uri, login_token]:
return http_error(400, data['request'], msg='Missing client_id or redirect_uri')
if data.get('response_type') == 'code':
pass
return 'UwU'
async def handle_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:
json_error(404, 'InvalidCommand')
return aiohttp.web.Response(
status=200,
content_type="application/json",
charset="utf-8",
text=json.dumps(msg)
)
async def handle_get(request):
command = request.match_info['name']
post_data = request.query
data = sanitize(post_data, command)
data['request'] = request
if callable(getattr(get_cmd, command, True)):
msg = eval('get_cmd.'+command+'(data)')
else:
http_error(404, 'InvalidCommand')
if type(msg) == aiohttp.web_response.Response:
return msg
return aiohttp.web.Response(
status=200,
content_type="text/html",
charset="utf-8",
text=msg
)

View file

@ -1,12 +1,13 @@
import os
import sys
import aiohttp
import logging as logger
from envbash import load_envbash
from os import environ as env
from os.path import abspath, dirname
from functions import boolean
from .functions import boolean
if getattr(sys, 'frozen', False):
@ -15,7 +16,7 @@ if getattr(sys, 'frozen', False):
else:
script_path = dirname(abspath(__file__))
stor_path = script_path+'/data'
stor_path = script_path+'/../data'
os.makedirs(stor_path+'/media', exist_ok=True)
pyenv = env.get('PYENV', 'default').lower()
@ -41,7 +42,7 @@ listen = env.get('LISTEN', '127.0.0.1')
port = int(env.get('PORT', 8020))
config = {
'version': '0.1',
'version': '0.1+pre-alpha',
'listen': listen,
'port': port,
'stream_listen': env.get('STREAM_LISTEN', listen),
@ -53,6 +54,7 @@ config = {
'log_errors': boolean(env.get('LOG_ERRORS')),
'log_date': boolean(env.get('LOG_DATE', True)),
'salt': env.get('PASS_SALT'),
'db_type': 'pg' if env.get('DB_TYPE').lower() == 'pg' else 'tiny',
'vars': {
'max_chars': env.get('MAX_CHARS', 69420),
'posts': env.get('PROFILE_POSTS', 20),
@ -63,7 +65,7 @@ config = {
'port': int(env.get('REDIS_PORT', 6379)),
},
'db': {
'pg': {
'host': env.get('DB_HOST', None),
'port': int(env.get('DB_PORT', 5432)),
'user': env.get('DB_USER', env.get('USER', 'social')),
@ -81,31 +83,30 @@ else:
log_date = '[%(asctime)s] '
## 50 Critical
## 40 Error
## 30 Warning
## 20 Info
## 10 Debug
logging = logger.getLogger()
log_format = '{}%(levelname)s: %(message)s'
log_format = '%(levelname)s: %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'
logging.setLevel(logger.DEBUG)
if config['log_errors'] == True:
logfile = logger.FileHandler(stor_path+'/errors.log')
logfile.name = 'Error Log'
logfile.level = logger.WARNING
logfile.formatter = logger.Formatter(log_format.format('[%(asctime)s] '))
logfile.formatter = logger.Formatter('[%(asctime)s] {log_format}', date_format)
logging.addHandler(logfile)
console = logger.StreamHandler()
console.name = 'Console Log'
console.level = eval('logger.'+config['log_level'])
console.formatter = logger.Formatter(log_format.format(log_date))
console.formatter = logger.Formatter(f'{log_date}{log_format}', date_format)
logging.addHandler(console)
header_string = f'aiohttp/{aiohttp.__version__} (Barkshark-Social/{config["version"]}; +https://{config["web_domain"]}/)'
if pyenv in ['prod', 'default']:
if pyenv == 'default':
logging.warning('No environment specified. Assuming development')

12
social/database.py Normal file
View file

@ -0,0 +1,12 @@
from .config import config, logging
if config['db_type'] == 'tiny':
from .db_tiny import get, put, update, delete, newtrans, query
elif config['db_type'] == 'pg':
from .db_pg import get, put, update, delete, newtrans, cache, update_timestamps
else:
logging.error('Invalid database type. Please use "pg" or "tinydb"')
__all__ = ['get', 'put', 'update', 'delete', 'newtrans']

View file

@ -5,17 +5,19 @@ import json
from DBUtils.PooledPg import PooledPg
from config import config, script_path, logging
from simplecache import TTLCache
from ..config import config, script_path, logging
from ..functions import genkey
from simplecache import TTLCache, LRUCache
class cache:
token = TTLCache(maxsize=4096, ttl='1h')
cookie = TTLCache(maxsize=4096, ttl='1h')
user = TTLCache(maxsize=4096, ttl='1h')
token = TTLCache(maxsize=4096, ttl='6h')
cookie = TTLCache(maxsize=4096, ttl='6h')
user = TTLCache(maxsize=4096, ttl='6h')
post = TTLCache(maxsize=4096, ttl='1d')
misc = LRUCache(maxsize=64)
DB_CONFIG = config['db']
DB_CONFIG = config['pg']
def dbconn(database, pooled=True):
@ -45,8 +47,6 @@ def db_check():
db_setup.query(database)
db_setup.close()
logging.info('Done :3')
pre_db.close()
@ -85,32 +85,15 @@ def first_setup():
for key in settings:
db.insert('settings', setting=key, val=settings[key])
logging.info('Database setup')
def settings():
setresults = db.query('SELECT * FROM settings').dictresult()
if setresults == []:
logging.warning('Can\'t find settings in the database')
return
set_dict = {}
for line in setresults:
if line['setting'] not in ['pubkey', 'privkey']:
set_dict.update({line['setting']: line['val']})
return set_dict
logging.info('Database setup finished :3')
def update_timestamps():
for token in cache.token.items:
db.update('auth_tokens', {'id': cache.token.items[token]['id']}, access=cache.token.items[token]['timestamp'])
db.update('auth_tokens', {'id': cache.token.items[token]['data']['id']}, access=cache.token.items[token]['timestamp'])
for token in cache.cookie.items:
db.update('login_cookies', {'id': cache.cookie.items[token]['id']}, access=cache.cookie.items[token]['timestamp'])
db.update('login_cookies', {'id': cache.cookie.items[token]['data']['id']}, access=cache.cookie.items[token]['timestamp'])
newtrans(first_setup())
SETTINGS = settings()

View file

@ -1,6 +1,4 @@
from . import db, cache, get
from config import logging
from . import db, cache, get, logging
def login_cookie(cid, cookie):

View file

@ -1,8 +1,29 @@
import json
from . import db, cache
from . import db, cache, logging, config
from ..functions import timestamp
from config import logging, config
def handle_to_userid(handle):
fetch = cache.user.fetch(handle)
if fetch != None:
logging.debug('Returning cached user id')
return fetch
user = db.query(f'SELECT * FROM users WHERE handle = \'{handle}\'').dictresult()
if user == []:
return None
else:
userid = user[0]['id']
logging.debug('Saving userid to the cache')
cache.user.store(handle, userid)
return userid
def api_token(token):
@ -12,7 +33,7 @@ def api_token(token):
logging.debug('Returning cached token')
return fetch
raw_token = db.query(f'SELECT * FROM tokens WHERE token = \'{token}\'').dictresult()
raw_token = db.query(f'SELECT * FROM auth_tokens WHERE token = \'{token}\'').dictresult()
if raw_token == []:
return
@ -101,51 +122,19 @@ def posts(username, postid, newtrans=False):
return posts
def userid_to_handle(userid):
fetch = cache.user.fetch(userid)
def user(user, filters=None):
if user == None:
return
if fetch != None:
logging.debug('Returning cached user handle')
return fetch
user = db.query(f'SELECT handle FROM users WHERE id = {userid}').dictresult()
if user == []:
return None
if isinstance(user, str):
userid = handle_to_userid(user.lower())
else:
handle = user[0]['handle']
userid = user
logging.debug('Saving user handle to the cache')
cache.user.store(userid, handle)
if userid == None:
return
return handle
def handle_to_userid(handle):
fetch = cache.user.fetch(handle)
if fetch != None:
logging.debug('Returning cached user id')
return fetch
user = db.query(f'SELECT id FROM users WHERE handle = {userid}').dictresult()
if user == []:
return None
else:
userid = user[0]['handle']
logging.debug('Saving userid to the cache')
cache.user.store(userid, handle)
return handle
def user(username, filters=None):
def filter_data(data, fields):
if data == None:
logging.warning('Missing data for filtering.')
@ -163,17 +152,14 @@ def user(username, filters=None):
else:
return data
if isinstance(username, int):
username = userid_to_handle(username)
fetch = cache.user.fetch(username)
fetch = cache.user.fetch(userid)
if fetch != None:
logging.debug('Returning cached user data')
return filter_data(fetch, filters)
raw_user_data = db.query(f'SELECT * FROM users WHERE handle = \'{username}\'').dictresult()
raw_user_data = db.query(f'SELECT * FROM users WHERE id = \'{userid}\'').dictresult()
if raw_user_data == []:
return None
@ -186,9 +172,10 @@ def user(username, filters=None):
user_data['info_table'] = json.loads(table)
logging.debug('Saving user data to cache')
cache.user.store(user_data['handle'], user_data)
cache.user.store(user_data['id'], user_data)
user_data = filter_data(cache.user.fetch(user_data['id']), filters)
return filter_data(cache.user.fetch(user_data['handle']), filters)
return user_data
def profile(handle, postid=None):
@ -206,3 +193,43 @@ def profile(handle, postid=None):
user_data.update(post_count[0])
return user_data
def server_stats():
ts = timestamp()
cached_stats = cache.misc.fetch('stats')
if cached_stats.get('timestamp') == None or cached_stats.get('timestamp') + 3600 < ts:
cache.misc.invalidate('stats')
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
}
cache.misc.store('stats', stats)
return cache.misc.fetch('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

@ -1,11 +1,9 @@
import pg
import time
import json
from . import db
from config import logging, config
from functions import mkhash, genkey, timestamp
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!?
@ -21,7 +19,7 @@ def user(handle, email, password, display, bio, table, sig):
user = db.insert('users',
handle=handle.lower(), name=display, bio=bio,
info_table=json.dumps(table), email=email, password=pass_hash,
permissions=4, timestamp=timestamp, forum_sig=sig,
permissions=4, timestamp=ts, forum_sig=sig,
pubkey=keys['pubkey'], privkey=keys['privkey']
)
@ -32,9 +30,9 @@ def user(handle, email, password, display, bio, table, sig):
if user['id'] == 1:
db.update('users', {'id': 1}, perms=0)
db.insert('tokens', userid=user['id'], appid=0, token=token)
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']}
return {'id': user['id'], 'token': token, 'password': pass_hash, 'username': user['handle'], 'name': user['name'], 'timestamp': ts}
def login_cookie(userid, password, address, agent):
@ -100,3 +98,13 @@ def local_post2(userid, posts):
db.end()
return 'Done'
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
)

View file

@ -1,8 +1,5 @@
from . import db, cache
from . import get
from config import logging, config
from functions import mkhash
from . import db, cache, get, logging, config
from ..functions import mkhash
def profile(handle, data):

View file

@ -0,0 +1,26 @@
from tinydb import TinyDB, Query
from tinydb_smartcache import SmartCacheTable
from ..config import stor_path, config
db_path = f'{stor_path}'
TinyDB.table_class = SmartCacheTable
db = TinyDB(f'{db_path}/db.json', create_dirs=True)
post_db = TinyDB(f'{db_path}/db.json', create_dirs=True)
query = Query()
users = db.table('users')
logins = db.table('logins')
tokens = db.table('tokens')
posts = post_db.table('posts')
settings = db.table('settings')
def newtrans(funct):
# postgresql needs this, but tinydb doesn't, so just pass the function alone
return funct
__all__ = ['db', 'query', 'newtrans', 'users', 'logins', 'tokens', 'posts']

3
social/db_tiny/delete.py Normal file
View file

@ -0,0 +1,3 @@
from tinyrecord import transaction
from . import *

122
social/db_tiny/get.py Normal file
View file

@ -0,0 +1,122 @@
import json
from . import *
from ..config import logging, config
def handle_to_userid(handle):
user = users.get(query.handle == handle)
return user.get('id')
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]
logging.debug('Caching new token')
cache.token.store(token_data['token'], token_data)
return token_data
def login_cookie(cookie):
login = logins.get(query.cookie == cookie)
if login == []:
return None
print(login)
return login
def post(postid):
try:
int(postid)
except ValueError:
return
post = posts.get(id == postid)
if post == []:
return None
return post
def posts(username, postid):
if postid != None:
page = f'and id < {postid}'
else:
page = ''
user_data = user(handle_to_userid(username))
postlimit = config['vars']['posts']
raw_posts = posts.get((query.id >= postid-postlimit) & (query.id < postid))
posts = []
for post in raw_posts:
posts.append(post)
posts[post]['user'] = user(post['userid'])
return posts
def user(userid, filters=None):
if user == None:
return
def filter_data(data, fields):
if data == None:
logging.warning('Missing data for filtering.')
return None
if fields != None:
new_data = {}
for item in data:
if item not in fields.split(','):
new_data[item] = data[item]
return new_data
else:
return data
user = db.get(query.id == userid)
if user == []:
return None
user_data = user.copy()
table = user['info_table']
if table != None:
user_data['info_table'] = json.loads(table)
return filter_data(user_data, filters)
def profile(handle, postid=None):
user = user(handle_to_userid(handle))
if user == None:
return None
user_data = {'profile': user.copy()}
userid = user['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

108
social/db_tiny/put.py Normal file
View file

@ -0,0 +1,108 @@
import time
import json
from tinyrecord import transaction
from . import *
from ..config import logging, config
from ..functions import mkhash, genkey, timestamp
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)
with transation(users) as tr:
user_id = tr.insert({
'handle': handle.lower(),
'name': display,
'bio': bio,
'info_table': json.dumps(table),
'email': email,
'password': pass_hash,
'permissions': 4,
'creation': ts,
'forum_sig': sig,
'pubkey': keys['pubkey'],
'privkey': keys['privkey']
})
print(user)
if user_id == 1:
tr.update({'perms': 0}, id=1)
return {'id': user_id}
def login_cookie(userid, password, address, agent):
ts = timestamp()
cookie = mkhash(password+config['salt']+str(ts))
with transaction(logins) as tr:
login_id = tr.insert({
'userid': userid,
'cookie': cookie,
'timestamp': ts,
'address': address,
'agent': agent
})
return logins.get(doc_id=login_id)
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'

3
social/db_tiny/update.py Normal file
View file

@ -0,0 +1,3 @@
from tinyrecord import transaction
from . import *

View file

@ -5,6 +5,7 @@ import json
import yaml
import os
import re
import sys
from colour import Color
from datetime import datetime
@ -14,6 +15,20 @@ from Crypto.PublicKey import RSA
from hashlib import md5
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'))
cssts = {
'color': css_check('color'),
'layout': css_check('layout')
}
error_codes = {
400: 'BadRequest',
404: 'NotFound',
@ -67,7 +82,7 @@ def http_error(code, request, msg=None):
cont_type = 'text/html'
data = {'login_token': request.cookies.get('login_token')}
if str(code)+'.html' not in os.listdir('frontend/templates/errors'):
if str(code)+'.html' not in os.listdir('social/templates/errors'):
logging.error(f'Hey! You specified a wrong error code: {code} {msg}')
body = aiohttp_jinja2.render_template('errors/500.html', request, {'data': data, 'msg': 'A wrong error template was specified'}, status=500)
@ -78,6 +93,19 @@ def http_error(code, request, msg=None):
return body
def css_ts():
color = css_check('color')
layout = css_check('layout')
if cssts['color'] != color or cssts['layout'] != layout:
cssts.update({
'color': color,
'layout': layout
})
return cssts['color'] + cssts['layout']
def mkhash(string, alg='sha512'):
if alg == 'sha512':
return SHA512.new(string.encode('UTF-8')).hexdigest()
@ -103,10 +131,14 @@ def todate(ts):
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
def ap_date(ts):
return datetime.fromtimestamp(ts).strftime('%Y-%m-%dT%H:%M:%SZ')
def themes():
theme_list = []
for theme in os.listdir('frontend/themes'):
for theme in os.listdir(f'{script_path}/themes'):
theme_list.append(theme.replace('.yml', ''))
theme_list.sort()
@ -123,7 +155,7 @@ def timestamp(integer=True):
# Generate css file for color styling
def color_css(theme):
try:
data = yaml.load(open('frontend/themes/'+theme+'.yml', 'r'), Loader=yaml.FullLoader)
data = yaml.load(open(f'{os.path.dirname(__file__)}/themes/'+theme+'.yml', 'r'), Loader=yaml.FullLoader)
except FileNotFoundError:
data = {}

View file

@ -3,20 +3,16 @@ import aiohttp
import binascii
import base64
import json
import aioredis
#from aiohttp.http_exceptions import *
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA, SHA256, SHA512
from Crypto.Signature import PKCS1_v1_5
from cachetools import LFUCache
from async_lru import alru_cache
from aiohttp_session import session_middleware
from aiohttp_session.redis_storage import RedisStorage
from simplecache import LRUCache
from urllib.parse import urlparse, quote_plus
from config import config, logging
from functions import json_error
from .database import SETTINGS, newtrans, get
from .config import config, logging, pyenv, header_string
from .functions import json_error
from .database import newtrans, get
def fetch_actor(actor):
@ -48,7 +44,7 @@ def build_signing_string(headers, used_headers):
return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers))
SIGSTRING_CACHE = LFUCache(1024)
SIGSTRING_CACHE = LRUCache(maxsize=1024)
def sign_signing_string(sigstring, key):
if sigstring in SIGSTRING_CACHE:
@ -85,9 +81,14 @@ def pass_hash():
return password_hash.hexdigest()
@alru_cache(maxsize=16384)
keys = LRUCache(maxsize=1024)
async def fetch_actor_key(actor):
actor_data = await fetch_actor(actor)
if actor not in keys.items:
pre_actor_data = await fetch_actor(actor)
keys.store(actor, pre_actor_data)
actor_data = keys.fetch(actor)
if not actor_data:
return None
@ -138,7 +139,6 @@ async def http_signatures(app, handler):
request['validated'] = False
if request.method == 'POST' and '/api' not in request.path:
pass
if 'signature' in request.headers:
data = await request.json()
@ -180,14 +180,17 @@ async def http_auth(app, handler):
async def http_auth_handler(request):
api_exclude_paths = [
'/api/native/register',
'/api/native/token'
'/api/native/token',
'/api/v1/apps'
]
cookie_include_paths = [
'/@',
'/:',
'/user',
'/welcome'
'/status',
'/welcome',
'/oauth/authorize'
]
if '/api' in request.path and request.path not in api_exclude_paths and request.method == 'POST':
@ -210,10 +213,14 @@ async def http_auth(app, handler):
if any(map(request.path.startswith, cookie_include_paths)):
if login_token == None:
return aiohttp.web.HTTPFound('/login')
if request.headers.get('Accept') == 'application/activity+json':
return json_error(401, 'NoToken')
else:
return aiohttp.web.HTTPFound(f'/login?redir={quote_plus(request.path)}&query={quote_plus(request.query_string)}')
elif login_token_val == None:
return aiohttp.web.HTTPFound('/login?msg=InvalidToken')
return aiohttp.web.HTTPFound(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']:
@ -243,5 +250,27 @@ async def http_trailing_slash(app, handler):
return (await handler(request))
return http_trailing_slash_handler
async def http_file_cache(request, response):
cache_ext = ['png', 'js', 'svg', 'ogg', 'flac', 'py']
always_cache = ['ico', 'css']
__all__ = ['http_signatures_middleware', 'http_auth_middleware', 'http_error_override', 'http_filter_middleware']
response.headers['server'] = header_string
raw_ext = request.path.split('.')[-1:]
ext = raw_ext[0] if len(raw_ext) > 0 else None
if ext in cache_ext and response.headers.get('Cache-Control') == None:
if pyenv == 'prod':
logging.debug('Returning "cacheable"')
response.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
elif pyenv in ['dev', 'default']:
logging.debug('Returning "non-cacheable"')
response.headers['Cache-Control'] = 'no-store'
elif ext in always_cache:
logging.debug('Returning "cacheable"')
response.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
else:
logging.debug('Returning "non-cacheable"')
response.headers['Cache-Control'] = 'no-store'

72
social/oauth.py Normal file
View file

@ -0,0 +1,72 @@
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

36
social/redirects.py Normal file
View file

@ -0,0 +1,36 @@
import aiohttp, aiohttp_jinja2
from .web_functions import json_check, json_user
# Redirect /user/[user] to /@[user]
async def user_get(request):
user = request.match_info['user']
if json_check(request.headers):
return json_user(request, user)
return aiohttp.web.HTTPFound(f'/@{user}')
# Redirect /status/[status] to /:[status]
async def post_get(request):
status = request.match_info['status']
if json_check(request.headers):
return json_user(request, status)
return aiohttp.web.HTTPFound(f'/:{status}')
# PATPAT
async def headpats(request):
return aiohttp.web.HTTPFound('https://static.barkshark.xyz/mastodon/main/custom_emojis/images/000/000/797/original/blobpatpat.png')
#socks
async def socks(request):
return aiohttp.web.HTTPFound('https://barkshark.xyz/@izalia/103155447990974282')
async def socks_old(request):
return aiohttp.web.HTTPFound('https://glaceon.social/@monorail/102496098954435127')

View file

@ -1,11 +1,16 @@
import aiohttp, aiohttp_jinja2, json
import aiohttp
import aiohttp_jinja2
import json
from config import config, logging
from functions import color_css, http_error
from backend.database import SETTINGS
from os.path import getmtime
from datetime import datetime
from .config import config, logging, script_path
from .functions import color_css, http_error, css_ts
from .database import get
# layout.css
# layout.css (currently unused)
async def layout_get(request):
response = aiohttp_jinja2.render_template('layout.css', request, {})
response.headers['Content-type'] = 'text/css'
@ -18,17 +23,18 @@ async def color_get(request):
theme = request.cookies.get('theme')
if theme == None:
theme = SETTINGS['theme']
theme = get.settings('theme')
response = aiohttp_jinja2.render_template('color.css', request, color_css(theme))
response.headers['Content-type'] = 'text/css'
response.headers['Last-Modified'] = datetime.utcfromtimestamp(css_ts()).strftime('%a, %d %b %Y %H:%M:%S GMT')
return response
async def manifest_get(request):
data = {
'name': SETTINGS['name'],
'name': get.settings('name'),
'short_name': 'BarkShark',
'description': '',
'icons': [
@ -47,26 +53,31 @@ async def manifest_get(request):
"background_color": "#11111",
"display":"standalone",
"start_url":"/",
'scope': 'https://'+config['web_domain'],
"scope": f"https://{config.get('web_domain')}",
}
return web.json_response(data, content_type='application/manifest+json', charset='utf-8')
response = aiohttp.web.json_response(data, content_type='application/manifest+json')
return response
async def favicon_get(request):
return aiohttp.web.Response(
response = aiohttp.web.Response(
status=200,
content_type='image/png',
charset='utf-8',
body=open('frontend/static/icon-64.png', 'rb').read()
body=open(f'{script_path}/static/icon-64.png', 'rb').read()
)
return response
# say 'fuck you' to crawlers that respect robots.txt
async def robots_txt(request):
return aiohttp.web.Response(
response = aiohttp.web.Response(
status=200,
content_type='text/plain',
charset='utf-8',
body='User-agent: *\nDisallow: /'
)
return response

74
social/routes.py Normal file
View file

@ -0,0 +1,74 @@
from webapp import webapp, modules
from . import views, resources, user, redirects, wellknown
from .api import native, mastodon, oauth
from .web_server import web
from .config import script_path, stor_path
# Public pages
web.router.add_get('/', views.home_get)
web.router.add_get('/register', views.register_get)
web.router.add_get('/login', views.login_get)
web.router.add_get('/logout', views.logout_get)
web.router.add_post('/register', views.register_post)
web.router.add_post('/login', views.login_post)
# Semi-public json
web.router.add_get('/user/{user}.json', views.user_json_get)
web.router.add_get('/status/{status}.json', views.post_json_get)
web.router.add_get('/@{user}.json', views.user_json_get)
web.router.add_get('/:{status}.json', views.post_json_get)
# Semi-public pages
web.router.add_get('/@{user}', views.user_get)
web.router.add_get('/:{status}', views.post_get)
web.router.add_get('/user/{user}', redirects.user_get)
web.router.add_get('/status/{status}', redirects.post_get)
web.router.add_post('/:{status}', user.post_delete_post)
# Frontend
web.router.add_get('/welcome', user.welcome_get)
web.router.add_get('/settings', user.settings_get)
web.router.add_get('/admin', user.admin_get)
web.router.add_post('/settings', user.settings_post)
web.router.add_post('/admin', user.admin_post)
web.router.add_post('/poast', user.poast_post)
# CSS, JS, etc
#web.router.add_get('/layout.css', resources.layout_get)
web.router.add_get('/style-{timestamp}.css', resources.color_get)
web.router.add_get('/manifest.json', resources.manifest_get)
web.router.add_get('/favicon.ico', resources.favicon_get)
web.router.add_get('/robots.txt', resources.robots_txt)
# Media
web.router.add_static('/static', path=f'{script_path}/static', name='static')
web.router.add_static('/media', path=f'{stor_path}/media', name='media')
# Native API
web.router.add_get('/api/native/{name}', native.handle_get)
web.router.add_post('/api/native/{name}', native.handle_post)
# Mastodon API
web.router.add_post('/api/v1/{name}', mastodon.handle_post)
web.router.add_get('/api/v1/{name}', mastodon.handle_get)
#web.router.add_get('/api/v1/streaming/{name}', mastodon.streaming_get)
# OAuth
web.router.add_get('/oauth/{name}', oauth.handle_get)
web.router.add_post('/oauth/{name}', oauth.handle_post)
# Various info endpoints
web.router.add_get('/nodeinfo/2.0.json', wellknown.nodeinfo_json)
#web.router.add_get('/.well-known/nodeinfo', wellknown.nodeinfo_get)
#web.router.add_get('/.well-known/host-meta', wellknown.hostmeta_get)
web.router.add_get('/.well-known/webfinger', wellknown.webfinger_get)
# Shitpost
web.router.add_get('/headpats', redirects.headpats)
web.router.add_get('/socks', redirects.socks)
# WebUI
#web.add_subapp('/web', webapp)
#web.router.add_get('/{module}.py', modules)

View file

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

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

@ -2,7 +2,7 @@ from jinja2 import BaseLoader, TemplateNotFound
from os.path import join, exists, getmtime, isfile
from os import environ as env
from config import stor_path, script_path, logging
from .config import stor_path, script_path, logging
from simplecache import LRUCache
@ -22,11 +22,11 @@ class CustomLoader(BaseLoader):
else:
logging.debug('Can\'t find custom template file: '+custom_path+template)
path = join(self.path, template)
path = join(f'{script_path}/{self.path}', template)
if isfile(path) == False:
logging.error('Can\'t find template file: '+path)
path = join(self.path, 'missing.html')
path = join(f'{script_path}/{self.path}', 'missing.html')
mtime = getmtime(path)

View file

@ -1,15 +1,25 @@
{% if cookies.login_token != None %}
{% set cookie = newtrans(get_cookie(cookies.login_token)) %}
{% set user = newtrans(get_user(cookie.userid, filters='pubkey,privkey,password')) %}
{% set cookie = newtrans(get_cookie(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 cookies.get('theme') == None %}
{% set theme = blue %}
{% else %}
{% set user = None %}
{% set theme = cookies.theme %}
{% endif %}
<!DOCTYPE html>
<html>
<head>
<title>{{name}}: {{page}}</title>
<link rel="stylesheet" type="text/css" href="https://{{domain}}/style.css">
<link rel="stylesheet" type="text/css" href="https://{{domain}}/style-{{theme}}-{{css_ts()}}.css">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.json">
<script type="text/javascript">
@ -34,18 +44,18 @@
</head>
<body>
{% include "components/menu.html" %}
<div id="header">
<h1 class="title"><a href="https://{{domain}}">{{name}}</a></h1>
</div>
<div id="content">
<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"></td>
<td class="col1">UvU</td>
<td class="col2">
<a href="https://git.barkshark.tk/izaliamae/social">Barkshark Social</a>
<a href="https://git.barkshark.xyz/izaliamae/social">Barkshark Social</a>
</td>
</tr>
</table>

View file

@ -38,7 +38,6 @@ input, textarea {
background-color:var(--primary-ui-element-background);
color: var(--primary-ui-lighter);
border: 1px solid transparent;
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
}
input:hover, textarea:hover {
@ -69,7 +68,10 @@ input:invalid {
#content {
background: {{background}};
border-color: transparent;
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
}
#header {
border-bottom: 1px solid {{desaturate(primary, 0.06)}};
}
#footer {
@ -80,7 +82,6 @@ input:invalid {
/* Dropdown menus */
#user_panel {
background-color: {{desaturate(darken(primary, 0.90), 0.85)}};
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
}
.submenu details[open] {
@ -101,11 +102,7 @@ input:invalid {
background-color: {{desaturate(darken(primary, 0.85), 0.80)}};
}
.post, #bio, #user_info {
box-shadow: 0 4px 8px 0 var(--shadow-color), 0 6px 20px 0 var(--shadow-color);
}
.post, #bio, #user_info {
.section {
background-color: {{desaturate(darken(primary, 0.90), 0.85)}};
}
@ -128,8 +125,3 @@ input:invalid {
#nav {
background-color: {{desaturate(darken(primary, 0.90), 0.85)}};
}
#settings_page .section {
box-shadow: 0 4px 8px 0 var(--shadow-color), 0 6px 20px 0 var(--shadow-color);
background-color: {{desaturate(darken(primary, 0.90), 0.85)}};
}

View file

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View file

@ -3,8 +3,8 @@
<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}}/welcome">User Panel</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>

View file

@ -71,12 +71,18 @@ 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;
}
@ -94,7 +100,7 @@ tr:last-child .col2 {
}
/* i dont know rn tbh */
/* Main page sections */
#header {
margin: 0 auto;
width: var(--page-width);
@ -106,39 +112,57 @@ tr:last-child .col2 {
}
#content {
padding: 10px 10px 0 10px;
padding: 0 10px;
margin: 0 auto;
margin-bottom: 10px;
width: var(--page-width);
border: 1px solid transparent;
border-radius: 5px;
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
}
#title {
/* Custom page elements */
.section {
margin: 20px 10px;
box-shadow: 0 4px 8px 0 var(--shadow-color), 0 6px 20px 0 var(--shadow-color);
border-radius: 10px;
}
.section:not(#posts), .post {
padding: 10px;
}
.section .title {
font-size: 36pt;
text-align: center;
}
/* Profile pages */
#user_info table {
width: 100%;
text-transform: uppercase;
}
.post {
border-bottom: 1px solid transparent;
border-bottom: 1px solid {{desaturate(darken(primary, 0.8), 0.8)}};
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 hr {
height: 1px;
}
.post .grid-container {
grid-template-columns: auto 50px;;
}
@ -161,57 +185,12 @@ tr:last-child .col2 {
float: right;
}
#user_content, #user_info, #footer {
margin-top: 20px;
}
.post, #bio, #user_info, input, textarea {
border-radius: 5px;
}
.post, #bio, #user_info, #user_info td, #footer {
padding: 5px;
}
#logreg_form .title {
font-size: 28pt;
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;
}
/* Grids */
.grid-container {
display: grid;
grid-template-columns: 50% auto;
grid-gap: 0;
width: 100;
}
.grid-item {
display: inline-grid;
}
/* footer */
/* Footer */
#footer {
clear: both;
}
#footer table {
width: 100%;
padding: 5px;
margin-top: 10px;
}
#footer .col2 {
@ -226,12 +205,10 @@ tr:last-child .col2 {
right: 0;
padding: 5px;
text-align: center;
}
#user_panel {
z-index: 10;
top: 0;
border-radius: 0 0 0 5px;
box-shadow: 0 4px 4px 0 var(--shadow-color), 0 6px 10px 0 var(--shadow-color);
}
.menu #menu_title {
@ -240,10 +217,6 @@ tr:last-child .col2 {
text-transform: uppercase;
}
#menu_title .text {
}
#user_panel .item {
padding: 5px 0;
text-transform: uppercase;
@ -284,17 +257,28 @@ summary:hover {
}
/* 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 {
display: block;
}
#settings_page h2 {
text-align: center;
}
#settings_page textarea {
/* width: 80%; */
width: 90%;
height: 8em;
}
@ -307,17 +291,9 @@ summary:hover {
width: 44%
}
#settings_page .section {
text-transform: uppercase;
border: 1px solid transparent;
border-radius: 5px;
margin-bottom: 25px;
padding-bottom: 15px;
}
/* General */
@media (max-width : 1014px) {
/* responsive design */
@media (max-width : 1000px) {
#content, #header {
margin: inherit 0;
width: auto;

View file

@ -1,11 +1,10 @@
{% 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 id="title">{{page}}</div>
<div class="title">{{page}}</div>
{% if msg != None %}
<div class="error message">{{msg}}</div>
{% else %}
@ -13,6 +12,8 @@
{% 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

@ -3,5 +3,7 @@
{% block content %}
{% include "components/post.html" %}
<div class="section single-post" id="posts">
{% include "components/post.html" %}
</div>
{% endblock %}

View file

@ -0,0 +1,38 @@
{% 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>
{{profile.count}} toots<br>
<br>
{% if profile.bio != None %}
{% for line in profile.bio %}
{{line}}<br>
{% endfor %}
{% else %}
This bitch empty YEET
{% 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>{{profile.last_id}}<a href="https://{{domain}}/@{{profile.handle}}?id={{last_id}}">[More Posts]</a></center></div>
{% endif %}
{% endblock %}

View file

@ -5,7 +5,7 @@
{% block content %}
<br>
<center><form action="/register" method="post" id="logreg_form" autocomplete="new-password">
<div id="title">{{page}}</div>
<div class="title">{{page}}</div>
{% if msg != None %}
<div class="error message">{{msg}}</div>
{% else %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% set page = 'Weclome' %}
{% set page = 'Admin' %}
{% block content %}
<br><br><br>

View file

@ -1,11 +1,11 @@
{% extends "base.html" %}
{% set page = 'Weclome' %}
{% set page = 'Settings' %}
{% block content %}
<div id="settings_page">
<div id="profile" class="section">
<h2>Profile</h2>
<div class="title">Profile</div>
<form action="/settings" method="post">
<div class="grid-container">
<div class="grid-item"><center>
@ -24,7 +24,7 @@
</div>
<div id="security" class="section">
<h2>Account</h2>
<div class="title">Account</div>
<div class="grid-container">
<div class="grid-item"><center>
<form action="/settings" method="post">
@ -58,7 +58,7 @@
</div>
<div id="options" class="section">
<h2>Options</h2>
<div class="title">Options</div>
<form action="/settings" method="post">
<div class="grid-container">
<div class="grid-item">

View file

@ -1,8 +1,8 @@
import aiohttp, aiohttp_jinja2, json
from config import config, logging
from functions import http_error, json_error
from backend.database import SETTINGS, newtrans, get, update, put, delete
from .config import config, logging
from .functions import http_error, json_error
from .database import newtrans, get, update, put, delete
# user home page (/welcome)
async def welcome_get(request):
@ -11,14 +11,15 @@ async def welcome_get(request):
async def settings_get(request):
login_token = request.cookies.get('login_token')
settings = get.settings('all')
token = newtrans(get.login_cookie(login_token))
handle = newtrans(get.userid_to_handle(token['userid']))
handle = newtrans(get.user(token['userid']))['id']
if token == None:
aiohttp.web.HTTPFound('/login?msg=InvalidToken')
return aiohttp_jinja2.render_template('pages/user/settings.html', request, {'settings': SETTINGS})
return aiohttp_jinja2.render_template('pages/user/settings.html', request, {'settings': settings})
async def settings_post(request):

View file

@ -1,8 +1,13 @@
import aiohttp, aiohttp_jinja2, json
import aiohttp
import aiohttp_jinja2
import json
from config import config, logging
from functions import color_css, http_error, json_error, mkhash, timestamp
from backend.database import SETTINGS, newtrans, get, put, delete
from urllib.parse import unquote_plus
from .config import config, logging
from .functions import color_css, http_error, json_error, mkhash, timestamp
from .web_functions import json_check, json_user, json_status
from .database import newtrans, get, put, delete
# home page (/)
@ -35,9 +40,12 @@ async def register_get(request):
async def register_post(request):
headers = request.headers
data = await request.post()
pass1 = data.get('newpassword1')
pass2 = data.get('newpassword2')
username = data.get('username')
password = data.get('password')
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')
@ -49,14 +57,14 @@ async def register_post(request):
if None in [username, password]:
aiohttp.web.HTTPFound('/register?msg=MissingData')
user_check = newtrans(get.user(username))
user_check = get.user(get.handle_to_userid(username))
if user_check != None:
aiohttp.web.HTTPFound('/register?msg=UserExists')
user = newtrans(put.user(username, email, password, name, bio, info_table, sig))
user = put.user(username, email, password, name, bio, info_table, sig)
token = newtrans(put.login_cookie(user['id'], user['password'], address, agent))
token = put.login_cookie(user['id'], user['password'], address, agent)
response = aiohttp.web.HTTPFound('/login')
response.set_cookie('login_token', token, max_age=60*60*24*14)
@ -83,10 +91,11 @@ async def login_get(request):
else:
msg = None
return aiohttp_jinja2.render_template('pages/login.html', request, {'msg': msg})
return aiohttp_jinja2.render_template('pages/login.html', request, {'msg': msg, 'redir': {'path': query.get('redir'), 'data': query.get('query')}})
async def login_post(request):
query = request.query
headers = request.headers
data = await request.post()
@ -94,24 +103,32 @@ async def login_post(request):
password = data.get('password')
address = headers.get('X-Real-Ip')
agent = headers.get('User-Agent')
redir = data.get('redir')
redir_data = data.get('redir_data')
if '' in [username, password, address] or address == None:
return http_error(400, request)
userid = newtrans(get.user(username.lower()))
user = newtrans(get.user(username.lower()))
pass_hash = mkhash(password+config['salt'])
if userid != None:
if userid['password'] == pass_hash:
login_token = newtrans(put.login_cookie(userid['id'], password, address, agent))
response = aiohttp.web.HTTPFound('/welcome')
if user != None:
if user['password'] == pass_hash:
login_token = newtrans(put.login_cookie(user['id'], password, address, agent))
if redir != 'None':
response = aiohttp.web.HTTPFound(f'{redir}?{unquote_plus(redir_data)}')
else:
response = aiohttp.web.HTTPFound('/welcome')
# Send login token. Lasts for 2 weeks (60 sec * 60 min * 24 hour * 14 day)
response.set_cookie('login_token', login_token, max_age=60*60*24*14)
return response
return aiohttp_jinja2.render_template('pages/login.html', request, {'msg': 'Wrong username or password'})
return aiohttp_jinja2.render_template('pages/login.html', request, {'msg': 'Wrong username or password', 'redir': redir})
async def logout_get(request):
@ -121,24 +138,30 @@ async def logout_get(request):
response = aiohttp.web.HTTPFound('/login?msg=LoggedOut')
if token != None:
newtrans(delete.login_cookie(get.login_cookie(token)['id'], token))
cookie = get.login_cookie(token)
response.set_cookie('login_token', token, max_age=0)
if cookie != None:
newtrans(delete.login_cookie(cookie['id'], token))
response.set_cookie('login_token', token, max_age=0)
return response
# user profiles
async def user_get(request):
user = request.match_info['user']
req_user = request.match_info['user']
postid = request.rel_url.query.get('id')
user_data = newtrans(get.profile(user, postid=postid))
user = newtrans(get.profile(req_user, postid=postid))
if user_data == None:
if user == None:
return http_error(404, request, msg='That user doesn\'t exist.')
if json_check(request.headers):
return json_user(request, req_user)
try:
posts = user_data['post']
posts = user['post']
if len(posts) < config['vars']['posts']:
last_id = None
@ -148,15 +171,15 @@ async def user_get(request):
except IndexError:
last_id = None
user_data['last_id'] = last_id
user['last_id'] = last_id
bio = []
for line in user_data['profile']['bio'].split('\n'):
for line in user['profile']['bio'].split('\n'):
bio.append(line)
user_data['profile']['bio'] = bio
user['profile']['bio'] = bio
return aiohttp_jinja2.render_template('pages/public/profile.html', request, user_data)
return aiohttp_jinja2.render_template('pages/public/profile.html', request, user)
# single post
@ -173,3 +196,15 @@ async def post_get(request):
post_data['post']['user'] = get.user(post_data['post']['userid'])
return aiohttp_jinja2.render_template('pages/public/post.html', request, post_data)
async def user_json_get(request):
req_user = request.match_info['user']
return json_user(request, req_user)
async def post_json_get(request):
postid = request.match_info['status']
return json_status(request, postid)

152
social/web_functions.py Normal file
View file

@ -0,0 +1,152 @@
import aiohttp
import json
from .config import config
from .database import get
from .functions import http_error, ap_date
def json_check(headers):
accept = headers.get('Accept')
if accept == 'application/activity+json':
return True
return False
def json_user(request, req_user):
settings = get.settings('all')
user = get.user(req_user)
if user == None:
return http_error(404, request, msg='That user doesn\'t exist.')
weburl = config['web_domain']
domain = settings['domain']
handle = user['handle']
display = user['name']
bio = user['bio']
pubkey = user['pubkey']
data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"Emoji": "toot:Emoji",
}
],
"id": f"https://{weburl}/users/{handle}",
"type": f"Person",
"following": f"https://{weburl}/users/{handle}/following",
"followers": f"https://{weburl}/users/{handle}/followers",
"inbox": f"https://{weburl}/users/{handle}/inbox",
"outbox": f"https://{weburl}/users/{handle}/outbox",
"featured": f"https://{weburl}/users/{handle}/collections/featured",
"preferredUsername": f"{handle}",
"name": f"{display}",
"summary": f"{bio}",
"url": f"https://{weburl}/@{handle}",
"manuallyApprovesFollowers": False,
"publicKey": {
"id": f"https://{weburl}/users/{handle}#main-key",
"owner": f"https://{weburl}/users/{handle}",
"publicKeyPem": f"{pubkey}"
},
"attachment": [
{
"type": "PropertyValue",
"name": "Pronouns",
"value": "She/Her, They/Them"
}
],
"endpoints": {
"sharedInbox": f"https://{weburl}/inbox"
},
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "" #avatar
},
"image": {
"type": "Image",
"mediaType": "image/png",
"url": "" #header
}
}
return aiohttp.web.Response(body=json.dumps(data), content_type='application/activity+json')
def json_status(request, status):
post = get.post(status)
if post == None:
return http_error(404, request, msg='That post doesn\'t exist.')
user = get.user(post['userid'])
if user == None:
return http_error(404, request, msg='Some how you found a post without a valid user')
settings = get.settings('all')
weburl = config['web_domain']
domain = settings['domain']
warning = post['warning']
content = post['content']
handle = user['handle']
date = ap_date(post['timestamp'])
def visibility():
vis = {
'public': f'https://{weburl}/users/{handle}/followers'
}
data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
"id": f"https://{weburl}/statuses/{status}",
"type": "Note",
"summary": warning if warning else None,
"inReplyTo": None,
"published": date,
"url": f"https://{weburl}/status/{status}",
"attributedTo": f"https://{weburl}/users/{handle}",
"to": [
f"https://{weburl}/users/{handle}/followers"
],
"cc": [
"https://www.w3.org/ns/activitystreams#Public"
],
"sensitive": True if warning else False,
"atomUri": f"https://{weburl}/users/{handle}/statuses/{status}",
"content": content,
"contentMap": {
"en": content
},
"attachment": [],
"tag": [],
}
return aiohttp.web.Response(body=json.dumps(data), content_type='application/activity+json')

104
social/web_server.py Normal file
View file

@ -0,0 +1,104 @@
import asyncio
import aiohttp
import jinja2
import aiohttp_jinja2
import json
import signal
import sys
from jinja2 import FileSystemLoader, select_autoescape
from .config import config, logging, stor_path, script_path
from . import middleware, wellknown
from .database import newtrans, get
from .functions import color, todate, themes, css_ts
from .template_loader import CustomLoader
async def glob_vars(request):
return {
'name': get.settings('name'),
'domain': config['web_domain'],
'settings': get.settings('all'),
'get_cookie': get.login_cookie,
'get_user': get.user,
'newtrans': newtrans,
'css_ts': css_ts,
'lighten': color().lighten,
'darken': color().darken,
'saturate': color().saturate,
'desaturate': color().desaturate,
'rgba': color().rgba,
'todate': todate,
'themes': themes,
'json': json
}
web = aiohttp.web.Application(middlewares=[
#middleware.http_signatures,
middleware.http_auth,
middleware.http_filter,
middleware.http_trailing_slash
])
aiohttp_jinja2.setup(web,
loader=CustomLoader('templates'),
autoescape=select_autoescape(['html', 'xml', 'css']),
context_processors=[glob_vars],
lstrip_blocks=True,
trim_blocks=True
)
web.on_response_prepare.append(middleware.http_file_cache)
async def start_web():
runner = aiohttp.web.AppRunner(web, access_log_format='%{X-Real-Ip}i %s %b "%r" "%{User-Agent}i"')
await runner.setup()
listen = config['listen']
port = config['port']
logging.info('Starting web server at {listen}:{port}'.format(listen=listen,port=port))
site = aiohttp.web.TCPSite(runner, listen, port)
await site.start()
async def save_tokens():
while True:
# Wait every 30 mins
await asyncio.sleep(60 * 30)
logging.info('Saving updated timestamps for login and auth tokens to the database')
#update_timestamps()
def safe_stop(*args):
logging.debug('Saving updated timestamps')
#update_timestamps()
logging.info('Bye')
sys.exit()
def main():
from . import routes
signal.signal(signal.SIGHUP, safe_stop)
signal.signal(signal.SIGINT, safe_stop)
signal.signal(signal.SIGQUIT, safe_stop)
signal.signal(signal.SIGTERM, safe_stop)
try:
loop = asyncio.get_event_loop()
asyncio.ensure_future(start_web())
asyncio.ensure_future(save_tokens())
loop.run_forever()
except KeyboardInterrupt:
pass
safe_stop()

View file

@ -1,14 +1,17 @@
import aiohttp
from config import config
from .database import SETTINGS, newtrans, get
from .config import config
from .database import newtrans, get
async def nodeinfo_json(request):
settings = get.settings
stats = get.server_stats()
data = {
'version': '2.0',
'usage': {
'users': {'total': 69},
'localPosts': 420
'users': {'total': stats['user_count']},
'localPosts': stats['status_count']
},
'software': {
'name': 'bsocial',
@ -19,8 +22,8 @@ async def nodeinfo_json(request):
'metadata': {
'staffAccounts': [],
'postFormats': ['text/plain', 'text/html', 'text/markdown'],
'nodeName': SETTINGS['name'],
'nodeDescription': SETTINGS['description']
'nodeName': settings('name'),
'nodeDescription': settings('description')
}
}
@ -55,32 +58,35 @@ async def hostmeta_get(request):
async def webfinger_get(request):
query = request.rel_url.query
settings = get.settings
if query.get('resource') != None:
resource = query['resource'].split('@')
user = resource[0]
web_domain = config['web_domain']
domain = get.settings('domain')
if resource[1] == SETTINGS['domain'] and newtrans(get.user(user)) != None:
if resource[1] == settings('domain') and newtrans(get.user(user)) != None:
data = {
'subject': 'acct:izaliamae@barkshark.tk',
'subject': f'acct:{user}@{domain}',
'aliases': [
'https://{domain}/@{user}'.format(domain=config['web_domain'], user=user),
'https://{domain}/user/{user}'.format(domain=config['web_domain'], user=user)
f'https://{web_domain}/@{user}',
f'https://{web_domain}/user/{user}'
],
'links': [
{
'rel': 'http://webfinger.net/rel/profile-page',
'type': 'text/html',
'href': 'https://{domain}/@{user}'.format(domain=config['web_domain'], user=user)
'href': f'https://{web_domain}/@{user}'
},
{
'rel': 'self',
'type': 'application/activity+json',
'href': 'https://{domain}/user/{user}'.format(domain=config['web_domain'], user=user)
'href': f'https://{web_domain}/user/{user}'
},
{
'rel': 'http://ostatus.org/schema/1.0/subscribe',
'template': 'https://{domain}/interact?url={{url}}'.format(domain=config['web_domain'])
'template': f'https://{web_domain}/interact?url={{url}}'
}
]
}

53
tests/oauth-apps.py Normal file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
import os, sys, json, urllib.request, getpass
import mastodon
from configobj import ConfigObj as config
stor_path = '/tmp'
confini = f'{stor_path}/config.ini'
if not os.path.exists(confini):
cfg = config()
cfg.filename = confini
cfg.update({
'app_tokens': {'key': None, 'secret': None},
'login': {'domain': None, 'token': None}
})
else:
cfg = config(confini)
print(cfg)
try:
domain = os.environ['domain']
username = os.environ['username']
password = os.environ['password']
except KeyError:
domain = input('Domain: ')
username = input('E-Mail: ')
password = getpass.getpass('Pass: ')
cfg['login']['domain'] = domain
scopes = ['read:notifications', 'write:notifications']
if None in [cfg['app_tokens']['key'], cfg['app_tokens']['secret']]:
client = mastodon.Mastodon.create_app('App Test', website = 'https://git.barkshark.tk/izaliamae', scopes = scopes, api_base_url = domain)
cfg['app_tokens'].update({'key': client[0], 'secret': client[1]})
print(cfg)
cfg.write()
else:
client = (cfg['app_tokens']['key'], cfg['app_tokens']['secret'])
login = mastodon.Mastodon(client_id = client[0], client_secret = client[1], api_base_url = cfg['login']['domain'])
print(login.auth_request_url(scopes=scopes))
user_token = login.log_in(code=input('Secret:'), scopes = scopes)
#user_token = userLogin.log_in(username, password, scopes = scopes, to_file=None)
print(user_token)
sys.exit()
cfg['login']['domain'] = domain
cfg['login']['token'] = user_token
#cfg.write(open(scriptPath + '/' + confINI, 'w'))

View file

@ -1,6 +1,64 @@
import aiohttp
import aiohttp_jinja2
import os
import subprocess
import json
import sys
from os.path import dirname, abspath
from jinja2 import select_autoescape
from social.config import logging, config
from social.functions import color, todate, themes, css_ts
from social import middleware
from social.database import newtrans, get
from social.template_loader import CustomLoader
from .views import main, modules
if getattr(sys, 'frozen', False):
script_path = dirname(abspath(sys.executable))
else:
script_path = dirname(abspath(__file__))
async def glob_vars(request):
return {
'name': settings['name'],
'domain': config['web_domain'],
'settings': get.settings(),
'get_cookie': get.login_cookie,
'get_user': get.user,
'newtrans': newtrans,
'css_ts': css_ts,
'lighten': color().lighten,
'darken': color().darken,
'saturate': color().saturate,
'desaturate': color().desaturate,
'rgba': color().rgba,
'todate': todate,
'themes': themes,
'json': json
}
webapp = aiohttp.web.Application(middlewares=[])
webapp.on_response_prepare.append(middleware.http_file_cache)
aiohttp_jinja2.setup(webapp,
loader=CustomLoader(f'{script_path}/templates'),
autoescape=select_autoescape(['html', 'xml', 'css']),
context_processors=[glob_vars],
lstrip_blocks=True,
trim_blocks=True
)
webapp.router.add_get('', main)
webapp.router.add_static('/js', path=f'{script_path}/js')
from config import script_path
def precompile():
os.system(f'transcrypt --map {script_path}/webapp/js/__init__.py')
logging.info('Compiling JS')
subprocess.Popen(['transcrypt', '--map', '--build', '--nomin', '--parent', 'window', f'{script_path}/webapp/js/__init__.py'], preexec_fn=os.setpgrp)

View file

@ -1 +0,0 @@
print('OwO')

13100
webapp/js/brython.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
webapp/js/main.py Normal file
View file

@ -0,0 +1,20 @@
import owo
from browser import document, aio
text = ''
async def read():
global text
req = await aio.ajax("GET", "/static/test.txt")
text = req.data
aio.run(read())
def write(*args):
div = document['text']
div.textContent = text
document['text'].bind('click', write)
owo.owo()

5
webapp/js/owo.py Normal file
View file

@ -0,0 +1,5 @@
from browser import document
def owo(*args):
div = document['text']
div.textContent = 'Click Me'

View file

@ -0,0 +1,32 @@
<html>
<head>
<title>UwU</title>
<style>
body {
background-color: #050505;
color: #DDD;
}
#content {
width: 100%;
height: 100%;
}
#text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48pt;
width: 400px;
text-align: center;
}
</style>
</head>
<body onload="brython({debug: 1, indexedDB: false})">
<div id="content">
<div id="text"></div>
</div>
<script src="/web/js/brython.js"></script>
<script src="/web/js/brython_modules.js"></script>
<script src="/web/js/main.py" type="text/python"></script>
</body>
</html>

11
webapp/views.py Normal file
View file

@ -0,0 +1,11 @@
import aiohttp
import aiohttp_jinja2
from social.config import script_path
async def main(request):
return aiohttp_jinja2.render_template('pages/main.html', request, None)
async def modules(request):
module = request.match_info['module']
return aiohttp.web.FileResponse(f'{script_path}/webapp/js/{module}.py')