Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for publishing Murmurations profiles for public groups #108

Draft
wants to merge 11 commits into
base: dev
Choose a base branch
from
25 changes: 25 additions & 0 deletions apps/backend/api/controllers/MurmurationsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
// post: async function (req, res) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is coming soon

// const postId = req.param('postId')
// const post = Post.find(postId)
// if (post.isPublic()) {
// const postObject = {
// title: post.title()
// }
// return res.ok(postObject.toJSON())
// } else {
// return res.forbidden()
// }
// },

group: async function (req, res) {
const groupSlug = req.param('groupSlug')
const group = await Group.findActive(groupSlug)
if (group.hasMurmurationsProfile()) {
const groupObject = await group.toMurmurationsObject()
return res.ok(groupObject)
} else {
return res.forbidden()
}
}
}
4 changes: 4 additions & 0 deletions apps/backend/api/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,8 @@ type GroupSettings {
locationDisplayPrecision: LocationDisplayPrecision
# Whether to display the members of the group to non-members
publicMemberDirectory: Boolean
# Whether to publish this group to the Murmurations Directory via API
publishMurmurationsProfile: Boolean
# Whether to display suggested skills to new members when they join this group
showSuggestedSkills: Boolean
}
Expand Down Expand Up @@ -2835,6 +2837,8 @@ input GroupSettingsInput {
locationDisplayPrecision: LocationDisplayPrecision
# Whether to display the members of the group to non-members
publicMemberDirectory: Boolean
# Whether to publish this group to the Murmurations Directory via API
publishMurmurationsProfile: Boolean
# Whether to display suggested skills to new members when they join this group
showSuggestedSkills: Boolean
}
Expand Down
17 changes: 8 additions & 9 deletions apps/backend/api/models/Comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,13 @@ module.exports = bookshelf.Model.extend(Object.assign({
return false
})
},

childComments: function () {
return this.hasMany(Comment, 'comment_id').query({where: {'comments.active': true}})
return this.hasMany(Comment, 'comment_id').query({ where: { 'comments.active': true } })
},

media: function (type) {
const relation = this.hasMany(Media)
return type ? relation.query({where: {type}}) : relation
return type ? relation.query({ where: { type } }) : relation
},

getTagsInComments: function (opts) {
Expand Down Expand Up @@ -136,21 +135,21 @@ module.exports = bookshelf.Model.extend(Object.assign({
})

const newCommentActivities = followers
.filter(u => u.id !== actorId)
.map(u => u.id)
.map(createActivity('newComment'))
.filter(u => u.id !== actorId)
.map(u => u.id)
.map(createActivity('newComment'))

const mentionActivities = mentionedIds
.filter(u => u.id !== actorId)
.map(createActivity('commentMention'))
.filter(u => u.id !== actorId)
.map(createActivity('commentMention'))

return Activity.saveForReasons(
newCommentActivities.concat(mentionActivities), trx)
}
}, EnsureLoad), {

find: function (id, options) {
return Comment.where({id: id}).fetch(options)
return Comment.where({ id: id }).fetch(options)
},

createdInTimeRange: function (collection, startTime, endTime) {
Expand Down
99 changes: 82 additions & 17 deletions apps/backend/api/models/Group.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* global GroupToGroupJoinQuestion, Location, Slack, Widget */
/* eslint-disable camelcase */
import knexPostgis from 'knex-postgis'
import { clone, defaults, difference, flatten, intersection, isEmpty, mapValues, merge, sortBy, pick, omit, omitBy, isUndefined, trim, xor } from 'lodash'
import mbxGeocoder from '@mapbox/mapbox-sdk/services/geocoding'
Expand Down Expand Up @@ -111,12 +113,12 @@ module.exports = bookshelf.Model.extend(merge({

groupRelationshipInvitesFrom () {
return this.hasMany(GroupRelationshipInvite, 'from_group_id')
.query({ where: { status: GroupRelationshipInvite.STATUS.Pending }})
.query({ where: { status: GroupRelationshipInvite.STATUS.Pending } })
},

groupRelationshipInvitesTo () {
return this.hasMany(GroupRelationshipInvite, 'to_group_id')
.query({ where: { status: GroupRelationshipInvite.STATUS.Pending }})
.query({ where: { status: GroupRelationshipInvite.STATUS.Pending } })
},

groupRoles () {
Expand All @@ -139,7 +141,15 @@ module.exports = bookshelf.Model.extend(merge({
})
},

isHidden() {
hasMurmurationsProfile () {
return this.get('visibility') === Group.Visibility.PUBLIC && this.getSetting('publish_murmurations_profile')
},

murmurationsProfileUrl () {
return process.env.PROTOCOL + '://' + process.env.DOMAIN + '/noo/group/' + this.get('slug') + '/murmurations'
},

isHidden () {
return this.get('visibility') === Group.Visibility.HIDDEN
},

Expand Down Expand Up @@ -226,7 +236,7 @@ module.exports = bookshelf.Model.extend(merge({

// Return # of prereq groups userId is not a member of yet
// This is used on front-end to figure out if user can see all prereqs or not
async numPrerequisitesLeft(userId) {
async numPrerequisitesLeft (userId) {
const prerequisiteGroups = await this.prerequisiteGroups().fetch()
let num = prerequisiteGroups.models.length
await Promise.map(prerequisiteGroups.models, async (prereq) => {
Expand Down Expand Up @@ -433,19 +443,50 @@ module.exports = bookshelf.Model.extend(merge({
const newPost = post.copy()
const time = new Date(now - (timeShift[post.get('type')] || 0) * 1000)
// TODO: why are we attaching Ed West as a follower to every welcome post??
return newPost.save({created_at: time, updated_at: time}, {transacting})
return newPost.save({ created_at: time, updated_at: time }, { transacting })
.then(() => Promise.all(flatten([
this.posts().attach(newPost, {transacting}),
this.posts().attach(newPost, { transacting }),
post.followers().fetch().then(followers =>
newPost.addFollowers(followers.map(f => f.id), {}, {transacting})
newPost.addFollowers(followers.map(f => f.id), {}, { transacting })
)
])))
}))
},

async removeMembers (usersOrIds, { transacting } = {}) {
return this.updateMembers(usersOrIds, {active: false}, {transacting}).then(() =>
this.save({ num_members: this.get('num_members') - usersOrIds.length }, { transacting }))
return this.updateMembers(usersOrIds, { active: false }, { transacting })
.then(() => this.save({ num_members: this.get('num_members') - usersOrIds.length }, { transacting }))
},

async toMurmurationsObject () {
const parentGroups = await this.parentGroups().fetch()
const childrenGroups = await this.childGroups().fetch()
const publicParents = parentGroups.filter(g => g.hasMurmurationsProfile()).map(g => ({ object_url: Frontend.Route.group(g), predicate_url: 'https://schema.org/memberOf' }))
const publicChildren = childrenGroups.filter(g => g.hasMurmurationsProfile()).map(g => ({ object_url: Frontend.Route.group(g), predicate_url: 'https://schema.org/member' }))
const profile = {
linked_schemas: [
'organizations_schema-v1.0.0'
],
name: this.get('name'),
primary_url: Frontend.Route.group(this),
mission: this.get('purpose') || '',
description: this.get('description') || '',
image: this.get('avatar_url') || '',
full_address: this.get('location') || '',
relationships: publicParents.concat(publicChildren)
}
if (this.get('banner_url')) {
profile.header_image = this.get('banner_url')
}
if (this.get('location_id')) {
const location = this.get('location_id') ? await this.locationObject().fetch() : null
profile.country_iso_3166 = location?.get('country_code') ? location?.get('country_code').toUpperCase() : ''
profile.geolocation = {
lat: location?.get('center').lat,
lon: location?.get('center').lng
}
}
return profile
},

async updateMembers (usersOrIds, attrs, { transacting } = {}) {
Expand All @@ -456,11 +497,11 @@ module.exports = bookshelf.Model.extend(merge({

const updatedAttribs = Object.assign(
{},
{settings: {}},
{ settings: {} },
pick(omitBy(attrs, isUndefined), GROUP_ATTR_UPDATE_WHITELIST)
)

return Promise.map(existingMemberships.models, ms => ms.updateAndSave(updatedAttribs, {transacting}))
return Promise.map(existingMemberships.models, ms => ms.updateAndSave(updatedAttribs, { transacting }))
},

update: async function (changes, updatedByUserId) {
Expand Down Expand Up @@ -615,6 +656,10 @@ module.exports = bookshelf.Model.extend(merge({
if (changes.location && changes.location !== this.get('location') && !changes.location_id) {
await Queue.classMethod('Group', 'geocodeLocation', { groupId: this.id })
}

if (this.hasMurmurationsProfile()) {
await Queue.classMethod('Group', 'publishToMurmurations', { groupId: this.id })
}
return this
},

Expand All @@ -624,7 +669,7 @@ module.exports = bookshelf.Model.extend(merge({
}

return Promise.resolve()
},
}
}, HasSettings), {
// ****** Class constants ****** //

Expand All @@ -651,7 +696,7 @@ module.exports = bookshelf.Model.extend(merge({
if (zapierTriggers && zapierTriggers.length > 0) {
const group = await Group.find(groupId)
for (const trigger of zapierTriggers) {
const response = await fetch(trigger.get('target_url'), {
await fetch(trigger.get('target_url'), {
method: 'post',
body: JSON.stringify(members.map(m => ({
id: m.id,
Expand Down Expand Up @@ -754,7 +799,7 @@ module.exports = bookshelf.Model.extend(merge({
if (parent) {
// Only allow for adding parent groups that the creator is a moderator of or that are Open
const parentGroupMembership = await GroupMembership.forIds(userId, parentId, {
query: q => { q.select('group_memberships.*', 'groups.accessibility as accessibility', 'groups.visibility as visibility')}
query: q => { q.select('group_memberships.*', 'groups.accessibility as accessibility', 'groups.visibility as visibility') }
}).fetch({ transacting: trx })

// TODO: fix hasRole
Expand Down Expand Up @@ -810,7 +855,7 @@ module.exports = bookshelf.Model.extend(merge({
return loop()
},

geocodeLocation: async function({ groupId }) {
geocodeLocation: async function ({ groupId }) {
const group = await Group.find(groupId)
if (group) {
const geocoder = mbxGeocoder({ accessToken: process.env.MAPBOX_TOKEN })
Expand All @@ -829,7 +874,7 @@ module.exports = bookshelf.Model.extend(merge({
}
},

messageStewards: async function(fromUserId, groupId) {
messageStewards: async function (fromUserId, groupId) {
// Make sure they can only message a group they can see
const group = await groupFilter(fromUserId)(Group.where({ id: groupId })).fetch()
// TODO: ADD RESP TO THIS ONE
Expand All @@ -848,7 +893,7 @@ module.exports = bookshelf.Model.extend(merge({
},

notifyAboutCreate: function (opts) {
return Group.find(opts.groupId, {withRelated: ['creator']})
return Group.find(opts.groupId, { withRelated: ['creator'] })
.then(g => {
const creator = g.relations.creator
const recipient = process.env.NEW_GROUP_EMAIL
Expand Down Expand Up @@ -884,6 +929,26 @@ module.exports = bookshelf.Model.extend(merge({
})
},

publishToMurmurations: async function ({ groupId }) {
const group = await Group.find(groupId)
if (group) {
sails.log.info('Publishing to Murmurations', groupId, group.murmurationsProfileUrl())
// post murmurations profile data to Murmurations index (https://app.swaggerhub.com/apis-docs/MurmurationsNetwork/IndexAPI/2.0.0#/Node%20Endpoints/post_nodes)
const response = await fetch(process.env.MURMURATIONS_INDEX_API_URL, {
method: 'POST',
body: JSON.stringify({ profile_url: group.murmurationsProfileUrl() }),
headers: { 'Content-Type': 'application/json' }
})
const responseJSON = await response.json()
if (response.ok) {
return responseJSON
} else {
sails.log.error('Group.publishToMurmurations error', response.status, response.statusText, responseJSON)
throw new Error(`Failed to publish to Murmurations: ${response.status}: ${response.statusText} - ${responseJSON.message}`)
}
}
},

async pluckIdsForMember (userOrId, where) {
return await this.selectIdsForMember(userOrId, where).pluck('groups.id')
},
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/config/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ module.exports.policies = {
unsubscribe: ['isSocket', 'sessionAuth', 'checkAndSetMembership']
},

MurmurationsController: {
group: true
},

PostController: {
updateLastRead: ['sessionAuth', 'checkAndSetPost'],
subscribe: ['isSocket', 'sessionAuth', 'checkAndSetPost'],
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ module.exports.routes = {
'POST /noo/threads/subscribe': 'PostController.subscribeToUpdates',
'POST /noo/threads/unsubscribe': 'PostController.unsubscribeFromUpdates',

'GET /noo/group/:groupSlug/murmurations': 'MurmurationsController.group',

'POST /noo/upload': 'UploadController.create',

'GET /noo/export/group': 'ExportController.groupData'

}
5 changes: 5 additions & 0 deletions apps/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@
"I’d love to show you how things work, would you like a quick tour?": "I’d love to show you how things work, would you like a quick tour?",
"Join": "Join",
"Join Hylo": "Join Hylo",
"Join Questions": "Join Questions",
"Join Project": "Join Project",
"Join Request Approved": "Join Request Approved",
"Join Requests": "Join Requests",
Expand Down Expand Up @@ -624,6 +625,8 @@
"Public Offerings": "Public Offerings",
"Public Stream": "Public Stream",
"Public stream": "Public stream",
"Publish to Murmurations": "Publish to Murmurations",
"Publish Murmurations Profile": "Publish Murmurations Profile",
"Purpose": "Purpose",
"Purpose Statement": "Purpose Statement",
"Push Notifications": "Push Notifications",
Expand Down Expand Up @@ -1011,6 +1014,8 @@
"member_plural": "member_plural",
"mentorship & advice": "mentorship & advice",
"multi-unrestricted": "multi-unrestricted",
"murmurationsHeader": "Add your group to the <1>Murmurations</1> directory so it can be found and easily added to third-party public maps. You must first set visibility to Public.",
"murmurationsDescription": "Your group is now published to the Murmurations directory. You can find your profile <1>here</1>.",
"new comments on posts you're following?": "new comments on posts you're following?",
"new markets": "new markets",
"no more than {{maxTags}} allowed": "no more than {{maxTags}} allowed",
Expand Down
5 changes: 5 additions & 0 deletions apps/web/public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@
"Join": "Unirse",
"Join Hylo": "Únete a Hylo",
"Join Project": "Únete al proyecto",
"Join Questions": "Preguntas de unión",
"Join Request Approved": "Solicitud de unión aprobada",
"Join Requests": "Solicitudes de unión",
"Join group was unsuccessful": "Unirse al grupo no tuvo éxito",
Expand Down Expand Up @@ -632,6 +633,8 @@
"Public Offerings": "Ofertas Públicas",
"Public Stream": "Frujo Público",
"Public stream": "Transmisión pública",
"Publish to Murmurations": "Publicar en Murmurations",
"Publish Murmurations Profile": "Publicar perfil en Murmurations",
"Purpose": "Propósito",
"Purpose Statement": "Declaración de propósito",
"Push Notifications": "Empujar notificaciones",
Expand Down Expand Up @@ -1017,6 +1020,8 @@
"member_plural": "miembro_plural",
"mentorship & advice": "mentoría y consejo",
"multi-unrestricted": "multi-sin restricciones",
"murmurationsHeader": "Agrega tu grupo al directorio de <1>Murmurations</1> para que pueda ser encontrado y fácilmente agregado a mapas públicos de terceros. Primero debes establecer la visibilidad como Pública.",
"murmurationsDescription": "Tu grupo ahora está publicado en el directorio de Murmurations. Puedes encontrar tu perfil <1>aquí</1>.",
"new comments on posts you're following?": "¿nuevos comentarios en las publicaciones que estás siguiendo?",
"new markets": "nuevos mercados",
"no more than {{maxTags}} allowed": "no más de {{maxTags}} permitido",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/routes/GroupSettings/GroupSettings.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function fetchGroupSettings (slug) {
hideExtensionData
locationDisplayPrecision
publicMemberDirectory
publishMurmurationsProfile
showSuggestedSkills
}
type
Expand Down
Loading