a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm
105
fork

Configure Feed

Select the types of activity you want to include in your feed.

README.md

@atcute/bluesky-moderation#

interpret Bluesky content moderation labels and user preferences.

npm install @atcute/bluesky-moderation

evaluates posts, profiles, lists, and other content against moderation labels, mutes, blocks, and keyword filters to determine how they should be displayed.

usage#

basic flow#

  1. fetch user preferences and labeler definitions
  2. run moderation functions on content
  3. get display restrictions for your UI context
import {
	DisplayContext,
	getDisplayRestrictions,
	interpretLabelerDefinitions,
	moderatePost,
	type ModerationPreferences,
} from '@atcute/bluesky-moderation';

// 1. set up preferences (see "loading preferences" below)
const prefs: ModerationPreferences = { ... };
const labelDefs = interpretLabelerDefinitions(labelers);

// 2. moderate content
const decision = moderatePost(post, {
	viewerDid: 'did:plc:...',
	prefs,
	labelDefs,
});

// 3. get display restrictions for your context
const ui = getDisplayRestrictions(decision, DisplayContext.ContentList);

if (ui.filters.length > 0) {
	// don't show this post in feeds
}

if (ui.blurs.length > 0) {
	// hide behind a content warning

	if (ui.noOverride) {
		// don't allow user to reveal
	}
}

if (ui.alerts.length > 0 || ui.informs.length > 0) {
	// show warning badges
}

display contexts#

use different contexts depending on where content appears:

// content in feeds/lists
getDisplayRestrictions(decision, DisplayContext.ContentList);

// content in expanded view
getDisplayRestrictions(decision, DisplayContext.ContentView);

// images/videos in content
getDisplayRestrictions(decision, DisplayContext.ContentMedia);

// profile in lists
getDisplayRestrictions(decision, DisplayContext.ProfileList);

// profile in expanded view
getDisplayRestrictions(decision, DisplayContext.ProfileView);

// profile avatar/banner
getDisplayRestrictions(decision, DisplayContext.ProfileMedia);

loading preferences#

import {
	interpretLabelerDefinitions,
	interpretMutedWordPreferences,
	LabelPreference,
	type ModerationPreferences,
} from '@atcute/bluesky-moderation';

// fetch user preferences
const { data } = await rpc.get('app.bsky.actor.getPreferences', {});

const prefs: ModerationPreferences = {
	adultContentEnabled: false,
	globalLabelPrefs: {},
	prefsByLabelers: {},
	keywordFilters: [],
	hiddenPosts: [],
	temporaryMutes: [],
};

for (const pref of data.preferences) {
	switch (pref.$type) {
		case 'app.bsky.actor.defs#adultContentPref':
			prefs.adultContentEnabled = pref.enabled;
			break;

		case 'app.bsky.actor.defs#contentLabelPref':
			// map visibility to LabelPreference
			const labelPref =
				pref.visibility === 'hide'
					? LabelPreference.Hide
					: pref.visibility === 'warn'
						? LabelPreference.Warn
						: LabelPreference.Ignore;

			if (pref.labelerDid) {
				prefs.prefsByLabelers[pref.labelerDid] ??= { labelPrefs: {} };
				prefs.prefsByLabelers[pref.labelerDid].labelPrefs[pref.label] = labelPref;
			} else {
				prefs.globalLabelPrefs[pref.label] = labelPref;
			}
			break;

		case 'app.bsky.actor.defs#mutedWordsPref':
			prefs.keywordFilters = interpretMutedWordPreferences(pref);
			break;

		case 'app.bsky.actor.defs#hiddenPostsPref':
			prefs.hiddenPosts = pref.items;
			break;
	}
}

// fetch labeler definitions
const { data: labelerData } = await rpc.get('app.bsky.labeler.getServices', {
	params: { dids: [...labelerDids], detailed: true },
});

const labelDefs = interpretLabelerDefinitions(
	labelerData.views.filter((v) => v.$type === 'app.bsky.labeler.defs#labelerViewDetailed'),
);

moderating different content types#

import {
	moderateFeedGenerator,
	moderateList,
	moderateNotification,
	moderatePost,
	moderateProfile,
} from '@atcute/bluesky-moderation';

const postDecision = moderatePost(post, opts);
const profileDecision = moderateProfile(profile, opts);
const listDecision = moderateList(list, opts);
const feedDecision = moderateFeedGenerator(feed, opts);
const notifDecision = moderateNotification(notification, opts);