This commit is contained in:
SouthFox 2022-09-18 11:23:33 +08:00
commit 0335fa66df
25 changed files with 1128 additions and 0 deletions

1
.flaskenv Normal file
View file

@ -0,0 +1 @@
FLASK_APP=BDSM

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
.DS_Store
.idea
*.log
tmp/
*.py[cod]
*.egg
build
htmlcov
*.secret
*.db

22
BDSM/__init__.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python3
import os
import sys
from flask import Flask, flash
from flask_sqlalchemy import SQLAlchemy
# SQLite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
prefix = 'sqlite:///'
else:
prefix = 'sqlite:////'
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev')
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(os.path.dirname(app.root_path), os.getenv('DATABASE_FILE', 'data.db'))
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
from BDSM import views, models, commands

15
BDSM/commands.py Normal file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import click
from BDSM import app, db
from BDSM.models import Settings
@app.cli.command()
@click.option('--drop', is_flag=True, help='Create after drop.')
def initdb(drop):
"""Initialize the database."""
if drop:
db.drop_all()
db.create_all()
click.echo('Initialized database.')

82
BDSM/models.py Normal file
View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
from BDSM import db
class Toot(db.Model):
id = db.Column(db.Integer, primary_key=True)
acct = db.Column(db.Text)
url = db.Column(db.Text)
created_at = db.Column(db.DateTime)
edited_at = db.Column(db.DateTime)
in_replay_to_id = db.Column(db.Integer)
in_replay_to_account_id = db.Column(db.Integer)
reblog_myself = db.Column(db.Boolean)
reblog_id = db.Column(db.Integer)
content = db.Column(db.Text)
media_list = db.Column(db.Text)
emoji_list = db.Column(db.Text)
spoiler_text = db.Column(db.Text)
poll_id = db.Column(db.Integer)
visibility = db.Column(db.Text)
reblogged = db.Column(db.Boolean)
favourited = db.Column(db.Boolean)
bookmarked = db.Column(db.Boolean)
sensitive = db.Column(db.Boolean)
replies_count = db.Column(db.Integer)
reblogs_count = db.Column(db.Integer)
favourites_count = db.Column(db.Integer)
language = db.Column(db.Text)
class Reblog(db.Model):
id = db.Column(db.Integer, primary_key=True)
acct = db.Column(db.Text)
url = db.Column(db.Text)
created_at = db.Column(db.DateTime)
edited_at = db.Column(db.DateTime)
in_replay_to_id = db.Column(db.Integer)
in_replay_to_account_id = db.Column(db.Integer)
content = db.Column(db.Text)
media_list = db.Column(db.Text)
emoji_list = db.Column(db.Text)
spoiler_text = db.Column(db.Text)
poll_id = db.Column(db.Integer)
visibility = db.Column(db.Text)
reblogged = db.Column(db.Boolean)
favourited = db.Column(db.Boolean)
bookmarked = db.Column(db.Boolean)
sensitive = db.Column(db.Boolean)
replies_count = db.Column(db.Integer)
reblogs_count = db.Column(db.Integer)
favourites_count = db.Column(db.Integer)
language = db.Column(db.Text)
class Tag(db.Model):
__table_args__ = {'sqlite_autoincrement': True}
tag_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer)
name = db.Column(db.Text)
class Emoji(db.Model):
shortcode = db.Column(db.Text, primary_key=True)
url = db.Column(db.Text)
static_url = db.Column(db.Text)
name = db.Column(db.Text)
count = db.Column(db.Integer)
class Media(db.Model):
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.Text)
url = db.Column(db.Text)
remote_url = db.Column(db.Text)
description = db.Column(db.Text)
class Poll(db.Model):
id = db.Column(db.Integer, primary_key=True)
expires_at = db.Column(db.DateTime)
multiple = db.Column(db.Boolean)
votes_count = db.Column(db.Integer)
options = db.Column(db.Text)
class Settings(db.Model):
account = db.Column(db.Text, primary_key=True)
timezone = db.Column(db.Text)
setup = db.Column(db.Boolean)

6
BDSM/static/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
BDSM/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

154
BDSM/static/css/style.css Normal file
View file

@ -0,0 +1,154 @@
body {
margin: auto !important;
padding: 5px;
max-width: 580px;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}
.toot {
padding-top: 10px;
border-top: 1px solid #393f4f;
}
.meta .time {
float: right;
text-decoration: underline;
color: #606984;
}
.visibility-icon .fa-globle {
color: #388E3C;
}
.visibility-icon .fa-unlock {
color: #1976D2;
}
.visibility-icon .fa-lock {
color: #FFA000;
}
.visibility-icon .fa-at {
color: #D32F2F;
}
.icon-bar span {
padding-right: 10px;
}
.pagination span {
padding-right: 10px;
}
.icon-bar span {
padding-right: 10px;
}
.avatar {
width: 40px;
}
nav {
padding-bottom: 15px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
nav li {
float: left;
}
nav li a {
display: block;
color: white;
text-align: center;
padding: 8px 12px;
text-decoration: none;
}
nav li a:hover {
background-color: #111;
}
.alert {
position: relative;
padding: 7px;
margin: 7px 0;
border: 1px solid transparent;
color: #004085;
background-color: #cce5ff;
border-color: #b8daff;
border-radius: 5px;
}
input[type=submit] {
font-family: inherit;
}
input[type=text] {
border: 1px solid #ddd;
}
input[name=year] {
width: 50px;
}
.invisible {
visibility: inherit !important;
}
.movie-list {
list-style-type: none;
padding: 0;
margin-bottom: 10px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.movie-list li {
padding: 12px 24px;
border-bottom: 1px solid #ddd;
}
.movie-list li:last-child {
border-bottom:none;
}
.movie-list li:hover {
background-color: #f8f9fa;
}
.float-right {
float: right;
}
.inline-form {
display: inline;
}
.imdb {
font-size: 12px;
font-weight: bold;
color: black;
text-decoration: none;
background: #F5C518;
border-radius: 5px;
padding: 3px 5px;
}
.totoro {
display: block;
margin: 0 auto;
height: 100px;
}
footer {
color: #888;
margin-top: 15px;
text-align: center;
padding: 10px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

40
BDSM/templates/base.html Normal file
View file

@ -0,0 +1,40 @@
<!doctype html>
<html>
<head>
{% block head %}
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Untitled</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='/css/all.min.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('static', filename='/css/bootstrap.min.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('static', filename='/css/style.css') }}" type="text/css">
<script src="{{ url_for('static', filename='/js/bootstrap.bundle.min.js') }}"
crossorigin="anonymous"></script>
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
{% endblock %}
</head>
<body>
<div class="row">
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
<nav>
<ul>
<li><a href="{{ url_for('index') }}">主页</a></li>
<li><a href="{{ url_for('settings') }}">设置</a></li>
<li><a href="{{ url_for('archive') }}">存档</a></li>
</ul>
</nav>
{% block content %}{% endblock %}
<footer>
<small><a href="https://git.southfox.me/southfox/mastodon-BDSM">Code</a></small>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<h3>授权</h3>
<p>Visit the following URL and authorize the app: </p>
<a href='{{ url }}' target="_blank">{{ url }}</a>
<p>Then paste the access token here:"</p>
<form method="post">
Token: <input type="text" name="token" autocomplete="off" required></br>
<input class="btn" type="submit" name="submit" value="提交">
</form>
{% endblock %}

View file

@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block content %}
<h3>设置</h3>
<form method="post" class="form-horizontal" role="form">
{% if settings is defined %}
<div class="form-group">
<label class="col-sm-2 control-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="account" autocomplete="off" required
value="{{ settings.account }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">时区</label>
<div class="col-sm-10">
<input type="text" name="timezone" autocomplete="off" required value="{{ settings.timezone }}"></br>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input class="btn btn-primary" type="submit" name="submit" value="保存">
</div>
</div>
{% else %}
<div class="form-group">
<label class="col-sm-2 control-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="account" autocomplete="off" required value="@用户名@实例">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">时区</label>
<div class="col-sm-10">
<input type="text" name="timezone" autocomplete="off" required value="Asia/Shanghai"></br>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input class="btn btn-primary" type="submit" name="submit" value="保存">
</div>
</div>
{% endif %}
{% if not app_init %}
<p> 似乎你还没有进行认证,请到这个<a href='/register'>页面</a>进行授权。</p>
{% endif %}
</form>
{% endblock %}

87
BDSM/templates/view.html Normal file
View file

@ -0,0 +1,87 @@
{% extends 'base.html' %}
{% block content %}
{% for toot in toots %}
<div class="toot">
<div class="status">
<div class="meta">
<strong><span class ="username">{{ toot.acct }}</span></strong>
<a href="{{ toot.url }}" target="_blank"
rel="noopener noreferrer">
<span class="time">
{% if toot. visibility == "public"%}
<span class="visibility-icon"><i class="fa-solid fa-earth-americas fa-globle"></i></span>
{% elif toot. visibility == "unlisted"%}
<span class="visibility-icon"><i class="fa-solid fa-lock-open fa-unlock"></i></span>
{% elif toot. visibility == "private"%}
<span class="visibility-icon"><i class="fa-solid fa-lock"></i></span>
{% elif toot. visibility == "direct"%}
<span class="visibility-icon"><i class="fa-solid fa-envelope fa-at"></i></span>
{% endif %}
<time>{{ toot.created_at }}</time></span></a>
</div>
<div class="content">
{{ toot.content|safe }}
</div>
<div class="icon-bar">
{% if toot.replies_count > 1 %}
<span><i class="fa-solid fa-reply-all"></i>{{ toot.replies_count }}</span>
{% else %}
<span><i class="fa-solid fa-reply"></i>{{ toot.replies_count }}</span>
{% endif %}
{% if toot.reblogged %}
<span><i class="fa-solid fa-arrow-rotate-right"></i>{{ toot.reblogs_count}}</span>
{% else %}
<span><i class="fa-solid fa-retweet"></i></i>{{ toot.reblogs_count}}</span>
{% endif %}
{% if toot.favourited %}
<span><i class="fa-solid fa-star"></i>{{ toot.favourites_count }}</span>
{% else %}
<span><i class="fa-regular fa-star"></i>{{ toot.favourites_count }}</span>
{% endif %}
{% if toot.bookmarked %}
<span><i class="fa-solid fa-bookmark"></i></span>
{% else %}
<span><i class="fa-regular fa-bookmark"></i></span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="pagination d-flex justify-content-center">
{% if pagination.has_prev %}
<span>
<a class='page-number' href="{{ url_for('index', page=pagination.prev_num) }}">
{{ '<<<' }}
</a>
</span>
{% endif %}
{% for number in pagination.iter_pages() %}
{% if number == None %}
<span>...</span>
{% elif pagination.page != number %}
<span>
<a class='page-number'
href="{{ url_for('index', page=number) }}">
{{ number }}
</a>
</span>
{% else %}
<span class='current-page-number'>{{ number }}</span>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<span>
<a class='page-number' href="{{ url_for('index', page=pagination.next_num) }}">
{{ '>>>' }}
</a>
</span>
{% endif %}
</div>
{% endblock %}

191
BDSM/toot.py Normal file
View file

@ -0,0 +1,191 @@
#!/usr/bin/env python3
from mastodon import Mastodon
from BDSM import db
from BDSM.models import Reblog, Toot, Tag, Media, Emoji, Poll
import sys
def app_register(url):
print("Registering app")
Mastodon.create_app(
'pyBDSM',
api_base_url = url,
to_file = 'pyBDSM_clientcred.secret',
scopes=["read"]
)
def archive_toot(url):
mastodon = Mastodon(
client_id='pyBDSM_clientcred.secret',
access_token='user.secret',
api_base_url=url
)
try:
user = mastodon.account_verify_credentials()
except Exception as e:
if "access token was revoked" in str(e):
print("revoked token")
sys.exit(0)
elif "Name or service not known" in str(e):
print("Error: the instance name is either misspelled or offline",
file=sys.stderr)
else:
print(e, file=sys.stderr)
# exit in either case
sys.exit(1)
acct = mastodon.me().acct
statuses_count = str(mastodon.me().statuses_count)
statuses = mastodon.account_statuses(user["id"], limit=20)
# xx = statuses['created_at'].astimezone(tz_cn)
# pprint(xx.strftime("%m/%d/%Y, %H:%M:%S"))
# pprint(statuses)
happy_counter = 20
while(True):
for status in statuses:
is_reblog = False
if status['reblog'] != None:
if acct == status['reblog']['account']['acct']:
reblog_myself = True
else:
reblog_myself = False
is_reblog = True
reblog_id = status['reblog']['id']
id = status['id']
created_at = status['created_at']
toot = Toot(id=id, created_at=created_at, reblog_myself=reblog_myself, reblog_id=reblog_id)
db.session.add(toot)
# cur.execute('''INSERT OR REPLACE INTO TOOT (id,created_at,reblog_myself,reblog_id) \
# VALUES (?,?,?,?)''',(id, created_at, reblog_myself, reblog_id))
if reblog_myself:
continue
status = status['reblog']
id = status['id']
acct = status['account']['acct']
url = status['url']
created_at = status['created_at']
edited_at = status['edited_at'] if status['edited_at'] != None else None
in_reply_to_id = status['in_reply_to_id']
in_reply_to_account_id = status['in_reply_to_account_id']
content = status['content']
if status['media_attachments'] != []:
media_list = []
for media_dict in status['media_attachments']:
media_list.append(media_dict['id'])
media = Media(id=media_dict['id'], type=media_dict['type'], url=media_dict['url'],
remote_url=media_dict['remote_url'], description=media_dict['description'])
db.session.add(media)
# cur.execute('''INSERT OR REPLACE INTO MEDIA (id,type,url,remote_url,description) \
# VALUES (?,?,?,?,?)''',(media_dict['id'], media_dict['type'], media_dict['url'], \
# media_dict['remote_url'], media_dict['description']))
else:
media_list = []
media_list = str(media_list)
spoiler_text = status['spoiler_text']
if status['poll'] != None:
poll_dict = status['poll']
poll_id = poll_dict['id']
expires_at = poll_dict['expires_at']
options = str(poll_dict['options'])
poll = Poll(id=poll_dict['id'], expires_at=expires_at, multiple=poll_dict['multiple'], \
votes_count=poll_dict['votes_count'], options=options)
db.session.add(poll)
# cur.execute('''INSERT OR REPLACE INTO POLL (id,expires_at,multiple,votes_count,options) \
# VALUES (?,?,?,?,?)''',(poll_dict['id'], expires_at, poll_dict['multiple'], \
# poll_dict['votes_count'], options))
else:
poll_id = None
if status['emojis'] != []:
emoji_list = []
for emoji in status['emojis']:
shortcode = emoji['shortcode']
emoji_list.append(shortcode)
counter = ':' + shortcode + ':'
count = content.count(counter)
data=Emoji.query.filter_by(shortcode=shortcode).first()
if data is None:
emoji_data = Emoji(shortcode=shortcode, url=emoji['url'], static_url=emoji['static_url'], count=count)
db.session.add(emoji_data)
# cur.execute('''INSERT INTO EMOJI (shortcode,url,static_url,count) \
# VALUES (?,?,?,?)''', (shortcode, emoji['url'], emoji['static_url'], count))
else:
data.count += count
# cur.execute("UPDATE EMOJI SET count = ? WHERE shortcode = ?",(count, shortcode))
else:
emoji_list = []
emoji_list = str(emoji_list)
if status['tags'] != []:
for tag in status['tags']:
tag_data = Tag(id=id, name=tag['name'])
db.session.add(tag_data)
# cur.execute('''INSERT OR REPLACE INTO TAG (id,name) \
# VALUES (?,?)''',(id, tag['name']))
visibility = status['visibility']
reblogged = status['reblogged']
favourited = status['favourited']
bookmarked = status['bookmarked']
sensitive = status['sensitive']
replies_count = status['replies_count']
reblogs_count = status['reblogs_count']
favourites_count = status['favourites_count']
language = status['language']
table = Reblog() if is_reblog else Toot()
table.id=id
table.acct = acct
table.url=url
table.created_at=created_at
table.edited_at=edited_at
table.in_reply_to_id=in_reply_to_id
table.in_reply_to_account_id=in_reply_to_account_id
table.content=content
table.media_list=media_list
table.spoiler_text=spoiler_text
table.poll_id=poll_id
table.emoji_list=emoji_list
table.visibility=visibility
table.reblogged=reblogged
table.favourited=favourited
table.bookmarked=bookmarked
table.sensitive=sensitive
table.replies_count=replies_count
table.reblogs_count=reblogs_count
table.favourites_count=favourites_count
table.language=language
db.session.add(table)
# sql = f'''INSERT OR REPLACE INTO {table} (id,url,created_at,edited_at,in_reply_to_id,in_reply_to_account_id,content,\
# media_list,spoiler_text,poll_id,emoji_list,visibility,reblogged,favourited,bookmarked,sensitive,reblogs_count,\
# favourites_count,language) \
# VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'''
# cur.execute(sql,(id,url,created_at,edited_at,in_reply_to_id,in_reply_to_account_id,content,media_list,spoiler_text,\
# poll_id,emoji_list,visibility,reblogged,favourited,bookmarked,sensitive,reblogs_count,favourites_count,language))
db.session.commit()
print(str(happy_counter) + ' / ' + statuses_count)
happy_counter += 20
statuses = mastodon.fetch_next(statuses)
# statuses = None
if statuses == None:
break

102
BDSM/views.py Normal file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
import os
from flask import render_template, request, url_for, redirect, flash
from BDSM import app, db
from BDSM.models import Settings, Toot
from BDSM.toot import app_register, archive_toot
from mastodon import Mastodon
@app.route('/', methods=['GET', 'POST'])
def index():
page = request.args.get('page', 1, type=int)
toots_ = Toot.query.order_by(Toot.created_at.desc()).paginate(page, per_page=50)
toots = []
for toot in toots_.items:
if toot.content == None:
continue
toots.append(toot)
return render_template('view.html', toots=toots, pagination=toots_)
@app.route('/settings', methods=['GET', 'POST'])
def settings():
if request.method == 'POST':
account = request.form['account']
timezone = request.form['timezone']
if not account or len(account) > 30:
flash('无效输入')
return redirect(url_for('settings'))
settings = Settings.query.first()
if settings == None:
settings = Settings(account=account, timezone=timezone)
db.session.add(settings)
else:
settings.account = account
settings.timezone = timezone
db.session.commit()
flash('设置已修改')
return redirect(url_for('settings'))
settings = Settings.query.first()
if settings == None:
flash('请输入用户名')
return render_template('settings.html')
app_init = os.path.isfile('pyBDSM_clientcred.secret') and os.path.isfile('user.secret')
return render_template('settings.html',settings=settings, app_init=app_init)
@app.route('/register', methods=['GET', 'POST'])
def register():
settings = Settings.query.first()
if settings == None:
flash('请先输入用户名!')
return redirect(url_for('settings'))
else:
account = settings.account[1:]
username, domain = account.split("@")
url = "https://" + domain
if request.method == 'POST':
token = request.form['token'].rstrip()
mastodon = Mastodon(client_id='pyBDSM_clientcred.secret', api_base_url=url)
mastodon.log_in(code=token, to_file='user.secret', scopes=['read'])
if os.path.isfile('user.secret'):
flash('应用已授权!')
return redirect(url_for('settings'))
if not os.path.isfile('pyBDSM_clientcred.secret'):
app_register(url)
if not os.path.isfile('user.secret'):
mastodon = Mastodon(client_id='pyBDSM_clientcred.secret', api_base_url=url)
url = mastodon.auth_request_url(client_id='pyBDSM_clientcred.secret', scopes=['read'])
return render_template('register.html',url=url)
flash('已授权过!')
return redirect(url_for('settings'))
@app.route('/archive', methods=['GET', 'POST'])
def archive():
settings = Settings.query.first()
if settings == None:
return redirect(url_for('settings'))
elif len(Toot.query.all()) > 0:
flash('现暂不支持重复存档!') #TODO
return redirect(url_for('index'))
else:
account = settings.account[1:]
username, domain = account.split("@")
url = "https://" + domain
archive_toot(url)
flash('存档完成……大概!')
return redirect(url_for('index'))

15
Pipfile Normal file
View file

@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
"mastodon.py" = "*"
flask = "*"
flask-sqlalchemy = "*"
python-dotenv = "*"
[dev-packages]
[requires]
python_version = "3.10"

324
Pipfile.lock generated Normal file
View file

@ -0,0 +1,324 @@
{
"_meta": {
"hash": {
"sha256": "5c49bdfaa2bb333c4af5bc5fa02d920a87c2d429460948ade1295a11dbac6a95"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"blurhash": {
"hashes": [
"sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d",
"sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"
],
"version": "==1.1.4"
},
"certifi": {
"hashes": [
"sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5",
"sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"
],
"markers": "python_version >= '3.6'",
"version": "==2022.9.14"
},
"charset-normalizer": {
"hashes": [
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.1"
},
"click": {
"hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.3"
},
"decorator": {
"hashes": [
"sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
"sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
],
"markers": "python_version >= '3.5'",
"version": "==5.1.1"
},
"flask": {
"hashes": [
"sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
"sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"
],
"index": "pypi",
"version": "==2.2.2"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912",
"sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"
],
"index": "pypi",
"version": "==2.5.1"
},
"greenlet": {
"hashes": [
"sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4",
"sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc",
"sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8",
"sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202",
"sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380",
"sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08",
"sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7",
"sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268",
"sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e",
"sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809",
"sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403",
"sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080",
"sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76",
"sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0",
"sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2",
"sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e",
"sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1",
"sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd",
"sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c",
"sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe",
"sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96",
"sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20",
"sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600",
"sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed",
"sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2",
"sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26",
"sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a",
"sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32",
"sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79",
"sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b",
"sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa",
"sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57",
"sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e",
"sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba",
"sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700",
"sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318",
"sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382",
"sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905",
"sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e",
"sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455",
"sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910",
"sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2",
"sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3",
"sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5",
"sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41",
"sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d",
"sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9",
"sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47",
"sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49",
"sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477",
"sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33",
"sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25",
"sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90",
"sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932"
],
"markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==1.1.3"
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"markupsafe": {
"hashes": [
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
"mastodon.py": {
"hashes": [
"sha256:2afddbad8b5d7326fcc8a8f8c62bfe956e34627f516b06c6694fc8c8fedc33ee",
"sha256:cc454cac0ed1ae4f105f7399ea53f5b31a1be5075d1882f47162d2e78a9e4064"
],
"index": "pypi",
"version": "==1.5.1"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"python-dotenv": {
"hashes": [
"sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5",
"sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"
],
"index": "pypi",
"version": "==0.21.0"
},
"python-magic": {
"hashes": [
"sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b",
"sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.27"
},
"pytz": {
"hashes": [
"sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197",
"sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"
],
"version": "==2022.2.1"
},
"requests": {
"hashes": [
"sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
"sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.28.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlalchemy": {
"hashes": [
"sha256:0002e829142b2af00b4eaa26c51728f3ea68235f232a2e72a9508a3116bd6ed0",
"sha256:0005bd73026cd239fc1e8ccdf54db58b6193be9a02b3f0c5983808f84862c767",
"sha256:0292f70d1797e3c54e862e6f30ae474014648bc9c723e14a2fda730adb0a9791",
"sha256:036d8472356e1d5f096c5e0e1a7e0f9182140ada3602f8fff6b7329e9e7cfbcd",
"sha256:05f0de3a1dc3810a776275763764bb0015a02ae0f698a794646ebc5fb06fad33",
"sha256:0990932f7cca97fece8017414f57fdd80db506a045869d7ddf2dda1d7cf69ecc",
"sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d",
"sha256:14576238a5f89bcf504c5f0a388d0ca78df61fb42cb2af0efe239dc965d4f5c9",
"sha256:199a73c31ac8ea59937cc0bf3dfc04392e81afe2ec8a74f26f489d268867846c",
"sha256:2082a2d2fca363a3ce21cfa3d068c5a1ce4bf720cf6497fb3a9fc643a8ee4ddd",
"sha256:22ff16cedab5b16a0db79f1bc99e46a6ddececb60c396562e50aab58ddb2871c",
"sha256:2307495d9e0ea00d0c726be97a5b96615035854972cc538f6e7eaed23a35886c",
"sha256:2ad2b727fc41c7f8757098903f85fafb4bf587ca6605f82d9bf5604bd9c7cded",
"sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330",
"sha256:361f6b5e3f659e3c56ea3518cf85fbdae1b9e788ade0219a67eeaaea8a4e4d2a",
"sha256:3e2ef592ac3693c65210f8b53d0edcf9f4405925adcfc031ff495e8d18169682",
"sha256:4676d51c9f6f6226ae8f26dc83ec291c088fe7633269757d333978df78d931ab",
"sha256:4ba7e122510bbc07258dc42be6ed45997efdf38129bde3e3f12649be70683546",
"sha256:5102fb9ee2c258a2218281adcb3e1918b793c51d6c2b4666ce38c35101bb940e",
"sha256:5323252be2bd261e0aa3f33cb3a64c45d76829989fa3ce90652838397d84197d",
"sha256:58bb65b3274b0c8a02cea9f91d6f44d0da79abc993b33bdedbfec98c8440175a",
"sha256:59bdc291165b6119fc6cdbc287c36f7f2859e6051dd923bdf47b4c55fd2f8bd0",
"sha256:5facb7fd6fa8a7353bbe88b95695e555338fb038ad19ceb29c82d94f62775a05",
"sha256:639e1ae8d48b3c86ffe59c0daa9a02e2bfe17ca3d2b41611b30a0073937d4497",
"sha256:8eb8897367a21b578b26f5713833836f886817ee2ffba1177d446fa3f77e67c8",
"sha256:90484a2b00baedad361402c257895b13faa3f01780f18f4a104a2f5c413e4536",
"sha256:9c56e19780cd1344fcd362fd6265a15f48aa8d365996a37fab1495cae8fcd97d",
"sha256:b67fc780cfe2b306180e56daaa411dd3186bf979d50a6a7c2a5b5036575cbdbb",
"sha256:c0dcf127bb99458a9d211e6e1f0f3edb96c874dd12f2503d4d8e4f1fd103790b",
"sha256:c23d64a0b28fc78c96289ffbd0d9d1abd48d267269b27f2d34e430ea73ce4b26",
"sha256:ccfd238f766a5bb5ee5545a62dd03f316ac67966a6a658efb63eeff8158a4bbf",
"sha256:cd767cf5d7252b1c88fcfb58426a32d7bd14a7e4942497e15b68ff5d822b41ad",
"sha256:ce8feaa52c1640de9541eeaaa8b5fb632d9d66249c947bb0d89dd01f87c7c288",
"sha256:d2e054aed4645f9b755db85bc69fc4ed2c9020c19c8027976f66576b906a74f1",
"sha256:e16c2be5cb19e2c08da7bd3a87fed2a0d4e90065ee553a940c4fc1a0fb1ab72b",
"sha256:e4b12e3d88a8fffd0b4ca559f6d4957ed91bd4c0613a4e13846ab8729dc5c251",
"sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d",
"sha256:eb30cf008850c0a26b72bd1b9be6730830165ce049d239cfdccd906f2685f892",
"sha256:f37fa70d95658763254941ddd30ecb23fc4ec0c5a788a7c21034fc2305dab7cc",
"sha256:f5ebeeec5c14533221eb30bad716bc1fd32f509196318fb9caa7002c4a364e4c",
"sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.4.41"
},
"urllib3": {
"hashes": [
"sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
"sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
"version": "==1.26.12"
},
"werkzeug": {
"hashes": [
"sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f",
"sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"
],
"markers": "python_version >= '3.7'",
"version": "==2.2.2"
}
},
"develop": {}
}