Discord bot that mirrors osu! mapping and group events into Discord channels.
It polls osu! APIs, stores events in SQLite, and dispatches formatted embeds to subscribed channels. It also exposes a minimal HTTP API for recent map/group event views.
- Map feed events:
nominatenomination_resetqualifydisqualifyrankunrank
- Group membership feed events:
- user add/remove history from osu! groups
- Per-channel feed subscriptions with filters:
- map rulesets (
osu,taiko,catch,mania) - map event types
- group ids
- map rulesets (
- Interactive setup via Discord components (
/setup-feed) - Persistent SQLite state (subscriptions, fetched events, cursor per channel/feed)
- HTTP API for recent events:
- View API with cursor pagination:
GET /api/events/map?limit=20&cursor=<eventId>GET /api/events/group?limit=20&cursor=<eventId>
- Scalar API reference UI (Swagger-like docs)
- View API with cursor pagination:
/setup-feed- Interactive flow to create or update a feed subscription in the current channel.
- This is the only supported creation path.
/unsubscribe-feed type:<map|group>- Removes subscription for the selected feed type in the current channel.
/feed-status- Shows active subscriptions and filters for the current channel.
The MappingFeed solution contains five projects:
| Project | Responsibility | Depends on |
|---|---|---|
MappingFeed.Common |
Shared configuration, entities, records, enums, and interfaces | None |
MappingFeed.Data |
EF Core, SQLite repositories, visibility rules, and database backfills | Common |
MappingFeed.Scraper |
osu! authentication, API access, and event fetching | Common |
MappingFeed.Discord |
Slash commands, setup interactions, event dispatch, and embeds | Common, Data |
MappingFeed.Web |
Application host, HTTP API, services, and background workers | Common, Data, Scraper, Discord |
MappingFeed.Web is the executable project. It starts the Discord bot and HTTP
API, registers the other projects, and runs the fetch, dispatch, and backfill
workers.
MappingFeed.Web/Workers/EventFetcherWorkerruns everyFeed.PollIntervalSecondsand invokesBeatmapEventsFetcherandGroupEventsFetcherfromMappingFeed.Scraper.- The fetchers use
IOsuApiService, implemented byMappingFeed.Scraper/Services/Osu/OsuApiService, to request events and related metadata from osu!. They parse the responses intoBeatmapsetEventandGroupEvententities and enrich them with map, user, group, and ruleset data. BeatmapEventServiceandGroupEventServiceremove events already present in the database. Their repositories inMappingFeed.Data/Repositoriesthen useMappingFeedDbContextto write the new entities to SQLite.MappingFeed.Web/Workers/FeedingDispatcherWorkerperiodically callsFeedEventsDispatcher. It loads the map and group subscriptions throughSubscribedFeedService, then routes each subscription toBeatmapEventsDispatcherorGroupEventsDispatcher.- Each dispatcher queries events with an
EventIdgreater than the subscription'sLastEventId. It applies the subscription's event type, ruleset, or group filters before usingFeedEmbedFactoryto build a Discord embed. - The dispatcher sends the message to the subscribed Discord channel through
NetCord's
RestClient.SubscribedFeedServiceadvancesLastEventIdafter a successful send or an intentional filter skip. If sending fails, the cursor stays unchanged and the next dispatch cycle retries the event.
- .NET SDK 10.0+
- A Discord bot token
- osu! OAuth app credentials (
client_id,client_secret)
Configuration is loaded from:
MappingFeed.Web/appsettings.json(optional)MappingFeed.Web/appsettings.{Environment}.json(optional)- Environment variables (override JSON)
Main sections:
DiscordToken(required)
OsuBaseUrl(defaulthttps://osu.ppy.sh)ClientId(required)ClientSecret(required)
FeedPollIntervalSeconds(default30)DispatchIntervalSeconds(configured value is clamped to minimum180in sender)EventsBatchSize(default25)DispatchBatchSize(configured value is clamped to max10in sender)ApiCacheMinutes(default10, clamped in API client to5..20)
Use MappingFeed.Web/appsettings.example.json as a template.
Run the following commands from the repository root.
- Create environment variables (recommended), for example:
export Discord__Token="<discord-token>"
export Osu__ClientId="<osu-client-id>"
export Osu__ClientSecret="<osu-client-secret>"
# optional:
export Osu__BaseUrl="https://osu.ppy.sh"- Run:
dotnet run --project MappingFeed.Web- Query recent events:
curl "http://localhost:5000/api/events/map?limit=10"
curl "http://localhost:5000/api/events/group?limit=10"Each response includes nextCursor. For next page, reuse it in the same endpoint as cursor=<nextCursor>.
- Open API docs UI:
xdg-open http://localhost:5000/scalar- Build:
dotnet buildThe Docker hosting files live in MappingFeed.Web. Change to that directory:
cd MappingFeed.Web- Copy the environment file:
cp .env.example .env- Fill required values in
.env:
DISCORD_TOKENOSU_CLIENT_IDOSU_CLIENT_SECRET
- Start:
docker compose up -d --buildSQLite is persisted through the volume defined in
MappingFeed.Web/docker-compose.yml:
- host:
${HOME}/.local/share/mappingfeed - container:
/root/.local/share/mappingfeed
- Bot currently requests only
Guildsgateway intent. - Slash commands only work in server channels.
- If a channel is inaccessible or not a text channel, dispatch is skipped for that subscription.
- Map event fetching uses osu! event type filters (
types[]) for only supported event kinds.