mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-23 03:07:12 +00:00
rebuild the engine in Rust
Python was never the house language here: the mw-* desktop surface it came
from is 39 bash scripts and was exactly one Python script, the one this was
extracted from. What it cost was distribution, a widget dependency that
needed pipx and a Python floor, and 24ms of interpreter startup before a line
of the program ran.
The port is verified rather than asserted. Both implementations were run over
199,446 day-records, 26 jurisdictions across 21 years, with every field of
every record required to match; the JSON, the iCalendar and 480 renderings of
the text output are identical too. examples/dump.rs is the harness.
What the type system actually buys, which is less than the usual claim and
more than nothing:
- Level is an ordered enum, strictest first, so reads as 'a is
stricter' and relaxed_to() is the one place a feast can lift a fast. No
rule can get the direction backwards.
- Date is a newtype over the Julian Day Number and JulianDate is a separate
type, so a civil date and an ecclesiastical one cannot be mixed up. That
confusion is the single most common way calendar code goes wrong.
- Octets is a newtype in the iCalendar folder, because the bug it replaced
counted characters against a limit RFC 5545 states in octets.
- Movables is a struct, not a map, so a mistyped key is a compile error
rather than a None at the far end of the program.
Of the six defects an adversarial review found in the Python, exactly one had
a shape a type system would have caught. The rest were logic, and the tests
came with them.
The menologion is compiled in with include_str!, so the binary answers on its
own: 930KB, no runtime, 1.2ms to start against Python's 36ms.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! Fasting disciplines. One module per tradition.
|
||||
|
||||
pub mod orthodox;
|
||||
pub mod ruthenian;
|
||||
|
||||
use crate::calendars::{Date, Weekday};
|
||||
use crate::fasting::{Fast, Level};
|
||||
use crate::jurisdictions::{Jurisdiction, RuleKind};
|
||||
use crate::menologion::Rank;
|
||||
use crate::paschalion::Movables;
|
||||
use crate::seasons::Season;
|
||||
|
||||
/// What a fasting rule is handed. Deliberately small.
|
||||
pub struct Context<'a> {
|
||||
pub civil: Date,
|
||||
/// The ecclesiastical (month, day).
|
||||
pub fixed: (u32, u32),
|
||||
/// The ecclesiastical year, for resolving a fixed date back to a civil one.
|
||||
pub fixed_year: i32,
|
||||
pub weekday: Weekday,
|
||||
pub mv: &'a Movables,
|
||||
pub season: Option<Season>,
|
||||
pub feast: String,
|
||||
pub rank: Option<Rank>,
|
||||
pub jurisdiction: &'a Jurisdiction,
|
||||
}
|
||||
|
||||
impl Context<'_> {
|
||||
/// The civil date of an ecclesiastical date in this day's church year.
|
||||
pub fn civil_of_fixed(&self, month: u32, day: u32) -> Date {
|
||||
self.jurisdiction.civil_of_fixed(self.fixed_year, month, day)
|
||||
}
|
||||
|
||||
pub fn is_weekend(&self) -> bool {
|
||||
self.weekday.is_weekend()
|
||||
}
|
||||
|
||||
/// Is this day within `days` of the given start, inclusive?
|
||||
pub fn within(&self, start: Date, days: i32) -> bool {
|
||||
start <= self.civil && self.civil <= start + days
|
||||
}
|
||||
}
|
||||
|
||||
/// How a tradition names the levels it uses, and the glyph it shows them with.
|
||||
pub struct Vocabulary {
|
||||
pub label: fn(Level) -> &'static str,
|
||||
pub emoji: fn(Level) -> &'static str,
|
||||
}
|
||||
|
||||
pub struct Rule {
|
||||
pub name: &'static str,
|
||||
pub title: &'static str,
|
||||
pub vocabulary: Vocabulary,
|
||||
pub fast_for: fn(&Context) -> Fast,
|
||||
/// Whether this tradition legislates the fast as precept at all.
|
||||
pub has_obligation: bool,
|
||||
}
|
||||
|
||||
pub fn get(kind: RuleKind) -> &'static Rule {
|
||||
match kind {
|
||||
RuleKind::Orthodox => &orthodox::RULE,
|
||||
RuleKind::Ruthenian => &ruthenian::RULE,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//! The Typikon's fast, as the Orthodox Church keeps it.
|
||||
//!
|
||||
//! This is the discipline printed in the Typikon and reproduced, with small
|
||||
//! variations, by every Orthodox jurisdiction's published calendar. It binds no
|
||||
//! one in the canonical sense: the Orthodox Church legislates fasting as
|
||||
//! tradition rather than as precept, so every day here is reported as
|
||||
//! customary. That is not a softening. It is the difference between a rule of
|
||||
//! life and a law, and the Eastern Catholic churches, which do have law about
|
||||
//! this, are handled in their own module.
|
||||
//!
|
||||
//! A saint of vigil or polyeleos rank lifts the weekday discipline of a fast:
|
||||
//! to wine and oil inside the Great Fast, as far as fish in the three lesser
|
||||
//! fasts. That is why the printed calendars mark so many Wednesdays in the
|
||||
//! Nativity Fast for wine and oil, and it is the rank in the menologion that
|
||||
//! decides it.
|
||||
//!
|
||||
//! Where usages differ, this follows the common published rule and says so in
|
||||
//! the reason. The known divergences:
|
||||
//!
|
||||
//! - **Great Thursday** is given wine and oil here. Some typika keep xerophagy.
|
||||
//! - **The Exaltation of the Cross** and the **Beheading of the Forerunner**
|
||||
//! are given wine and oil. Several jurisdictions print them as strict.
|
||||
//! - **Lazarus Saturday** is given wine and oil; caviar is the traditional
|
||||
//! addition, which this vocabulary has no level for.
|
||||
//! - **Great Week** is kept at xerophagy on its weekdays whatever saint falls
|
||||
//! in it, because the days of the Passion outrank the menologion.
|
||||
//! - **Monastic practice**, and the Holy Mountain in particular, is stricter
|
||||
//! than any of this throughout.
|
||||
//!
|
||||
//! A calendar cannot settle those. Your bishop and your spiritual father can.
|
||||
|
||||
use super::{Context, Rule, Vocabulary};
|
||||
use crate::calendars::Weekday;
|
||||
use crate::fasting::{Fast, Level};
|
||||
use crate::menologion::Rank;
|
||||
use crate::seasons::Season;
|
||||
|
||||
fn label(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Strict => "no food until evening",
|
||||
Level::Xerophagy => "xerophagy \u{2014} no oil or wine",
|
||||
Level::WineOil => "wine and oil permitted",
|
||||
Level::Fish => "fish, wine and oil",
|
||||
Level::Dairy => "dairy and eggs; no meat",
|
||||
Level::None => "no fast",
|
||||
}
|
||||
}
|
||||
|
||||
fn emoji(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Strict => "\u{26d4}",
|
||||
Level::Xerophagy => "\u{1f96c}",
|
||||
Level::WineOil => "\u{1fad2}",
|
||||
Level::Fish => "\u{1f41f}",
|
||||
Level::Dairy => "\u{1f9c0}",
|
||||
Level::None => "\u{1f356}",
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a feast lifts a fast day.
|
||||
fn relaxed_by(rank: Option<Rank>) -> Option<Level> {
|
||||
match rank {
|
||||
Some(Rank::Great) => Some(Level::Fish),
|
||||
Some(Rank::Vigil) | Some(Rank::Polyeleos) => Some(Level::WineOil),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Relax `level` for the day's saint, never past `cap`, never tightening.
|
||||
fn for_feast(level: Level, ctx: &Context, cap: Level, reason: &str) -> Fast {
|
||||
let Some(target) = relaxed_by(ctx.rank) else {
|
||||
return Fast::customary(level, reason);
|
||||
};
|
||||
let lifted = level.relaxed_to(target, cap);
|
||||
if lifted == level {
|
||||
return Fast::customary(level, reason);
|
||||
}
|
||||
Fast::customary(lifted, &format!("{} \u{2014} the fast is relaxed", ctx.feast))
|
||||
}
|
||||
|
||||
pub fn fast_for(ctx: &Context) -> Fast {
|
||||
let md = ctx.fixed;
|
||||
let mv = ctx.mv;
|
||||
let weekend = ctx.is_weekend();
|
||||
|
||||
// The fast-free stretches, settled before anything else can override them.
|
||||
// A Friday inside Bright Week is not a fast day, and the ordinary Friday
|
||||
// rule further down would say otherwise.
|
||||
match ctx.season {
|
||||
Some(Season::BrightWeek) => return Fast::free("Bright Week \u{2014} no fasting"),
|
||||
Some(Season::PentecostWeek) => {
|
||||
return Fast::free("the week after Pentecost \u{2014} no fasting")
|
||||
}
|
||||
Some(Season::Christmastide) => {
|
||||
return Fast::free("the Nativity to the eve of Theophany")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if ctx.within(mv.publican_pharisee, 6) {
|
||||
// Fast-free precisely because the Pharisee boasted of fasting twice a week.
|
||||
return Fast::free("fast-free week (the Publican and the Pharisee)");
|
||||
}
|
||||
|
||||
// The Great Fast and Great Week.
|
||||
if matches!(ctx.season, Some(Season::GreatFast) | Some(Season::GreatWeek)) {
|
||||
let season = ctx.season.unwrap();
|
||||
// The Annunciation is tested before the strict days, not after. It can
|
||||
// land on Great Friday or Great Saturday, and when it does the feast
|
||||
// still lifts the total fast to wine and oil: it does not vanish.
|
||||
if md == (3, 25) {
|
||||
if ctx.civil == mv.great_friday || ctx.civil == mv.holy_saturday {
|
||||
return Fast::customary(
|
||||
Level::WineOil,
|
||||
"the Annunciation, on a day of the total fast",
|
||||
);
|
||||
}
|
||||
return Fast::customary(Level::Fish, "the Annunciation");
|
||||
}
|
||||
if ctx.civil == mv.palm_sunday {
|
||||
return Fast::customary(Level::Fish, "Palm Sunday");
|
||||
}
|
||||
if ctx.civil == mv.lazarus_saturday {
|
||||
return Fast::customary(Level::WineOil, "Lazarus Saturday");
|
||||
}
|
||||
if ctx.civil == mv.clean_monday {
|
||||
return Fast::customary(Level::Strict, "the first day of the Great Fast");
|
||||
}
|
||||
if ctx.civil == mv.great_friday {
|
||||
return Fast::customary(Level::Strict, "Great and Holy Friday");
|
||||
}
|
||||
if ctx.civil == mv.holy_saturday {
|
||||
// The one Saturday of the year kept without oil.
|
||||
return Fast::customary(Level::Xerophagy, "Great and Holy Saturday");
|
||||
}
|
||||
if ctx.civil == mv.great_thursday {
|
||||
return Fast::customary(Level::WineOil, "Great and Holy Thursday");
|
||||
}
|
||||
if season == Season::GreatWeek {
|
||||
return Fast::customary(
|
||||
Level::Xerophagy,
|
||||
&format!("a weekday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(
|
||||
Level::WineOil,
|
||||
&format!("Saturday and Sunday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
return for_feast(
|
||||
Level::Xerophagy,
|
||||
ctx,
|
||||
Level::WineOil,
|
||||
&format!("a weekday of the {}", season.name()),
|
||||
);
|
||||
}
|
||||
|
||||
// Cheesefare: meat is set aside for good, everything else stays,
|
||||
// Wednesday and Friday included.
|
||||
if ctx.season == Some(Season::CheesefareWeek) {
|
||||
return Fast::customary(Level::Dairy, "Cheesefare Week");
|
||||
}
|
||||
if ctx.civil == mv.meatfare {
|
||||
return Fast::free("Meatfare Sunday \u{2014} the last day of meat");
|
||||
}
|
||||
|
||||
// The Nativity Fast, which tightens for its last five days.
|
||||
if ctx.season == Some(Season::NativityFast) {
|
||||
if md == (12, 24) {
|
||||
// The eve is relaxed to wine and oil on a Saturday or Sunday,
|
||||
// because the Liturgy of St Basil is served with Vespers.
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "the eve of the Nativity, on a weekend");
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "the eve of the Nativity");
|
||||
}
|
||||
if md == (11, 21) {
|
||||
return Fast::customary(Level::Fish, "the Entrance of the Theotokos");
|
||||
}
|
||||
if md >= (12, 20) {
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) || weekend {
|
||||
return Fast::customary(Level::WineOil, "the forefeast of the Nativity");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::WineOil, "the forefeast of the Nativity");
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(Level::Fish, "Saturday and Sunday of the Nativity Fast");
|
||||
}
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) {
|
||||
return for_feast(Level::WineOil, ctx, Level::Fish, "Tuesday and Thursday of the Nativity Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::Fish, "Monday, Wednesday and Friday of the Nativity Fast");
|
||||
}
|
||||
|
||||
// The Apostles' Fast, kept the same way.
|
||||
if ctx.season == Some(Season::Apostles) {
|
||||
if weekend {
|
||||
return Fast::customary(Level::Fish, "Saturday and Sunday of the Apostles' Fast");
|
||||
}
|
||||
if matches!(ctx.weekday, Weekday::Tuesday | Weekday::Thursday) {
|
||||
return for_feast(Level::WineOil, ctx, Level::Fish, "Tuesday and Thursday of the Apostles' Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::Fish, "Monday, Wednesday and Friday of the Apostles' Fast");
|
||||
}
|
||||
|
||||
// The Dormition Fast, kept like the Great Fast.
|
||||
if ctx.season == Some(Season::Dormition) {
|
||||
if md == (8, 6) {
|
||||
return Fast::customary(Level::Fish, "the Transfiguration");
|
||||
}
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "Saturday and Sunday of the Dormition Fast");
|
||||
}
|
||||
return for_feast(Level::Xerophagy, ctx, Level::WineOil, "a weekday of the Dormition Fast");
|
||||
}
|
||||
|
||||
// The three fixed fast days outside any season.
|
||||
if md == (1, 5) {
|
||||
if weekend {
|
||||
return Fast::customary(Level::WineOil, "the eve of Theophany, on a weekend");
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "the eve of Theophany");
|
||||
}
|
||||
if md == (9, 14) {
|
||||
return Fast::customary(Level::WineOil, "the Exaltation of the Holy Cross");
|
||||
}
|
||||
if md == (8, 29) {
|
||||
return Fast::customary(Level::WineOil, "the Beheading of the Forerunner");
|
||||
}
|
||||
|
||||
// Ordinary Wednesdays and Fridays, which a feast can lift.
|
||||
if matches!(ctx.weekday, Weekday::Wednesday | Weekday::Friday) {
|
||||
if let Some(target) = relaxed_by(ctx.rank) {
|
||||
return Fast::customary(target, &format!("{} \u{2014} the fast is relaxed", ctx.feast));
|
||||
}
|
||||
return Fast::customary(Level::Xerophagy, "Wednesday and Friday");
|
||||
}
|
||||
|
||||
Fast::new(Level::None, "", None)
|
||||
}
|
||||
|
||||
pub static RULE: Rule = Rule {
|
||||
name: "orthodox",
|
||||
title: "the Typikon",
|
||||
vocabulary: Vocabulary { label, emoji },
|
||||
fast_for,
|
||||
// The Orthodox Church does not legislate the fast as precept, so nothing
|
||||
// here is reported as binding. See the module docs.
|
||||
has_obligation: false,
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
//! Byzantine Catholic particular law, alongside the tradition it sits on.
|
||||
//!
|
||||
//! The Eastern Catholic churches differ from the Orthodox here in a way worth
|
||||
//! being exact about: they have *law* on fasting, and it requires very much
|
||||
//! less than the Typikon describes. Under current Ruthenian particular law the
|
||||
//! Church binds only:
|
||||
//!
|
||||
//! - strict abstinence, no meat, fish, eggs or dairy, on the first day of the
|
||||
//! Great Fast and on Great and Holy Friday;
|
||||
//! - abstinence from meat on the Wednesdays and Fridays of the Great Fast.
|
||||
//!
|
||||
//! Everything else, the Apostles', Dormition and Nativity Fasts, the fuller
|
||||
//! keeping of the Great Fast, ordinary Fridays through the year, is encouraged
|
||||
//! and traditional and does **not** bind. So this module reports two things at
|
||||
//! once: the level at which the day is kept by tradition, and the minimum that
|
||||
//! actually obliges. A calendar that says "obligatory" where nothing obliges is
|
||||
//! worse than no calendar; one that reports only the binding minimum makes the
|
||||
//! obligatory days look laxer than the voluntary ones beside them.
|
||||
//!
|
||||
//! The three degrees this law knows map exactly onto the shared vocabulary: its
|
||||
//! strict abstinence is `WineOil`, since it does not regulate oil or wine, and
|
||||
//! its simple abstinence is `Dairy`, meat alone being set aside.
|
||||
//!
|
||||
//! **A known reading.** "The Wednesdays and Fridays of the Great Fast" is taken
|
||||
//! here to mean the Forty Days, not Great and Holy Week, which the calendar
|
||||
//! counts as its own season. Great and Holy Friday binds in its own right, so
|
||||
//! only Great and Holy Wednesday turns on the reading. If your eparchy reads it
|
||||
//! the other way, that Wednesday binds too.
|
||||
//!
|
||||
//! Source: the Byzantine Catholic Metropolitan Church of Pittsburgh. Other
|
||||
//! Eastern Catholic churches legislate their own; where this library maps them
|
||||
//! onto this ruleset it is an approximation, and the jurisdiction entry says so.
|
||||
|
||||
use super::{Context, Rule, Vocabulary};
|
||||
use crate::calendars::{Date, Weekday};
|
||||
use crate::fasting::{Fast, Level};
|
||||
use crate::menologion::Rank;
|
||||
use crate::seasons::Season;
|
||||
|
||||
fn label(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::WineOil => "no meat, fish, eggs or dairy",
|
||||
Level::Dairy => "no meat",
|
||||
Level::None => "no fast",
|
||||
// Levels this law makes no distinction at, kept legible if ever emitted.
|
||||
Level::Strict => "no food until evening",
|
||||
Level::Xerophagy => "no meat, fish, eggs or dairy",
|
||||
Level::Fish => "no meat",
|
||||
}
|
||||
}
|
||||
|
||||
fn emoji(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Strict | Level::Xerophagy | Level::WineOil => "\u{1f96c}",
|
||||
Level::Fish | Level::Dairy => "\u{1f41f}",
|
||||
Level::None => "\u{1f356}",
|
||||
}
|
||||
}
|
||||
|
||||
/// Vigil fasts are kept on the preceding Friday when they land on a weekend.
|
||||
///
|
||||
/// "If the vigils of Christmas or Theophany fall on Saturday or Sunday, the
|
||||
/// fasting and abstinence is done on the previous Friday."
|
||||
fn off_weekend(day: Date) -> Date {
|
||||
match day.weekday() {
|
||||
Weekday::Saturday => day - 1,
|
||||
Weekday::Sunday => day - 2,
|
||||
_ => day,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fast_for(ctx: &Context) -> Fast {
|
||||
let mv = ctx.mv;
|
||||
|
||||
// The two days the Church binds strictly.
|
||||
if ctx.civil == mv.clean_monday {
|
||||
return Fast::binding(Level::WineOil, "the first day of the Great Fast", Level::WineOil);
|
||||
}
|
||||
if ctx.civil == mv.great_friday {
|
||||
return Fast::binding(Level::WineOil, "Great and Holy Friday", Level::WineOil);
|
||||
}
|
||||
|
||||
// And the days it binds to abstinence from meat. Kept strictly by
|
||||
// tradition; binding only as far as meat.
|
||||
if ctx.season == Some(Season::GreatFast)
|
||||
&& matches!(ctx.weekday, Weekday::Wednesday | Weekday::Friday)
|
||||
{
|
||||
return Fast::binding(
|
||||
Level::WineOil,
|
||||
"Wednesday and Friday of the Great Fast",
|
||||
Level::Dairy,
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed vigils and the Exaltation, moved off a weekend where the rule says.
|
||||
for (when, label) in [
|
||||
(ctx.civil_of_fixed(9, 14), "the Exaltation of the Holy Cross"),
|
||||
(off_weekend(ctx.civil_of_fixed(12, 24)), "the vigil of the Nativity"),
|
||||
(off_weekend(ctx.civil_of_fixed(1, 5)), "the vigil of Theophany"),
|
||||
] {
|
||||
if ctx.civil == when {
|
||||
return Fast::customary(Level::WineOil, label);
|
||||
}
|
||||
}
|
||||
|
||||
// The fast-free stretches, which have to be settled before the ordinary
|
||||
// Wednesday and Friday rule below or they get overridden by it. Christmas
|
||||
// Day 2026 is a Friday, and telling you to abstain from meat on it would be
|
||||
// plainly wrong.
|
||||
match ctx.season {
|
||||
Some(Season::BrightWeek) => return Fast::free("Bright Week \u{2014} no fasting"),
|
||||
Some(Season::PentecostWeek) => {
|
||||
return Fast::free("the week after Pentecost \u{2014} no fasting")
|
||||
}
|
||||
Some(Season::Christmastide) => return Fast::free("Christmastide \u{2014} no fasting"),
|
||||
_ => {}
|
||||
}
|
||||
if ctx.within(mv.publican_pharisee, 6) {
|
||||
return Fast::free("fast-free week (the Publican and the Pharisee)");
|
||||
}
|
||||
|
||||
// The rest of the Great Fast and Great Week.
|
||||
if let Some(season @ (Season::GreatFast | Season::GreatWeek)) = ctx.season {
|
||||
if ctx.is_weekend() {
|
||||
return Fast::customary(Level::Dairy, season.name());
|
||||
}
|
||||
return Fast::customary(Level::WineOil, season.name());
|
||||
}
|
||||
|
||||
if ctx.season == Some(Season::CheesefareWeek) {
|
||||
return Fast::customary(Level::Dairy, "Cheesefare Week");
|
||||
}
|
||||
|
||||
// The three voluntary seasons.
|
||||
if let Some(season @ (Season::Apostles | Season::Dormition | Season::NativityFast)) =
|
||||
ctx.season
|
||||
{
|
||||
return Fast::customary(Level::Dairy, season.name());
|
||||
}
|
||||
|
||||
// A great feast releases the ordinary weekday abstinence.
|
||||
if matches!(ctx.weekday, Weekday::Wednesday | Weekday::Friday) {
|
||||
if ctx.rank == Some(Rank::Great) {
|
||||
return Fast::free(&format!("{} \u{2014} the fast is released", ctx.feast));
|
||||
}
|
||||
return Fast::customary(
|
||||
Level::Dairy,
|
||||
if ctx.weekday == Weekday::Friday { "Friday" } else { "Wednesday" },
|
||||
);
|
||||
}
|
||||
|
||||
Fast::new(Level::None, "", None)
|
||||
}
|
||||
|
||||
pub static RULE: Rule = Rule {
|
||||
name: "ruthenian",
|
||||
title: "Ruthenian particular law",
|
||||
vocabulary: Vocabulary { label, emoji },
|
||||
fast_for,
|
||||
has_obligation: true,
|
||||
};
|
||||
Reference in New Issue
Block a user