summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2016-11-07 23:34:36 +0200
committerGitHub <noreply@github.com>2016-11-07 23:34:36 +0200
commit4d41e70e12d3e638028a3137fd958e841099763d (patch)
tree2aed5026ed3e91832623ee1055a9591aa05ebe55
parent91f9cf12b7dfbdf7a7d2cfa53821964e3fbb9a04 (diff)
parent43c180c247a82ed20b2bc9e2de493305f7f5a43b (diff)
downloadwekan-4d41e70e12d3e638028a3137fd958e841099763d.tar.gz
wekan-4d41e70e12d3e638028a3137fd958e841099763d.tar.bz2
wekan-4d41e70e12d3e638028a3137fd958e841099763d.zip
Merge pull request #1 from dwrensha/wefork-sandstorm-update
Wefork sandstorm update
-rw-r--r--client/components/sidebar/sidebar.jade9
-rw-r--r--client/components/sidebar/sidebar.js3
-rw-r--r--models/users.js40
-rw-r--r--sandstorm-pkgdef.capnp45
-rw-r--r--sandstorm.js297
5 files changed, 349 insertions, 45 deletions
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 4f5586cb..f3fdd1bc 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -30,10 +30,13 @@ template(name="membersWidget")
.board-widget-content
each currentBoard.activeMembers
+userAvatar(userId=this.userId showStatus=true)
- unless isSandstorm
- if currentUser.isBoardAdmin
- a.member.add-member.js-manage-board-members
+ if isSandstorm
+ if currentUser.isBoardMember
+ a.member.add-member.sandstorm-powerbox-request-identity
i.fa.fa-plus
+ else if currentUser.isBoardAdmin
+ a.member.add-member.js-manage-board-members
+ i.fa.fa-plus
.clearfix
if isInvited
hr
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 0af32f8f..f32a27c5 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -163,6 +163,9 @@ Template.membersWidget.helpers({
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .sandstorm-powerbox-request-identity'() {
+ window.sandstormRequestIdentity();
+ },
'click .js-member-invite-accept'() {
const boardId = Session.get('currentBoard');
Meteor.user().removeInvite(boardId);
diff --git a/models/users.js b/models/users.js
index 790ee0a1..bdc5ddfe 100644
--- a/models/users.js
+++ b/models/users.js
@@ -1,3 +1,7 @@
+// Sandstorm context is detected using the METEOR_SETTINGS environment variable
+// in the package definition.
+const isSandstorm = Meteor.settings && Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
Users = Meteor.users;
Users.attachSchema(new SimpleSchema({
@@ -394,24 +398,26 @@ if (Meteor.isServer) {
return fakeUserId.get() || getUserId();
};
- Users.after.insert((userId, doc) => {
- const fakeUser = {
- extendAutoValueContext: {
- userId: doc._id,
- },
- };
-
- fakeUserId.withValue(doc._id, () => {
- // Insert the Welcome Board
- Boards.insert({
- title: TAPi18n.__('welcome-board'),
- permission: 'private',
- }, fakeUser, (err, boardId) => {
-
- ['welcome-list1', 'welcome-list2'].forEach((title) => {
- Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+ if (!isSandstorm) {
+ Users.after.insert((userId, doc) => {
+ const fakeUser = {
+ extendAutoValueContext: {
+ userId: doc._id,
+ },
+ };
+
+ fakeUserId.withValue(doc._id, () => {
+ // Insert the Welcome Board
+ Boards.insert({
+ title: TAPi18n.__('welcome-board'),
+ permission: 'private',
+ }, fakeUser, (err, boardId) => {
+
+ ['welcome-list1', 'welcome-list2'].forEach((title) => {
+ Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+ });
});
});
});
- });
+ }
}
diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp
index d9e7da3d..2db5b6b6 100644
--- a/sandstorm-pkgdef.capnp
+++ b/sandstorm-pkgdef.capnp
@@ -173,8 +173,48 @@ const pkgdef :Spk.PackageDefinition = (
#
# XXX Administrators configuration options aren’t implemented yet, so this
# role is currently useless.
- )]
- )
+ )],
+
+ eventTypes = [(
+ name = "addBoardMember",
+ verbPhrase = (defaultText = "added to board"),
+ ), (
+ name = "createList",
+ verbPhrase = (defaultText = "created new list"),
+ ), (
+ name = "archivedList",
+ verbPhrase = (defaultText = "archived list"),
+ ), (
+ name = "restoredList",
+ verbPhrase = (defaultText = "restored list"),
+ ), (
+ name = "createCard",
+ verbPhrase = (defaultText = "created new card"),
+ ), (
+ name = "moveCard",
+ verbPhrase = (defaultText = "moved card"),
+ ), (
+ name = "archivedCard",
+ verbPhrase = (defaultText = "archived card"),
+ ), (
+ name = "restoredCard",
+ verbPhrase = (defaultText = "restored card"),
+ ), (
+ name = "addComment",
+ verbPhrase = (defaultText = "added comment"),
+ ), (
+ name = "addAttachement",
+ verbPhrase = (defaultText = "added attachment"),
+ ), (
+ name = "joinMember",
+ verbPhrase = (defaultText = "added to card"),
+ ), (
+ name = "unjoinMember",
+ verbPhrase = (defaultText = "removed from card"),
+ ), ],
+ ),
+
+ saveIdentityCaps = true,
),
);
@@ -184,6 +224,7 @@ const myCommand :Spk.Manifest.Command = (
environ = [
# Note that this defines the *entire* environment seen by your app.
(key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"),
+ (key = "SANDSTORM", value = "1"),
(key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}")
]
);
diff --git a/sandstorm.js b/sandstorm.js
index e7a67f76..5a800b24 100644
--- a/sandstorm.js
+++ b/sandstorm.js
@@ -21,6 +21,186 @@ const sandstormBoard = {
};
if (isSandstorm && Meteor.isServer) {
+ const fs = require('fs');
+ const Capnp = require('capnp');
+ const Package = Capnp.importSystem('sandstorm/package.capnp');
+ const Powerbox = Capnp.importSystem('sandstorm/powerbox.capnp');
+ const Identity = Capnp.importSystem('sandstorm/identity.capnp');
+ const SandstormHttpBridge =
+ Capnp.importSystem('sandstorm/sandstorm-http-bridge.capnp').SandstormHttpBridge;
+
+ let httpBridge = null;
+ let capnpConnection = null;
+
+ const bridgeConfig = Capnp.parse(
+ Package.BridgeConfig,
+ fs.readFileSync('/sandstorm-http-bridge-config'));
+
+ function getHttpBridge() {
+ if (!httpBridge) {
+ capnpConnection = Capnp.connect('unix:/tmp/sandstorm-api');
+ httpBridge = capnpConnection.restore(null, SandstormHttpBridge);
+ }
+ return httpBridge;
+ }
+
+ Meteor.methods({
+ sandstormClaimIdentityRequest(token, descriptor) {
+ check(token, String);
+ check(descriptor, String);
+
+ const parsedDescriptor = Capnp.parse(
+ Powerbox.PowerboxDescriptor,
+ new Buffer(descriptor, 'base64'),
+ { packed: true });
+
+ const tag = Capnp.parse(Identity.Identity.PowerboxTag, parsedDescriptor.tags[0].value);
+ const permissions = [];
+ if (tag.permissions[1]) {
+ permissions.push('configure');
+ }
+
+ if (tag.permissions[0]) {
+ permissions.push('participate');
+ }
+
+ const sessionId = this.connection.sandstormSessionId();
+ const httpBridge = getHttpBridge();
+ const session = httpBridge.getSessionContext(sessionId).context;
+ const api = httpBridge.getSandstormApi(sessionId).api;
+
+ Meteor.wrapAsync((done) => {
+ session.claimRequest(token).then((response) => {
+ const identity = response.cap.castAs(Identity.Identity);
+ const promises = [api.getIdentityId(identity), identity.getProfile(),
+ httpBridge.saveIdentity(identity)];
+ return Promise.all(promises).then((responses) => {
+ const identityId = responses[0].id.toString('hex').slice(0, 32);
+ const profile = responses[1].profile;
+ return profile.picture.getUrl().then((response) => {
+ const sandstormInfo = {
+ id: identityId,
+ name: profile.displayName.defaultText,
+ permissions,
+ picture: `${response.protocol}://${response.hostPath}`,
+ preferredHandle: profile.preferredHandle,
+ pronouns: profile.pronouns,
+ };
+
+ const login = Accounts.updateOrCreateUserFromExternalService(
+ 'sandstorm', sandstormInfo,
+ { profile: { name: sandstormInfo.name, fullname: sandstormInfo.name } });
+
+ updateUserPermissions(login.userId, permissions);
+ done();
+ });
+ });
+ }).catch((e) => {
+ done(e, null);
+ });
+ })();
+ },
+ });
+
+ function reportActivity(sessionId, path, type, users, caption) {
+ const httpBridge = getHttpBridge();
+ const session = httpBridge.getSessionContext(sessionId).context;
+ Meteor.wrapAsync((done) => {
+ return Promise.all(users.map((user) => {
+ return httpBridge.getSavedIdentity(user.id).then((response) => {
+ // Call getProfile() to make sure that the identity successfully resolves.
+ // (In C++ we would instead call whenResolved() here.)
+ const identity = response.identity;
+ return identity.getProfile().then(() => {
+ return { identity,
+ mentioned: !!user.mentioned,
+ subscribed: !!user.subscribed,
+ };
+ }).catch(() => {
+ // Ignore identities that fail to resolve. Probably they have lost access to the board.
+ });
+ });
+ })).then((maybeUsers) => {
+ const users = maybeUsers.filter((u) => !!u);
+ const event = { path, type, users };
+ if (caption) {
+ event.notification = { caption };
+ }
+
+ return session.activity(event);
+ }).then(() => done(),
+ (e) => done(e));
+ })();
+ }
+
+ Meteor.startup(() => {
+ Activities.after.insert((userId, doc) => {
+ // HACK: We need the connection that's making the request in order to read the
+ // Sandstorm session ID.
+ const invocation = DDP._CurrentInvocation.get(); // eslint-disable-line no-undef
+ if (invocation) {
+ const sessionId = invocation.connection.sandstormSessionId();
+
+ const eventTypes = bridgeConfig.viewInfo.eventTypes;
+
+ const defIdx = eventTypes.findIndex((def) => def.name === doc.activityType );
+ if (defIdx >= 0) {
+ const users = {};
+ function ensureUserListed(userId) {
+ if (!users[userId]) {
+ const user = Meteor.users.findOne(userId);
+ if (user) {
+ users[userId] = { id: user.services.sandstorm.id };
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function mentionedUser(userId) {
+ if (ensureUserListed(userId)) {
+ users[userId].mentioned = true;
+ }
+ }
+
+ function subscribedUser(userId) {
+ if (ensureUserListed(userId)) {
+ users[userId].subscribed = true;
+ }
+ }
+
+ let path = '';
+ let caption = null;
+
+ if (doc.cardId) {
+ path = `b/sandstorm/libreboard/${doc.cardId}`;
+ Cards.findOne(doc.cardId).members.map(subscribedUser);
+ }
+
+ if (doc.memberId) {
+ mentionedUser(doc.memberId);
+ }
+
+ if (doc.activityType === 'addComment') {
+ const comment = CardComments.findOne(doc.commentId);
+ caption = { defaultText: comment.text };
+ const activeMembers =
+ _.pluck(Boards.findOne(sandstormBoard._id).activeMembers(), 'userId');
+ (comment.text.match(/\B@(\w*)/g) || []).forEach((username) => {
+ const user = Meteor.users.findOne({ username: username.slice(1)});
+ if (user && activeMembers.indexOf(user._id) !== -1) {
+ mentionedUser(user._id);
+ }
+ });
+ }
+
+ reportActivity(sessionId, path, defIdx, _.values(users), caption);
+ }
+ }
+ });
+ });
+
function updateUserPermissions(userId, permissions) {
const isActive = permissions.indexOf('participate') > -1;
const isAdmin = permissions.indexOf('configure') > -1;
@@ -58,29 +238,6 @@ if (isSandstorm && Meteor.isServer) {
Location: base + boardPath,
});
res.end();
-
- // `accounts-sandstorm` populate the Users collection when new users
- // accesses the document, but in case a already known user comes back, we
- // need to update his associated document to match the request HTTP headers
- // informations.
- // XXX We need to update this document even if the initial route is not `/`.
- // Unfortuanlty I wasn't able to make the Webapp.rawConnectHandlers solution
- // work.
- const user = Users.findOne({
- 'services.sandstorm.id': req.headers['x-sandstorm-user-id'],
- });
- if (user) {
- // XXX At this point the user.services.sandstorm credentials haven't been
- // updated, which mean that the user will have to restart the application
- // a second time to see its updated name and avatar.
- Users.update(user._id, {
- $set: {
- 'profile.fullname': user.services.sandstorm.name,
- 'profile.avatarUrl': user.services.sandstorm.picture,
- },
- });
- updateUserPermissions(user._id, user.services.sandstorm.permissions);
- }
});
// On the first launch of the instance a user is automatically created thanks
@@ -126,6 +283,29 @@ if (isSandstorm && Meteor.isServer) {
updateUserPermissions(doc._id, doc.services.sandstorm.permissions);
});
+ Meteor.startup(() => {
+ Users.find().observeChanges({
+ changed(userId, fields) {
+ const sandstormData = (fields.services || {}).sandstorm || {};
+ if (sandstormData.name) {
+ Users.update(userId, {
+ $set: { 'profile.fullname': sandstormData.name },
+ });
+ }
+
+ if (sandstormData.picture) {
+ Users.update(userId, {
+ $set: { 'profile.avatarUrl': sandstormData.picture },
+ });
+ }
+
+ if (sandstormData.permissions) {
+ updateUserPermissions(userId, sandstormData.permissions);
+ }
+ },
+ });
+ });
+
// Wekan v0.8 didn’t implement the Sandstorm sharing model and instead kept
// the visibility setting (“public” or “private”) in the UI as does the main
// Meteor application. We need to enforce “public” visibility as the sharing
@@ -137,6 +317,77 @@ if (isSandstorm && Meteor.isServer) {
}
if (isSandstorm && Meteor.isClient) {
+ let rpcCounter = 0;
+ const rpcs = {};
+
+ window.addEventListener('message', (event) => {
+ if (event.source === window) {
+ // Meteor likes to postmessage itself.
+ return;
+ }
+
+ if ((event.source !== window.parent) ||
+ typeof event.data !== 'object' ||
+ typeof event.data.rpcId !== 'number') {
+ throw new Error(`got unexpected postMessage: ${event}`);
+ }
+
+ const handler = rpcs[event.data.rpcId];
+ if (!handler) {
+ throw new Error(`no such rpc ID for event ${event}`);
+ }
+
+ delete rpcs[event.data.rpcId];
+ handler(event.data);
+ });
+
+ function sendRpc(name, message) {
+ const id = rpcCounter++;
+ message.rpcId = id;
+ const obj = {};
+ obj[name] = message;
+ window.parent.postMessage(obj, '*');
+ return new Promise((resolve, reject) => {
+ rpcs[id] = (response) => {
+ if (response.error) {
+ reject(new Error(response.error));
+ } else {
+ resolve(response);
+ }
+ };
+ });
+ }
+
+ const powerboxDescriptors = {
+ identity: 'EAhQAQEAABEBF1EEAQH_GN1RqXqYhMAAQAERAREBAQ',
+ // Generated using the following code:
+ //
+ // Capnp.serializePacked(
+ // Powerbox.PowerboxDescriptor,
+ // { tags: [ {
+ // id: "13872380404802116888",
+ // value: Capnp.serialize(Identity.PowerboxTag, { permissions: [true, false] })
+ // }]}).toString('base64')
+ // .replace(/\//g, "_")
+ // .replace(/\+/g, "-");
+ };
+
+ function doRequest(serializedPowerboxDescriptor, onSuccess) {
+ return sendRpc('powerboxRequest', {
+ query: [serializedPowerboxDescriptor],
+ }).then((response) => {
+ if (!response.canceled) {
+ onSuccess(response);
+ }
+ });
+ }
+
+ window.sandstormRequestIdentity = function () {
+ doRequest(powerboxDescriptors.identity, (response) => {
+ Meteor.call('sandstormClaimIdentityRequest', response.token, response.descriptor);
+ });
+ };
+
// Since the Sandstorm grain is displayed in an iframe of the Sandstorm shell,
// we need to explicitly expose meta data like the page title or the URL path
// so that they could appear in the browser window.