Merge branch 'dev'

このコミットが含まれているのは:
n9k 2022-06-16 03:18:46 +00:00
コミット e9e0862445
44個のファイルの変更287行の追加74行の削除

74
HACKING.md ノーマルファイル
ファイルの表示

@ -0,0 +1,74 @@
## Hacking
By default anonstream has two APIs it exposes through two UNIX sockets:
the control socket `control.sock` and the event socket `event.sock`. If
the platform you are using does not support UNIX sockets, they can be
disabled in the config.
### Control socket
The control socket allows reading and modifying internal state, e.g.
setting the title or changing a user's name. Currently the control
socket has checks to see if what you're doing is sane, but they're not
comprehensive; you could craft commands that lead to undefined
behaviour. If you have `socat`, you can use the control socket
interactively like this:
```sh
rlwrap socat STDIN UNIX-CONNECT:control.sock
```
`rlwrap` only adds line editing and is optional. If you don't have it
you can still get (inferior) line editing by doing:
```sh
socat READLINE UNIX-CONNECT:control.sock
```
Once connected, type "help" and press enter to get a list of commands.
### Event socket
The event socket is a read-only socket that sends out internal events as
they happen. Currently the only supported event is a chat message being
added. The intended use is to hook into other applications that depend
on chat, e.g. text-to-speech or Twitch Plays Pokémon.
View events like this:
```sh
socat UNIX-CONNECT:event.sock STDOUT
```
Sidenote, this will still read from stdin, and if you send anything on
stdin the event socket will close itself. If you want to ignore stdin,
I couldn't figure out how to get `socat` to do it so you can do it like
this:
```sh
cat > /dev/null | socat UNIX-CONNECT:event.sock STDOUT
```
If you do this `cat` will not exit when the connection is closed so you
will probably have to interrupt it with `^C`.
#### Examples
If you have `jq` you can view prettified events like this:
```sh
socat UNIX-CONNECT:event.sock STDOUT | jq
```
(On older versions of `jq` you have to say `jq .` when reading from
stdin.)
Use this to get each new chat message on a new line:
```sh
socat UNIX-CONNECT:event.sock STDOUT | jq 'select(.type == "message") | .event.nomarkup'
```
##### Text-to-speech
This command will take each new chat message with the prefix "!say ",
strip the prefix, and synthesize the rest of the message as speech using
`espeak`:
```sh
socat UNIX-CONNECT:event.sock STDOUT \
| jq --unbuffered 'select(.type == "message") | .event.nomarkup' \
| grep -E --line-buffered '^"!say ' \
| sed -Eu 's/^"!say /"/' \
| jq -r --unbuffered \
| espeak
```

ファイルの表示

@ -13,7 +13,8 @@ These mirrors also exist:
## Setup
You must have Python 3.10 at a minimum.
You must have Python 3.10 at a minimum. You can check your version of Python
with `python --version`.
Clone the repo:
```sh
@ -28,12 +29,14 @@ source venv/bin/activate
python -m pip install -r requirements.txt
```
Before you run it you should edit [/config.toml][config], e.g. these
options:
Before you run it you may want to edit the config ([/config.toml][config]).
Most of the defaults are probably okay, but here are some that you might want
to know what they do:
* `secret_key`:
used for cryptography, make it any long random string
(e.g. `$ dd if=/dev/urandom bs=16 count=1 | base64`)
used for cryptography, make it any long random string (e.g.
`$ dd if=/dev/urandom bs=16 count=1 | base64`), definitely set this
yourself before running in "production" (whatever that is for you)
* `segments/directory`:
directory containing stream segments, the default is `stream/` in
@ -49,15 +52,16 @@ options:
Run it:
```sh
python -m uvicorn app:app --port 5051
python -m anonstream
```
This will start a webserver listening on localhost port 5051.
This will start a webserver listening on the local host at port 5051 (use
`--port PORT` to override).
If you go to `http://localhost:5051` in a web browser now you should see
the site. When you started the webserver some credentials were
printed in the terminal; you can log in with those at
`http://localhost:5051/login` (requires cookies).
the site. When you started the webserver some credentials were printed
in the terminal; you can log in with those at
`http://localhost:5051/login`.
The only things left are (1) streaming, and (2) letting other people
access your stream. [/STREAMING.md][streaming] has instructions for
@ -65,6 +69,30 @@ setting up OBS Studio and a Tor onion service. If you want to use
different streaming software and put your stream on the Internet some
other way, read those instructions and copy the gist.
## Running
Start anonstream like this:
```sh
python -m anonstream
```
The default port is 5051. Append `--help` to see options.
If you want to use a different ASGI server, point it to the app factory
at `asgi:create_app()`. For example with `uvicorn`:
```sh
python -m uvicorn asgi:create_app --factory --port 5051
```
In either case you can explicitly set the location of the config file
using the `ANONSTREAM_CONFIG` environment variable.
## Hacking
anonstream has APIs for accessing internal state and hooking into
internal events. They can be used by humans and other programs. See
[/HACKING.md][hacking].
## Copying
anonstream is AGPL 3.0 or later, see
@ -104,6 +132,7 @@ anonstream is AGPL 3.0 or later, see
([BSD 3-Clause][werkzeug])
[config]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/config.toml
[hacking]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/HACKING.md
[licence]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/LICENSES/AGPL-3.0-or-later.md
[settings.svg]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/anonstream/static/settings.svg
[streaming]: https://git.076.ne.jp/ninya9k/anonstream/src/branch/master/STREAMING.md

ファイルの表示

@ -1,9 +1,8 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from collections import OrderedDict
import toml
from quart_compress import Compress
from anonstream.config import update_flask_from_toml
@ -12,15 +11,12 @@ from anonstream.quart import Quart
compress = Compress()
def create_app(config_file):
def create_app(toml_config):
app = Quart('anonstream')
app.jinja_options['trim_blocks'] = True
app.jinja_options['lstrip_blocks'] = True
with open(config_file) as fp:
toml_config = toml.load(fp)
auth_password = update_flask_from_toml(toml_config, app.config)
print('Broadcaster username:', app.config['AUTH_USERNAME'])
print('Broadcaster password:', auth_password)

51
anonstream/__main__.py ノーマルファイル
ファイルの表示

@ -0,0 +1,51 @@
import argparse
import os
import toml
import uvicorn
from anonstream import create_app
DEFAULT_PORT = 5051
DEFAULT_CONFIG = 'config.toml'
def want_rel(path):
'''
Prepend './' to relative paths.
>>> want_rel('/some/abs/path')
'/some/abs/path'
>>> want_rel('config.toml')
'./config.toml'
'''
if os.path.isabs(path):
return path
else:
return os.path.join('.', path)
formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=26)
parser = argparse.ArgumentParser(
'python -m anonstream',
description='Start the anonstream webserver locally.',
formatter_class=formatter,
)
parser.add_argument(
'--config', '-c',
metavar='FILE',
default=os.environ.get('ANONSTREAM_CONFIG', 'config.toml'),
help=(
'location of config.toml '
f'(default: $ANONSTREAM_CONFIG or {want_rel(DEFAULT_CONFIG)})'
),
)
parser.add_argument(
'--port', '-p',
type=int,
default=DEFAULT_PORT,
help=f'bind webserver to this port (default: {DEFAULT_PORT})',
)
args = parser.parse_args()
with open(args.config) as fp:
config = toml.load(fp)
app = create_app(config)
uvicorn.run(app, port=args.port)

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from quart import current_app

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import secrets

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import time

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
import secrets

ファイルの表示

@ -1,5 +1,8 @@
class Exit(Exception):
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
class ControlSocketExit(Exception):
pass
class Fail(Exception):
class CommandFailed(Exception):
pass

ファイルの表示

@ -1,4 +1,7 @@
from anonstream.control.spec import NoParse, Ambiguous, Parsed
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from anonstream.control.spec import ParseException, Parsed
from anonstream.control.spec.common import Str
from anonstream.control.spec.methods.chat import SPEC as SPEC_CHAT
from anonstream.control.spec.methods.exit import SPEC as SPEC_EXIT
@ -25,10 +28,7 @@ async def parse(request):
while True:
try:
spec, n_consumed, more_args = spec.consume(words, index)
except NoParse as e:
normal, response = None, e.args[0] + '\n'
break
except Ambiguous as e:
except ParseException as e:
normal, response = None, e.args[0] + '\n'
break
except Parsed as e:

ファイルの表示

@ -1,6 +1,9 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio
from anonstream.control.exceptions import Exit, Fail
from anonstream.control.exceptions import ControlSocketExit, CommandFailed
from anonstream.control.parse import parse
def start_control_server_at(address):
@ -15,9 +18,9 @@ async def serve_control_client(reader, writer):
else:
try:
normal, response = await parse(request)
except Fail as e:
except CommandFailed as e:
normal, response = None, e.args[0] + '\n'
except Exit:
except ControlSocketExit:
writer.close()
break

ファイルの表示

@ -1,7 +1,16 @@
class NoParse(Exception):
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
class ParseException(Exception):
pass
class Ambiguous(Exception):
class NoParse(ParseException):
pass
class Ambiguous(ParseException):
pass
class BadArgument(ParseException):
pass
class Parsed(Exception):

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import json
from anonstream.control.spec import Spec, NoParse, Ambiguous, Parsed

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import itertools
from anonstream.chat import delete_chat_messages

ファイルの表示

@ -1,8 +1,11 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from anonstream.control.spec.common import Str, End
from anonstream.control.exceptions import Exit
from anonstream.control.exceptions import ControlSocketExit
async def cmd_exit():
raise Exit
raise ControlSocketExit
async def cmd_exit_help():
normal = ['exit', 'help']

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from anonstream.control.spec.common import Str, End
async def cmd_help():

ファイルの表示

@ -1,6 +1,9 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import json
from anonstream.control.exceptions import Fail
from anonstream.control.exceptions import CommandFailed
from anonstream.control.spec import Spec, NoParse
from anonstream.control.spec.common import Str, End, ArgsJsonString
from anonstream.control.spec.utils import get_item, json_dumps_contiguous
@ -27,7 +30,7 @@ async def cmd_title_set(title):
try:
await set_stream_title(title)
except OSError as e:
raise Fail(f'could not set title: {e}') from e
raise CommandFailed(f'could not set title: {e}') from e
normal = ['title', 'set', json_dumps_contiguous(title)]
response = ''
return normal, response

ファイルの表示

@ -1,9 +1,12 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import json
from quart import current_app
from anonstream.control.exceptions import Fail
from anonstream.control.spec import NoParse
from anonstream.control.exceptions import CommandFailed
from anonstream.control.spec import BadArgument
from anonstream.control.spec.common import Str, End, ArgsInt, ArgsString, ArgsJson, ArgsJsonString
from anonstream.control.spec.utils import get_item, json_dumps_contiguous
from anonstream.utils.user import USER_WEBSOCKET_ATTRS
@ -17,7 +20,7 @@ class ArgsJsonTokenUser(ArgsJsonString):
try:
user = USERS_BY_TOKEN[token]
except KeyError:
raise NoParse(f'no user with token {token!r}')
raise BadArgument(f'no user with token {token!r}')
return user
class ArgsJsonHashUser(ArgsString):
@ -26,7 +29,7 @@ class ArgsJsonHashUser(ArgsString):
if user['token_hash'] == token_hash:
break
else:
raise NoParse(f'no user with token_hash {token_hash!r}')
raise BadArgument(f'no user with token_hash {token_hash!r}')
return user
def ArgsUser(spec):
@ -69,11 +72,11 @@ async def cmd_user_get(user, attr):
try:
value = user[attr]
except KeyError as e:
raise Fail('user has no such attribute') from e
raise CommandFailed('user has no such attribute') from e
try:
value_json = json.dumps(value)
except (TypeError, ValueError) as e:
raise Fail('value is not representable in json') from e
raise CommandFailed('value is not representable in json') from e
normal = [
'user',
'get',
@ -86,7 +89,7 @@ async def cmd_user_get(user, attr):
async def cmd_user_set(user, attr, value):
if attr not in user:
raise Fail(f'user has no attribute {attr!r}')
raise CommandFailed(f'user has no attribute {attr!r}')
user[attr] = value
if attr in USER_WEBSOCKET_ATTRS:
USERS_UPDATE_BUFFER.add(user['token'])

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import json
def get_item(index, words):

ファイルの表示

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio
import json

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import base64

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import hashlib

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import base64

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import hashlib

ファイルの表示

@ -1,3 +1,9 @@
# This file is pretty much entirely based on a snippet from asgi.py in
# the Quart repository (MIT, see README.md). That means it takes on the
# MIT licence I guess(???) If not then it's the same as every other file
# by me: 2022 n9k <https://git.076.ne.jp/ninya9k>, AGPL 3.0 or any later
# version.
import asyncio
from werkzeug.wrappers import Response as WerkzeugResponse

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import anonstream.routes.core

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import math

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from quart import current_app, request, render_template, redirect, url_for, escape, Markup

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import hashlib

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import itertools

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import operator

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import hashlib

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import base64

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import re

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import secrets

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import base64

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
from enum import Enum

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio

ファイルの表示

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
import time

11
app.py
ファイルの表示

@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-License-Identifier: AGPL-3.0-or-later
import os
import anonstream
config_file = os.path.join(os.path.dirname(__file__), 'config.toml')
app = anonstream.create_app(config_file)
if __name__ == '__main__':
app.run(port=5051, debug=True)

26
asgi.py ノーマルファイル
ファイルの表示

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2022 n9k <https://git.076.ne.jp/ninya9k>
# SPDX-License-Identifier: AGPL-3.0-or-later
if __name__ == '__main__':
import sys
message = (
'To start anonstream, run one of:\n'
' $ python -m anonstream\n'
' $ python -m uvicorn asgi:create_app --factory --port 5051\n'
)
print(message, file=sys.stderr, end='')
exit(1)
import os
import toml
import anonstream
config_file = os.environ.get(
'ANONSTREAM_CONFIG',
os.path.join(os.path.dirname(__file__), 'config.toml'),
)
def create_app():
with open(config_file) as fp:
config = toml.load(fp)
return anonstream.create_app(config)