2024-10-05 06:49:36 +00:00
|
|
|
from flask import Flask, Response, redirect
|
|
|
|
from flask import request
|
|
|
|
from markupsafe import escape
|
|
|
|
from mastodon import Mastodon
|
|
|
|
from email.headerregistry import Address
|
|
|
|
import jwt
|
|
|
|
from gevent.pywsgi import WSGIServer
|
|
|
|
import os
|
2024-10-05 10:38:36 +00:00
|
|
|
import sqlite3
|
|
|
|
import zulip
|
|
|
|
import random
|
|
|
|
import string
|
2024-10-05 06:49:36 +00:00
|
|
|
|
|
|
|
SECRET = os.environ["SECRET"]
|
|
|
|
ZULIP = f"https://{os.environ['ZULIP']}/accounts/login/jwt/"
|
2024-10-05 10:38:36 +00:00
|
|
|
REDIRECT = f"https://{os.environ['ZULIP']}/fedi-auth/callback"
|
|
|
|
DB = os.environ.get("DB", "/var/lib/fedi-zulip/db/applications")
|
2024-10-05 17:08:54 +00:00
|
|
|
scopes = ["read"]
|
2024-10-05 10:38:36 +00:00
|
|
|
|
|
|
|
print(f"""
|
|
|
|
Zulip is: {os.environ['ZULIP']}
|
|
|
|
DB location is: {DB}
|
|
|
|
""")
|
|
|
|
|
|
|
|
con = sqlite3.connect(DB)
|
|
|
|
cur = con.cursor()
|
|
|
|
|
|
|
|
zulip_client = zulip.Client()
|
|
|
|
|
|
|
|
def get_zulip_user(handle):
|
2024-10-05 17:08:54 +00:00
|
|
|
print(f"Querying Zulip for handle: {handle}")
|
2024-10-05 10:38:36 +00:00
|
|
|
zulip_client.call_endpoint(
|
|
|
|
url=f"/users/{handle}",
|
|
|
|
method="GET"
|
|
|
|
)
|
|
|
|
|
|
|
|
def create_zulip_user(handle):
|
2024-10-05 17:08:54 +00:00
|
|
|
print(f"Creating Zulip user with handle: {handle}")
|
|
|
|
password = ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=40))
|
|
|
|
full_name = handle.split('@')[0]
|
|
|
|
payload = {
|
2024-10-05 10:38:36 +00:00
|
|
|
"email": handle,
|
|
|
|
"password": password,
|
2024-10-05 17:08:54 +00:00
|
|
|
"full_name": full_name
|
|
|
|
}
|
2024-10-05 10:38:36 +00:00
|
|
|
|
2024-10-05 17:08:54 +00:00
|
|
|
response = zulip_client.create_user(payload)
|
2024-10-05 10:38:36 +00:00
|
|
|
|
2024-10-05 17:08:54 +00:00
|
|
|
if response["result"] == "success":
|
2024-10-16 16:20:32 +00:00
|
|
|
print(f"Zulip user created for handle: {handle}")
|
2024-10-05 17:08:54 +00:00
|
|
|
return True
|
|
|
|
elif response["result"] == "error" and response["msg"] == f"Email '{handle}' already in use":
|
|
|
|
print(response["msg"] + ", this is okay.")
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
print(response["msg"])
|
|
|
|
return False
|
2024-10-05 10:38:36 +00:00
|
|
|
|
|
|
|
cur.execute("CREATE TABLE IF NOT EXISTS applications(instance TEXT PRIMARY KEY, client TEXT, secret TEXT, disabled BOOLEAN DEFAULT FALSE)")
|
|
|
|
|
|
|
|
def get_application(instance):
|
|
|
|
res = cur.execute("SELECT client, secret FROM applications WHERE instance = ?", [instance])
|
|
|
|
return res.fetchone();
|
|
|
|
|
|
|
|
def set_application(instance, client, secret):
|
|
|
|
res = cur.execute("INSERT INTO applications(instance, client, secret) values (?, ?, ?)", (instance, client, secret));
|
|
|
|
con.commit();
|
2024-10-05 06:49:36 +00:00
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
2024-10-05 10:38:36 +00:00
|
|
|
@app.get("/fedi-auth/")
|
2024-10-05 06:49:36 +00:00
|
|
|
def index():
|
|
|
|
return f"""
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<body>
|
|
|
|
<h1>Login to Pleroma Chat Using Handle</h1>
|
|
|
|
<p>
|
|
|
|
You can use this page to login to {os.environ['ZULIP']} using your
|
2024-10-05 17:08:54 +00:00
|
|
|
Pleroma, Akkoma or Mastodon handle. Format is <code>nickname@server.tld</code>.
|
2024-10-05 06:49:36 +00:00
|
|
|
</p>
|
2024-10-05 10:38:36 +00:00
|
|
|
<form action="/fedi-auth/login" method="post">
|
2024-10-05 06:49:36 +00:00
|
|
|
<label for="nickname">Fediverse handle</label>
|
|
|
|
<br>
|
|
|
|
<input type="email" id="nickname" name="nickname">
|
|
|
|
<button>login</button>
|
|
|
|
</form>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2024-10-05 10:38:36 +00:00
|
|
|
@app.post("/fedi-auth/login")
|
2024-10-05 06:49:36 +00:00
|
|
|
def login():
|
2024-10-05 10:38:36 +00:00
|
|
|
print("Login POST", flush=True)
|
2024-10-05 06:49:36 +00:00
|
|
|
instance = Address(addr_spec=request.form["nickname"]).domain
|
2024-10-05 10:38:36 +00:00
|
|
|
print(f"Instance is: {instance}.", flush=True)
|
2024-10-05 06:49:36 +00:00
|
|
|
try:
|
2024-10-05 10:38:36 +00:00
|
|
|
app = get_application(instance)
|
2024-10-05 06:49:36 +00:00
|
|
|
if app == None:
|
2024-10-05 17:08:54 +00:00
|
|
|
print(f"There is no OAuth application for {instance} so creating one.")
|
2024-10-05 06:49:36 +00:00
|
|
|
(client, secret) = Mastodon.create_app(
|
|
|
|
"zulip",
|
2024-10-05 17:08:54 +00:00
|
|
|
scopes=scopes,
|
2024-10-05 06:49:36 +00:00
|
|
|
redirect_uris=REDIRECT,
|
|
|
|
api_base_url=f"https://{instance}",
|
|
|
|
)
|
2024-10-05 10:38:36 +00:00
|
|
|
app = (client, secret)
|
|
|
|
set_application(instance, client, secret)
|
2024-10-05 06:49:36 +00:00
|
|
|
|
2024-10-05 10:38:36 +00:00
|
|
|
(client, secret) = app
|
2024-10-05 06:49:36 +00:00
|
|
|
masto = Mastodon(
|
|
|
|
client_id=client,
|
|
|
|
client_secret=secret,
|
|
|
|
api_base_url=f"https://{instance}",
|
|
|
|
)
|
2024-10-05 10:38:36 +00:00
|
|
|
print(f"Getting login URI for {instance}.", flush=True)
|
2024-10-05 06:49:36 +00:00
|
|
|
oauth = Mastodon.auth_request_url(
|
|
|
|
masto,
|
2024-10-05 17:08:54 +00:00
|
|
|
scopes=scopes,
|
2024-10-05 06:49:36 +00:00
|
|
|
force_login=True,
|
|
|
|
redirect_uris=REDIRECT,
|
|
|
|
state=instance,
|
|
|
|
)
|
2024-10-05 10:38:36 +00:00
|
|
|
print(f"Sending user to: {REDIRECT}", flush=True)
|
2024-10-05 06:49:36 +00:00
|
|
|
return redirect(oauth)
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
return Response("fail", status=400)
|
|
|
|
|
|
|
|
|
2024-10-05 10:38:36 +00:00
|
|
|
@app.get("/fedi-auth/callback")
|
2024-10-05 06:49:36 +00:00
|
|
|
def callback():
|
|
|
|
oauth = request.args.get("code")
|
|
|
|
instance = request.args.get("state")
|
2024-10-05 10:38:36 +00:00
|
|
|
print(f"oauth: {oauth is not None} instance: {instance is not None}")
|
2024-10-05 06:49:36 +00:00
|
|
|
if oauth != None and instance != None:
|
|
|
|
try:
|
2024-10-05 10:38:36 +00:00
|
|
|
app = get_application(instance)
|
2024-10-05 06:49:36 +00:00
|
|
|
if app != None:
|
|
|
|
masto = Mastodon(
|
|
|
|
client_id=app[0],
|
|
|
|
client_secret=app[1],
|
|
|
|
api_base_url=f"https://{instance}",
|
|
|
|
)
|
2024-10-05 17:08:54 +00:00
|
|
|
Mastodon.log_in(masto, code=oauth, scopes=scopes)
|
2024-10-05 06:49:36 +00:00
|
|
|
creds = Mastodon.account_verify_credentials(masto)
|
2024-10-05 17:08:54 +00:00
|
|
|
|
|
|
|
if hasattr(creds, "error"):
|
|
|
|
print(f"Verifying credentials for instance: {instance} error: {creds.error}")
|
|
|
|
return Response("fail", status=400)
|
|
|
|
|
2024-10-05 10:38:36 +00:00
|
|
|
handle = f"{creds.acct}@{instance}"
|
|
|
|
|
2024-10-05 17:19:56 +00:00
|
|
|
success = create_zulip_user(handle)
|
2024-10-05 17:08:54 +00:00
|
|
|
if not success:
|
|
|
|
return Response("fail", status=400)
|
2024-10-05 10:38:36 +00:00
|
|
|
|
2024-10-05 06:49:36 +00:00
|
|
|
token = jwt.encode(
|
2024-10-05 10:38:36 +00:00
|
|
|
{"email": handle},
|
2024-10-05 06:49:36 +00:00
|
|
|
SECRET,
|
|
|
|
algorithm="HS256",
|
|
|
|
)
|
|
|
|
return f"""
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<body>
|
2024-10-05 17:08:54 +00:00
|
|
|
<p>Please wait while you are logged in...</p>
|
2024-10-05 06:49:36 +00:00
|
|
|
<form name="zulip" action="{ZULIP}" method="post">
|
|
|
|
<input type="hidden" name="token" value={escape(token)}>
|
|
|
|
</form>
|
|
|
|
<script type="text/javascript">
|
|
|
|
document.zulip.submit();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
2024-10-05 10:38:36 +00:00
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
2024-10-05 06:49:36 +00:00
|
|
|
pass
|
2024-10-05 10:38:36 +00:00
|
|
|
print("Some field wasn't set.")
|
2024-10-05 06:49:36 +00:00
|
|
|
return Response("fail", status=400)
|