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
65 changes: 38 additions & 27 deletions docs-snippets/use-cases/automated-polling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import PubNub, { PubNubError } from '../../lib/types';
import PubNub from '../../lib/types';

const pubnub = new PubNub({
publishKey: 'demo',
Expand All @@ -7,25 +7,31 @@ const pubnub = new PubNub({
});

// snippet.automatedPollingPublishTriggeredPoll
const pollsByReaction: Record<string, { title: string; options: { id: number; text: string }[] }> = {
'\u{1F621}': {
title: 'Which team is playing dirtiest?',
options: [
{ id: 1, text: 'Home team' },
{ id: 2, text: 'Away team' },
],
},
'\u{1F389}': {
title: 'Whose fans are celebrating hardest?',
options: [
{ id: 1, text: 'Home team' },
{ id: 2, text: 'Away team' },
],
},
};
const pollsByReaction = new Map([
[
'\u{1F621}',
{
title: 'Which team is playing dirtiest?',
options: [
{ id: 1, text: 'Home team' },
{ id: 2, text: 'Away team' },
],
},
],
[
'\u{1F389}',
{
title: 'Whose fans are celebrating hardest?',
options: [
{ id: 1, text: 'Home team' },
{ id: 2, text: 'Away team' },
],
},
],
]);

async function openPollForReaction(reaction: string) {
const template = pollsByReaction[reaction];
async function openPollForReaction(reaction = '') {
const template = pollsByReaction.get(reaction);

if (!template) {
console.log('no poll is defined for', reaction);
Expand All @@ -46,10 +52,9 @@ async function openPollForReaction(reaction: string) {
});
console.log('triggered poll published at timetoken:', response.timetoken);
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(
`Publishing the triggered poll failed: ${error}.${
(error as PubNubError).status ? ` Additional information: ${(error as PubNubError).status}` : ''
}`,
`Publishing the triggered poll failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
);
}
}
Expand All @@ -73,17 +78,23 @@ function shouldOpenPoll() {
// snippet.end

// snippet.automatedPollingReceiveTrigger
type PollTrigger = { reaction: string };

const triggerSubscription = pubnub.channel('game.poll-triggers').subscription({ receivePresenceEvents: false });

triggerSubscription.onMessage = (event) => {
const trigger = event.message as PollTrigger;
const trigger = event.message;
const rawReaction =
typeof trigger === 'object' && trigger !== null && !Array.isArray(trigger) && 'reaction' in trigger
? trigger.reaction
: undefined;

if (typeof rawReaction !== 'string') return;

const reaction = rawReaction;

console.log('open a poll because fans keep tapping', trigger.reaction);
console.log('open a poll because fans keep tapping', reaction);

if (shouldOpenPoll()) {
void openPollForReaction(trigger.reaction);
void openPollForReaction(reaction);
}
};

Expand Down
261 changes: 215 additions & 46 deletions docs-snippets/use-cases/fan-behavior-management.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import PubNub, { PubNubError } from '../../lib/types';
import PubNub from '../../lib/types';

// Restricting what a fan may do is a server-side operation, so this client is
// configured with the keyset's secret key and runs on your own infrastructure.
Expand All @@ -16,75 +16,244 @@ const fanClient = new PubNub({
userId: 'fan-42',
});

// snippet.fanBehaviorStateStore
import fs from 'node:fs';

// A real token service tracks issued tokens and each fan's status in a database.
// This tutorial persists the same information in a JSON file next to the script,
// so `grant`, `mute`, `unmute`, and `ban` share state across separate `node server.js`
// invocations without needing a database just to run the tutorial.
const stateFilePath = new URL('./fan-state.json', import.meta.url);

function loadState() {
try {
return JSON.parse(fs.readFileSync(stateFilePath, 'utf8'));
} catch {
return {};
}
}

function saveState(state = {}) {
fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
}
// snippet.end

// snippet.fanBehaviorGrantChatAccess
try {
const token = await server.grantToken({
ttl: 60,
authorizedUserId: 'fan-42',
resources: {
channels: {
'game.chat': { read: true, write: true },
async function grantChatAccess(userId = '') {
const state = loadState();
const entry = state[userId] ?? { status: 'active', tokens: [] };

if (entry.status === 'banned') {
console.log(`${userId} is banned, so no token was issued`);
return;
}

const canWrite = entry.status !== 'muted';

try {
const token = await server.grantToken({
ttl: 60,
authorizedUserId: userId,
resources: {
channels: {
'game.chat': { read: true, write: canWrite },
},
},
},
});
console.log('token that allows reading and writing chat:', token);
} catch (error) {
console.error(
`Granting chat access failed: ${error}.${
(error as PubNubError).status ? ` Additional information: ${(error as PubNubError).status}` : ''
}`,
);
});

entry.status = entry.status ?? 'active';
entry.tokens = [...entry.tokens, { token, write: canWrite }];
state[userId] = entry;
saveState(state);

console.log(`token that allows ${canWrite ? 'reading and writing' : 'reading'} chat:`, token);
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Granting chat access failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
}
// snippet.end

// snippet.fanBehaviorMuteFan
try {
const token = await server.grantToken({
ttl: 15,
authorizedUserId: 'fan-42',
resources: {
channels: {
'game.chat': { read: true },
async function muteFan(userId = '') {
const state = loadState();
const entry = state[userId] ?? { status: 'active', tokens: [] };

if (entry.status === 'banned') {
console.log(`${userId} is already banned, so there is nothing left to mute`);
return;
}

const writableTokens = entry.tokens.filter((issued = { token: '', write: false }) => issued.write);
const stillValid = [];

for (const issued of writableTokens) {
try {
await server.revokeToken(issued.token);
console.log('revoked an outstanding writable token');
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Revoking a writable token failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
// Keep tracking a token you couldn't revoke, so the next mute or ban retries it.
stillValid.push(issued);
}
}

// Record the mute before issuing anything new, so a later grant can't hand out write access.
entry.status = 'muted';
entry.tokens = [...entry.tokens.filter((issued = { token: '', write: false }) => !issued.write), ...stillValid];
state[userId] = entry;
saveState(state);

if (stillValid.length > 0) {
console.error(`${stillValid.length} writable token(s) are still valid. Run mute again to retry revoking them.`);
}

try {
const token = await server.grantToken({
ttl: 15,
authorizedUserId: userId,
resources: {
channels: {
'game.chat': { read: true },
},
},
},
});
console.log('token that allows reading chat but not writing to it:', token);
} catch (error) {
console.error(
`Muting the fan failed: ${error}.${
(error as PubNubError).status ? ` Additional information: ${(error as PubNubError).status}` : ''
}`,
);
});

entry.tokens = [...entry.tokens, { token, write: false }];
state[userId] = entry;
saveState(state);

console.log('token that allows reading chat but not writing to it:', token);
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Muting the fan failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
}
// snippet.end

// snippet.fanBehaviorRevokeToken
try {
const response = await server.revokeToken('replace-with-the-token-to-revoke');
console.log('token revoked:', response);
} catch (error) {
console.error(
`Revoking the token failed: ${error}.${
(error as PubNubError).status ? ` Additional information: ${(error as PubNubError).status}` : ''
}`,
);
// snippet.fanBehaviorUnmuteFan
async function unmuteFan(userId = '') {
const state = loadState();
const entry = state[userId] ?? { status: 'active', tokens: [] };

// This is the one command that clears a ban as well as a mute. Calling it is always
// a deliberate decision by whoever operates server.js, never a side effect of anything
// else in this tutorial.
try {
const token = await server.grantToken({
ttl: 60,
authorizedUserId: userId,
resources: {
channels: {
'game.chat': { read: true, write: true },
},
},
});

entry.status = 'active';
entry.tokens = [...entry.tokens, { token, write: true }];
state[userId] = entry;
saveState(state);

console.log('token that allows reading and writing chat again:', token);
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Unmuting the fan failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
}
// snippet.end

// snippet.fanBehaviorApplyToken
fanClient.setToken('replace-with-the-token-your-server-returned');
// snippet.fanBehaviorBanFan
async function banFan(userId = '') {
const state = loadState();
const entry = state[userId] ?? { status: 'active', tokens: [] };

const stillValid = [];

for (const issued of entry.tokens) {
try {
await server.revokeToken(issued.token);
console.log('revoked an outstanding token');
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Revoking a token failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
// Keep tracking a token you couldn't revoke, so the next ban retries it.
stillValid.push(issued);
}
}

entry.status = 'banned';
entry.tokens = stillValid;
state[userId] = entry;
saveState(state);

if (stillValid.length > 0) {
console.error(
`${userId} is banned, but ${stillValid.length} token(s) are still valid. Run ban again to retry revoking them.`,
);
} else {
console.log(`${userId} is banned. Every outstanding token, read-only and writable, is now revoked.`);
}
}
// snippet.end

// snippet.fanBehaviorServerDispatch
const [, , command, userIdArgument] = process.argv;
const targetUserId = userIdArgument ?? 'fan-42';

if (command === 'grant') {
await grantChatAccess(targetUserId);
} else if (command === 'mute') {
await muteFan(targetUserId);
} else if (command === 'unmute') {
await unmuteFan(targetUserId);
} else if (command === 'ban') {
await banFan(targetUserId);
} else {
console.log('usage: node server.js <grant|mute|unmute|ban> [userId]');
}
// snippet.end

// snippet.fanBehaviorHandleAccessDenied
fanClient.addListener({
status: (event) => {
if (event.category === 'PNAccessDeniedCategory') {
if (event.category === 'PNConnectedCategory') {
console.log('connected to game.chat');
} else if (event.category === 'PNAccessDeniedCategory') {
console.log('this fan may no longer write to', event.affectedChannels);
}
},
});
// snippet.end

// snippet.fanBehaviorApplyToken
const suppliedToken = process.argv[2];

if (!suppliedToken) {
console.error('usage: node fan.js <token>');
process.exit(1);
}

fanClient.setToken(suppliedToken);
// snippet.end

// snippet.fanBehaviorSubscribeAndPublish
const subscription = fanClient.channel('game.chat').subscription();
subscription.subscribe();

try {
const response = await fanClient.publish({
channel: 'game.chat',
message: { text: 'Come on!' },
});
console.log('chat message published at timetoken:', response.timetoken);
} catch (error) {
const status = error instanceof Error && 'status' in error ? error.status : undefined;
console.error(`Publishing to game.chat failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
// snippet.end

// snippet.fanBehaviorInspectToken
const parsed = fanClient.parseToken('replace-with-the-token-to-inspect');

Expand Down
Loading
Loading