mirror of
https://github.com/moparisthebest/xeps
synced 2024-12-21 07:08:51 -05:00
Create tool to issue Call For Experience, similar to send-updates
This commit is contained in:
parent
a9fbfe2b07
commit
2a5de8c7c4
249
tools/issue-cfe.py
Normal file
249
tools/issue-cfe.py
Normal file
@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
import configparser
|
||||
import email.message
|
||||
import os
|
||||
import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from xeplib import (
|
||||
Status, load_xepinfos, choose,
|
||||
make_fake_smtpconn,
|
||||
interactively_extend_smtp_config,
|
||||
make_smtpconn,
|
||||
wraptext,
|
||||
)
|
||||
|
||||
|
||||
XEP_URL_PREFIX = "https://xmpp.org/extensions/"
|
||||
|
||||
SUBJECT_TEMPLATE = "Call for Experience: XEP-{info[number]:04d}: {info[title]}"
|
||||
|
||||
MAIL_TEMPLATE = """\
|
||||
The XEP Editor would like to Call for Experience with XEP-{info[number]:04d} \
|
||||
before presenting it to the {info[approver]} for advancing it to Final status.
|
||||
|
||||
|
||||
During the Call for Experience, please answer the following questions:
|
||||
|
||||
1. What software has XEP-{info[number]:04d} implemented? Please note that the \
|
||||
protocol must be implemented in at least two separate codebases (at least one \
|
||||
of which must be free or open-source software) in order to advance from Draft \
|
||||
to Final.
|
||||
|
||||
2. Have developers experienced any problems with the protocol as defined in \
|
||||
XEP-{info[number]:04d}? If so, please describe the problems and, if possible, \
|
||||
suggested solutions.
|
||||
|
||||
3. Is the text of XEP-{info[number]:04d} clear and unambiguous? Are more \
|
||||
examples needed? Is the conformance language (MAY/SHOULD/MUST) appropriate? \
|
||||
Have developers found the text confusing at all? Please describe any \
|
||||
suggestions you have for improving the text.
|
||||
|
||||
If you have any comments about advancing XEP-{info[number]:04d} from Draft \
|
||||
to Final, please provide them by the close of business on {enddate}. After \
|
||||
the Call for Experience, this XEP might undergo revisions to address feedback \
|
||||
received, after which it will be presented to the XMPP Council for voting to \
|
||||
a status of Final.
|
||||
|
||||
|
||||
You can review the specification here:
|
||||
|
||||
{url}
|
||||
|
||||
Please send all feedback to the standards@xmpp.org discussion list.
|
||||
"""
|
||||
|
||||
|
||||
def make_mail(info, enddate):
|
||||
kwargs = {
|
||||
"info": info,
|
||||
"url": "{}xep-{:04d}.html".format(
|
||||
XEP_URL_PREFIX,
|
||||
info["number"],
|
||||
),
|
||||
"enddate": enddate
|
||||
}
|
||||
|
||||
mail = email.message.EmailMessage()
|
||||
mail["Subject"] = SUBJECT_TEMPLATE.format(**kwargs)
|
||||
mail["XSF-XEP-Action"] = "CFE"
|
||||
mail["XSF-XEP-Title"] = info["title"]
|
||||
mail["XSF-XEP-Type"] = info["type"]
|
||||
mail["XSF-XEP-Status"] = info["status"].value
|
||||
mail["XSF-XEP-Url"] = kwargs["url"]
|
||||
mail["XSF-XEP-Approver"] = info["approver"]
|
||||
mail.set_content(
|
||||
wraptext(MAIL_TEMPLATE.format(**kwargs)),
|
||||
"plain",
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
return mail
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-c", "--config",
|
||||
metavar="FILE",
|
||||
type=argparse.FileType("r"),
|
||||
help="Configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
dest="ask_confirmation",
|
||||
default=True,
|
||||
action="store_false",
|
||||
help="'I trust this script to do the right thing and send emails"
|
||||
"without asking for confirmation.'"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--dry-run",
|
||||
dest="dry_run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Instead of sending emails, print them to stdout (implies -y)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--duration", "-d",
|
||||
metavar="DAYS",
|
||||
default=14,
|
||||
help="Duration of the CFE in days (default and at least: 14)",
|
||||
type=int,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--xeplist",
|
||||
default=None,
|
||||
type=argparse.FileType("r")
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-x", "--xep",
|
||||
type=int,
|
||||
dest="xeps",
|
||||
action="append",
|
||||
default=[],
|
||||
help="XEP(s) to issue a CFE for"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"to",
|
||||
nargs="+",
|
||||
help="The mail addresses to send the update mails to."
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
can_be_interactive = (
|
||||
os.isatty(sys.stdin.fileno()) and
|
||||
os.isatty(sys.stdout.fileno())
|
||||
)
|
||||
|
||||
if not args.xeps:
|
||||
print("nothing to do (use -x/--xep)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.duration < 14:
|
||||
print("duration must be at least 14", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
enddate = (datetime.utcnow() + timedelta(days=args.duration)).date()
|
||||
|
||||
if args.dry_run:
|
||||
args.ask_confirmation = False
|
||||
|
||||
if args.ask_confirmation and not can_be_interactive:
|
||||
print("Cannot ask for confirmation (stdio is not a TTY), but -y is",
|
||||
"not given either. Aborting.", sep="\n", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
if args.config is not None:
|
||||
config.read_file(args.config)
|
||||
|
||||
if args.xeplist is None:
|
||||
args.xeplist = open("build/xeplist.xml", "rb")
|
||||
|
||||
with args.xeplist as f:
|
||||
tree = etree.parse(f)
|
||||
accepted, _ = load_xepinfos(tree)
|
||||
|
||||
matched_xeps = []
|
||||
has_error = False
|
||||
for num in args.xeps:
|
||||
try:
|
||||
info = accepted[num]
|
||||
except KeyError:
|
||||
print("no such xep: {}".format(num), file=sys.stderr)
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
if info["status"] != Status.DRAFT:
|
||||
print("XEP-{:04d} is in {}, but must be Draft".format(
|
||||
num,
|
||||
info["status"].value,
|
||||
))
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
matched_xeps.append(info)
|
||||
|
||||
if has_error:
|
||||
sys.exit(1)
|
||||
|
||||
if args.dry_run:
|
||||
smtpconn = make_fake_smtpconn()
|
||||
else:
|
||||
if can_be_interactive:
|
||||
interactively_extend_smtp_config(config)
|
||||
|
||||
try:
|
||||
smtpconn = make_smtpconn(config)
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError) as exc:
|
||||
print("Missing configuration: {}".format(exc),
|
||||
file=sys.stderr)
|
||||
print("(cannot ask for configuration on stdio because it is "
|
||||
"not a TTY)", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
|
||||
try:
|
||||
for info in matched_xeps:
|
||||
mail = make_mail(info, enddate)
|
||||
mail["Date"] = datetime.utcnow()
|
||||
mail["From"] = config.get("smtp", "from")
|
||||
mail["To"] = args.to
|
||||
|
||||
if args.ask_confirmation:
|
||||
print()
|
||||
print("---8<---")
|
||||
print(mail.as_string())
|
||||
print("--->8---")
|
||||
print()
|
||||
choice = choose(
|
||||
"Send this email? [y]es, [n]o, [a]bort: ",
|
||||
"yna",
|
||||
eof="a",
|
||||
)
|
||||
|
||||
if choice == "n":
|
||||
continue
|
||||
elif choice == "a":
|
||||
print("Exiting on user request.", file=sys.stderr)
|
||||
sys.exit(4)
|
||||
|
||||
smtpconn.send_message(mail)
|
||||
finally:
|
||||
smtpconn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import configparser
|
||||
import getpass
|
||||
import itertools
|
||||
import email.message
|
||||
import os
|
||||
import smtplib
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
@ -12,7 +10,12 @@ from datetime import datetime
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from xeplib import Status, Action, load_xepinfos, choose
|
||||
from xeplib import (
|
||||
Status, Action, load_xepinfos, choose,
|
||||
make_fake_smtpconn,
|
||||
interactively_extend_smtp_config,
|
||||
make_smtpconn,
|
||||
)
|
||||
|
||||
|
||||
DESCRIPTION = """\
|
||||
@ -234,78 +237,6 @@ def make_nonproto_mail(action, info):
|
||||
return mail
|
||||
|
||||
|
||||
def get_or_ask(config, section, name, prompt):
|
||||
try:
|
||||
return config.get(section, name)
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
return input(prompt)
|
||||
|
||||
|
||||
def interactively_extend_smtp_config(config):
|
||||
try:
|
||||
host = config.get("smtp", "host")
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
host = input("SMTP server: ").strip()
|
||||
port = int(input("SMTP port (blank for 587): ").strip() or "587")
|
||||
user = input(
|
||||
"SMTP user (leave blank for anon): "
|
||||
).strip() or None
|
||||
if user:
|
||||
password = getpass.getpass()
|
||||
else:
|
||||
password = None
|
||||
else:
|
||||
port = config.getint("smtp", "port", fallback=587)
|
||||
user = config.get("smtp", "user", fallback=None)
|
||||
password = config.get("smtp", "password", fallback=None)
|
||||
|
||||
try:
|
||||
from_ = config.get("smtp", "from")
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
from_ = input("From address: ").strip()
|
||||
|
||||
if not config.has_section("smtp"):
|
||||
config.add_section("smtp")
|
||||
config.set("smtp", "host", host)
|
||||
config.set("smtp", "port", str(port))
|
||||
if user:
|
||||
config.set("smtp", "user", user)
|
||||
if password is None:
|
||||
password = getpass.getpass()
|
||||
config.set("smtp", "password", password)
|
||||
config.set("smtp", "from", from_)
|
||||
|
||||
|
||||
def make_smtpconn(config):
|
||||
host = config.get("smtp", "host")
|
||||
port = config.getint("smtp", "port")
|
||||
user = config.get("smtp", "user", fallback=None)
|
||||
password = config.get("smtp", "password", fallback=None)
|
||||
|
||||
conn = smtplib.SMTP(host, port)
|
||||
conn.starttls()
|
||||
if user is not None:
|
||||
conn.login(user, password)
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
def make_fake_smtpconn():
|
||||
class Fake:
|
||||
def send_message(self, mail):
|
||||
print("---8<---")
|
||||
print(mail.as_string())
|
||||
print("--->8---")
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
return Fake()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
|
@ -1,4 +1,9 @@
|
||||
import configparser
|
||||
import enum
|
||||
import getpass
|
||||
import itertools
|
||||
import smtplib
|
||||
import textwrap
|
||||
|
||||
import xml.dom.minidom
|
||||
|
||||
@ -169,3 +174,83 @@ def choose(prompt, options, *,
|
||||
continue
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def get_or_ask(config, section, name, prompt):
|
||||
try:
|
||||
return config.get(section, name)
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
return input(prompt)
|
||||
|
||||
|
||||
def interactively_extend_smtp_config(config):
|
||||
try:
|
||||
host = config.get("smtp", "host")
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
host = input("SMTP server: ").strip()
|
||||
port = int(input("SMTP port (blank for 587): ").strip() or "587")
|
||||
user = input(
|
||||
"SMTP user (leave blank for anon): "
|
||||
).strip() or None
|
||||
if user:
|
||||
password = getpass.getpass()
|
||||
else:
|
||||
password = None
|
||||
else:
|
||||
port = config.getint("smtp", "port", fallback=587)
|
||||
user = config.get("smtp", "user", fallback=None)
|
||||
password = config.get("smtp", "password", fallback=None)
|
||||
|
||||
try:
|
||||
from_ = config.get("smtp", "from")
|
||||
except (configparser.NoSectionError,
|
||||
configparser.NoOptionError):
|
||||
from_ = input("From address: ").strip()
|
||||
|
||||
if not config.has_section("smtp"):
|
||||
config.add_section("smtp")
|
||||
config.set("smtp", "host", host)
|
||||
config.set("smtp", "port", str(port))
|
||||
if user:
|
||||
config.set("smtp", "user", user)
|
||||
if password is None:
|
||||
password = getpass.getpass()
|
||||
config.set("smtp", "password", password)
|
||||
config.set("smtp", "from", from_)
|
||||
|
||||
|
||||
def make_smtpconn(config):
|
||||
host = config.get("smtp", "host")
|
||||
port = config.getint("smtp", "port")
|
||||
user = config.get("smtp", "user", fallback=None)
|
||||
password = config.get("smtp", "password", fallback=None)
|
||||
|
||||
conn = smtplib.SMTP(host, port)
|
||||
conn.starttls()
|
||||
if user is not None:
|
||||
conn.login(user, password)
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
def make_fake_smtpconn():
|
||||
class Fake:
|
||||
def send_message(self, mail):
|
||||
print("---8<---")
|
||||
print(mail.as_string())
|
||||
print("--->8---")
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
return Fake()
|
||||
|
||||
|
||||
def wraptext(text):
|
||||
return "\n".join(
|
||||
itertools.chain(
|
||||
*[textwrap.wrap(line) if line else [line] for line in text.split("\n")]
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user