mirror of https://gitlab.com/litecord/litecord.git
Merge branch 'master' of https://gitlab.com/luna/litecord
This commit is contained in:
commit
c597e08f92
|
|
@ -105,3 +105,5 @@ venv.bak/
|
|||
|
||||
config.py
|
||||
images/*
|
||||
|
||||
.DS_Store
|
||||
|
|
|
|||
109
README.md
109
README.md
|
|
@ -1,72 +1,107 @@
|
|||
# litecord
|
||||

|
||||
|
||||
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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
|
|
@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Loading…
Reference in New Issue