summaryrefslogtreecommitdiffstats
path: root/sandstorm.js
diff options
context:
space:
mode:
Diffstat (limited to 'sandstorm.js')
-rw-r--r--sandstorm.js305
1 files changed, 282 insertions, 23 deletions
diff --git a/sandstorm.js b/sandstorm.js
index e7a67f76..3e04d79f 100644
--- a/sandstorm.js
+++ b/sandstorm.js
@@ -21,6 +21,187 @@ 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 } });
+
+ 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 restore. Either they were added before we set
+ // `saveIdentityCaps` to true, or 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 +239,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 +284,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 +318,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.
@@ -176,6 +428,13 @@ if (isSandstorm && Meteor.isClient) {
return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, '');
};
Meteor.absoluteUrl.defaultOptions = _defaultOptions;
+
+ // XXX Hack to fix https://github.com/wefork/wekan/issues/27
+ // Sandstorm Wekan instances only ever have a single board, so there is no need
+ // to cache per-board subscriptions.
+ SubsManager.prototype.subscribe = function(...params) {
+ return Meteor.subscribe(...params);
+ };
}
// We use this blaze helper in the UI to hide some templates that does not make