There is a camp of nostr developers that believe spam filtering needs to be done by relays. Or at the very least by DVMs. I concur. In this way, once you configure what you want to see, it applies to all nostr clients.
But we are not there yet.
In the mean time we have ReplyGuy, and gossip needed some changes to deal with it.
Strategies in Short
- WEB OF TRUST: Only accept events from people you follow, or people they follow - this avoids new people entirely until somebody else that you follow friends them first, which is too restrictive for some people.
- TRUSTED RELAYS: Allow every post from relays that you trust to do good spam filtering.
- REJECT FRESH PUBKEYS: Only accept events from people you have seen before - this allows you to find new people, but you will miss their very first post (their second post must count as someone you have seen before, even if you discarded the first post)
- PATTERN MATCHING: Scan for known spam phrases and words and block those events, either on content or metadata or both or more.
- TIE-IN TO EXTERNAL SYSTEMS: Require a valid NIP-05, or other nostr event binding their identity to some external identity
- PROOF OF WORK: Require a minimum proof-of-work
All of these strategies are useful, but they have to be combined properly.
filter.rhai
Gossip loads a file called “filter.rhai” in your gossip directory if it exists. It must be a Rhai language script that meets certain requirements (see the example in the gossip source code directory). Then it applies it to filter spam.
This spam filtering code is being updated currently. It is not even on unstable yet, but it will be there probably tomorrow sometime. Then to master. Eventually to a release.
Here is an example using all of the techniques listed above:
// This is a sample spam filtering script for the gossip nostr
// client. The language is called Rhai, details are at:
// https://rhai.rs/book/
//
// For gossip to find your spam filtering script, put it in
// your gossip profile directory. See
// https://docs.rs/dirs/latest/dirs/fn.data_dir.html
// to find the base directory. A subdirectory "gossip" is your
// gossip data directory which for most people is their profile
// directory too. (Note: if you use a GOSSIP_PROFILE, you'll
// need to put it one directory deeper into that profile
// directory).
//
// This filter is used to filter out and refuse to process
// incoming events as they flow in from relays, and also to
// filter which events get/ displayed in certain circumstances.
// It is only run on feed-displayable event kinds, and only by
// authors you are not following. In case of error, nothing is
// filtered.
//
// You must define a function called 'filter' which returns one
// of these constant values:
// DENY (the event is filtered out)
// ALLOW (the event is allowed through)
// MUTE (the event is filtered out, and the author is
// automatically muted)
//
// Your script will be provided the following global variables:
// 'caller' - a string that is one of "Process",
// "Thread", "Inbox" or "Global" indicating
// which part of the code is running your
// script
// 'content' - the event content as a string
// 'id' - the event ID, as a hex string
// 'kind' - the event kind as an integer
// 'muted' - if the author is in your mute list
// 'name' - if we have it, the name of the author
// (or your petname), else an empty string
// 'nip05valid' - whether nip05 is valid for the author,
// as a boolean
// 'pow' - the Proof of Work on the event
// 'pubkey' - the event author public key, as a hex
// string
// 'seconds_known' - the number of seconds that the author
// of the event has been known to gossip
// 'spamsafe' - true only if the event came in from a
// relay marked as SpamSafe during Process
// (even if the global setting for SpamSafe
// is off)
fn filter() {
// Show spam on global
// (global events are ephemeral; these won't grow the
// database)
if caller=="Global" {
return ALLOW;
}
// Block ReplyGuy
if name.contains("ReplyGuy") || name.contains("ReplyGal") {
return DENY;
}
// Block known DM spam
// (giftwraps are unwrapped before the content is passed to
// this script)
if content.to_lower().contains(
"Mr. Gift and Mrs. Wrap under the tree, KISSING!"
) {
return DENY;
}
// Reject events from new pubkeys, unless they have a high
// PoW or we somehow already have a nip05valid for them
//
// If this turns out to be a legit person, we will start
// hearing their events 2 seconds from now, so we will
// only miss their very first event.
if seconds_known <= 2 && pow < 25 && !nip05valid {
return DENY;
}
// Mute offensive people
if content.to_lower().contains(" kike") ||
content.to_lower().contains("kike ") ||
content.to_lower().contains(" nigger") ||
content.to_lower().contains("nigger ")
{
return MUTE;
}
// Reject events from muted people
//
// Gossip already does this internally, and since we are
// not Process, this is rather redundant. But this works
// as an example.
if muted {
return DENY;
}
// Accept if the PoW is large enough
if pow >= 25 {
return ALLOW;
}
// Accept if their NIP-05 is valid
if nip05valid {
return ALLOW;
}
// Accept if the event came through a spamsafe relay
if spamsafe {
return ALLOW;
}
// Reject the rest
DENY
}