mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-09 04:17:37 +00:00
fix(monkeyd): make /monkeyd_crawl public and fix PDF font resolution
Advance the crawler submodule to the fix for the production failure "stat usr/share/fonts/...: no such file or directory": the PDF writer was handing fpdf a font path, and fpdf rewrote the absolute path into a working-directory-relative one. Font data is now passed as bytes, with a fallback font compiled into the binary when the host has none. The runtime image therefore no longer installs DejaVuSans, which also removes the font layer from the builder stage. /monkeyd_crawl becomes public. The host allowlist and the single in-flight export were already the controls that bound its cost; they now carry that job alone, so both are load-bearing.
This commit is contained in:
+2
-8
@@ -1,12 +1,6 @@
|
||||
FROM golang:1.26.5-alpine AS builder
|
||||
WORKDIR /src
|
||||
|
||||
# The monkeyd module renders PDFs with an embedded TrueType font, and the
|
||||
# runtime image below ships no fonts at all. DejaVuSans covers the Latin
|
||||
# Extended Additional block that Vietnamese diacritics live in; it is installed
|
||||
# here and copied into the final stage.
|
||||
RUN apk add --no-cache font-dejavu
|
||||
|
||||
# The monkeyd-crawler submodule is resolved through a `replace` directive, so
|
||||
# its go.mod must be present before `go mod download` can read the build list.
|
||||
# Only the module files are copied here, keeping this layer cached across
|
||||
@@ -26,9 +20,9 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
./cmd/server
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
# No fonts are installed here: the monkeyd module's PDF renderer falls back to a
|
||||
# font compiled into the binary when the host has none.
|
||||
COPY --from=builder /out/server /server
|
||||
# pdfout.FindFont() probes system font paths; this is one of the paths it knows.
|
||||
COPY --from=builder /usr/share/fonts/dejavu/DejaVuSans.ttf /usr/share/fonts/dejavu/DejaVuSans.ttf
|
||||
USER nonroot:nonroot
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/server"]
|
||||
|
||||
@@ -16,7 +16,7 @@ Atlas via long polling and an in-process cron scheduler.
|
||||
| `gold` | Gold paper trading (opt-in; VNAppMob SJC buy/sell VND/luong) |
|
||||
| `coin` | Crypto paper trading in USD (Binance -> Coinbase -> CoinGecko price fallback) |
|
||||
| `stats` | `/stats` (top commands), `/stats users`, `/stats user <username>`, `/stats cmd <command_name>` |
|
||||
| `monkeyd` | `/monkeyd_crawl <url>` — export a monkeydd.com novel as a PDF (admin-only) |
|
||||
| `monkeyd` | `/monkeyd_crawl <url>` — export a monkeydd.com novel as a PDF |
|
||||
|
||||
Disable modules with the `MODULES` environment variable.
|
||||
|
||||
@@ -138,24 +138,29 @@ crawling and rendering come from the
|
||||
[monkeyd-crawler](https://github.com/tiennm99/monkeyd-crawler) submodule; the
|
||||
module is the Telegram surface around it.
|
||||
|
||||
The command is admin-only, because one invocation makes hundreds of outbound
|
||||
requests spread over several minutes. Only `monkeydd.com` URLs are accepted —
|
||||
the extractor is written against that site's markup, and the allowlist also
|
||||
keeps the bot from being used to fetch arbitrary URLs. A missing scheme is
|
||||
filled in, so a pasted bare hostname works.
|
||||
The command is public, so any member of a chat can run it. One invocation makes
|
||||
hundreds of outbound requests spread over several minutes, so two things bound
|
||||
the cost and are deliberate: only `monkeydd.com` URLs are accepted, and exactly
|
||||
one export runs at a time across the whole bot. A missing scheme is filled in,
|
||||
so a pasted bare hostname works.
|
||||
|
||||
Exports run one at a time. The bot replies immediately that the export started,
|
||||
then sends the PDF when it is ready; a second request while one is in flight is
|
||||
told which novel is currently running. Requests are spaced out by the crawler,
|
||||
so the run is deliberately slow rather than aggressive.
|
||||
The host allowlist is not only about the parser — the extractor is written
|
||||
against that site's markup and would find nothing elsewhere — it also stops the
|
||||
command being used to make the bot fetch arbitrary URLs.
|
||||
|
||||
The bot replies immediately that the export started, then sends the PDF when it
|
||||
is ready; a second request while one is in flight is told which novel is
|
||||
currently running. Requests are spaced out by the crawler, so a run is
|
||||
deliberately slow rather than aggressive.
|
||||
|
||||
Raw pages are cached under the system temp directory, so re-exporting the same
|
||||
novel costs no requests. The cache is not pruned and a container restart clears
|
||||
it. Finished PDFs are deleted after upload. Telegram caps bot uploads at 50 MB;
|
||||
a larger book is reported instead of being sent.
|
||||
|
||||
The runtime image installs DejaVuSans, since the PDF embeds a font that covers
|
||||
Vietnamese diacritics and the distroless base ships none.
|
||||
The PDF embeds a font covering Vietnamese diacritics. The crawler prefers a
|
||||
system font and falls back to one compiled into the binary, so the runtime
|
||||
image needs no fonts installed.
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ func TestCommandDiscovery_AllPublicCommandsHaveSafeMetadata(t *testing.T) {
|
||||
"gold_sell": "<luong>",
|
||||
"lol": "[date]",
|
||||
"loldle": "[champion]",
|
||||
"monkeyd_crawl": "<url>",
|
||||
"random": "<option,...>",
|
||||
"stats": "[users | user <username> | cmd <command_name>]",
|
||||
"stock_events": "<ticker> [days]",
|
||||
|
||||
@@ -246,15 +246,24 @@ func TestCrawl_RepliesBusyWhileAnotherExportRuns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A Protected command must not respond at all to an unauthorized sender, or its
|
||||
// existence leaks.
|
||||
func TestCrawl_SilentForUnauthorizedSender(t *testing.T) {
|
||||
rb, _ := install(t, 999)
|
||||
rb.Bot.ProcessUpdate(context.Background(),
|
||||
testutil.NewPrivateMessage(12345, "/monkeyd_crawl "+testNovelURL))
|
||||
// The command is public: a sender who is neither owner nor admin must still get
|
||||
// a reply, including in a group.
|
||||
func TestCrawl_AvailableToAnySender(t *testing.T) {
|
||||
rb, r := install(t, 999)
|
||||
dir := t.TempDir()
|
||||
r.exporter = func(context.Context, export.Request) (*export.Result, error) {
|
||||
return stubPDF(t, dir, "Example-Novel.pdf", 2048), nil
|
||||
}
|
||||
|
||||
if calls := rb.Sent(); len(calls) != 0 {
|
||||
t.Errorf("expected no reply to an unauthorized sender, got %+v", calls)
|
||||
rb.Bot.ProcessUpdate(context.Background(),
|
||||
testutil.NewGroupMessage(-100, 12345, "/monkeyd_crawl "+testNovelURL))
|
||||
|
||||
calls := rb.Sent()
|
||||
if len(calls) == 0 {
|
||||
t.Fatal("a non-admin sender got no reply from a public command")
|
||||
}
|
||||
if last := calls[len(calls)-1]; last.Method != "sendDocument" {
|
||||
t.Errorf("last call = %q, want sendDocument", last.Method)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +276,8 @@ func TestRegistration(t *testing.T) {
|
||||
if cmd.Name != commandName {
|
||||
t.Errorf("Name = %q, want %q", cmd.Name, commandName)
|
||||
}
|
||||
if cmd.Visibility != modules.VisibilityProtected {
|
||||
t.Errorf("Visibility = %v, want Protected", cmd.Visibility)
|
||||
if cmd.Visibility != modules.VisibilityPublic {
|
||||
t.Errorf("Visibility = %v, want Public", cmd.Visibility)
|
||||
}
|
||||
if cmd.Parameters != "<url>" {
|
||||
t.Errorf("Parameters = %q, want %q", cmd.Parameters, "<url>")
|
||||
|
||||
@@ -39,9 +39,11 @@ func newModule(r *runner) modules.Module {
|
||||
Commands: []modules.Command{
|
||||
{
|
||||
Name: commandName,
|
||||
Visibility: modules.VisibilityProtected,
|
||||
// One invocation makes hundreds of outbound requests over
|
||||
// several minutes, so it stays off the public surface.
|
||||
Visibility: modules.VisibilityPublic,
|
||||
// Public despite being expensive: one invocation makes
|
||||
// hundreds of outbound requests over several minutes. The
|
||||
// host allowlist and the single in-flight export are what
|
||||
// bound the cost, so both are load-bearing here.
|
||||
Description: "Export a " + AllowedHostsHint + " novel as a PDF",
|
||||
Parameters: "<url>",
|
||||
Handler: r.handle,
|
||||
|
||||
Vendored
+1
-1
Submodule third_party/monkeyd-crawler updated: c925f10ae0...fb903e3262
Reference in New Issue
Block a user