Friend List and Status Feed
Using Subscription Management features and the Presence service, you can create simple friend graphs and a status message feed. The friend graph is a simple follow/followers model. For anything more complex, such as a true multi-degree graph or an application that requires querying or traversing graphs, consider a specialized implementation that uses a graph database such as Neo4J or other more sophisticated data mechanisms; these types of use cases are fairly uncommon.
The essential feature required to implement this is the Channel Group feature, which can also be thought of as a Subscribe Group. A Channel Group groups channels into a persistent collection of channels that you can modify dynamically, and it allows you to subscribe to all these channels by subscribing to the group. Learn More about Channel Groups here.
Channel Group's multiplexing capabilities enables us to create friend lists and status feeds. The diagram and the steps to implement follow:
User Identification
All Presence features use the User ID that's set on PubNub client initialization for tracking that client, which typically is the user. Refer to Managing User IDs to learn more.
User Channels
Every user will need two channels:
- A channel the user publishes for their status messages.
- A channel the user subscribes to, to indicate they're online. This channel will be unique for each user. For convenience, we'll use user-[letter], like user-a or user-b, as the unique identifier.
We'll follow this naming convention:
ch-user-a-status
(publish)ch-user-a-present
(subscribe)
These channels will be added into Channel Groups to enable the monitoring and aggregation of both status messages into a feed, and presence for online status. It's not required to do both if you only want to support friend lists/presence or you only want to support status feeds.
- JavaScript
- Swift
- Objective-C
- Java
- C#
- Python
// Publish a status message
pubnub.publish(
{
channel: "ch-user-a-status",
message: {
author: "user-a",
status: "I am reading about Advanced Channel Groups!",
timestamp: Date.now() / 1000
}
},
function (status, response) {
if (status.error) {
console.log(status);
}
else {
show all 19 lineslet timestamp = Date().timeIntervalSince1970
pubnub.publish(
channel: "channelSwift",
message: [
"author": "user-a",
"status": "I am reading about Advanced Channel Groups!",
"timestamp": timestamp
]
) { result in
switch result {
case let .success(response):
print("Successful Publish Response: \(response)")
case let .failure(error):
print("Failed Publish Response: \(error.localizedDescription)")
show all 17 linesNSTimeInterval timestamp = [NSDate date].timeIntervalSince1970;
NSDictionary *data = @{
@"author": @"user-a",
@"status": @"I am reading about Advanced Channel Groups!",
@"timestamp": @(timestamp)
};
[self.pubnub publish:data toChannel:@"ch-user-a-status"
withCompletion:^(PNPublishStatus *status) {
if (!status.isError) {
NSLog(@"Message Published w/ timetoken: %@", status.data.timetoken);
}
else {
NSLog(@"Error happened while publishing: %@", status.errorData.information);
}
show all 17 linesDate date = new Date();
JsonObject data = new JsonObject();
data.put("author", "user-a");
data.put("status", "I am reading about Advanced Channel Groups!");
data.put("timestamp", date.getTime() / 1000);
pubnub.publish()
.channel("ch-user-a-status")
.message(data)
.async(result -> {
result.onSuccess(res -> {
System.out.println("message Published w/ timetoken "
+ res.getTimetoken());
}).onFailure(exception -> {
System.out.println("error happened while publishing: "
show all 18 lines// Publish a status message
pubnub.Publish()
.Message(data)
.Channel("ch-user-a-status")
.Execute(new DemoPublishResult());
public class DemoPublishResult : PNCallback<PNPublishResult> {
public override void OnResponse(PNPublishResult result, PNStatus status) {
if (status.Error) {
Console.WriteLine("error happened while publishing: " +
pubnub.JsonPluggableLibrary.SerializeToJsonString(status));
}
else {
Console.WriteLine("message Published w/ timetoken "
+ result.Timetoken.ToString());
show all 18 lines# Publish a status message
import time
data = {
'author': 'user-a',
'status': 'I am reading about Advanced Channel Groups!',
'timestamp': time.time()
}
try:
envelope = pubnub.publish()\
.channel("ch-user-b-present")\
.message(data)\
.sync()
show all 18 linesUser Channel Groups
Each user will also have two channel groups:
- A channel group for observing the online status of friends.
- A channel group to receive status updates in real time.
Again, we'll use the same convention for unique user identifiers:
cg-user-a-friends
cg-user-a-status-feed
Creating channel groups requires you to add at least one channel to the channel group. The easiest way to do this is add the user's present channel (ch-user-a-present
) to each channel group.
Channel Group Management
You should only call the Add/Remove Channel to/from Channel Group APIs from your back-end server when a user registers for an account. Doing so from the client side reduces the security of your channels.
- JavaScript
- Swift
- Objective-C
- Java
- C#
- Python
// Add ch-user-a-present to cg-user-a-friends
pubnub.channelGroups.addChannels(
{
channels: ["ch-user-a-present"],
channelGroup: "cg-user-a-friends",
},
function(status) {
if (status.error) {
console.log("operation failed w/ status: ", status);
}
else {
console.log("Channel added to channel group");
}
}
);
show all 31 lines// Add ch-user-a-present to cg-user-a-friends
pubnub.addChannels(
["ch-user-a-present"],
to: "cg-user-a-friends"
) { result in
switch result {
case let .success(response):
print("Successful Add Channels Response: \(response)")
case let .failure(error):
print("Failed Add Channels Response: \(error.localizedDescription)")
}
}
// Add ch-user-a-present to cg-user-a-status-feed
pubnub.addChannels(
show all 25 lines// Add ch-user-a-present to cg-user-a-friends
[self.client addChannels:@[@"ch-user-a-present"] toGroup:@"cg-user-a-friends"
withCompletion:^(PNAcknowledgmentStatus *status) {
if (!status.isError) {
NSLog(@"Channel added to channel group.");
}
else {
NSLog(@"Operation failed w/ status: %@", status.errorData.information);
}
}
];
// Add ch-user-a-present to cg-user-a-status-feed
[self.client addChannels:@[@"ch-user-a-present"] toGroup:@"cg-user-a-status-feed"
show all 25 lines// Add ch-user-a-present to cg-user-a-friends
pubnub.addChannelsToChannelGroup()
.channelGroup("cg-user-a-friends")
.channels(Arrays.asList("ch-user-a-present"))
.async(result -> {
result.onSuccess(res -> {
System.out.println("Channel added to channel group");
}).onFailure(exception -> {
System.out.println("Operation failed w/ status: "
+ exception.toString());
});
});
// Add ch-user-a-present to cg-user-a-status-feed
pubnub.addChannelsToChannelGroup()
show all 25 lines// Add ch-user-a-present to cg-user-a-friends
pubnub.AddChannelsToChannelGroup()
.ChannelGroup("cg-user-a-friends")
.Channels(new string[] { "ch-user-a-present" })
.Execute(new DemoChannelGroupAddChannel());
// Add ch-user-a-present to cg-user-a-status-feed
pubnub.AddChannelsToChannelGroup()
.ChannelGroup("cg-user-a-status-feed")
.Channels(new string[] { "ch-user-a-present" })
.Execute(new DemoChannelGroupAddChannel());
public class DemoChannelGroupAddChannel : PNCallback<PNChannelGroupsAddChannelResult> {
public override void OnResponse(PNChannelGroupsAddChannelResult result, PNStatus status) {
if (status.Error) {
show all 22 lines# Add ch-user-a-present to cg-user-a-friends
pubnub.add_channel_to_channel_group()\
.channels(["ch-user-a-present"])\
.channel_group("cg-user-a-friends")\
.sync()
# Add ch-user-a-present to cg-user-a-status-feed
pubnub.add_channel_to_channel_group()\
.channels(["ch-user-a-present"])\
.channel_group("cg-user-a-status-feed")\
.sync()
Friending
Expanding the friend graph through friending is straightforward: you add channels to channel groups. When User A and User B become friends, you add each user's -present
channel to the other's friend group, and add the -status
channel to each user's status-feed
group. Again, you should only call these APIs from your back-end server when you receive the friendship confirmation from both users.
User A and User B become friends:
- JavaScript
- Swift
- Objective-C
- Java
- C#
- Python
// ************************************
// * User A and User B become friends
// ************************************
// Add User B to User A's groups: Add ch-user-b-present to cg-user-a-friends
pubnub.channelGroups.addChannels(
{
channels: ["ch-user-b-present"],
channelGroup: "cg-user-a-friends"
},
function(status) {
if (status.error) {
console.log("operation failed w/ status: ", status);
}
else {
show all 67 lines// ************************************
// * User A and User B become friends
// ************************************
// Add User B to User A's groups: Add ch-user-b-present to cg-user-a-friends
pubnub.addChannels(
["ch-user-a-present"],
to: "cg-user-a-friends"
) { result in
switch result {
case let .success(response):
print("Successful Add Channels Response: \(response)")
case let .failure(error):
print("Failed Add Channels Response: \(error.localizedDescription)")
}
show all 55 lines// Add User B to User A's groups: Add ch-user-b-present to cg-user-a-friends
[self.pubnub addChannels:@[@"ch-user-b-present"] toGroup:@"cg-user-a-friends"
withCompletion:^(PNAcknowledgmentStatus *status) {
if (!status.isError) {
NSLog(@"Channel added to channel group.");
}
else {
NSLog(@"Operation failed w/ status: %@", status.errorData.information);
}
}
];
// Add User B to User A's groups: ch-user-b-status to cg-user-a-status-feed
[self.pubnub addChannels:@[@"ch-user-b-status"] toGroup:@"cg-user-a-status-feed"
show all 50 lines// ************************************
// * User A and User B become friends
// ************************************
// Add User B to User A's groups: Add ch-user-b-present to cg-user-a-friends
pubnub.addChannelsToChannelGroup()
.channelGroup("cg-user-a-friends")
.channels(Arrays.asList("ch-user-b-present"))
.async(result -> {
result.onSuccess(res -> {
System.out.println("Channel added to channel group");
}).onFailure(exception -> {
System.out.println("Operation failed w/ status:" + exception.toString());
});
});
show all 51 lines// ************************************
// * User A and User B become friends
// ************************************
// Add ch-user-a-present to cg-user-a-friends
pubnub.AddChannelsToChannelGroup()
.ChannelGroup("cg-user-a-friends")
.Channels(new string[] { "ch-user-b-present" })
.Execute(new DemoChannelGroupAddChannel());
// Add User B to User A's groups: ch-user-b-status to cg-user-a-status-feed
pubnub.AddChannelsToChannelGroup()
.ChannelGroup("cg-user-a-status-feed")
.Channels(new string[] { "ch-user-b-status" })
.Execute(new DemoChannelGroupAddChannel());
show all 39 lines# ************************************
# * User A and User B become friends
# ************************************
try:
result = pubnub.add_channel_to_channel_group()\
.channel_group("cg-user-a-friends")\
.channels("ch-user-b-present")\
.sync()
print("Channel added to channel group")
except PubNubException as e:
print("Operation failed w/ status: %s" % e)
try:
show all 43 linesSubscribe
To see these working, it comes to how you subscribe. What is a bit different here is that you'll subscribe to one channel group for messages (status-feed) but subscribe to the other channel group's presence event channel group for the online/offline status of friends.
Friends Online/Offline
User ID / UUID
User ID is also referred to as UUID
/uuid
in some APIs and server responses but holds the value of the userId
parameter you set during initialization.
For presence, we track the subscribers on a channel, and we create a side channel based on the channel name for all the presence events on the main channel. This side channel is the channel name + -pnpres
. So for channel ch-user-a-present
, the presence side channel is ch-user-a-present-pnpres
and that is where PubNub publishes presence events that occur on ch-user-a-present
.
The presence events are as follows:
join
- client subscribedleave
- client unsubscribedtimeout
- client disconnected without unsubscribing (occurs after the timeout period expires)state-change
- client changed the contents of the state object
And for each event, we also include the User ID and the channel occupancy (how many subscribers).
To see your friends online/offline status and be updated in real time, you subscribe to the friends channel group, but not directly. You subscribe to the presence event side channel group only, by appending -pnpres
to the channel group name.
The reason you do not want to subscribe directly to the channel group is because Channel Groups are Subscribe
groups, and therefore you would inadvertently subscribe to all of that users' friends' presence
channels. We are only interested in the presence events of this channel group and not the message of those users.
- JavaScript
- Swift
- Objective-C
- Java
- C#
- Python
// Get the List of Friends
pubnub.channelGroups.listChannels(
{
channelGroup: "cg-user-a-friends"
},
function (status, response) {
if (status.error) {
console.log("operation failed w/ error:", status);
return;
}
console.log("FRIENDLIST: ")
response.channels.forEach( function (channel) {
console.log(channel);
});
show all 41 lines// Get the List of Friends
pubnub.listChannels(for: "cg-user-a-friends") { result in
switch result {
case let .success(response):
print("Successful List Channels Response: \(response)")
case let .failure(error):
print("Failed List Channels Response: \(error.localizedDescription)")
}
}
// Which Friends are online right now
pubnub.hereNow(
on: [],
and: ["cg-user-a-friends"]
) { result in
show all 36 lines// Get the List of Friends
[self.pubnub channelsForGroup:@"cg-user-a-friends"
withCompletion:^(PNChannelGroupChannelsResult *result, PNErrorStatus *status) {
if (!status) {
NSLog(@"Friendslist: %@", result.data.channels);
}
else {
NSLog(@"Operation failed w/ status: %@", status.errorData.information);
}
}
];
// Which Friends are online right now
[self.client hereNowForChannelGroup:@"cg-user-a-friends"
show all 35 lines// Get the List of Friends
pubnub.listChannelsForChannelGroup()
.channelGroup("cg-user-a-friends")
.async(result -> {
result.onSuccess(res -> {
System.out.println("FRIENDLIST:");
res.getChannels().stream().forEach((channelName) -> {
System.out.println("channels: " + channelName);
});
}).onFailure(exception -> {
System.out.println("operation failed w/ status:" + exception.toString());
});
});
// Which Friends are online right now
show all 36 lines// Get the List of Friends
pubnub.ListChannelsForChannelGroup()
.ChannelGroup("cg-user-a-friends")
.Execute(new DemoListChannelGroupAllChannels());
// Which Friends are online right now
pubnub.HereNow()
.ChannelGroups(new string[] { "cg-user-a-friends" })
.Execute(new DemoHereNowResult());
// Watch Friends come online / go offline
ChannelGroup friendsGroupPresence = pubnub.ChannelGroup("cg-user-a-friends-pnpres");
Subscription friendsGroupPresenceSubscription = friendsGroupPresence.Subscription();
friendsGroupPresenceSubscription.AddListener(new DemoSubscribeCallback());
show all 54 lines# Get the List of Friends
try:
env = pubnub.list_channels_in_channel_group()\
.channel_group("cg-user-a-friends")\
.sync()
print("FRIEND LIST:")
for channel in env.result.channels:
print(channel)
except PubNubException as e:
print("Operation failed w/ status: %s" % e)
show all 44 linesStatus Feed (Messages)
The status-feed channel group is much more straightforward, you're going to subscribe directly to the channel group and you'll receive status updates in real time via each channel in the channel group.
Since we include the user's present channel (ch-user-a-present
) for this user in the channel group, this will also have the net effect of subscribing to that channel. It also means it generates a join
event for every channel group that includes this user's present channel. So, if User B is friends with User A, when User A subscribes to this status-feed channel group, it also subscribes to User A's present channel and generates that join
event, in addition to all the other presence events.
Friend List Only Implementation
If you're implementing only the friend list and not the status feed, User A will need to subscribe to this ch-user-a-present
channel directly since User A isn't subscribing to the status feed group which includes this channel.
- JavaScript
- Swift
- Objective-C
- Java
- C#
- Python
pubnub.addListener({
message: function(message) {
console.log("STATUS: ", message);
}
});
// Get Status Feed Messages
pubnub.subscribe({channelGroups: ["cg-user-a-status-feed"]});
// Get Status Feed Messages
pubnub.subscribe(
to: [],
and: ["cg-user-a-friends"],
withPresence: true
) { result in
switch result {
case let .success(response):
print("Successful Response: \(response)")
case let .failure(error):
print("Failed Response: \(error.localizedDescription)")
}
}
// Get Status Feed Messages
[self.pubnub addListener:self];
[self.pubnub subscribeToChannelGroups:@[@"cg-user-a-friends"] withPresence:YES];
- (void)client:(PubNub *)client didReceiveMessage:(PNMessageResult *)message {
NSLog(@"Status: %@", message.data.message);
}
pubnub.addListener(new EventListener() {
@Override
public void message(PubNub pubnub, PNMessageResult message) {
System.out.println("STATUS" + message);
}
});
// Get Status Feed Messages
pubnub.subscribe()
.channelGroups(Arrays.asList("cg-user-a-friends"))
.execute();
pubnub.AddListener(new DemoSubscribeCallback());
// Get Status Feed Messages
var friendsGroup = pubnub.ChannelGroup("cg-user-a-friends");
Subscription friendsGroupSubscription = friendsGroup.Subscription();
friendsGroupSubscription.Subscribe<string>();
public class DemoSubscribeCallback : SubscribeCallback {
public override void Message<T>(Pubnub pubnub, PNMessageResult<T> message) {
Console.WriteLine("STATUS:" +
pubnub.JsonPluggableLibrary.SerializeToJsonString(message));
}
public override void Presence(Pubnub pubnub, PNPresenceEventResult presence) {
show all 20 lineschannel_group = pubnub.channel_group("cg-user-a-friends")
cg_subscription = channel_group.subscription()
# Add event-specific listeners
# using closure for reusable listener
def on_message(listener):
def message_callback(message):
print(f"\033[94mMessage received on: {listener}: \n{message.message}\033[0m\n")
return message_callback
# without closure
def on_presence(listener):
def presence_callback(presence):
print(f"\033[0;32mPresence received on: {listener}: \t{presence.uuid} {presence.event}s "
show all 30 linesRetrieve History
Retrieving history of status messages can be a bit more work as you have to retrieve messages from each channel in the group individually (each friend) and mash/sort them together client side. You can retrieve message from multiple channels in a single request, but you still have to mash/sort them together when you get those messages.
Complex Use Cases
There are other complex use cases of changing status feeds that can make things a bit trickier, one is weighting the status message, so that it's no longer chronological, but rather based on interests, or activity, or other things. Another layer of complexity that you can add is commenting on status items. Again, it requires a bit more logic here, and PubNub has some additional features, like Message Reactions, that will provide some assistance.
Summary
Simple and powerful friend graphs are fairly easy to do with PubNub. It's much easier than trying to develop a full back end to support the presence and status feeds, and on top of that, it's real-time.