This commit is contained in:
gabixdev 2018-12-04 08:22:32 +01:00
commit c597e08f92
11 changed files with 287 additions and 47 deletions

2
.gitignore vendored
View File

@ -105,3 +105,5 @@ venv.bak/
config.py
images/*
.DS_Store

109
README.md
View File

@ -1,72 +1,107 @@
# litecord
![Litecord logo](static/logo/logo.png)
Litecord is an open source implementation of Discord's backend and API in
Python.
Litecord is an open source, [clean-room design][clean-room] reimplementation of
Discord's HTTP API and Gateway in Python 3.
This project is a rewrite of [litecord-reference].
[clean-room]: https://en.wikipedia.org/wiki/Clean_room_design
[litecord-reference]: https://gitlab.com/luna/litecord-reference
## Notes
## Project Goals
- Unit testing isn't completly perfect.
- No voice is planned to be developed, for now.
- You must figure out connecting to the server yourself. Litecord will not distribute
Discord's official client code nor provide ways to modify the client.
- Being able to unit test bots in an autonomous fashion.
- Doing research and exploration on the Discord API.
## Install
### Non-goals
- Being used as a "self-hostable Discord alternative".
## Caveats
- Unit testing is incomplete.
- Currently, there are no plans to support voice chat.
- You must figure out how to connect to a Litecord instance. Litecord will not
distribute official client code from Discord nor provide ways to modify the
official client.
## Installation
Requirements:
- Python 3.7 or higher
- PostgreSQL
- gifsicle for GIF emoji and avatar handling.
- [Pipenv]
- **Python 3.7+**
- PostgreSQL (tested using 9.6+)
- gifsicle for GIF emoji and avatar handling
- [pipenv]
[pipenv]: https://github.com/pypa/pipenv
### Download the code
```sh
$ git clone https://gitlab.com/luna/litecord.git && cd litecord
$ git clone https://gitlab.com/litecord/litecord.git && cd litecord
```
# Setup the database:
# don't forget that you can create a specific
# postgres user just for the litecord database
### Setting up the database
It's recommended to create a separate user for the `litecord` database.
```sh
# Create the PostgreSQL database.
$ createdb litecord
# Apply the base schema to the database.
$ psql -f schema.sql litecord
```
# Configure litecord:
# edit config.py as you wish
$ cp config.example.py config.py
Then, you should run database migrations:
# run database migrations (this is a
# required step in setup)
```sh
$ pipenv run ./manage.py migrate
```
# Install all packages:
### Configuring
Copy the `config.example.py` file and edit it to configure your instance:
```sh
$ cp config.example.py config.py
$ $EDITOR config.py
```
### Install packages
```sh
$ pipenv install --dev
```
## Running
Hypercorn is used to run litecord. By default, it will bind to `0.0.0.0:5000`.
You can use the `-b` option to change it (e.g. `-b 0.0.0.0:45000`).
Use `--access-log -` to output access logs to stdout.
Hypercorn is used to run Litecord. By default, it will bind to `0.0.0.0:5000`.
This will expose your Litecord instance to the world. You can use the `-b`
option to change it (e.g. `-b 0.0.0.0:45000`).
```sh
$ pipenv run hypercorn run:app
```
*It is recommended to run litecord behind NGINX.* Because of that,
there is a basic `nginx.conf` file at the root.
You can use `--access-log -` to output access logs to stdout.
### Checking if it is working
**It is recommended to run litecord behind [NGINX].** You can use the
`nginx.conf` file at the root of the repository as a template.
You can check if your instance is running by checking the `/api/v6/gateway`
path. For basic websocket testing a tool such as
[nginx]: https://www.nginx.com
### Does it work?
You can check if your instance is running by performing an HTTP `GET` request on
the `/api/v6/gateway` endpoint. For basic websocket testing, a tool such as
[ws](https://github.com/hashrocket/ws) can be used.
## Updating
Update the code and run any new database migrations:
```sh
$ git pull
$ pipenv run ./manage.py migrate
@ -74,16 +109,16 @@ $ pipenv run ./manage.py migrate
## Running tests
To run tests we must create users that we know the passwords of.
Because of that, **never setup a testing environment in production.**
Running tests involves creating dummy users with known passwords. Because of
this, you should never setup a testing environment in production.
```sh
# setup the testing users
# Setup any testing users:
$ pipenv run ./manage.py setup_tests
# make sure you have tox installed
# Install tox:
$ pip install tox
# run basic linter and tests
# Run lints and tests:
$ tox
```

View File

@ -4,7 +4,7 @@ from random import randint
import bcrypt
from asyncpg import UniqueViolationError
from itsdangerous import Signer, BadSignature
from itsdangerous import TimestampSigner, BadSignature
from logbook import Logger
from quart import request, current_app as app
@ -38,7 +38,7 @@ async def raw_token_check(token, db=None):
if not pwd_hash:
raise Unauthorized('User ID not found')
signer = Signer(pwd_hash)
signer = TimestampSigner(pwd_hash)
try:
signer.unsign(token)

View File

@ -25,7 +25,7 @@ async def check_password(pwd_hash: str, given_password: str) -> bool:
def make_token(user_id, user_pwd_hash) -> str:
"""Generate a single token for a user."""
signer = itsdangerous.Signer(user_pwd_hash)
signer = itsdangerous.TimestampSigner(user_pwd_hash)
user_id = base64.b64encode(str(user_id).encode())
return signer.sign(user_id).decode()

View File

@ -1,22 +1,25 @@
import random
from os import urandom
from asyncpg import UniqueViolationError
from quart import Blueprint, jsonify, request, current_app as app
from logbook import Logger
from ..auth import token_check
from ..errors import Forbidden, BadRequest
from ..errors import Forbidden, BadRequest, Unauthorized
from ..schemas import validate, USER_UPDATE, GET_MENTIONS
from .guilds import guild_check
from .auth import check_password
from litecord.auth import hash_data, check_username_usage
from litecord.auth import token_check, hash_data, check_username_usage
from litecord.blueprints.guild.mod import remove_member
from litecord.enums import PremiumType
from litecord.images import parse_data_uri
from litecord.permissions import base_permissions
from litecord.blueprints.auth import check_password
bp = Blueprint('user', __name__)
log = Logger(__name__)
async def mass_user_update(user_id, app_=None):
@ -450,3 +453,102 @@ async def _get_mentions():
)
return jsonify(res)
def rand_hex(length: int = 8) -> str:
"""Generate random hex characters."""
return urandom(length).hex()[:length]
async def _del_from_table(table: str, user_id: int):
column = {
'channel_overwrites': 'target_user',
'user_settings': 'id'
}.get(table, 'user_id')
res = await app.db.execute(f"""
DELETE FROM {table}
WHERE {column} = $1
""", user_id)
log.info('Deleting uid {} from {}, res: {!r}',
user_id, table, res)
@bp.route('/users/@me/delete')
async def delete_account():
"""Delete own account.
There isn't any inherent need to dispatch
events to connected users, so this is mostly
DB operations.
"""
user_id = await token_check()
j = await request.get_json()
try:
password = j['password']
except KeyError:
raise BadRequest('password required')
owned_guilds = await app.db.fetchval("""
SELECT COUNT(*)
FROM guilds
WHERE owner_id = $1
""", user_id)
if owned_guilds > 0:
raise BadRequest('You still own guilds.')
pwd_hash = await app.db.fetchval("""
SELECT password_hash
FROM users
WHERE id = $1
""", user_id)
if not await check_password(pwd_hash, password):
raise Unauthorized('password does not match')
new_username = f'Deleted User {rand_hex()}'
await app.db.execute("""
UPDATE users
SET
username = $1,
email = NULL,
mfa_enabled = false,
verified = false
avatar = NULL,
flags = 0,
premium_since = NULL,
phone = '',
password_hash = '123'
WHERE
id = $2
""", new_username, user_id)
# remove the user from various tables
await _del_from_table('user_settings', user_id)
await _del_from_table('user_payment_sources', user_id)
await _del_from_table('user_subscriptions', user_id)
await _del_from_table('user_payments', user_id)
await _del_from_table('user_read_state', user_id)
await _del_from_table('guild_settings', user_id)
await _del_from_table('guild_settings_channel_overrides', user_id)
await app.db.execute("""
DELETE FROM relationships
WHERE user_id = $1 OR peer_id = $1
""", user_id)
# DMs are still maintained, but not the state.
await _del_from_table('dm_channel_state', user_id)
# TODO: group DMs
await _del_from_table('members', user_id)
await _del_from_table('member_roles', user_id)
await _del_from_table('channel_overwrites', user_id)
return '', 204

View File

@ -26,8 +26,6 @@ class GuildDispatcher(DispatcherWithState):
user_id, chan_id,
storage=self.main_dispatcher.app.storage)
print(user_id, chan_id, chan_perms.bits.read_messages)
if not chan_perms.bits.read_messages:
log.debug('skipping cid={}, no read messages',
chan_id)

View File

@ -13,6 +13,7 @@ from litecord.permissions import (
Permissions, overwrite_find_mix, get_permissions, role_permissions
)
from litecord.utils import index_by_func
from litecord.utils import mmh3
log = Logger(__name__)
@ -267,7 +268,31 @@ class GuildMemberList:
"""get the id of the member list."""
return ('everyone'
if self.channel_id == self.guild_id
else str(self.channel_id))
else self._calculated_id)
@property
def _calculated_id(self):
"""Calculate an id used by the client."""
if not self.list:
return str(self.channel_id)
# list of strings holding the hash input
ovs_i = []
for actor_id, overwrite in self.list.overwrites.items():
allow, deny = (
Permissions(overwrite['allow']),
Permissions(overwrite['deny'])
)
if allow.bits.read_messages:
ovs_i.append(f'allow:{actor_id}')
elif deny.bits.read_messages:
ovs_i.append(f'deny:{actor_id}')
hash_in = ','.join(ovs_i)
return str(mmh3(hash_in))
def _set_empty_list(self):
"""Set the member list as being empty."""

View File

@ -37,3 +37,76 @@ def index_by_func(function, indexable: iter) -> int:
return index
return None
def _u(val):
"""convert to unsigned."""
return val % 0x100000000
def mmh3(key: str, seed: int = 0):
"""MurMurHash3 implementation.
This seems to match Discord's JavaScript implementaiton.
Based off
https://github.com/garycourt/murmurhash-js/blob/master/murmurhash3_gc.js
"""
key = [ord(c) for c in key]
remainder = len(key) & 3
bytecount = len(key) - remainder
h1 = seed
# mm3 constants
c1 = 0xcc9e2d51
c2 = 0x1b873593
i = 0
while i < bytecount:
k1 = (
(key[i] & 0xff) |
((key[i + 1] & 0xff) << 8) |
((key[i + 2] & 0xff) << 16) |
((key[i + 3] & 0xff) << 24)
)
i += 4
k1 = ((((k1 & 0xffff) * c1) + ((((_u(k1) >> 16) * c1) & 0xffff) << 16))) & 0xffffffff
k1 = (k1 << 15) | (_u(k1) >> 17)
k1 = ((((k1 & 0xffff) * c2) + ((((_u(k1) >> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
h1 ^= k1
h1 = (h1 << 13) | (_u(h1) >> 19);
h1b = ((((h1 & 0xffff) * 5) + ((((_u(h1) >> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
h1 = (((h1b & 0xffff) + 0x6b64) + ((((_u(h1b) >> 16) + 0xe654) & 0xffff) << 16))
k1 = 0
v = None
if remainder == 3:
v = (key[i + 2] & 0xff) << 16
elif remainder == 2:
v = (key[i + 1] & 0xff) << 8
elif remainder == 1:
v = (key[i] & 0xff)
if v is not None:
k1 ^= v
k1 = (((k1 & 0xffff) * c1) + ((((_u(k1) >> 16) * c1) & 0xffff) << 16)) & 0xffffffff
k1 = (k1 << 15) | (_u(k1) >> 17)
k1 = (((k1 & 0xffff) * c2) + ((((_u(k1) >> 16) * c2) & 0xffff) << 16)) & 0xffffffff
h1 ^= k1
h1 ^= len(key)
h1 ^= _u(h1) >> 16
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((_u(h1) >> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff
h1 ^= _u(h1) >> 13
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((_u(h1) >> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff
h1 ^= _u(h1) >> 16
return _u(h1) >> 0

BIN
static/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

5
static/logo/logo.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="372px" height="71px" viewBox="0 0 372 71" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<!-- Generated by Pixelmator Pro 1.2 -->
<path id="path" d="M183.28 70.96 C168.112 70.96 159.087 61.839 159.087 49.071 159.087 33.616 168.976 21.04 186.64 21.04 199.792 21.04 208.719 28.049 209.007 38.609 L193.36 41.105 C193.648 36.113 191.728 32.753 186.544 32.753 179.44 32.753 175.025 42.161 175.025 50.513 175.025 55.985 178.192 59.441 183.376 59.441 186.832 59.441 190.095 56.943 191.343 52.911 L205.937 57.616 C202.289 65.872 194.128 70.96 183.28 70.96 Z M236.655 70.96 C221.967 70.96 212.944 61.551 212.944 49.167 212.944 33.615 222.832 21.04 240.304 21.04 254.992 21.04 264.017 30.351 264.017 42.831 264.017 58.287 254.127 70.96 236.655 70.96 Z M15.472 70.864 C3.952 70.864 -0.752 64.048 1.743 53.392 L13.743 1.648 30.064 1.648 18.159 53.2 C17.103 57.52 18.543 59.536 22.671 59.536 24.207 59.536 25.264 59.439 26.128 59.151 L23.247 69.712 C21.135 70.576 18.928 70.864 15.472 70.864 Z M81.712 70.864 C69.808 70.864 62.799 64.625 65.583 52.433 L69.903 33.616 60.976 33.616 63.663 21.999 72.495 21.999 74.896 11.632 92.368 7.025 88.911 21.999 101.968 21.999 99.28 33.616 86.224 33.616 81.903 52.048 C80.655 57.04 83.439 59.536 88.335 59.536 91.215 59.536 92.848 58.961 94.384 58.193 L93.423 68.655 C89.967 70.287 85.936 70.864 81.712 70.864 Z M127.216 70.864 C113.008 70.864 103.407 62.224 103.407 49.071 103.407 32.944 113.775 21.136 130.671 21.136 144.687 21.136 152.751 29.488 152.751 42.544 152.751 45.04 152.56 47.728 151.792 50.319 L118.575 50.319 C118.671 56.271 122.416 59.536 127.888 59.536 132.496 59.536 135.184 57.136 136.72 53.968 L150.159 59.247 C146.703 66.543 137.968 70.864 127.216 70.864 Z M326.991 70.864 C317.391 70.864 310.288 63.951 310.288 51.568 310.288 36.112 319.503 21.04 333.999 21.04 340.431 21.04 346.383 24.304 347.919 32.079 L354.929 1.648 371.247 1.648 355.407 69.999 340.624 69.999 342.255 60.976 C339.183 67.12 333.519 70.864 326.991 70.864 Z M29.583 69.999 L40.72 21.999 57.04 21.999 45.903 69.999 29.583 69.999 Z M267.28 69.999 L278.415 21.999 292.911 21.999 290.991 33.04 C294.351 25.745 299.92 21.04 307.696 21.04 309.808 21.04 311.248 21.424 312.304 21.903 L308.368 35.632 C307.12 35.056 305.488 34.673 302.8 34.673 296.752 34.673 290.992 38.416 288.784 47.823 L283.601 69.999 267.28 69.999 Z M236.847 59.441 C243.471 59.441 247.984 49.839 247.984 41.487 247.984 36.016 245.2 32.56 240.017 32.56 233.489 32.56 228.88 42.065 228.88 50.513 228.88 55.985 231.759 59.441 236.847 59.441 Z M334.097 58.96 C340.624 58.96 345.136 49.745 345.136 41.776 345.136 36.401 342.351 33.04 337.743 33.04 330.543 33.04 326.415 42.544 326.415 49.743 326.415 55.599 329.009 58.96 334.097 58.96 Z M119.728 41.487 L137.105 41.487 C137.2 40.912 137.2 40.337 137.2 39.857 137.2 34.961 134.416 32.464 130.288 32.464 124.336 32.464 121.167 36.496 119.728 41.487 Z M51.087 16.433 C45.423 16.433 41.968 14.224 42.64 9.136 43.216 3.184 46.672 0.784 52.624 0.784 58.48 0.784 61.743 3.184 61.167 7.984 60.591 14.32 57.039 16.433 51.087 16.433 Z" fill="#000000" fill-opacity="1" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
static/logo/logo@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB