Skip to content

Repository files navigation

HappyQOTD

HappyQOTD serves a daily quote through the classic Quote of the Day protocol over TCP and UDP, plus an ASP.NET Core HTTP API. The daily quote is selected by UTC date and persisted in SQLite, so all three services return the same quote.

Note

The included seed database contains only a small set of SFW sample quotes. It is separate from the quote collection used by the public instance.

Services

  • TCP Quote of the Day server on port 17
  • UDP Quote of the Day server on port 17
  • ASP.NET Core Minimal API
  • SQLite quote storage
  • Optional Mission Control telemetry

API

The local development URL is http://localhost:5269. The production deployment in the VPS compose stack listens on http://127.0.0.1:5193.

Method Path Authentication Description
GET / None Returns HappyQOTD.
GET /health/live None Liveness check.
GET /health/ready None Readiness check. Verifies that SQLite can be opened and queried.
GET /api/quotes/today None Returns the quote selected for the current UTC date. Returns 404 when no quote is available.
GET /api/quotes/random None Returns a random active quote. Returns 404 when no quote is available.
POST /api/quotes X-HappyQOTD-Key Creates one quote.
POST /api/quotes/batch X-HappyQOTD-Key Creates multiple quotes.
PUT /api/quotes/today X-HappyQOTD-Key Sets the current UTC day's quote.
DELETE /api/quotes/{id} X-HappyQOTD-Key Deletes a quote by numeric ID.

Write requests return 503 when no admin key is configured and 401 when the supplied key is missing or incorrect. Write endpoints are limited to 5 requests per minute. Read endpoints are limited to 120 requests per minute per client, except loopback clients.

Quote request

POST /api/quotes accepts:

{
  "text": "Readable code is a favor you leave for your future self.",
  "author": "",
  "source": "HappyQOTD original"
}

Quote validation rules:

  • text is required and may contain up to 1,000 characters.
  • author may contain up to 200 characters.
  • source may contain up to 300 characters.

POST /api/quotes/batch accepts an array of the same request objects:

[
  {
    "text": "First quote",
    "author": "Author",
    "source": "Example"
  },
  {
    "text": "Second quote"
  }
]

PUT /api/quotes/today accepts:

{
  "quoteId": 4
}

DELETE /api/quotes/{id} returns 204 when the quote is deleted and 404 when the ID does not exist.

Examples

Read today's quote:

curl http://localhost:5269/api/quotes/today

Create a quote:

curl -X POST http://localhost:5269/api/quotes \
  -H "Content-Type: application/json" \
  -H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}" \
  -d '{"text":"A useful quote","author":"An author","source":"Example"}'

Set today's quote:

curl -X PUT http://localhost:5269/api/quotes/today \
  -H "Content-Type: application/json" \
  -H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}" \
  -d '{"quoteId":4}'

Delete a quote:

curl -X DELETE http://localhost:5269/api/quotes/4 \
  -H "X-HappyQOTD-Key: ${HAPPYQOTD_ADMIN_API_KEY}"

When running in the Development environment, the app maps the generated OpenAPI document through ASP.NET Core OpenAPI.

Quote of the Day Protocol

The TCP server sends the current quote and closes the connection. The UDP server ignores the contents of each received datagram and replies with the current quote.

Query the local TCP server:

nc 127.0.0.1 17

Query the local UDP server:

printf 'hello\n' | nc -u -w2 127.0.0.1 17

Try the public instance

Query my live deployment over TCP:

nc qotd.kgivler.com 17

Or over UDP:

printf 'hello\n' | nc -u -w2 qotd.kgivler.com 17

Warning

This instance uses my personal, unfiltered quote collection. Most quotes are programming-related, but some contain strong or obscene language or discuss suicide and other sensitive subjects.

Both listeners use QOTD:ListenAddress and QOTD:Port. Set QOTD:ListenAddress to :: and QOTD:DualMode to true for an IPv4/IPv6 dual-stack deployment. Binding to port 17 may require NET_BIND_SERVICE or elevated privileges on Linux.

Set QOTD:TruncateQuoteResponses to true with QOTD:MaximumQuoteResponseCharacters set to 512 for RFC-style response lengths. Truncation can be disabled to return the complete formatted quote.

Daily Selection

The quote of the day is keyed by the current UTC date. The repository selects and stores a quote when that date has no existing selection. Subsequent HTTP, TCP, and UDP requests for that date return the stored selection, including after restarts.

Configuration

The main configuration sections are:

Setting Description
QotdSecurity:AdminApiKey API key required by quote write endpoints.
QOTD:ListenAddress TCP and UDP listener address. Use :: for IPv6 or dual-stack binding.
QOTD:Port TCP and UDP listener port. Defaults to 17.
QOTD:DualMode Enables IPv4 and IPv6 on an IPv6 listener. Use with ListenAddress set to ::.
QOTD:EnableTcpServer Enables the shared hosted TCP server.
QOTD:EnableUdpServer Enables the UDP server.
QOTD:TruncateQuoteResponses Truncates TCP and UDP responses when enabled.
QOTD:MaximumQuoteResponseCharacters Maximum response length when truncation is enabled. Defaults to 512.
QOTD:ApiBaseUrl Configured HTTP API base URL for integrations.
QOTD:MaxConcurrentConnections Maximum simultaneous TCP connections.
QOTD:RequestTimeoutSeconds Configured request-timeout value reserved for the TCP service.
QOTD:TelemetryIgnoredRemoteAddresses TCP and UDP client addresses excluded from served-quote telemetry.
MissionControl:Enabled Enables Mission Control telemetry.
MissionControl:BaseUrl Mission Control Gateway base URL.
MissionControl:ApiKey Mission Control source API key.
MissionControl:TimeoutMilliseconds Mission Control request timeout.

Environment variables use double underscores, for example:

QotdSecurity__AdminApiKey
QOTD__ListenAddress
QOTD__Port
QOTD__DualMode
QOTD__EnableTcpServer
QOTD__EnableUdpServer
QOTD__TruncateQuoteResponses
QOTD__MaximumQuoteResponseCharacters
QOTD__ApiBaseUrl
QOTD__MaxConcurrentConnections
QOTD__RequestTimeoutSeconds
QOTD__TelemetryIgnoredRemoteAddresses__0
MissionControl__Enabled
MissionControl__BaseUrl
MissionControl__ApiKey
MissionControl__TimeoutMilliseconds

Mission Control is disabled by default in appsettings.json. When enabled, its API key must match a configured Gateway event source and be at least 32 characters long.

Local Development

Requirements:

  • .NET 10 SDK
  • Local JoyfulReaperLib packages available through NuGet.config

Restore and run:

dotnet restore
dotnet run --project HappyQOTD/HappyQOTD.csproj

The Development launch profile uses http://localhost:5269 and enables the Development environment. Change QOTD:Port to a non-privileged port, such as 1717, when port 17 cannot be bound.

Build the project:

dotnet build HappyQOTD/HappyQOTD.csproj

Docker

Build the production image:

docker build -t happyqotd .

The VPS deployment uses host networking for HappyQOTD. Its relevant settings are:

network_mode: host

environment:
  ASPNETCORE_URLS: http://127.0.0.1:5193
  QOTD__ListenAddress: "::"
  QOTD__Port: 17
  QOTD__DualMode: "true"
  QOTD__EnableTcpServer: "true"
  QOTD__EnableUdpServer: "true"
  QOTD__TruncateQuoteResponses: "false"
  QOTD__MaximumQuoteResponseCharacters: 512
  MissionControl__Enabled: "true"
  MissionControl__BaseUrl: http://127.0.0.1:5190

depends_on:
  gateway:
    condition: service_healthy

With host networking, Docker ports mappings are not used. The host and provider firewalls must allow both TCP port 17 and UDP port 17.

The Gateway health check is /health/ready, which waits for its RabbitMQ connection and channel to be open. Use docker compose up -d --build after changing the image or application code. Use --force-recreate when container configuration or environment variables changed and the existing containers need to be replaced.

The container also needs a writable /app/Data volume for SQLite and NET_BIND_SERVICE when binding port 17:

volumes:
  - /var/lib/happyqotd/data:/app/Data

cap_drop:
  - ALL

cap_add:
  - NET_BIND_SERVICE

Mission Control Telemetry

When enabled, HappyQOTD publishes these event types:

  • happyqotd.service.started
  • happyqotd.qotd.served
  • happyqotd.api.qotd.served
  • happyqotd.api.quote.added
  • happyqotd.api.quotes.batch_added
  • happyqotd.api.randomquote.served
  • happyqotd.api.quote.deleted

The happyqotd.qotd.served payload includes a protocol field with values such as tcp and udp, allowing requests to be distinguished by transport.

Telemetry failures are logged and do not normally prevent the HTTP API, TCP server, or UDP server from serving quotes. The startup event is attempted once when the QOTD service starts; restarting HappyQOTD retries it.

Code Layout

License

HappyQOTD is licensed under the MIT License.

Copyright 2026 Kyle Givler.

About

Classic TCP Quote of the Day server on port 17 with a modern ASP.NET Core API and SQLite-backed daily quote persistence.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages