packages/oae-jitsi/lib/activity.js
/*!
* Copyright 2016 Apereo Foundation (AF) Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import { format } from 'node:util';
import _ from 'underscore';
import * as ActivityAPI from 'oae-activity/lib/api.js';
import * as ActivityModel from 'oae-activity/lib/model.js';
import * as ActivityUtil from 'oae-activity/lib/util.js';
import * as AuthzUtil from 'oae-authz/lib/util.js';
import * as MessageBoxAPI from 'oae-messagebox';
import * as MessageBoxUtil from 'oae-messagebox/lib/util.js';
import * as PrincipalsUtil from 'oae-principals/lib/util.js';
import * as TenantsUtil from 'oae-tenants/lib/util.js';
import { AuthzConstants } from 'oae-authz/lib/constants.js';
import { ActivityConstants } from 'oae-activity/lib/constants.js';
import * as MeetingsDAO from './internal/dao.js';
import * as MeetingsAPI from './api.js';
import { MeetingsConstants } from './constants.js';
/**
* Meeting create
*/
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_CREATE, {
groupBy: [{ actor: true }],
streams: {
activity: {
router: {
actor: ['self', 'followers'],
object: ['self', 'members']
}
},
notification: {
router: {
object: ['members']
}
},
email: {
router: {
object: ['members']
}
}
}
});
/*!
* Post a meeting-create activity when a user creates a meeting.
*/
MeetingsAPI.emitter.on(MeetingsConstants.events.CREATED_MEETING, (ctx, meeting, memberChangeInfo) => {
const millis = Date.now();
const actorResource = new ActivityModel.ActivitySeedResource('user', ctx.user().id, {
user: ctx.user()
});
const objectResource = new ActivityModel.ActivitySeedResource('meeting-jitsi', meeting.id, {
'meeting-jitsi': meeting
});
let targetResource = null;
// Get the extra members
const extraMembers = _.chain(memberChangeInfo.changes)
.keys()
.filter((member) => member !== ctx.user().id)
.value();
// If we only added 1 extra user or group, we set the target to that entity
if (extraMembers.length === 1) {
const extraMember = _.first(extraMembers);
const targetResourceType = PrincipalsUtil.isGroup(extraMember) ? 'group' : 'user';
targetResource = new ActivityModel.ActivitySeedResource(targetResourceType, extraMember);
}
// Generate the activity seed and post it to the queue
const activitySeed = new ActivityModel.ActivitySeed(
MeetingsConstants.activity.ACTIVITY_MEETING_CREATE,
millis,
ActivityConstants.verbs.CREATE,
actorResource,
objectResource,
targetResource
);
ActivityAPI.postActivity(ctx, activitySeed);
});
/// ///////////////////////////////////////////////////////////////////////
// MEETING-SHARE, MEETING-ADD-TO-LIBRARY and MEETING-UPDATE-MEMBER-ROLE //
/// ///////////////////////////////////////////////////////////////////////
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_SHARE, {
groupBy: [
// "Branden Visser shared Content Item with 5 users and groups"
{ actor: true, object: true },
// "Branden Visser shared 8 files with OAE Team"
{ actor: true, target: true }
],
streams: {
activity: {
router: {
actor: ['self'],
object: ['managers'],
target: ['self', 'members', 'followers']
}
},
notification: {
router: {
target: ['self']
}
},
email: {
router: {
target: ['self']
}
}
}
});
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE_MEMBER_ROLE, {
groupBy: [{ actor: true, target: true }],
streams: {
activity: {
router: {
actor: ['self'],
object: ['self', 'members'],
target: ['managers']
}
}
}
});
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_ADD_TO_LIBRARY, {
// "Branden Visser added 5 items to his library"
groupBy: [{ actor: true }],
streams: {
activity: {
router: {
actor: ['self', 'followers'],
object: ['managers']
}
}
}
});
/**
* Post a meeting-share, meeting-add-to-library or meeting-update-member role activity based on meeting sharing
*/
MeetingsAPI.emitter.on(MeetingsConstants.events.UPDATED_MEETING_MEMBERS, (ctx, meeting, memberChangeInfo, options) => {
if (options.invitation) {
// If this member update came from an invitation, we bypass adding activity as there is a
// dedicated activity for that
return;
}
const addedPrincipalIds = _.pluck(memberChangeInfo.members.added, 'id');
const updatedPrincipalIds = _.pluck(memberChangeInfo.members.updated, 'id');
const millis = Date.now();
const actorResource = new ActivityModel.ActivitySeedResource('user', ctx.user().id, {
user: ctx.user()
});
const meetingResource = new ActivityModel.ActivitySeedResource('meeting-jitsi', meeting.id, {
'meeting-jitsi': meeting
});
// For users that are newly added to the meeting, post either a share or "add to library" activity, depending on context
_.each(addedPrincipalIds, (principalId) => {
if (principalId === ctx.user().id) {
// Users can't "share" with themselves, they actually "add it to their library"
ActivityAPI.postActivity(
ctx,
new ActivityModel.ActivitySeed(
MeetingsConstants.activity.ACTIVITY_MEETING_ADD_TO_LIBRARY,
millis,
ActivityConstants.verbs.ADD,
actorResource,
meetingResource
)
);
} else {
// A user shared meeting with some other user, fire the meeting share activity
const principalResourceType = PrincipalsUtil.isGroup(principalId) ? 'group' : 'user';
const principalResource = new ActivityModel.ActivitySeedResource(principalResourceType, principalId);
ActivityAPI.postActivity(
ctx,
new ActivityModel.ActivitySeed(
MeetingsConstants.activity.ACTIVITY_MEETING_SHARE,
millis,
ActivityConstants.verbs.SHARE,
actorResource,
meetingResource,
principalResource
)
);
}
});
// For users whose role changed, post the meeting-update-member-role activity
_.each(updatedPrincipalIds, (principalId) => {
const principalResourceType = PrincipalsUtil.isGroup(principalId) ? 'group' : 'user';
const principalResource = new ActivityModel.ActivitySeedResource(principalResourceType, principalId);
ActivityAPI.postActivity(
ctx,
new ActivityModel.ActivitySeed(
MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE_MEMBER_ROLE,
millis,
ActivityConstants.verbs.UPDATE,
actorResource,
principalResource,
meetingResource
)
);
});
});
/// ///////////////////////////////////////////////
// MEETING-UPDATE and MEETING-UPDATE-VISIBILITY //
/// ///////////////////////////////////////////////
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE, {
streams: {
activity: {
router: {
actor: ['self'],
object: ['self', 'members']
}
},
notification: {
router: {
object: ['managers']
}
},
email: {
router: {
object: ['managers']
}
}
}
});
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE_VISIBILITY, {
streams: {
activity: {
router: {
actor: ['self'],
object: ['self', 'members']
}
},
notification: {
router: {
object: ['managers']
}
}
}
});
/**
* Post either a meeting-update or meeting-update-visibility activity when an user updates a meeting's metadata
*/
MeetingsAPI.emitter.on(MeetingsConstants.events.UPDATED_MEETING, (ctx, newMeeting, oldMeeting) => {
const millis = Date.now();
const actorResource = new ActivityModel.ActivitySeedResource('user', ctx.user().id, {
user: ctx.user()
});
const objectResource = new ActivityModel.ActivitySeedResource('meeting-jitsi', newMeeting.id, {
'meeting-jitsi': newMeeting
});
// We discriminate between general updates and visibility changes.
// If the visibility has changed, we fire a visibility changed activity *instead* of an update activity
let activityType = null;
activityType =
newMeeting.visibility === oldMeeting.visibility
? MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE
: MeetingsConstants.activity.ACTIVITY_MEETING_UPDATE_VISIBILITY;
const activitySeed = new ActivityModel.ActivitySeed(
activityType,
millis,
ActivityConstants.verbs.UPDATE,
actorResource,
objectResource
);
ActivityAPI.postActivity(ctx, activitySeed);
});
/// //////////////////
// MEETING-MESSAGE //
/// //////////////////
ActivityAPI.registerActivityType(MeetingsConstants.activity.ACTIVITY_MEETING_MESSAGE, {
groupBy: [{ target: true }],
streams: {
activity: {
router: {
actor: ['self'],
target: ['message-contributors', 'members']
}
},
notification: {
router: {
target: ['message-contributors', 'members']
}
},
email: {
router: {
target: ['message-contributors', 'members']
}
},
message: {
transient: true,
router: {
// Route the activity to the meeting
target: ['self']
}
}
}
});
/**
* Post a meeting-jitsi-message activity when an user comments on a meeting
*/
MeetingsAPI.emitter.on(MeetingsConstants.events.CREATED_MEETING_MESSAGE, (ctx, message, meeting) => {
const millis = Date.now();
const actorResource = new ActivityModel.ActivitySeedResource('user', ctx.user().id, {
user: ctx.user()
});
const objectResource = new ActivityModel.ActivitySeedResource('meeting-jitsi-message', message.id, {
meetingId: meeting.id,
message
});
const targetResource = new ActivityModel.ActivitySeedResource('meeting-jitsi', meeting.id, {
'meeting-jitsi': meeting
});
const activitySeed = new ActivityModel.ActivitySeed(
MeetingsConstants.activity.ACTIVITY_MEETING_MESSAGE,
millis,
ActivityConstants.verbs.POST,
actorResource,
objectResource,
targetResource
);
ActivityAPI.postActivity(ctx, activitySeed);
});
/// ////////////////////////
// ACTIVITY ENTITY TYPES //
/// ////////////////////////
/*
* Produces a persistent 'meeting' activity entity
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingProducer = function (resource, callback) {
const meeting =
resource.resourceData && resource.resourceData['meeting-jitsi'] ? resource.resourceData['meeting-jitsi'] : null;
// If the meeting item was fired with the resource, use it instead of fetching
if (meeting) {
return callback(null, _createPersistentMeetingActivityEntity(meeting));
}
MeetingsDAO.getMeeting(resource.resourceId, (error, meeting) => {
if (error) {
return callback(error);
}
return callback(null, _createPersistentMeetingActivityEntity(meeting));
});
};
/*
* Produces an persistent activity entity that represents a message that was posted
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingMessageProducer = function (resource, callback) {
const { message, meetingId } = resource.resourceData;
MeetingsDAO.getMeeting(meetingId, (error, meeting) => {
if (error) {
return callback(error);
}
MessageBoxUtil.createPersistentMessageActivityEntity(message, (error, entity) => {
if (error) {
return callback(error);
}
// Store the meeting id and visibility on the entity as these are required for routing the activities
entity.objectType = 'meeting-jitsi-message';
entity.meetingId = meeting.id;
entity.meetingVisibility = meeting.visibility;
return callback(null, entity);
});
});
};
/**
* Create the persistent meeting entity that can be transformed into an activity entity for the UI.
*
* @param {Meeting} meeting The meeting that provides the data for the entity.
* @return {Object} An object containing the entity data that can be transformed into a UI meeting activity entity
* @api private
*/
const _createPersistentMeetingActivityEntity = function (meeting) {
return new ActivityModel.ActivityEntity('meeting-jitsi', meeting.id, meeting.visibility, {
'meeting-jitsi': meeting
});
};
/*
* Transform the meeting persistent activity entities into UI-friendly ones
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingTransformer = function (ctx, activityEntities, callback) {
const transformedActivityEntities = {};
_.each(activityEntities, (entities, activityId) => {
transformedActivityEntities[activityId] = transformedActivityEntities[activityId] || {};
_.each(entities, (entity, entityId) => {
// Transform the persistent entity into an ActivityStrea.ms compliant format
transformedActivityEntities[activityId][entityId] = _transformPersistentMeetingActivityEntity(ctx, entity);
});
});
return callback(null, transformedActivityEntities);
};
/*
* Transform the persisted message activity entities into UI-friendly ones
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingMessageTransformer = function (ctx, activityEntities, callback) {
const transformedActivityEntities = {};
for (const activityId of _.keys(activityEntities)) {
transformedActivityEntities[activityId] = transformedActivityEntities[activityId] || {};
for (const entityId of _.keys(activityEntities[activityId])) {
const entity = activityEntities[activityId][entityId];
const { meetingId } = entity;
const resource = AuthzUtil.getResourceFromId(meetingId);
const profilePath = format('/meeting-jitsi/%s/%s', resource.tenanAlias, resource.resourceId);
const urlFormat = '/api/meeting-jitsi/' + meetingId + '/messages/%s';
transformedActivityEntities[activityId][entityId] = MessageBoxUtil.transformPersistentMessageActivityEntity(
ctx,
entity,
profilePath,
urlFormat
);
}
}
return callback(null, transformedActivityEntities);
};
/*!
* Transform the meeting persistent activity entities into their OAE profiles
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingInternalTransformer = function (ctx, activityEntities, callback) {
const transformedActivityEntities = {};
_.each(activityEntities, (entities, activityId) => {
transformedActivityEntities[activityId] = transformedActivityEntities[activityId] || {};
_.each(entities, (entity, entityId) => {
// Transform the persistent entity into the OAE model
transformedActivityEntities[activityId][entityId] = entity['meeting-jitsi'];
});
});
return callback(null, transformedActivityEntities);
};
/*
* Transform the persisted message activity entities into UI-friendly ones
* @see ActivityAPI#registerActivityEntityType
*/
const _meetingMessageInternalTransformer = function (ctx, activityEntities, callback) {
const transformedActivityEntities = {};
for (const activityId of _.keys(activityEntities)) {
transformedActivityEntities[activityId] = transformedActivityEntities[activityId] || {};
for (const entityId of _.keys(activityEntities[activityId])) {
const entity = activityEntities[activityId][entityId];
transformedActivityEntities[activityId][entityId] =
MessageBoxUtil.transformPersistentMessageActivityEntityToInternal(ctx, entity.message);
}
}
return callback(null, transformedActivityEntities);
};
/**
* Transform a meeting object into an activity entity suitable to be displayed in an activity stream.
*
* For more details on the transformed entity model, @see ActivityAPI#registerActivityEntityTransformer
*
* @param {Context} ctx Current execution context
* @param {Object} entity The persistent activity entity to transform
* @return {ActivityEntity} The activity entity that represents the given meeting item
*/
const _transformPersistentMeetingActivityEntity = function (ctx, entity) {
const meeting = entity['meeting-jitsi'];
// Generate URLs for this activity
const tenant = ctx.tenant();
const baseUrl = TenantsUtil.getBaseUrl(tenant);
const globalId = baseUrl + '/api/meeting-jitsi/' + meeting.id;
const resource = AuthzUtil.getResourceFromId(meeting.id);
const profileUrl = baseUrl + '/meeting-jitsi/' + resource.tenantAlias + '/' + resource.resourceId;
const options = {};
options.url = profileUrl;
options.displayName = meeting.displayName;
options.ext = {};
options.ext[ActivityConstants.properties.OAE_ID] = meeting.id;
options.ext[ActivityConstants.properties.OAE_VISIBILITY] = meeting.visibility;
options.ext[ActivityConstants.properties.OAE_PROFILEPATH] = meeting.profilePath;
return new ActivityModel.ActivityEntity('meeting-jitsi', globalId, meeting.visibility, options);
};
ActivityAPI.registerActivityEntityType('meeting-jitsi', {
producer: _meetingProducer,
transformer: {
activitystreams: _meetingTransformer,
internal: _meetingInternalTransformer
},
propagation(associationsCtx, entity, callback) {
ActivityUtil.getStandardResourcePropagation(
entity['meeting-jitsi'].visibility,
AuthzConstants.joinable.NO,
callback
);
}
});
ActivityAPI.registerActivityEntityType('meeting-jitsi-message', {
producer: _meetingMessageProducer,
transformer: {
activitystreams: _meetingMessageTransformer,
internal: _meetingMessageInternalTransformer
},
propagation(associationsCtx, entity, callback) {
return callback(null, [{ type: ActivityConstants.entityPropagation.ALL }]);
}
});
/// ///////////////////////////////
// ACTIVITY ENTITY ASSOCIATIONS //
/// ///////////////////////////////
/*
* Register an association that presents the meeting
*/
ActivityAPI.registerActivityEntityAssociation('meeting-jitsi', 'self', (associationsCtx, entity, callback) =>
callback(null, [entity[ActivityConstants.properties.OAE_ID]])
);
/*
* Register an association that presents the members of a meeting categorized by role
*/
ActivityAPI.registerActivityEntityAssociation(
'meeting-jitsi',
'members-by-role',
(associationsCtx, entity, callback) => {
ActivityUtil.getAllAuthzMembersByRole(entity[ActivityConstants.properties.OAE_ID], callback);
}
);
/*
* Register an association that presents all the indirect members of a meeting
*/
ActivityAPI.registerActivityEntityAssociation('meeting-jitsi', 'members', (associationsCtx, entity, callback) => {
associationsCtx.get('members-by-role', (error, membersByRole) => {
if (error) {
return callback(error);
}
return callback(null, _.values(membersByRole).flat());
});
});
/*
* Register an association that presents all the managers of a meeting
*/
ActivityAPI.registerActivityEntityAssociation('meeting-jitsi', 'managers', (associationsCtx, entity, callback) => {
associationsCtx.get('members-by-role', (error, membersByRole) => {
if (error) {
return callback(error);
}
return callback(null, membersByRole[MeetingsConstants.roles.MANAGER]);
});
});
/*
* Register an assocation that presents all the commenting contributors of a meeting
*/
ActivityAPI.registerActivityEntityAssociation(
'meeting-jitsi',
'message-contributors',
(associationsCtx, entity, callback) => {
MessageBoxAPI.getRecentContributions(entity[ActivityConstants.properties.OAE_ID], null, 100, callback);
}
);
/*!
* Register an association that presents the meeting for a meeting-message entity
*/
ActivityAPI.registerActivityEntityAssociation('meeting-jitsi-message', 'self', (associationsCtx, entity, callback) =>
callback(null, [entity.meetingId])
);