Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ jobs:

# -------------------------------------------------------------------------
# Job 3: Docker Build (~10 min)
# Builds full image (deps + server compile + runtime) and smoke tests it
# Builds full image (deps + server compile + runtime) and smoke tests it.
# This is the CI home of the daemon link+start smoke (the `dawn --help` run):
# the daemon unconditionally links ONNX Runtime + Piper, which aren't apt
# packages, so this image is the only place on stock runners the full binary
# links and runs. It replaces the server-config slice of the old pre-push
# preset-matrix smoke; the WEBUI-off / +email ML variants stay a developer
# release-time check (./tests/smoke_test.sh on hardware).
# -------------------------------------------------------------------------
docker-build:
needs: format-check
Expand All @@ -99,7 +105,7 @@ jobs:
- name: Build Docker image
run: docker build -t dawn .

- name: Smoke test
- name: Smoke test (daemon links + starts)
run: docker run --rm dawn --help

# -------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ set(DAWN_SOURCES
src/tools/html_parser.c
src/core/time_query_parser.c
src/core/iso8601.c
src/core/str_fuzzy.c
src/core/scheduled_context.c
src/tools/toml.c

# Config subsystem
Expand Down Expand Up @@ -651,11 +653,13 @@ if(ENABLE_WEBUI)
src/messaging/messaging_engine.c
src/messaging/messaging_engine_session.c
src/messaging/messaging_engine_channels.c
src/messaging/messaging_engine_read.c
src/messaging/messaging_engine_link.c
src/messaging/messaging_engine_inbound.c
src/messaging/messaging_telegram.c
src/messaging/messaging_sms.c
src/messaging/messaging_discord.c
src/messaging/messaging_discord_read.c
src/messaging/messaging_slack.c
src/messaging/messaging_split.c
src/messaging/messaging_format.c
Expand Down
84 changes: 83 additions & 1 deletion docs/MESSAGING_CHANNELS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ active window; see [SMS](#sms)). Send `/new` (Slack: ask the assistant to

## Discord

v1 is **DM-only, text-only**.
Conversations are **DM-only, text-only** (you talk to the bot in a DM). The bot
can additionally **read and summarize server channels** on request — see
[Reading & summarizing Discord channels](#reading--summarizing-discord-channels).

1. Create an application + bot at
[discord.com/developers/applications](https://discord.com/developers/applications).
Expand Down Expand Up @@ -129,6 +131,86 @@ local TTS/banner are suppressed for it.

---

## Reading & summarizing Discord channels

Friday can read recent messages from a Discord **server channel** and summarize
them — "catch me up on #general from today". This is **Discord-only** (Telegram
bots can't read channel history and SMS has no channels), and it is **read-only
and pull-based**: the bot reads a channel only when you (or a scheduled digest)
ask. It does not start watching server traffic.

### One-time setup — invite the bot to your server

Reading needs the bot to be a member of the server with permission to see the
channels:

1. In the [Developer Portal](https://discord.com/developers/applications) →
your app → **OAuth2 → URL Generator**, select the **bot** scope and the
**View Channels** + **Read Message History** permissions.
2. Open the generated URL and add the bot to your server.

The **Message Content Intent** you already enabled for DMs also covers reading
channel content over REST — no extra toggle.

### Using it

Just ask, from any chat with Friday (or the WebUI):

- "Catch me up on #general."
- "Summarize the dev-chat channel from this morning."
- "What did I miss in #announcements in the last 2 hours?"

Or summarize a **whole server** at once — every readable channel, each summarized:

- "Sum up everything on my server."
- "What's been happening across the server today?"

If the bot is in more than one server, name it ("…on My Server") or Friday will
ask which. A whole-server sweep is bounded — the most-recent messages per
channel, up to ~30 channels — and quiet channels are noted as having no recent
activity, so a busy server stays fast and a turn never runs away.
Comment thread
Copilot marked this conversation as resolved.

Friday matches the channel name against the channels the bot can see (fuzzy, so
"dev chat" finds `#dev-chat`). If the same name exists in more than one server,
she'll ask which server — you can also say it up front ("#general in My
Server").

**Time range.** You can bound how far back to read with a `since` (start) and an
optional `until` (end) — natural phrases ("today", "this morning", "last week",
"last month", "yesterday", "2 hours ago") or exact dates ("2026-06-01"):

- *"…since last week"* → from then up to now.
- *"…last month"* → roughly the last month up to now.
- *"…between June 1 and June 7"* → a closed range (use dates for precision;
vague phrases like "until yesterday" land at about now).
- No range given → the most-recent messages (up to 300 per channel; the newest
are kept if a channel is very busy).

> **Visibility note.** Friday can read **any** text/announcement channel the
> *bot* has been added to — which may include channels you personally aren't in.
> The only access control is which servers and channels you invite the bot to.
> Invite it only where you're comfortable having its contents summarized.

Reads are rate-limited per user and audited in the daemon log (who read which
channel, and how many messages) — never the message bodies, never the token.

### Scheduled digests

Because `read_channel` is a normal schedulable tool, you can ask for a recurring
digest delivered to a channel:

- "Every weekday at 8am, summarize #announcements and send it to my Discord DM."

The scheduler runs the read, the assistant summarizes, and the summary is
delivered to the channel you named (see [Delivering scheduled
events](#delivering-scheduled-events-to-a-channel)). Deliver digests to a **DM**
rather than back into a channel the digest itself reads, so tomorrow's digest
doesn't summarize today's. (Only the read-only actions — `read_channel`,
`read_server`, and `list_discord_channels` — may run from a schedule; `send` and
other actions require a live conversation.)

---

## Per-channel model & reasoning

Each channel's conversation carries its own LLM settings — the same
Expand Down
61 changes: 61 additions & 0 deletions include/core/scheduled_context.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* By contributing to this project, you agree to license your contributions
* under the GPLv3 (or any later version) or any future licenses chosen by
* the project author(s).
*
* Scheduled-origin context — a thread-local marker the scheduler sets around
* a scheduled tool-callback invocation so tools can (a) recover the owning
* user_id (the scheduler thread has no session, so session_get_command_context
* returns NULL and tools would otherwise fall back to user 1) and (b) gate
* which actions may run unattended. Layer 1 / Foundation: libc only, no DAWN
* state, always compiled.
*/
#ifndef SCHEDULED_CONTEXT_H
#define SCHEDULED_CONTEXT_H

#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
* @brief Mark the current thread as executing on behalf of @p user_id from a
* scheduled (briefing/task) context. Pair with scheduled_context_clear.
*
* @param user_id Owning user (> 0).
*/
void scheduled_context_set(int user_id);

/**
* @brief Clear the scheduled-origin marker for the current thread.
*/
void scheduled_context_clear(void);

/**
* @brief Query whether the current thread is in a scheduled-origin scope.
*
* @param user_id_out If non-NULL, set to the scheduled user_id (0 when not in
* a scheduled scope).
* @return true if currently executing in a scheduled-origin scope.
*/
bool scheduled_context_get(int *user_id_out);

#ifdef __cplusplus
}
#endif

#endif /* SCHEDULED_CONTEXT_H */
75 changes: 75 additions & 0 deletions include/core/str_fuzzy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* By contributing to this project, you agree to license your contributions
* under the GPLv3 (or any later version) or any future licenses chosen by
* the project author(s).
*
* Lightweight fuzzy name matcher — shared by surfaces that resolve a
* user-typed name (Home Assistant entities, Discord channels, ...) against
* a list of candidate names. Layer 1 / Foundation: depends only on libc,
* no DAWN state. Extracted from the byte-identical helper that previously
* lived static in homeassistant_service.c.
*/

#ifndef STR_FUZZY_H
#define STR_FUZZY_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

/* Score tiers returned by str_fuzzy_score(). */
#define STR_FUZZY_SCORE_EXACT 100 /* candidate == needle */
#define STR_FUZZY_SCORE_CONTAINS 80 /* candidate contains needle as substring */
#define STR_FUZZY_SCORE_TOKEN_BONUS 20 /* per whitespace-delimited needle token found */

/**
* @brief Lowercase @p src into @p dst (ASCII tolower), bounded by @p max_len.
*
* Always NUL-terminates within @p max_len. Intended to pre-normalize both
* candidate and needle strings before scoring.
*
* @param dst Output buffer.
* @param src Source string.
* @param max_len Size of @p dst (must be >= 1).
*/
void str_fuzzy_tolower(char *dst, const char *src, size_t max_len);

/**
* @brief Score how well @p needle_lower matches @p haystack_lower.
*
* Both arguments MUST already be lowercased (see str_fuzzy_tolower()).
* Tiered, allocation-free scoring:
* - 100 exact match
* - 80 haystack contains needle as a substring
* - +20 per whitespace-delimited needle token found in haystack
*
* The tiers are not mutually exclusive in spirit but return early: an exact
* or substring hit short-circuits before token scoring. Callers compare
* scores across candidates and apply their own acceptance threshold.
*
* @param haystack_lower Candidate name, lowercased.
* @param needle_lower User-supplied query, lowercased.
* @return Match score (0 = no overlap; higher is better).
*/
int str_fuzzy_score(const char *haystack_lower, const char *needle_lower);

#ifdef __cplusplus
}
#endif

#endif /* STR_FUZZY_H */
76 changes: 76 additions & 0 deletions include/messaging/messaging_discord_internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* By contributing to this project, you agree to license your contributions
* under the GPLv3 (or any later version) or any future licenses chosen by
* the project author(s).
*
* Discord driver — INTERNAL shared surface. Private to the
* src/messaging/messaging_discord*.c translation units; lets the
* channel-history READ path (messaging_discord_read.c) share the bot token,
* snowflake validation, and REST constants with the gateway/send core
* (messaging_discord.c) after the file was split for size. NOT a public API —
* external code uses include/messaging/messaging_discord.h.
*/
#ifndef MESSAGING_DISCORD_INTERNAL_H
#define MESSAGING_DISCORD_INTERNAL_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include "messaging/messaging_driver.h" /* messaging_read_window_t */

/* REST surface shared by the send core and the read path. */
#define DC_BOT_TOKEN_MAX 256
#define DC_REST_BASE_URL "https://discord.com/api/v10"
#define DC_USER_AGENT "DAWN-Discord/0.1 (libcurl, libwebsockets)"
#define DC_SNOWFLAKE_MAX_DIGITS 20 /* a 64-bit snowflake is <= 20 digits */

/* Bot token — defined in messaging_discord.c, read by the REST surfaces. */
extern char s_bot_token[DC_BOT_TOKEN_MAX];

/**
* @brief Validate a Discord snowflake: decimal digits only, <= 20 of them.
*
* Shared defense-in-depth gate before any id is interpolated into a REST URL,
* and a length cap so a downstream strtoull() can't silently saturate. Inline
* so both translation units get a copy without a cross-TU symbol.
*/
static inline bool dc_is_valid_snowflake(const char *s) {
if (!s || !s[0]) {
return false;
}
size_t i;
for (i = 0; s[i] != '\0'; i++) {
/* Fail fast on over-length input — don't walk an arbitrarily long
* attacker-controlled string just to reject it. */
if (i >= DC_SNOWFLAKE_MAX_DIGITS) {
return false;
}
if (s[i] < '0' || s[i] > '9') {
return false;
}
}
return true;
}

/* Read path — defined in messaging_discord_read.c, wired into the driver
* descriptor in messaging_discord.c. */
int dc_list_readable_channels(char **out_json);
int dc_read_history(const char *channel_id, const messaging_read_window_t *window, char **out_json);
void dc_invalidate_channel_cache(void); /* drop discovery cache (recover from miss) */
void dc_read_shutdown(void); /* free the read CURL handle + discovery cache */

#endif /* MESSAGING_DISCORD_INTERNAL_H */
Loading