def configure(): canonical_upstream = cfg("dispatch.sr.ht::gitlab", "canonical-upstream") upstreams = [k for k in cfgkeys("dispatch.sr.ht::gitlab") if k not in [ "enabled", "canonical-upstream", canonical_upstream, ]] return render_template("gitlab/select-instance.html", canonical_upstream=canonical_upstream, upstreams=upstreams, instance_name=lambda inst: cfg("dispatch.sr.ht::gitlab", inst).split(":")[0])
def inject(): git_user = cfg("man.sr.ht", "git-user") origin = urlparse(cfg("man.sr.ht", "origin")) return { "repo_uri": lambda user=None, wiki=None: ("{}@{}:{}".format( git_user.split(":")[0], origin.netloc, "~{}/{}".format( user, wiki) if user and wiki else "root")), }
def _unsubscribe(dest, mail): sender = parseaddr(mail["From"]) user = User.query.filter(User.email == sender[1]).one_or_none() if user: sub = Subscription.query.filter( Subscription.list_id == dest.id, Subscription.user_id == user.id).one_or_none() else: sub = Subscription.query.filter( Subscription.list_id == dest.id, Subscription.email == sender[1]).one_or_none() list_addr = dest.owner.canonical_name + "/" + dest.name message = None if sub is None: reply = MIMEText("""Hi {}! We got your request to unsubscribe from {}, but we did not find a subscription from your email. If you continue to receive undesirable emails from this list, please reply to this email for support.""".format(sender[0] or sender[1], list_addr)) else: db.session.delete(sub) reply = MIMEText("""Hi {}! You have been successfully unsubscribed from the {} mailing list. If you wish to re-subscribe, send an email to: {}+subscribe@{} Feel free to reply to this email if you have any questions.""".format( sender[0] or sender[1], list_addr, list_addr, cfg("lists.sr.ht", "posting-domain"))) reply["To"] = mail["From"] reply["From"] = "mailer@" + cfg("lists.sr.ht", "posting-domain") reply["In-Reply-To"] = mail["Message-ID"] reply["Subject"] = "Re: " + (mail.get("Subject") or "Your subscription request") reply["Reply-To"] = "{} <{}>".format(cfg("sr.ht", "owner-name"), cfg("sr.ht", "owner-email")) print(reply.as_string(unixfrom=True)) smtp = smtplib.SMTP(smtp_host, smtp_port) smtp.ehlo() smtp.starttls() smtp.login(smtp_user, smtp_password) smtp.sendmail(smtp_user, [sender[1]], reply.as_string(unixfrom=True)) smtp.quit() db.session.commit()
def _forward(dest, mail): domain = cfg("lists.sr.ht", "posting-domain") list_name = "{}/{}".format(dest.owner.canonical_name, dest.name) list_unsubscribe = list_name + "+unsubscribe@" + domain list_subscribe = list_name + "+subscribe@" + domain for overwrite in [ "List-Unsubscribe", "List-Subscribe", "List-Archive", "List-Post", "List-ID", "Sender" ]: del mail[overwrite] mail["List-Unsubscribe"] = ( "<mailto:{}?subject=unsubscribe>".format(list_unsubscribe)) mail["List-Subscribe"] = ( "<mailto:{}?subject=subscribe>".format(list_subscribe)) mail["List-Archive"] = "<{}/{}>".format(cfg("lists.sr.ht", "origin"), list_name) mail["List-Post"] = "<mailto:{}@{}>".format(list_name, domain) mail["List-ID"] = "{} <{}@{}>".format(dest.name, list_name, domain) mail["Sender"] = "{} <{}@{}>".format(list_name, list_name, domain) # TODO: Encrypt emails smtp = smtplib.SMTP(smtp_host, smtp_port) smtp.ehlo() smtp.starttls() smtp.login(smtp_user, smtp_password) froms = mail.get_all('From', []) tos = mail.get_all('To', []) ccs = mail.get_all('Cc', []) recipients = set([a[1] for a in getaddresses(froms + tos + ccs)]) for sub in dest.subscribers: to = sub.email if sub.user: to = sub.user.email if to in recipients: print(to + " is already copied, skipping") continue print("Forwarding message to " + to) smtp.sendmail(smtp_user, [to], mail.as_string(unixfrom=True, maxheaderlen=998)) smtp.quit()
def gitlab_redirect(upstream, return_to): gl_authorize_url = f"https://{upstream}/oauth/authorize" gl_client = cfg("dispatch.sr.ht::gitlab", upstream, default=None) if not gl_client: return redirect(url_for("gitlab_no_instance")) [instance_name, client_id, secret] = gl_client.split(":") parameters = { "client_id": client_id, "scope": "api", "state": return_to, "response_type": "code", "redirect_uri": _root + url_for("gitlab_callback", upstream=upstream), } return redirect("{}?{}".format(gl_authorize_url, urlencode(parameters)))
def invoice_POST(invoice_id): invoice = Invoice.query.filter(Invoice.id == invoice_id).one_or_none() if not invoice: abort(404) if (invoice.user_id != current_user.id and current_user.user_type != UserType.admin): abort(401) valid = Validation(request) bill_to = valid.optional("address-to") if not bill_to: bill_to = "~" + invoice.user.username bill_from = [l for l in [ cfg("meta.sr.ht::billing", "address-line1", default=None), cfg("meta.sr.ht::billing", "address-line2", default=None), cfg("meta.sr.ht::billing", "address-line3", default=None), cfg("meta.sr.ht::billing", "address-line4", default=None) ] if l] # Split bill_to to first row (rendered as heading) and others [bill_from_head, *bill_from_tail] = bill_from or [None] html = render_template("billing-invoice-pdf.html", invoice=invoice, amount=f"${invoice.cents / 100:.2f}", source=invoice.source, created=invoice.created.strftime("%Y-%m-%d"), valid_thru=invoice.valid_thru.strftime("%Y-%m-%d"), bill_to=bill_to, bill_from_head=bill_from_head, bill_from_tail=bill_from_tail) pdf = HTML(string=html).write_pdf() filename = f"invoice_{invoice.id}.pdf" headers = [('Content-Disposition', f'attachment; filename="{filename}"')] return Response(pdf, mimetype="application/pdf", headers=headers)
def svg_page(jobs): name = request.args.get("name", default=cfg("sr.ht", "site-name")) job = (get_jobs(jobs).filter( Job.status.in_( [JobStatus.success, JobStatus.failed, JobStatus.timeout])).first()) if not job: badge = badge_unknown.replace("__NAME__", name) elif job.status == JobStatus.success: badge = badge_success.replace("__NAME__", name) else: badge = badge_failure.replace("__NAME__", name) return Response(badge, mimetype="image/svg+xml", headers={ "Cache-Control": "no-cache", "ETag": hashlib.sha1(badge.encode()).hexdigest(), })
def __init__(self): super().__init__("meta.sr.ht", __name__, oauth_service=MetaOAuthService(), oauth_provider=MetaOAuthProvider()) from metasrht.blueprints.api import register_api from metasrht.blueprints.auth import auth from metasrht.blueprints.billing import billing from metasrht.blueprints.invites import invites from metasrht.blueprints.keys import keys from metasrht.blueprints.oauth_exchange import oauth_exchange from metasrht.blueprints.oauth_web import oauth_web from metasrht.blueprints.privacy import privacy from metasrht.blueprints.profile import profile from metasrht.blueprints.security import security self.register_blueprint(auth) self.register_blueprint(invites) self.register_blueprint(keys) self.register_blueprint(oauth_exchange) self.register_blueprint(oauth_web) self.register_blueprint(privacy) self.register_blueprint(profile) self.register_blueprint(security) register_api(self) if cfg("meta.sr.ht::billing", "enabled") == "yes": self.register_blueprint(billing) @self.context_processor def inject(): return {'UserType': UserType} @self.login_manager.user_loader def load_user(username): # TODO: Session tokens return User.query.filter(User.username == username).first()
def confirm_account(token): if current_user: return redirect(onboarding_redirect) user = User.query.filter(User.confirmation_hash == token).one_or_none() if not user: return render_template("already-confirmed.html", redir=onboarding_redirect) if user.new_email: user.confirmation_hash = None audit_log("email updated", "{} became {}".format(user.email, user.new_email)) user.email = user.new_email user.new_email = None db.session.commit() elif user.user_type == UserType.unconfirmed: user.confirmation_hash = None user.user_type = UserType.active_non_paying audit_log("account created") db.session.commit() login_user(user, remember=True) if cfg("meta.sr.ht::billing", "enabled") == "yes": return redirect(url_for("billing.billing_initial_GET")) metrics.meta_confirmations.inc() return redirect(onboarding_redirect)
def gitlab_callback(upstream): code = request.args.get("code") state = request.args.get("state") gl_client = cfg("dispatch.sr.ht::gitlab", upstream, default=None) if not gl_client: abort(400) [instance_name, client_id, secret] = gl_client.split(":") resp = requests.post(f"https://{upstream}/oauth/token", headers={"Accept": "application/json"}, data={ "grant_type": "authorization_code", "client_id": client_id, "client_secret": secret, "code": code, "redirect_uri": _root + url_for("gitlab_callback", upstream=upstream), }) if resp.status_code != 200: # TODO: Proper error page print(resp.text) return "An error occured" json = resp.json() access_token = json.get("access_token") auth = GitLabAuthorization() auth.user_id = current_user.id auth.oauth_token = access_token auth.upstream = upstream db.session.add(auth) db.session.commit() return redirect(state)
def privacy_GET(): owner = {'name': cfg("sr.ht", "owner-name"), 'email': cfg("sr.ht", "owner-email")} return render_template("privacy.html", pgp_key_id=site_key_id, owner=owner)
import metasrht.webhooks from metasrht.oauth import MetaOAuthService, MetaOAuthProvider from metasrht.types import User, UserType from prometheus_client import make_wsgi_app from srht.config import cfg from srht.database import DbSession from srht.flask import SrhtFlask from urllib.parse import quote_plus from werkzeug.wsgi import DispatcherMiddleware db = DbSession(cfg("meta.sr.ht", "connection-string")) db.init() class MetaApp(SrhtFlask): def __init__(self): super().__init__("meta.sr.ht", __name__, oauth_service=MetaOAuthService(), oauth_provider=MetaOAuthProvider()) from metasrht.blueprints.api import register_api from metasrht.blueprints.auth import auth from metasrht.blueprints.billing import billing from metasrht.blueprints.invites import invites from metasrht.blueprints.keys import keys from metasrht.blueprints.oauth_exchange import oauth_exchange from metasrht.blueprints.oauth_web import oauth_web from metasrht.blueprints.privacy import privacy from metasrht.blueprints.profile import profile from metasrht.blueprints.security import security
from listssrht.filters import diffstat, format_body, post_address from listssrht.oauth import ListsOAuthService from srht.config import cfg from srht.database import DbSession from srht.flask import SrhtFlask from urllib.parse import quote db = DbSession(cfg("lists.sr.ht", "connection-string")) db.init() class ListsApp(SrhtFlask): def __init__(self): super().__init__("lists.sr.ht", __name__, oauth_service=ListsOAuthService()) self.url_map.strict_slashes = False from listssrht.blueprints.archives import archives from listssrht.blueprints.user import user self.register_blueprint(archives) self.register_blueprint(user) @self.context_processor def inject(): return { "diffstat": diffstat, "format_body": format_body, "post_address": post_address, "quote": quote, }
from github import Github from flask import Blueprint, redirect, request, render_template, url_for, abort from flask_login import current_user from jinja2 import Markup from uuid import UUID, uuid4 from srht.database import Base, db from srht.config import cfg from srht.flask import icon, csrf_bypass from srht.validation import Validation from dispatchsrht.tasks import TaskDef from dispatchsrht.tasks.github.auth import GitHubAuthorization from dispatchsrht.tasks.github.auth import githubloginrequired from dispatchsrht.tasks.github.auth import submit_build from dispatchsrht.types import Task _root = cfg("dispatch.sr.ht", "origin") _builds_sr_ht = cfg("builds.sr.ht", "origin", default=None) _github_client_id = cfg("dispatch.sr.ht::github", "oauth-client-id", default=None) _github_client_secret = cfg("dispatch.sr.ht::github", "oauth-client-secret", default=None) class GitHubPRToBuild(TaskDef): name = "github_pr_to_build" enabled = bool(_github_client_id and _github_client_secret and _builds_sr_ht) def description():
from flask import Blueprint, current_app, request from flask import render_template, abort from flask_login import current_user import requests from srht.config import cfg from srht.flask import paginate_query from srht.search import search from scmsrht.repos.repository import RepoVisibility from sqlalchemy import or_ public = Blueprint('public', __name__) meta_uri = cfg("meta.sr.ht", "origin") @public.route("/") def index(): if current_user: Repository = current_app.Repository repos = (Repository.query .filter(Repository.owner_id == current_user.id) .filter(Repository.visibility != RepoVisibility.autocreated) .order_by(Repository.updated.desc()) .limit(10)).all() return render_template("dashboard.html", repos=repos) return render_template("index.html") @public.route("/~<username>") @public.route("/~<username>/") def user_index(username): User = current_app.User user = User.query.filter(User.username == username).first()
from dispatchsrht.app import app from srht.config import cfg, cfgi import os app.static_folder = os.path.join(os.getcwd(), "static") if __name__ == '__main__': app.run(host=cfg("dispatch.sr.ht", "debug-host"), port=cfgi("dispatch.sr.ht", "debug-port"), debug=True)
from srht.config import cfg from srht.database import db from srht.flask import SrhtFlask from srht.oauth import AbstractOAuthService from todosrht import urls, filters from todosrht.types import EventType from todosrht.types import TicketAccess, TicketStatus, TicketResolution from todosrht.types import User client_id = cfg("todo.sr.ht", "oauth-client-id") client_secret = cfg("todo.sr.ht", "oauth-client-secret") class TodoOAuthService(AbstractOAuthService): def __init__(self): super().__init__(client_id, client_secret, user_class=User) class TodoApp(SrhtFlask): def __init__(self): super().__init__("todo.sr.ht", __name__, oauth_service=TodoOAuthService()) self.url_map.strict_slashes = False from todosrht.blueprints.html import html from todosrht.blueprints.tracker import tracker from todosrht.blueprints.ticket import ticket self.register_blueprint(html) self.register_blueprint(tracker) self.register_blueprint(ticket)
from flask import Blueprint, Response, render_template, request, redirect from flask_login import current_user from metasrht.audit import audit_log from metasrht.types import User, PGPKey from metasrht.email import send_email from srht.config import cfg from srht.database import db from srht.flask import loginrequired from srht.validation import Validation privacy = Blueprint('privacy', __name__) site_key = cfg("mail", "pgp-pubkey", None) site_key_id = cfg("mail", "pgp-key-id", None) @privacy.route("/privacy") @loginrequired def privacy_GET(): owner = {'name': cfg("sr.ht", "owner-name"), 'email': cfg("sr.ht", "owner-email")} return render_template("privacy.html", pgp_key_id=site_key_id, owner=owner) @privacy.route("/privacy/pubkey") def privacy_pubkey_GET(): if site_key: with open(site_key, "r") as f: pubkey = f.read() else: pubkey = '' return Response(pubkey, mimetype="text/plain")
from todosrht.types import Event, EventType, EventNotification from todosrht.types import Tracker, Ticket, TicketStatus, TicketAccess from todosrht.types import Label, TicketLabel from todosrht.urls import tracker_url from srht.config import cfg from srht.database import db from srht.flask import paginate_query, loginrequired from srht.validation import Validation from datetime import datetime from sqlalchemy.orm import subqueryload tracker = Blueprint("tracker", __name__) name_re = re.compile(r"^([a-z][a-z0-9_.-]*?)+$") smtp_user = cfg("mail", "smtp-user", default=None) smtp_from = cfg("mail", "smtp-from", default=None) notify_from = cfg("todo.sr.ht", "notify-from", default=smtp_from) @tracker.route("/tracker/create") @loginrequired def create_GET(): return render_template("tracker-create.html") @tracker.route("/tracker/create", methods=["POST"]) @loginrequired def create_POST(): valid = Validation(request) name = valid.require("tracker_name", friendly_name="Name")
from srht.config import cfg, cfgi from srht.database import DbSession, db if not hasattr(db, "session"): db = DbSession(cfg("lists.sr.ht", "connection-string")) import listssrht.types db.init() from listssrht.types import Email, List, User, Subscription, ListAccess import email import email.utils import io import json import smtplib from celery import Celery from datetime import datetime from email import policy from email.mime.text import MIMEText from email.utils import parseaddr, getaddresses from unidiff import PatchSet dispatch = Celery("lists.sr.ht", broker=cfg("lists.sr.ht", "redis")) smtp_host = cfg("mail", "smtp-host", default=None) smtp_port = cfgi("mail", "smtp-port", default=None) smtp_user = cfg("mail", "smtp-user", default=None) smtp_password = cfg("mail", "smtp-password", default=None) def _forward(dest, mail): domain = cfg("lists.sr.ht", "posting-domain") list_name = "{}/{}".format(dest.owner.canonical_name, dest.name)
import os from srht.email import send_email, lookup_key import html.parser import pystache from srht.config import cfg, cfgi from flask_login import current_user origin = cfg("todo.sr.ht", "origin") def notify(sub, template, subject, headers, **kwargs): encrypt_key = None if sub.email: to = sub.email elif sub.user: to = sub.user.email encrypt_key = lookup_key(sub.user.username, sub.user.oauth_token) else: return # TODO with open(os.path.join(os.path.dirname(__file__), "emails", template)) as f: body = html.unescape( pystache.render(f.read(), { 'user': current_user, 'root': origin, **kwargs })) send_email(body, to, subject, encrypt_key=encrypt_key, **headers)
from srht.config import cfg from srht.oauth import AbstractOAuthService from listssrht.types import User client_id = cfg("lists.sr.ht", "oauth-client-id") client_secret = cfg("lists.sr.ht", "oauth-client-secret") class ListsOAuthService(AbstractOAuthService): def __init__(self): super().__init__(client_id, client_secret, user_class=User)
from alembic import op import sqlalchemy as sa import sqlalchemy_utils as sau from enum import Enum from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, Session as BaseSession, relationship from srht.config import cfg import requests try: from tqdm import tqdm except ImportError: def tqdm(iterable): yield from iterable metasrht = cfg("meta.sr.ht", "origin") Session = sessionmaker() Base = declarative_base() class UserType(Enum): unconfirmed = "unconfirmed" active_non_paying = "active_non_paying" active_free = "active_free" active_paying = "active_paying" active_delinquent = "active_delinquent" admin = "admin" class User(Base): __tablename__ = 'user' id = sa.Column(sa.Integer, primary_key=True)
meta_scopes = { 'profile': 'profile information', 'audit': 'audit log', 'keys': 'SSH and PGP keys', } meta_access = { 'profile': 'read', 'audit': 'read', 'keys': 'read', } meta_aliases = {"meta.sr.ht": None} for key in cfgkeys("meta.sr.ht::aliases"): meta_aliases[key] = cfg("meta.sr.ht::aliases", key) class MetaOAuthProvider(AbstractOAuthProvider): def get_alias(self, client_id): return meta_aliases.get(client_id) def resolve_scope(self, scope): if scope.client_id: client = (OAuthClient.query.filter( OAuthClient.client_id == scope.client_id)).one_or_none() if not client: raise Exception('Unknown client ID {}'.format(scope.client_id)) scope.client = client else: scope.client = None
import subprocess from srht.database import db from srht.config import cfg from mansrht.types import Wiki import shutil import re import os repos_path = cfg("man.sr.ht", "repo-path") def validate_name(valid, owner, wiki_name): if not valid.ok: return None valid.expect(re.match(r'^[a-z._-][a-z0-9._-]*$', wiki_name), "Name must match [a-z._-][a-z0-9._-]*", field="name") existing = (Wiki.query.filter(Wiki.owner_id == owner.id).filter( Wiki.name.ilike(wiki_name)).first()) valid.expect(not existing, "This name is already in use.", field="name") return None def create_wiki(valid, owner): wiki_name = valid.require("name", friendly_name="Name") if not valid.ok: return None wiki = Wiki() wiki.name = wiki_name wiki.owner_id = owner.id
import os import srht.email import html.parser import pystache from srht.config import cfg, cfgi from flask_login import current_user origin = cfg("meta.sr.ht", "origin") owner_name = cfg("sr.ht", "owner-name") site_name = cfg("sr.ht", "site-name") def send_email(template, *args, encrypt_key=None, headers={}, **kwargs): with open(os.path.join(os.path.dirname(__file__), "emails", template)) as f: body = html.parser.HTMLParser().unescape(\ pystache.render(f.read(), { 'owner-name': owner_name, 'site-name': site_name, 'user': current_user, 'root': origin, **kwargs })) srht.email.send_email(body, *args, encrypt_key=encrypt_key, **headers)
def _subscribe(dest, mail): sender = parseaddr(mail["From"]) user = User.query.filter(User.email == sender[1]).one_or_none() if user: perms = dest.account_permissions sub = Subscription.query.filter( Subscription.list_id == dest.id, Subscription.user_id == user.id).one_or_none() else: perms = dest.nonsubscriber_permissions sub = Subscription.query.filter( Subscription.list_id == dest.id, Subscription.email == sender[1]).one_or_none() list_addr = dest.owner.canonical_name + "/" + dest.name message = None # TODO: User-specific/email-specific overrides if ListAccess.browse not in perms: reply = MIMEText("""Hi {}! We got your request to subscribe to {}, but unfortunately subscriptions to this list are restricted. Your request has been disregarded.{} """.format(sender[0] or sender[1], list_addr, (""" However, you are permitted to post mail to this list at this address: {}@{}""".format(list_addr, cfg("lists.sr.ht", "posting-domain")) if ListAccess.post in perms else ""))) elif sub is None: reply = MIMEText("""Hi {}! Your subscription to {} is confirmed! To unsubscribe in the future, send an email to this address: {}+unsubscribe@{} Feel free to reply to this email if you have any questions.""".format( sender[0] or sender[1], list_addr, list_addr, cfg("lists.sr.ht", "posting-domain"))) sub = Subscription() sub.user_id = user.id if user else None sub.list_id = dest.id sub.email = sender[1] if not user else None db.session.add(sub) else: reply = MIMEText("""Hi {}! We got an email asking to subscribe you to the {} mailing list. However, it looks like you're already subscribed. To unsubscribe, send an email to: {}+unsubscribe@{} Feel free to reply to this email if you have any questions.""".format( sender[0] or sender[1], list_addr, list_addr, cfg("lists.sr.ht", "posting-domain"))) reply["To"] = mail["From"] reply["From"] = "mailer@" + cfg("lists.sr.ht", "posting-domain") reply["In-Reply-To"] = mail["Message-ID"] reply["Subject"] = "Re: " + (mail.get("Subject") or "Your subscription request") reply["Reply-To"] = "{} <{}>".format(cfg("sr.ht", "owner-name"), cfg("sr.ht", "owner-email")) print(reply.as_string(unixfrom=True)) smtp = smtplib.SMTP(smtp_host, smtp_port) smtp.ehlo() smtp.starttls() smtp.login(smtp_user, smtp_password) smtp.sendmail(smtp_user, [sender[1]], reply.as_string(unixfrom=True)) smtp.quit() db.session.commit()
from todosrht.app import app from srht.config import cfg, cfgi import os app.static_folder = os.path.join(os.getcwd(), "static") if __name__ == '__main__': app.run(host=cfg("todo.sr.ht", "debug-host"), port=cfgi("todo.sr.ht", "debug-port"), debug=True)
from flask import Blueprint, render_template, request from flask_login import current_user from metasrht.types import User, UserAuthFactor, FactorType from metasrht.email import send_email from metasrht.audit import audit_log from metasrht.webhooks import UserWebhook from srht.config import cfg from srht.database import db from srht.flask import loginrequired from srht.validation import Validation, valid_url profile = Blueprint('profile', __name__) site_name = cfg("sr.ht", "site-name") @profile.route("/profile") @loginrequired def profile_GET(): return render_template("profile.html") @profile.route("/profile", methods=["POST"]) @loginrequired def profile_POST(): valid = Validation(request) user = User.query.filter(User.id == current_user.id).one() email = valid.optional("email", user.email) email = email.strip()
import sqlalchemy_utils as sau from dispatchsrht.tasks.gitlab.common import GitLabAuthorization from dispatchsrht.tasks.gitlab.common import gitlabloginrequired from dispatchsrht.tasks.gitlab.common import submit_gitlab_build from dispatchsrht.tasks import TaskDef from dispatchsrht.types import Task from flask import Blueprint, redirect, render_template, request, url_for from jinja2 import Markup from srht.config import cfg, cfgb, cfgkeys from srht.database import Base, db from srht.flask import icon, csrf_bypass from srht.oauth import current_user, loginrequired from srht.validation import Validation from uuid import UUID, uuid4 _root = cfg("dispatch.sr.ht", "origin") _builds_sr_ht = cfg("builds.sr.ht", "origin", default=None) _gitlab_enabled = cfgb("dispatch.sr.ht::gitlab", "enabled", default=False) if _gitlab_enabled: from gitlab import Gitlab class GitLabMRToBuild(TaskDef): name = "gitlab_mr_to_build" enabled = bool(_gitlab_enabled and _builds_sr_ht) def description(): return (icon("gitlab") + Markup(" GitLab merge requests ") + icon("caret-right") + Markup(" builds.sr.ht jobs"))