snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
// Subscribe to the `meteor_autoupdate_clientVersions` collection,
// which contains the set of acceptable client versions.
//
// A "hard code push" occurs when the running client version is not in
// the set of acceptable client versions (or the server updates the
// collection, there is a published client version marked `current` and
// the running client version is no longer in the set).
//
// When the `reload` package is loaded, a hard code push causes
// the browser to reload, so that it will load the latest client
// version from the server.
//
// A "soft code push" represents the situation when the running client
// version is in the set of acceptable versions, but there is a newer
// version available on the server.
//
// `Autoupdate.newClientAvailable` is a reactive data source which
// becomes `true` if a new version of the client is available on
// the server.
//
// This package doesn't implement a soft code reload process itself,
// but `newClientAvailable` could be used for example to display a
// "click to reload" link to the user.
// The client version of the client code currently running in the
// browser.
import { Meteor } from 'meteor/meteor';
import { ClientVersions } from './client_versions.js';
const clientArch = Meteor.isCordova ? 'web.cordova' : Meteor.isModern ? 'web.browser' : 'web.browser.legacy';
const autoupdateVersions = ((__meteor_runtime_config__.autoupdate || {}).versions || {})[clientArch] || {
version: 'unknown',
versionRefreshable: 'unknown',
versionNonRefreshable: 'unknown',
assets: [],
};
export const Autoupdate = {};
// Stores acceptable client versions.
const clientVersions = (Autoupdate._clientVersions = // Used by a self-test and hot-module-replacement
new ClientVersions());
Meteor.connection.registerStoreClient('meteor_autoupdate_clientVersions', clientVersions.createStore());
Autoupdate.newClientAvailable = function () {
return clientVersions.newClientAvailable(clientArch, ['versionRefreshable', 'versionNonRefreshable'], autoupdateVersions);
};
// Set to true if the link.onload callback ever fires for any <link> node.
let knownToSupportCssOnLoad = false;
const retry = new Retry({
// Unlike the stream reconnect use of Retry, which we want to be instant
// in normal operation, this is a wacky failure. We don't want to retry
// right away, we can start slowly.
//
// A better way than timeconstants here might be to use the knowledge
// of when we reconnect to help trigger these retries. Typically, the
// server fixing code will result in a restart and reconnect, but
// potentially the subscription could have a transient error.
minCount: 0, // don't do any immediate retries
baseTimeout: 30 * 1000, // start with 30s
});
let failures = 0;
Autoupdate._retrySubscription = () => {
Meteor.subscribe('meteor_autoupdate_clientVersions', {
onError(error) {
Meteor._debug('autoupdate subscription failed', error);
failures++;
retry.retryLater(failures, function () {
// Just retry making the subscription, don't reload the whole
// page. While reloading would catch more cases (for example,
// the server went back a version and is now doing old-style hot
// code push), it would also be more prone to reload loops,
// which look really bad to the user. Just retrying the
// subscription over DDP means it is at least possible to fix by
// updating the server.
Autoupdate._retrySubscription();
});
},
onReady() {
// Call checkNewVersionDocument with a slight delay, so that the
// const handle declaration is guaranteed to be initialized, even if
// the added or changed callbacks are called synchronously.
const resolved = Promise.resolve();
function check(doc) {
resolved.then(() => checkNewVersionDocument(doc));
}
const stop = clientVersions.watch(check);
const reloadDelayInSeconds = Meteor.isProduction ? 60 : 0;
function checkNewVersionDocument(doc) {
if (doc._id !== clientArch) {
return;
}
if (doc.versionNonRefreshable !== autoupdateVersions.versionNonRefreshable) {
// Non-refreshable assets have changed, so we have to reload the
// whole page rather than just replacing <link> tags.
if (stop) stop();
if (Package.reload) {
// The reload package should be provided by ddp-client, which
// is provided by the ddp package that autoupdate depends on.
// Delay reload in 60 seconds
console.warn(
'Client version changed from',
autoupdateVersions.versionNonRefreshable,
'to',
doc.versionNonRefreshable,
`Page will reload in ${reloadDelayInSeconds} seconds`,
);
document.dispatchEvent(new Event('client_changed'));
}
return;
}
if (doc.versionRefreshable !== autoupdateVersions.versionRefreshable) {
autoupdateVersions.versionRefreshable = doc.versionRefreshable;
// Switch out old css links for the new css links. Inspired by:
// https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
var newCss = doc.assets || [];
var oldLinks = [];
Array.prototype.forEach.call(document.getElementsByTagName('link'), function (link) {
if (link.className === '__meteor-css__') {
oldLinks.push(link);
}
});
function waitUntilCssLoads(link, callback) {
var called;
link.onload = function () {
knownToSupportCssOnLoad = true;
if (!called) {
called = true;
callback();
}
};
if (!knownToSupportCssOnLoad) {
var id = Meteor.setInterval(function () {
if (link.sheet) {
if (!called) {
called = true;
callback();
}
Meteor.clearInterval(id);
}
}, 50);
}
}
let newLinksLeftToLoad = newCss.length;
function removeOldLinks() {
if (oldLinks.length > 0 && --newLinksLeftToLoad < 1) {
oldLinks.splice(0).forEach((link) => {
link.parentNode.removeChild(link);
});
}
}
if (newCss.length > 0) {
newCss.forEach((css) => {
const newLink = document.createElement('link');
newLink.setAttribute('rel', 'stylesheet');
newLink.setAttribute('type', 'text/css');
newLink.setAttribute('class', '__meteor-css__');
newLink.setAttribute('href', css.url);
waitUntilCssLoads(newLink, function () {
Meteor.setTimeout(removeOldLinks, 200);
});
const head = document.getElementsByTagName('head').item(0);
head.appendChild(newLink);
});
} else {
removeOldLinks();
}
}
}
},
});
};
Autoupdate._retrySubscription();

View File

@@ -0,0 +1,166 @@
// Publish the current client versions for each client architecture
// (web.browser, web.browser.legacy, web.cordova). When a client observes
// a change in the versions associated with its client architecture,
// it will refresh itself, either by swapping out CSS assets or by
// reloading the page. Changes to the replaceable version are ignored
// and handled by the hot-module-replacement package.
//
// There are four versions for any given client architecture: `version`,
// `versionRefreshable`, `versionNonRefreshable`, and
// `versionReplaceable`. The refreshable version is a hash of just the
// client resources that are refreshable, such as CSS. The replaceable
// version is a hash of files that can be updated with HMR. The
// non-refreshable version is a hash of the rest of the client assets,
// excluding the refreshable ones: HTML, JS that is not replaceable, and
// static files in the `public` directory. The `version` version is a
// combined hash of everything.
//
// If the environment variable `AUTOUPDATE_VERSION` is set, it will be
// used in place of all client versions. You can use this variable to
// control when the client reloads. For example, if you want to force a
// reload only after major changes, use a custom AUTOUPDATE_VERSION and
// change it only when something worth pushing to clients happens.
//
// The server publishes a `meteor_autoupdate_clientVersions` collection.
// The ID of each document is the client architecture, and the fields of
// the document are the versions described above.
import { Meteor } from 'meteor/meteor';
import { ClientVersions } from './client_versions.js';
export const Autoupdate = (__meteor_runtime_config__.autoupdate = {
// Map from client architectures (web.browser, web.browser.legacy,
// web.cordova) to version fields { version, versionRefreshable,
// versionNonRefreshable, refreshable } that will be stored in
// ClientVersions documents (whose IDs are client architectures). This
// data gets serialized into the boilerplate because it's stored in
// __meteor_runtime_config__.autoupdate.versions.
versions: {},
});
// Stores acceptable client versions.
const clientVersions = new ClientVersions();
// The client hash includes __meteor_runtime_config__, so wait until
// all packages have loaded and have had a chance to populate the
// runtime config before using the client hash as our default auto
// update version id.
// Note: Tests allow people to override Autoupdate.autoupdateVersion before
// startup.
Autoupdate.autoupdateVersion = null;
Autoupdate.autoupdateVersionRefreshable = null;
Autoupdate.autoupdateVersionCordova = null;
Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID;
var syncQueue = new Meteor._AsynchronousQueue();
async function updateVersions(shouldReloadClientProgram) {
// Step 1: load the current client program on the server
if (shouldReloadClientProgram) {
await WebAppInternals.reloadClientPrograms();
}
const {
// If the AUTOUPDATE_VERSION environment variable is defined, it takes
// precedence, but Autoupdate.autoupdateVersion is still supported as
// a fallback. In most cases neither of these values will be defined.
AUTOUPDATE_VERSION = Autoupdate.autoupdateVersion,
} = process.env;
// Step 2: update __meteor_runtime_config__.autoupdate.versions.
const clientArchs = Object.keys(WebApp.clientPrograms);
clientArchs.forEach((arch) => {
Autoupdate.versions[arch] = {
version: AUTOUPDATE_VERSION || WebApp.calculateClientHash(arch),
versionRefreshable: AUTOUPDATE_VERSION || WebApp.calculateClientHashRefreshable(arch),
versionNonRefreshable: AUTOUPDATE_VERSION || WebApp.calculateClientHashNonRefreshable(arch),
versionReplaceable: AUTOUPDATE_VERSION || WebApp.calculateClientHashReplaceable(arch),
versionHmr: WebApp.clientPrograms[arch].hmrVersion,
};
});
// Step 3: form the new client boilerplate which contains the updated
// assets and __meteor_runtime_config__.
if (shouldReloadClientProgram) {
await WebAppInternals.generateBoilerplate();
}
// Step 4: update the ClientVersions collection.
// We use `onListening` here because we need to use
// `WebApp.getRefreshableAssets`, which is only set after
// `WebApp.generateBoilerplate` is called by `main` in webapp.
WebApp.onListening(() => {
clientArchs.forEach((arch) => {
const payload = {
...Autoupdate.versions[arch],
assets: WebApp.getRefreshableAssets(arch),
};
clientVersions.set(arch, payload);
});
});
}
Meteor.publish(
'meteor_autoupdate_clientVersions',
function (appId) {
// `null` happens when a client doesn't have an appId and passes
// `undefined` to `Meteor.subscribe`. `undefined` is translated to
// `null` as JSON doesn't have `undefined.
check(appId, Match.OneOf(String, undefined, null));
// Don't notify clients using wrong appId such as mobile apps built with a
// different server but pointing at the same local url
if (Autoupdate.appId && appId && Autoupdate.appId !== appId) return [];
// Random value to delay the updates for 2-10 minutes
const randomInterval = Meteor.isProduction ? (Math.floor(Math.random() * 8) + 2) * 1000 * 60 : 0;
const stop = clientVersions.watch((version, isNew) => {
setTimeout(() => {
(isNew ? this.added : this.changed).call(this, 'meteor_autoupdate_clientVersions', version._id, version);
}, randomInterval);
});
this.onStop(() => stop());
this.ready();
},
{ is_auto: true },
);
Meteor.startup(async function () {
await updateVersions(false);
// Force any connected clients that are still looking for these older
// document IDs to reload.
['version', 'version-refreshable', 'version-cordova'].forEach((_id) => {
clientVersions.set(_id, {
version: 'outdated',
});
});
});
function enqueueVersionsRefresh() {
syncQueue.queueTask(async function () {
await updateVersions(true);
});
}
const setupListeners = () => {
// Listen for messages pertaining to the client-refresh topic.
import { onMessage } from 'meteor/inter-process-messaging';
onMessage('client-refresh', enqueueVersionsRefresh);
// Another way to tell the process to refresh: send SIGHUP signal
process.on(
'SIGHUP',
Meteor.bindEnvironment(function () {
enqueueVersionsRefresh();
}, 'handling SIGHUP signal for refresh'),
);
};
WebApp.onListening(function () {
Promise.resolve(setupListeners());
});

View File

@@ -0,0 +1,101 @@
import { Tracker } from 'meteor/tracker';
export class ClientVersions {
constructor() {
this._versions = new Map();
this._watchCallbacks = new Set();
}
// Creates a Livedata store for use with `Meteor.connection.registerStore`.
// After the store is registered, document updates reported by Livedata are
// merged with the documents in this `ClientVersions` instance.
createStore() {
return {
update: ({ id, msg, fields }) => {
if (msg === 'added' || msg === 'changed') {
this.set(id, fields);
}
},
};
}
hasVersions() {
return this._versions.size > 0;
}
get(id) {
return this._versions.get(id);
}
// Adds or updates a version document and invokes registered callbacks for the
// added/updated document. If a document with the given ID already exists, its
// fields are merged with `fields`.
set(id, fields) {
let version = this._versions.get(id);
let isNew = false;
if (version) {
Object.assign(version, fields);
} else {
version = {
_id: id,
...fields,
};
isNew = true;
this._versions.set(id, version);
}
this._watchCallbacks.forEach(({ fn, filter }) => {
if (!filter || filter === version._id) {
fn(version, isNew);
}
});
}
// Registers a callback that will be invoked when a version document is added
// or changed. Calling the function returned by `watch` removes the callback.
// If `skipInitial` is true, the callback isn't be invoked for existing
// documents. If `filter` is set, the callback is only invoked for documents
// with ID `filter`.
watch(fn, { skipInitial, filter } = {}) {
if (!skipInitial) {
const resolved = Promise.resolve();
this._versions.forEach((version) => {
if (!filter || filter === version._id) {
resolved.then(() => fn(version, true));
}
});
}
const callback = { fn, filter };
this._watchCallbacks.add(callback);
return () => this._watchCallbacks.delete(callback);
}
// A reactive data source for `Autoupdate.newClientAvailable`.
newClientAvailable(id, fields, currentVersion) {
function isNewVersion(version) {
return version._id === id && fields.some((field) => version[field] !== currentVersion[field]);
}
const dependency = new Tracker.Dependency();
const version = this.get(id);
dependency.depend();
const stop = this.watch(
(version) => {
if (isNewVersion(version)) {
dependency.changed();
stop();
}
},
{ skipInitial: true },
);
return !!version && isNewVersion(version);
}
}

View File

@@ -0,0 +1,20 @@
Package.describe({
name: 'autoupdate',
summary: 'Update the client when new client code is available',
version: '2.0.1',
});
Package.onUse(function (api) {
api.use(['webapp', 'check', 'inter-process-messaging'], 'server');
api.use(['tracker', 'retry'], 'client');
api.use('reload', 'client', { weak: true });
api.use(['ecmascript', 'ddp'], ['client', 'server']);
api.mainModule('autoupdate_server.js', 'server');
api.mainModule('autoupdate_client.js', 'client');
api.export('Autoupdate');
});

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,22 @@
accounts-linkedin
============================
A meteor package for LinkedIn's OAuth flow.
### Important
BREAKING CHANGE LinkedIn -> Linkedin inside code
from v4.0.0 works with Meteor@1.6.1 & up
From March 1, 2019 Linkedin will be using only V2 API [Docs](https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/migration-faq?context=linkedin/consumer/context)
Install
-----------
```
meteor add pauli:linkedin-oauth
```
License
-----------
[MIT](https://github.com/PauliBuccini/meteor-accounts-linkedin/blob/master/LICENSE)

View File

@@ -0,0 +1,11 @@
export const Linkedin = {};
// Request LinkedIn credentials for the user
// @param options {optional}
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
Linkedin.requestCredential = async function (options, credentialRequestCompleteCallback) {
// This function will be replaced by meteorOverrides/login/linkedin.ts
throw new Error('Linkedin integration error - invalid reference to original requestCredential implementation.');
};

View File

@@ -0,0 +1,117 @@
import { Accounts } from 'meteor/accounts-base';
import { fetch } from 'meteor/fetch';
import { OAuth } from 'meteor/oauth';
import { ServiceConfiguration } from 'meteor/service-configuration';
export const Linkedin = {};
// returns an object containing:
// - accessToken
// - expiresIn: lifetime of token in seconds
const getTokenResponse = async function (query) {
const config = await ServiceConfiguration.configurations.findOneAsync({ service: 'linkedin' });
if (!config) throw new Accounts.ConfigError('Service not configured');
let responseContent;
try {
// Request an access token
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
code: query.code,
redirect_uri: OAuth._redirectUri('linkedin', config),
});
const response = await fetch('https://api.linkedin.com/uas/oauth2/accessToken', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
});
if (!response.ok) {
throw new Error(responseContent.error_description);
}
responseContent = await response.json();
} catch (err) {
throw new Error(`Failed to complete OAuth handshake with Linkedin. ${err.message}`);
}
// Success! Extract access token and expiration
const accessToken = responseContent.access_token;
const expiresIn = responseContent.expires_in;
if (!accessToken) {
throw new Error(
`Failed to complete OAuth handshake with Linkedin -- can't find access token in HTTP response. ${JSON.stringify(responseContent)}`,
);
}
return {
accessToken,
expiresIn,
};
};
// Request available fields from profile
const getIdentity = async function (accessToken) {
try {
const url = encodeURI(`https://api.linkedin.com/v2/userinfo`);
const request = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!request.ok) {
throw new Error(await request.text());
}
return request.json();
} catch (err) {
throw new Error(`Failed to fetch identity from Linkedin. ${err.message}`);
}
};
OAuth.registerService('linkedin', 2, null, async (query) => {
const response = await getTokenResponse(query);
const { accessToken } = response;
const identity = await getIdentity(accessToken);
const { sub, given_name, family_name, picture, email } = identity;
if (!sub) {
throw new Error('Linkedin did not provide an id');
}
const fields = {
linkedinId: sub,
firstName: given_name,
lastName: family_name,
profilePicture: picture,
emailAddress: email,
email,
};
const serviceData = {
id: sub,
accessToken,
expiresAt: +new Date() + 1000 * response.expiresIn,
...fields,
};
return {
serviceData,
options: {
profile: fields,
},
};
});
Linkedin.retrieveCredential = function (credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};

View File

@@ -0,0 +1,19 @@
Package.describe({
name: 'pauli:linkedin-oauth',
version: '6.0.0',
summary: 'LinkedIn OAuth flow, use with Meteor 1.6.1 & up',
git: 'https://github.com/PoBuchi/pauli-linkedin-oauth',
documentation: 'README.md',
});
Package.onUse((api) => {
api.use('ecmascript');
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('fetch', ['server']);
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);
api.mainModule('linkedin-client.js', 'client');
api.mainModule('linkedin-server.js', 'server');
});

View File

@@ -0,0 +1,617 @@
import { Meteor } from 'meteor/meteor';
let fetch;
let WebApp;
if (Meteor.isServer) {
WebApp = require('meteor/webapp').WebApp;
} else {
fetch = require('meteor/fetch').fetch;
}
const NoOp = () => {};
const urlRE = /\/___cookie___\/set/;
const rootUrl = Meteor.isServer
? process.env.ROOT_URL
: window.__meteor_runtime_config__.ROOT_URL || window.__meteor_runtime_config__.meteorEnv.ROOT_URL || false;
const mobileRootUrl = Meteor.isServer
? process.env.MOBILE_ROOT_URL
: window.__meteor_runtime_config__.MOBILE_ROOT_URL || window.__meteor_runtime_config__.meteorEnv.MOBILE_ROOT_URL || false;
const helpers = {
isUndefined(obj) {
return obj === void 0;
},
isArray(obj) {
return Array.isArray(obj);
},
clone(obj) {
if (!this.isObject(obj)) return obj;
return this.isArray(obj) ? obj.slice() : Object.assign({}, obj);
},
};
const _helpers = ['Number', 'Object', 'Function'];
for (let i = 0; i < _helpers.length; i++) {
helpers['is' + _helpers[i]] = function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + _helpers[i] + ']';
};
}
/**
* @url https://github.com/jshttp/cookie/blob/master/index.js
* @name cookie
* @author jshttp
* @license
* (The MIT License)
*
* Copyright (c) 2012-2014 Roman Shtylman <shtylman@gmail.com>
* Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* 'Software'), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const decode = decodeURIComponent;
const encode = encodeURIComponent;
const pairSplitRegExp = /; */;
/**
* RegExp to match field-content in RFC 7230 sec 3.2
*
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
* obs-text = %x80-FF
*/
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
/**
* @function
* @name tryDecode
* @param {String} str
* @param {Function} d
* @summary Try decoding a string using a decoding function.
* @private
*/
const tryDecode = (str, d) => {
try {
return d(str);
} catch (e) {
return str;
}
};
/**
* @function
* @name parse
* @param {String} str
* @param {Object} [options]
* @return {Object}
* @summary
* Parse a cookie header.
* Parse the given cookie header string into an object
* The object has the various cookies as keys(names) => values
* @private
*/
const parse = (str, options) => {
if (typeof str !== 'string') {
throw new Meteor.Error(404, 'argument str must be a string');
}
const obj = {};
const opt = options || {};
let val;
let key;
let eqIndx;
str.split(pairSplitRegExp).forEach((pair) => {
eqIndx = pair.indexOf('=');
if (eqIndx < 0) {
return;
}
key = pair.substr(0, eqIndx).trim();
key = tryDecode(unescape(key), opt.decode || decode);
val = pair.substr(++eqIndx, pair.length).trim();
if (val[0] === '"') {
val = val.slice(1, -1);
}
if (void 0 === obj[key]) {
obj[key] = tryDecode(val, opt.decode || decode);
}
});
return obj;
};
/**
* @function
* @name antiCircular
* @param data {Object} - Circular or any other object which needs to be non-circular
* @private
*/
const antiCircular = (_obj) => {
const object = helpers.clone(_obj);
const cache = new Map();
return JSON.stringify(object, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.get(value)) {
return void 0;
}
cache.set(value, true);
}
return value;
});
};
/**
* @function
* @name serialize
* @param {String} name
* @param {String} val
* @param {Object} [options]
* @return { cookieString: String, sanitizedValue: Mixed }
* @summary
* Serialize data into a cookie header.
* Serialize the a name value pair into a cookie string suitable for
* http headers. An optional options object specified cookie parameters.
* serialize('foo', 'bar', { httpOnly: true }) => "foo=bar; httpOnly"
* @private
*/
const serialize = (key, val, opt = {}) => {
let name;
if (!fieldContentRegExp.test(key)) {
name = escape(key);
} else {
name = key;
}
let sanitizedValue = val;
let value = val;
if (!helpers.isUndefined(value)) {
if (helpers.isObject(value) || helpers.isArray(value)) {
const stringified = antiCircular(value);
value = encode(`JSON.parse(${stringified})`);
sanitizedValue = JSON.parse(stringified);
} else {
value = encode(value);
if (value && !fieldContentRegExp.test(value)) {
value = escape(value);
}
}
} else {
value = '';
}
const pairs = [`${name}=${value}`];
if (helpers.isNumber(opt.maxAge)) {
pairs.push(`Max-Age=${opt.maxAge}`);
}
if (opt.domain && typeof opt.domain === 'string') {
if (!fieldContentRegExp.test(opt.domain)) {
throw new Meteor.Error(404, 'option domain is invalid');
}
pairs.push(`Domain=${opt.domain}`);
}
if (opt.path && typeof opt.path === 'string') {
if (!fieldContentRegExp.test(opt.path)) {
throw new Meteor.Error(404, 'option path is invalid');
}
pairs.push(`Path=${opt.path}`);
} else {
pairs.push('Path=/');
}
opt.expires = opt.expires || opt.expire || false;
if (opt.expires === Infinity) {
pairs.push('Expires=Fri, 31 Dec 9999 23:59:59 GMT');
} else if (opt.expires instanceof Date) {
pairs.push(`Expires=${opt.expires.toUTCString()}`);
} else if (opt.expires === 0) {
pairs.push('Expires=0');
} else if (helpers.isNumber(opt.expires)) {
pairs.push(`Expires=${new Date(opt.expires).toUTCString()}`);
}
if (opt.httpOnly) {
pairs.push('HttpOnly');
}
if (opt.secure) {
pairs.push('Secure');
}
if (opt.firstPartyOnly) {
pairs.push('First-Party-Only');
}
if (opt.sameSite) {
pairs.push(opt.sameSite === true ? 'SameSite' : `SameSite=${opt.sameSite}`);
}
return { cookieString: pairs.join('; '), sanitizedValue };
};
const isStringifiedRegEx = /JSON\.parse\((.*)\)/;
const isTypedRegEx = /false|true|null/;
const deserialize = (string) => {
if (typeof string !== 'string') {
return string;
}
if (isStringifiedRegEx.test(string)) {
let obj = string.match(isStringifiedRegEx)[1];
if (obj) {
try {
return JSON.parse(decode(obj));
} catch (e) {
console.error('[ostrio:cookies] [.get()] [deserialize()] Exception:', e, string, obj);
return string;
}
}
return string;
} else if (isTypedRegEx.test(string)) {
try {
return JSON.parse(string);
} catch (e) {
return string;
}
}
return string;
};
/**
* @locus Anywhere
* @class __cookies
* @param opts {Object} - Options (configuration) object
* @param opts._cookies {Object|String} - Current cookies as String or Object
* @param opts.TTL {Number|Boolean} - Default cookies expiration time (max-age) in milliseconds, by default - session (false)
* @param opts.runOnServer {Boolean} - Expose Cookies class to Server
* @param opts.response {http.ServerResponse|Object} - This object is created internally by a HTTP server
* @param opts.allowQueryStringCookies {Boolean} - Allow passing Cookies in a query string (in URL). Primary should be used only in Cordova environment
* @param opts.allowedCordovaOrigins {Regex|Boolean} - [Server] Allow setting Cookies from that specific origin which in Meteor/Cordova is localhost:12XXX (^http://localhost:12[0-9]{3}$)
* @summary Internal Class
*/
class __cookies {
constructor(opts) {
this.__pendingCookies = [];
this.TTL = opts.TTL || false;
this.response = opts.response || false;
this.runOnServer = opts.runOnServer || false;
this.allowQueryStringCookies = opts.allowQueryStringCookies || false;
this.allowedCordovaOrigins = opts.allowedCordovaOrigins || false;
if (this.allowedCordovaOrigins === true) {
this.allowedCordovaOrigins = /^http:\/\/localhost:12[0-9]{3}$/;
}
this.originRE = new RegExp(`^https?:\/\/(${rootUrl ? rootUrl : ''}${mobileRootUrl ? '|' + mobileRootUrl : ''})$`);
if (helpers.isObject(opts._cookies)) {
this.cookies = opts._cookies;
} else {
this.cookies = parse(opts._cookies);
}
}
/**
* @locus Anywhere
* @memberOf __cookies
* @name get
* @param {String} key - The name of the cookie to read
* @param {String} _tmp - Unparsed string instead of user's cookies
* @summary Read a cookie. If the cookie doesn't exist a null value will be returned.
* @returns {String|void}
*/
get(key, _tmp) {
const cookieString = _tmp ? parse(_tmp) : this.cookies;
if (!key || !cookieString) {
return void 0;
}
if (cookieString.hasOwnProperty(key)) {
return deserialize(cookieString[key]);
}
return void 0;
}
/**
* @locus Anywhere
* @memberOf __cookies
* @name set
* @param {String} key - The name of the cookie to create/overwrite
* @param {String} value - The value of the cookie
* @param {Object} opts - [Optional] Cookie options (see readme docs)
* @summary Create/overwrite a cookie.
* @returns {Boolean}
*/
set(key, value, opts = {}) {
if (key && !helpers.isUndefined(value)) {
if (helpers.isNumber(this.TTL) && opts.expires === undefined) {
opts.expires = new Date(+new Date() + this.TTL);
}
const { cookieString, sanitizedValue } = serialize(key, value, opts);
this.cookies[key] = sanitizedValue;
if (Meteor.isClient) {
document.cookie = cookieString;
} else if (this.response) {
this.__pendingCookies.push(cookieString);
this.response.setHeader('Set-Cookie', this.__pendingCookies);
}
return true;
}
return false;
}
/**
* @locus Anywhere
* @memberOf __cookies
* @name remove
* @param {String} key - The name of the cookie to create/overwrite
* @param {String} path - [Optional] The path from where the cookie will be
* readable. E.g., "/", "/mydir"; if not specified, defaults to the current
* path of the current document location (string or null). The path must be
* absolute (see RFC 2965). For more information on how to use relative paths
* in this argument, see: https://developer.mozilla.org/en-US/docs/Web/API/document.cookie#Using_relative_URLs_in_the_path_parameter
* @param {String} domain - [Optional] The domain from where the cookie will
* be readable. E.g., "example.com", ".example.com" (includes all subdomains)
* or "subdomain.example.com"; if not specified, defaults to the host portion
* of the current document location (string or null).
* @summary Remove a cookie(s).
* @returns {Boolean}
*/
remove(key, path = '/', domain = '') {
if (key && this.cookies.hasOwnProperty(key)) {
const { cookieString } = serialize(key, '', {
domain,
path,
expires: new Date(0),
});
delete this.cookies[key];
if (Meteor.isClient) {
document.cookie = cookieString;
} else if (this.response) {
this.response.setHeader('Set-Cookie', cookieString);
}
return true;
} else if (!key && this.keys().length > 0 && this.keys()[0] !== '') {
const keys = Object.keys(this.cookies);
for (let i = 0; i < keys.length; i++) {
this.remove(keys[i]);
}
return true;
}
return false;
}
/**
* @locus Anywhere
* @memberOf __cookies
* @name has
* @param {String} key - The name of the cookie to create/overwrite
* @param {String} _tmp - Unparsed string instead of user's cookies
* @summary Check whether a cookie exists in the current position.
* @returns {Boolean}
*/
has(key, _tmp) {
const cookieString = _tmp ? parse(_tmp) : this.cookies;
if (!key || !cookieString) {
return false;
}
return cookieString.hasOwnProperty(key);
}
/**
* @locus Anywhere
* @memberOf __cookies
* @name keys
* @summary Returns an array of all readable cookies from this location.
* @returns {[String]}
*/
keys() {
if (this.cookies) {
return Object.keys(this.cookies);
}
return [];
}
/**
* @locus Client
* @memberOf __cookies
* @name send
* @param cb {Function} - Callback
* @summary Send all cookies over XHR to server.
* @returns {void}
*/
send(cb = NoOp) {
if (Meteor.isServer) {
cb(new Meteor.Error(400, "Can't run `.send()` on server, it's Client only method!"));
}
if (this.runOnServer) {
let path = `${
window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || window.__meteor_runtime_config__.meteorEnv.ROOT_URL_PATH_PREFIX || ''
}/___cookie___/set`;
let query = '';
if (Meteor.isCordova && this.allowQueryStringCookies) {
const cookiesKeys = this.keys();
const cookiesArray = [];
for (let i = 0; i < cookiesKeys.length; i++) {
const { sanitizedValue } = serialize(cookiesKeys[i], this.get(cookiesKeys[i]));
const pair = `${cookiesKeys[i]}=${sanitizedValue}`;
if (!cookiesArray.includes(pair)) {
cookiesArray.push(pair);
}
}
if (cookiesArray.length) {
path = Meteor.absoluteUrl('___cookie___/set');
query = `?___cookies___=${encodeURIComponent(cookiesArray.join('; '))}`;
}
}
fetch(`${path}${query}`, {
credentials: 'include',
type: 'cors',
})
.then((response) => {
cb(void 0, response);
})
.catch(cb);
} else {
cb(new Meteor.Error(400, "Can't send cookies on server when `runOnServer` is false."));
}
return void 0;
}
}
/**
* @function
* @locus Server
* @summary Middleware handler
* @private
*/
const __middlewareHandler = (request, response, opts) => {
let _cookies = {};
if (opts.runOnServer) {
if (request.headers && request.headers.cookie) {
_cookies = parse(request.headers.cookie);
}
return new __cookies({
_cookies,
TTL: opts.TTL,
runOnServer: opts.runOnServer,
response,
allowQueryStringCookies: opts.allowQueryStringCookies,
});
}
throw new Meteor.Error(400, "Can't use middleware when `runOnServer` is false.");
};
/**
* @locus Anywhere
* @class Cookies
* @param opts {Object}
* @param opts.TTL {Number} - Default cookies expiration time (max-age) in milliseconds, by default - session (false)
* @param opts.auto {Boolean} - [Server] Auto-bind in middleware as `req.Cookies`, by default `true`
* @param opts.handler {Function} - [Server] Middleware handler
* @param opts.runOnServer {Boolean} - Expose Cookies class to Server
* @param opts.allowQueryStringCookies {Boolean} - Allow passing Cookies in a query string (in URL). Primary should be used only in Cordova environment
* @param opts.allowedCordovaOrigins {Regex|Boolean} - [Server] Allow setting Cookies from that specific origin which in Meteor/Cordova is localhost:12XXX (^http://localhost:12[0-9]{3}$)
* @summary Main Cookie class
*/
class Cookies extends __cookies {
constructor(opts = {}) {
opts.TTL = helpers.isNumber(opts.TTL) ? opts.TTL : false;
opts.runOnServer = opts.runOnServer !== false ? true : false;
opts.allowQueryStringCookies = opts.allowQueryStringCookies !== true ? false : true;
if (Meteor.isClient) {
opts._cookies = document.cookie;
super(opts);
} else {
opts._cookies = {};
super(opts);
opts.auto = opts.auto !== false ? true : false;
this.opts = opts;
this.handler = helpers.isFunction(opts.handler) ? opts.handler : false;
this.onCookies = helpers.isFunction(opts.onCookies) ? opts.onCookies : false;
if (opts.runOnServer && !Cookies.isLoadedOnServer) {
Cookies.isLoadedOnServer = true;
if (opts.auto) {
WebApp.connectHandlers.use((req, res, next) => {
if (urlRE.test(req._parsedUrl.path)) {
const matchedCordovaOrigin =
!!req.headers.origin && this.allowedCordovaOrigins && this.allowedCordovaOrigins.test(req.headers.origin);
const matchedOrigin = matchedCordovaOrigin || (!!req.headers.origin && this.originRE.test(req.headers.origin));
if (matchedOrigin) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}
const cookiesArray = [];
let cookiesObject = {};
if (matchedCordovaOrigin && opts.allowQueryStringCookies && req.query.___cookies___) {
cookiesObject = parse(decodeURIComponent(req.query.___cookies___));
} else if (req.headers.cookie) {
cookiesObject = parse(req.headers.cookie);
}
const cookiesKeys = Object.keys(cookiesObject);
if (cookiesKeys.length) {
for (let i = 0; i < cookiesKeys.length; i++) {
const { cookieString } = serialize(cookiesKeys[i], cookiesObject[cookiesKeys[i]]);
if (!cookiesArray.includes(cookieString)) {
cookiesArray.push(cookieString);
}
}
if (cookiesArray.length) {
res.setHeader('Set-Cookie', cookiesArray);
}
}
helpers.isFunction(this.onCookies) && this.onCookies(__middlewareHandler(req, res, opts));
res.writeHead(200);
res.end('');
} else {
req.Cookies = __middlewareHandler(req, res, opts);
helpers.isFunction(this.handler) && this.handler(req.Cookies);
next();
}
});
}
}
}
}
/**
* @locus Server
* @memberOf Cookies
* @name middleware
* @summary Get Cookies instance into callback
* @returns {void}
*/
middleware() {
if (!Meteor.isServer) {
throw new Meteor.Error(500, "[ostrio:cookies] Can't use `.middleware()` on Client, it's Server only!");
}
return (req, res, next) => {
helpers.isFunction(this.handler) && this.handler(__middlewareHandler(req, res, this.opts));
next();
};
}
}
if (Meteor.isServer) {
Cookies.isLoadedOnServer = false;
}
/* Export the Cookies class */
export { Cookies };

View File

@@ -0,0 +1,14 @@
Package.describe({
name: 'ostrio:cookies',
version: '2.7.2',
summary: 'Isomorphic bulletproof Server, Client, Browser, and Cordova cookies',
git: 'https://github.com/veliovgroup/Meteor-Cookies',
documentation: 'README.md',
});
Package.onUse((api) => {
api.use('ecmascript', ['client', 'server']);
api.use('webapp', 'server');
api.use('fetch', 'client');
api.mainModule('cookies.js', ['client', 'server']);
});

View File

@@ -0,0 +1,52 @@
// Hijack core node API and attach data to the response dynamically
// We are simply using this hack because, there is no way to alter
// Meteor's html content on the server side
Inject._hijackWrite = function (res) {
const originalWrite = res.write;
res.write = function (chunk, encoding) {
// prevent hijacking other http requests
if (!res.iInjected && encoding === undefined && /^<!DOCTYPE html>/.test(chunk)) {
chunk = chunk.toString();
for (id in Inject.rawModHtmlFuncs) {
chunk = Inject.rawModHtmlFuncs[id](chunk, res);
if (!_.isString(chunk)) {
throw new Error(`Inject func id "${id}" must return HTML, not ${typeof chunk}\n${JSON.stringify(chunk, null, 2)}`);
}
}
res.iInjected = true;
}
originalWrite.call(res, chunk, encoding);
};
};
WebApp.connectHandlers.use(function (req, res, next) {
// We only separate this to make testing easier
Inject._hijackWrite(res);
next();
});
// meteor algorithm to check if this is a meteor serving http request or not
Inject.appUrl = function (url) {
if (url === '/favicon.ico' || url === '/robots.txt') return false;
// NOTE: app.manifest is not a web standard like favicon.ico and
// robots.txt. It is a file id we have chosen to use for HTML5
// appcache URLs. It is included here to prevent using an appcache
// then removing it from poisoning an app permanently. Eventually,
// once we have server side routing, this won't be needed as
// unknown URLs with return a 404 automatically.
if (url === '/app.manifest') return false;
// Avoid serving app HTML for declared routes such as /sockjs/.
if (typeof RoutePolicy !== 'undefined' && RoutePolicy.classify(url)) return false;
if (url === '/voice-call-popup.html') return false;
// we currently return app HTML on all URLs by default
return true;
};

View File

@@ -0,0 +1,156 @@
function escapeReplaceString(str) {
/*
* When using string.replace(str, newSubStr), the dollar sign ("$") is
* considered a special character in newSubStr, and needs to be escaped
* as "$$". We have to do this twice, for escaping the newSubStr in
* this function, and for the resulting string which is passed back.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
*/
return str.replace(/\$/g, '$$$$');
}
Inject = {
// stores in a script type=application/ejson tag, accessed with Injected.obj('id')
obj(id, data, res) {
this._checkForObjOrFunction(data, 'Inject.obj(id, data [,res]) expects `data` to be an Object or Function');
if (res) {
this._resAssign(res, 'objList', id, data);
} else {
this.objList[id] = data;
}
},
objList: {},
// Inserts a META called `id`, whose `content` can be accessed with Injected.meta()
meta(id, data, res) {
this._checkForTextOrFunction(data, 'Inject.meta(id, data [,res]) expects `data` to be an String or Function');
if (res) {
this._resAssign(res, 'metaList', id, data);
} else {
this.metaList[id] = data;
}
},
metaList: {},
rawHead(id, textOrFunc, res) {
this._checkForTextOrFunction(textOrFunc, 'Inject.rawHead(id, content [,res]) expects `content` to be an String or Function');
if (res) {
this._resAssign(res, 'rawHeads', id, textOrFunc);
} else {
this.rawHeads[id] = textOrFunc;
}
},
rawHeads: {},
rawBody(id, textOrFunc, res) {
this._checkForTextOrFunction(textOrFunc, 'Inject.rawBody(id, content [,res]) expects `content` to be an String or Function');
if (res) {
this._resAssign(res, 'rawBodies', id, textOrFunc);
} else {
this.rawBodies[id] = textOrFunc;
}
},
rawBodies: {},
// The callback receives the entire HTML page and must return a modified version
rawModHtml(id, func) {
if (!_.isFunction(func)) {
const message = `Inject func id "${id}" should be a function, not ${typeof func}`;
throw new Error(message);
}
this.rawModHtmlFuncs[id] = func;
},
rawModHtmlFuncs: {},
_injectObjects(html, res) {
const objs = _.extend({}, Inject.objList, res.Inject && res.Inject.objList);
if (_.isEmpty(objs)) {
return html;
}
let obj;
let injectHtml = '';
for (id in objs) {
obj = _.isFunction(objs[id]) ? objs[id](res) : objs[id];
injectHtml += ` <script id='${id.replace("'", '&apos;')}' type='application/ejson'>${EJSON.stringify(obj)}</script>\n`;
}
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`);
},
_injectMeta(html, res) {
const metas = _.extend({}, Inject.metaList, res.Inject && res.Inject.metaList);
if (_.isEmpty(metas)) return html;
let injectHtml = '';
for (id in metas) {
const meta = this._evalToText(metas[id], res, html);
(injectHtml += ` <meta id='${id.replace("'", '&apos;')}' content='${meta.replace("'", '&apos;')}'>\n`), res;
}
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`);
},
_injectHeads(html, res) {
const heads = _.extend({}, Inject.rawHeads, res.Inject && res.Inject.rawHeads);
if (_.isEmpty(heads)) return html;
let injectHtml = '';
for (id in heads) {
const head = this._evalToText(heads[id], res, html);
injectHtml += `${head}\n`;
}
return html.replace('<head>', `<head>\n${escapeReplaceString(injectHtml)}`);
},
_injectBodies(html, res) {
const bodies = _.extend({}, Inject.rawBodies, res.Inject && res.Inject.rawBodies);
if (_.isEmpty(bodies)) return html;
let injectHtml = '';
for (id in bodies) {
const body = this._evalToText(bodies[id], res, html);
injectHtml += `${body}\n`;
}
return html.replace('<body>', `<body>\n${escapeReplaceString(injectHtml)}`);
},
// ensure object exists and store there
_resAssign(res, key, id, value) {
if (!res.Inject) res.Inject = {};
if (!res.Inject[key]) res.Inject[key] = {};
res.Inject[key][id] = value;
},
_checkForTextOrFunction(arg, message) {
if (!(_.isString(arg) || _.isFunction(arg))) {
throw new Error(message);
}
},
_checkForObjOrFunction(arg, message) {
if (!(_.isObject(arg) || _.isFunction(arg))) {
throw new Error(message);
}
},
// we don't handle errors here. Let them to handle in a higher level
_evalToText(textOrFunc, res, html) {
if (_.isFunction(textOrFunc)) {
return textOrFunc(res, html);
}
return textOrFunc;
},
};
Inject.rawModHtml('injectHeads', Inject._injectHeads.bind(Inject));
Inject.rawModHtml('injectMeta', Inject._injectMeta.bind(Inject));
Inject.rawModHtml('injectBodies', Inject._injectBodies.bind(Inject));
Inject.rawModHtml('injectObjects', Inject._injectObjects.bind(Inject));

View File

@@ -0,0 +1,16 @@
Package.describe({
summary: 'Allow injection of arbitrary data to initial Meteor HTML page',
version: '1.0.5',
git: 'https://github.com/meteorhacks/meteor-inject-initial.git',
name: 'meteorhacks:inject-initial',
});
Package.onUse(function (api) {
api.use(['routepolicy', 'webapp'], 'server');
api.use(['ejson', 'underscore'], ['server']);
api.addFiles('lib/inject-server.js', 'server');
api.addFiles('lib/inject-core.js', 'server');
api.export('Inject', 'server');
});

View File

@@ -0,0 +1,36 @@
// TODO: Remove this after meteor fixes the issue
// This override fixes a recent issue introduced after the update to Meteor 3.0
// MinimongoCollection.remove(query) where `query` has `_id: { $in: Array }` removes only the first found record.
LocalCollection.prototype['_eachPossiblyMatchingDocAsync'] = async function (selector, fn) {
const specificIds = LocalCollection._idsMatchedBySelector(selector);
if (specificIds) {
for (const id of specificIds) {
const doc = this._docs.get(id);
if (doc && (await fn(doc, id)) === false) {
// Changed from `!fn(doc,id)`
break;
}
}
} else {
await this._docs.forEachAsync(fn);
}
};
LocalCollection.prototype['_eachPossiblyMatchingDocSync'] = function (selector, fn) {
const specificIds = LocalCollection._idsMatchedBySelector(selector);
if (specificIds) {
for (const id of specificIds) {
const doc = this._docs.get(id);
if (doc && fn(doc, id) === false) {
// Changed from `!fn(doc,id)`
break;
}
}
} else {
this._docs.forEach(fn);
}
};

View File

@@ -0,0 +1,25 @@
/**
* Run as a user
* @param {String} userId The id of user to run as
* @param {Function} f Function to run as user
* @return {Any} Returns function result
*/
Meteor.runAsUser = function (userId, f) {
var currentInvocation = DDP._CurrentInvocation.get();
// Create a new method invocation
var invocation = new DDPCommon.MethodInvocation(
currentInvocation
? currentInvocation
: {
connection: null,
},
);
// Now run as user on this invocation
invocation.setUserId(userId);
return DDP._CurrentInvocation.withValue(invocation, function () {
return f.apply(invocation, [userId]);
});
};

View File

@@ -0,0 +1,22 @@
Package.describe({
name: 'dispatch:run-as-user',
version: '1.1.1',
summary: 'Adds Meteor.runAsUser(user, f) and Meteor.isRestricted()',
git: 'https://github.com/DispatchMe/Meteor-run-as-user.git',
documentation: 'README.md',
});
Package.onUse(function (api) {
api.use([
'meteor',
'check',
'underscore',
'mongo',
'ddp',
'minimongo',
// 'ddp-common',
// 'ddp-client'
]);
api.addFiles(['lib/common.js', 'lib/collection.overwrites.js'], ['client', 'server']);
});

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

View File

@@ -0,0 +1,45 @@
{
"lockfileVersion": 4,
"dependencies": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="
},
"istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="
},
"istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="
},
"istanbul-reports": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
"integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="
},
"make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="
},
"semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="
}
}
}

View File

@@ -0,0 +1,18 @@
Package.describe({
name: 'rocketchat:coverage',
summary: '',
version: '1.0.0',
});
Package.onUse(function (api) {
api.use('ecmascript');
api.use('isobuild:compiler-plugin@1.0.0');
api.mainModule('plugin/compile-version.js', 'server');
});
Npm.depends({
'istanbul-lib-report': '3.0.1',
'istanbul-reports': '3.1.7',
'istanbul-lib-coverage': '3.2.2',
});

View File

@@ -0,0 +1,56 @@
import { exec } from 'child_process';
import os from 'os';
import util from 'util';
import libReport from 'istanbul-lib-report';
import reports from 'istanbul-reports';
import libCoverage from 'istanbul-lib-coverage';
const dir = process.env.COVERAGE_DIR;
const fileName = process.env.COVERAGE_FILE_NAME;
const reporter = process.env.COVERAGE_REPORTER || 'lcov';
console.log('Coverage plugin started');
if (!dir && !reporter) {
return console.log('Coverage plugin not configured');
}
if (!dir || !reporter) {
console.log('Coverage plugin not fully configured');
return;
}
process.on('exit', async () => {
try {
if (!dir) {
throw new Error('No coverage dir');
}
if (!reporter) {
throw new Error('No coverage reporter');
}
console.log('Coverage plugin triggered');
const coverageMap = libCoverage.createCoverageMap(globalThis['__coverage__']);
const configWatermarks = {
statements: [50, 80],
functions: [50, 80],
branches: [50, 80],
lines: [50, 80],
};
const context = libReport.createContext({
dir,
coverageMap,
});
console.log('Generating coverage report', { dir, reporter, fileName });
const report = reports.create(reporter, { file: fileName });
report.execute(context);
} catch (e) {
console.log('Error', e);
}
});

View File

@@ -0,0 +1,6 @@
This package contains internationalization for Rocket.Chat.
Due to limitations of the i18n-library used, _only_ translations from this very folder will be respected.
Thus, if you extend Rocket.Chat with custom packages which have got own translation files, you'll have to put them into this package. They should be named `<package>.<language>.i18n.(json|yaml)`.
Alternatively, you can add the i18n-files in your package and then copy it to this folder during build time. In this case, you should add the copied files to the `.gitignore`. See [how livechat did](../package.js) as an example.

View File

@@ -0,0 +1 @@
../../../../packages/i18n/dist/resources

View File

@@ -0,0 +1,32 @@
Package.describe({
name: 'rocketchat:i18n',
version: '0.0.1',
summary: 'RocketChat i18n',
git: '',
});
const additionalPackages = {};
const fs = Npm.require('fs');
Package.onUse(function (api) {
const workingDir = process.env.PWD || '.';
const i18nDir = `${workingDir}/packages/rocketchat-i18n/i18n`;
Object.keys(additionalPackages).forEach(function (current) {
const fullPath = `${workingDir}/packages/${additionalPackages[current]}`;
fs.readdirSync(fullPath).forEach(function (filename) {
if (filename.indexOf('.json') > -1 && fs.statSync(`${fullPath}/${filename}`).size > 16) {
fs.writeFileSync(`${i18nDir}/${current}.${filename}`, fs.readFileSync(`${fullPath}/${filename}`));
}
});
});
fs.readdirSync(i18nDir).forEach(function (filename) {
if (filename.indexOf('.json') > -1 && filename.indexOf('livechat.') === -1 && fs.statSync(`${i18nDir}/${filename}`).size > 16) {
api.addFiles(`i18n/${filename}`);
}
});
api.use('rocketchat:tap-i18n');
});

View File

@@ -0,0 +1,2 @@
/public/
.npm/

View File

@@ -0,0 +1,39 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>
<!-- Access: http://localhost:3000/packages/rocketchat_livechat/assets/demo.html -->
<body style="background-color: #efefef">
<h1 style="color: #000">test</h1>
<p style="color: #000">Talk to us.</p>
<h2>changing page title</h2>
<a href="#page-0" onclick="document.title='page-0'">page 0</a><br />
<a href="#page-1" onclick="document.title='page-1'">page 1</a><br />
<a href="#page-2" onclick="document.title='page-2'">page 2</a><br />
<a href="#page-3" onclick="document.title='page-3'">page 3</a><br />
<h2>without changing page title</h2>
<a href="#page-4">page 4</a><br />
<a href="#page-5">page 5</a><br />
<a href="#page-6">page 6</a><br />
<a href="#page-7">page 7</a><br />
<script type="text/javascript">
(function (w, d, s, u) {
w.RocketChat = function (c) {
w.RocketChat._.push(c);
};
w.RocketChat._ = [];
w.RocketChat.url = u;
var h = d.getElementsByTagName(s)[0],
j = d.createElement(s);
j.async = true;
j.src = `${window.location.origin}/livechat/rocketchat-livechat.min.js?_=201903270000`;
h.parentNode.insertBefore(j, h);
})(window, document, 'script', `${window.location.origin}/livechat`);
</script>
</body>
</html>

View File

@@ -0,0 +1,786 @@
/*!
* EventEmitter v5.1.0 - git.io/ee
* Unlicense - http://unlicense.org/
* Oliver Caldwell - http://oli.me.uk/
* @preserve
*/
(function(exports) {
'use strict';
/**
* Class for managing events.
* Can be extended to provide event functionality in other classes.
*
* @class EventEmitter Manages event registering and emitting.
*/
function EventEmitter() {}
// Shortcuts to improve speed and size
var proto = EventEmitter.prototype;
var originalGlobalValue = exports.EventEmitter;
/**
* Finds the index of the listener for the event in its storage array.
*
* @param {Function[]} listeners Array of listeners to search through.
* @param {Function} listener Method to look for.
* @return {Number} Index of the specified listener, -1 if not found
* @api private
*/
function indexOfListener(listeners, listener) {
var i = listeners.length;
while (i--) {
if (listeners[i].listener === listener) {
return i;
}
}
return -1;
}
/**
* Alias a method while keeping the context correct, to allow for overwriting of target method.
*
* @param {String} name The name of the target method.
* @return {Function} The aliased method
* @api private
*/
function alias(name) {
return function aliasClosure() {
return this[name].apply(this, arguments);
};
}
/**
* Returns the listener array for the specified event.
* Will initialise the event object and listener arrays if required.
* Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
* Each property in the object response is an array of listener functions.
*
* @param {String|RegExp} evt Name of the event to return the listeners from.
* @return {Function[]|Object} All listener functions for the event.
*/
proto.getListeners = function getListeners(evt) {
var events = this._getEvents();
var response;
var key;
// Return a concatenated array of all matching events if
// the selector is a regular expression.
if (evt instanceof RegExp) {
response = {};
for (key in events) {
if (events.hasOwnProperty(key) && evt.test(key)) {
response[key] = events[key];
}
}
} else {
response = events[evt] || (events[evt] = []);
}
return response;
};
/**
* Takes a list of listener objects and flattens it into a list of listener functions.
*
* @param {Object[]} listeners Raw listener objects.
* @return {Function[]} Just the listener functions.
*/
proto.flattenListeners = function flattenListeners(listeners) {
var flatListeners = [];
var i;
for (i = 0; i < listeners.length; i += 1) {
flatListeners.push(listeners[i].listener);
}
return flatListeners;
};
/**
* Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
*
* @param {String|RegExp} evt Name of the event to return the listeners from.
* @return {Object} All listener functions for an event in an object.
*/
proto.getListenersAsObject = function getListenersAsObject(evt) {
var listeners = this.getListeners(evt);
var response;
if (listeners instanceof Array) {
response = {};
response[evt] = listeners;
}
return response || listeners;
};
function isValidListener(listener) {
if (typeof listener === 'function' || listener instanceof RegExp) {
return true;
} else if (listener && typeof listener === 'object') {
return isValidListener(listener.listener);
} else {
return false;
}
}
/**
* Adds a listener function to the specified event.
* The listener will not be added if it is a duplicate.
* If the listener returns true then it will be removed after it is called.
* If you pass a regular expression as the event name then the listener will be added to all events that match it.
*
* @param {String|RegExp} evt Name of the event to attach the listener to.
* @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addListener = function addListener(evt, listener) {
if (!isValidListener(listener)) {
throw new TypeError('listener must be a function');
}
var listeners = this.getListenersAsObject(evt);
var listenerIsWrapped = typeof listener === 'object';
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
listeners[key].push(listenerIsWrapped ? listener : {
listener: listener,
once: false
});
}
}
return this;
};
/**
* Alias of addListener
*/
proto.on = alias('addListener');
/**
* Semi-alias of addListener. It will add a listener that will be
* automatically removed after its first execution.
*
* @param {String|RegExp} evt Name of the event to attach the listener to.
* @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addOnceListener = function addOnceListener(evt, listener) {
return this.addListener(evt, {
listener: listener,
once: true
});
};
/**
* Alias of addOnceListener.
*/
proto.once = alias('addOnceListener');
/**
* Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad.
* You need to tell it what event names should be matched by a regex.
*
* @param {String} evt Name of the event to create.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.defineEvent = function defineEvent(evt) {
this.getListeners(evt);
return this;
};
/**
* Uses defineEvent to define multiple events.
*
* @param {String[]} evts An array of event names to define.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.defineEvents = function defineEvents(evts) {
for (var i = 0; i < evts.length; i += 1) {
this.defineEvent(evts[i]);
}
return this;
};
/**
* Removes a listener function from the specified event.
* When passed a regular expression as the event name, it will remove the listener from all events that match it.
*
* @param {String|RegExp} evt Name of the event to remove the listener from.
* @param {Function} listener Method to remove from the event.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeListener = function removeListener(evt, listener) {
var listeners = this.getListenersAsObject(evt);
var index;
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key)) {
index = indexOfListener(listeners[key], listener);
if (index !== -1) {
listeners[key].splice(index, 1);
}
}
}
return this;
};
/**
* Alias of removeListener
*/
proto.off = alias('removeListener');
/**
* Adds listeners in bulk using the manipulateListeners method.
* If you pass an object as the first argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added.
* You can also pass it a regular expression to add the array of listeners to all events that match it.
* Yeah, this function does quite a bit. That's probably a bad thing.
*
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.addListeners = function addListeners(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(false, evt, listeners);
};
/**
* Removes listeners in bulk using the manipulateListeners method.
* If you pass an object as the first argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be removed.
* You can also pass it a regular expression to remove the listeners from all events that match it.
*
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to remove.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeListeners = function removeListeners(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(true, evt, listeners);
};
/**
* Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
* The first argument will determine if the listeners are removed (true) or added (false).
* If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be added/removed.
* You can also pass it a regular expression to manipulate the listeners of all events that match it.
*
* @param {Boolean} remove True if you want to remove listeners, false if you want to add.
* @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add/remove.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) {
var i;
var value;
var single = remove ? this.removeListener : this.addListener;
var multiple = remove ? this.removeListeners : this.addListeners;
// If evt is an object then pass each of its properties to this method
if (typeof evt === 'object' && !(evt instanceof RegExp)) {
for (i in evt) {
if (evt.hasOwnProperty(i) && (value = evt[i])) {
// Pass the single listener straight through to the singular method
if (typeof value === 'function') {
single.call(this, i, value);
} else {
// Otherwise pass back to the multiple function
multiple.call(this, i, value);
}
}
}
} else {
// So evt must be a string
// And listeners must be an array of listeners
// Loop over it and pass each one to the multiple method
i = listeners.length;
while (i--) {
single.call(this, evt, listeners[i]);
}
}
return this;
};
/**
* Removes all listeners from a specified event.
* If you do not specify an event then all listeners will be removed.
* That means every event will be emptied.
* You can also pass a regex to remove all events that match it.
*
* @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.removeEvent = function removeEvent(evt) {
var type = typeof evt;
var events = this._getEvents();
var key;
// Remove different things depending on the state of evt
if (type === 'string') {
// Remove all listeners for the specified event
delete events[evt];
} else if (evt instanceof RegExp) {
// Remove all events matching the regex.
for (key in events) {
if (events.hasOwnProperty(key) && evt.test(key)) {
delete events[key];
}
}
} else {
// Remove all listeners in all events
delete this._events;
}
return this;
};
/**
* Alias of removeEvent.
*
* Added to mirror the node API.
*/
proto.removeAllListeners = alias('removeEvent');
/**
* Emits an event of your choice.
* When emitted, every listener attached to that event will be executed.
* If you pass the optional argument array then those arguments will be passed to every listener upon execution.
* Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
* So they will not arrive within the array on the other side, they will be separate.
* You can also pass a regular expression to emit to all events that match it.
*
* @param {String|RegExp} evt Name of the event to emit and execute listeners for.
* @param {Array} [args] Optional array of arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.emitEvent = function emitEvent(evt, args) {
var listenersMap = this.getListenersAsObject(evt);
var listeners;
var listener;
var i;
var key;
var response;
for (key in listenersMap) {
if (listenersMap.hasOwnProperty(key)) {
listeners = listenersMap[key].slice(0);
for (i = 0; i < listeners.length; i++) {
// If the listener returns true then it shall be removed from the event
// The function is executed either with a basic call or an apply if there is an args array
listener = listeners[i];
if (listener.once === true) {
this.removeListener(evt, listener.listener);
}
response = listener.listener.apply(this, args || []);
if (response === this._getOnceReturnValue()) {
this.removeListener(evt, listener.listener);
}
}
}
}
return this;
};
/**
* Alias of emitEvent
*/
proto.trigger = alias('emitEvent');
/**
* Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on.
* As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it.
*
* @param {String|RegExp} evt Name of the event to emit and execute listeners for.
* @param {...*} Optional additional arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.emit = function emit(evt) {
var args = Array.prototype.slice.call(arguments, 1);
return this.emitEvent(evt, args);
};
/**
* Sets the current value to check against when executing listeners. If a
* listeners return value matches the one set here then it will be removed
* after execution. This value defaults to true.
*
* @param {*} value The new value to check for when executing listeners.
* @return {Object} Current instance of EventEmitter for chaining.
*/
proto.setOnceReturnValue = function setOnceReturnValue(value) {
this._onceReturnValue = value;
return this;
};
/**
* Fetches the current value to check against when executing listeners. If
* the listeners return value matches this one then it should be removed
* automatically. It will return true by default.
*
* @return {*|Boolean} The current value to check for or the default, true.
* @api private
*/
proto._getOnceReturnValue = function _getOnceReturnValue() {
if (this.hasOwnProperty('_onceReturnValue')) {
return this._onceReturnValue;
} else {
return true;
}
};
/**
* Fetches the events object and creates one if required.
*
* @return {Object} The events storage object.
* @api private
*/
proto._getEvents = function _getEvents() {
return this._events || (this._events = {});
};
/**
* Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version.
*
* @return {Function} Non conflicting EventEmitter class.
*/
EventEmitter.noConflict = function noConflict() {
exports.EventEmitter = originalGlobalValue;
return EventEmitter;
};
// Expose the class either via AMD, CommonJS or the global object
if (typeof module === 'object' && module.exports) {
module.exports = EventEmitter;
} else {
exports.EventEmitter = EventEmitter;
}
}(this || {}));
(function(w) {
w.RocketChat = w.RocketChat || { _: [] };
var config = {};
var widget;
var iframe;
var hookQueue = [];
var ready = false;
var smallScreen = false;
var bodyStyle;
var scrollPosition;
var widgetWidth = '320px';
var widgetHeightOpened = '350px';
var widgetHeightClosed = '30px';
var validCallbacks = [
'chat-maximized',
'chat-minimized',
'chat-started',
'chat-ended',
'pre-chat-form-submit',
'offline-form-submit'
];
var callbacks = new EventEmitter();
var registerCallback = function(eventName, fn) {
if (validCallbacks.indexOf(eventName) === -1) {
return false;
}
return callbacks.on(eventName, fn);
};
var emitCallback = function(eventName, data) {
if (typeof data !== 'undefined') {
callbacks.emit(eventName, data);
} else {
callbacks.emit(eventName);
}
};
// hooks
var callHook = function(action, params) {
if (!ready) {
return hookQueue.push(arguments);
}
var data = {
src: 'rocketchat',
fn: action,
args: params
};
iframe.contentWindow.postMessage(data, '*');
};
var closeWidget = function() {
if (widget.dataset.state === 'closed') {
return;
}
if (smallScreen) {
document.body.style.cssText = bodyStyle;
document.body.scrollTop = scrollPosition;
}
widget.dataset.state = 'closed';
widget.style.height = widgetHeightClosed;
widget.style.right = '50px';
widget.style.bottom = '0px';
callHook('widgetClosed');
emitCallback('chat-minimized');
};
var openWidget = function() {
if (widget.dataset.state === 'opened') {
return;
}
if (smallScreen) {
scrollPosition = document.body.scrollTop;
bodyStyle = document.body.style.cssText;
document.body.style.cssText += 'overflow: hidden; height: 100%; width: 100%; position: fixed; top:' + scrollPosition + 'px;';
}
widget.dataset.state = 'opened';
widget.style.height = widgetHeightOpened;
callHook('widgetOpened');
document.querySelector('.rocketchat-widget iframe').focus();
emitCallback('chat-maximized');
};
var api = {
ready: function() {
ready = true;
if (hookQueue.length > 0) {
hookQueue.forEach(function(hookParams) {
callHook.apply(this, hookParams);
});
hookQueue = [];
}
},
toggleWindow: function(/*forceClose*/) {
if (widget.dataset.state === 'closed') {
openWidget();
} else {
closeWidget();
}
},
restoreWindow: function() {
if (widget.dataset.state === 'closed') {
openWidget();
}
},
startDragWindow: function(offset) {
if (widget.dataset.state !== 'opened') {
return;
}
this.dragOffset = offset;
},
stopDragWindow: function() {
if (widget.dataset.state !== 'opened') {
return;
}
this.dragOffset = null;
},
dragWindow: function(displacement) {
if (!this.dragOffset) {
return;
}
var right = parseInt(widget.style.right.replace(/px$/, ''), 10);
var bottom = parseInt(widget.style.bottom.replace(/px$/, ''), 10);
widget.style.right = (right - (displacement.x - this.dragOffset.x)) + 'px';
widget.style.bottom = (bottom - (displacement.y - this.dragOffset.y)) + 'px';
},
openPopout: function() {
closeWidget();
var popup = window.open(config.url + '?mode=popout', 'livechat-popout', 'width=400, height=450, toolbars=no');
popup.focus();
},
openWidget: function() {
openWidget();
},
removeWidget: function() {
document.getElementsByTagName('body')[0].removeChild(widget);
},
callback: function(eventName, data) {
emitCallback(eventName, data);
}
};
var pageVisited = function(change) {
callHook('pageVisited', {
change: change,
location: JSON.parse(JSON.stringify(document.location)),
title: document.title
});
};
var setCustomField = function(key, value, overwrite) {
if (typeof overwrite === 'undefined') {
overwrite = true;
}
callHook('setCustomField', [key, value, overwrite]);
};
var setTheme = function(theme) {
callHook('setTheme', theme);
};
var setDepartment = function(department) {
callHook('setDepartment', department);
};
var setGuestToken = function(token) {
callHook('setGuestToken', token);
};
var setGuestName = function(name) {
callHook('setGuestName', name);
};
var setGuestEmail = function(email) {
callHook('setGuestEmail', email);
};
var registerGuest = function(guest) {
callHook('registerGuest', guest);
};
var clearDepartment = function() {
callHook('clearDepartment');
};
var currentPage = {
href: null,
title: null
};
var trackNavigation = function() {
setInterval(function() {
if (document.location.href !== currentPage.href) {
pageVisited('url');
currentPage.href = document.location.href;
}
if (document.title !== currentPage.title) {
pageVisited('title');
currentPage.title = document.title;
}
}, 800);
};
var init = function(url) {
if (!url) {
return;
}
config.url = url;
var chatWidget = document.createElement('div');
chatWidget.dataset.state = 'closed';
chatWidget.className = 'rocketchat-widget';
chatWidget.innerHTML = '<div class="rocketchat-container" style="width:100%;height:100%">' +
'<iframe id="rocketchat-iframe" src="' + url + '" style="width:100%;height:100%;border:none;background-color:transparent" allowTransparency="true" allow="microphone; camera"></iframe> ' +
'</div><div class="rocketchat-overlay"></div>';
chatWidget.style.position = 'fixed';
chatWidget.style.width = widgetWidth;
chatWidget.style.height = widgetHeightClosed;
chatWidget.style.borderTopLeftRadius = '5px';
chatWidget.style.borderTopRightRadius = '5px';
chatWidget.style.bottom = '0';
chatWidget.style.right = '50px';
chatWidget.style.zIndex = '12345';
document.getElementsByTagName('body')[0].appendChild(chatWidget);
widget = document.querySelector('.rocketchat-widget');
iframe = document.getElementById('rocketchat-iframe');
w.addEventListener('message', function(msg) {
if (typeof msg.data === 'object' && msg.data.src !== undefined && msg.data.src === 'rocketchat') {
if (api[msg.data.fn] !== undefined && typeof api[msg.data.fn] === 'function') {
var args = [].concat(msg.data.args || []);
api[msg.data.fn].apply(null, args);
}
}
}, false);
var mediaqueryresponse = function(mql) {
if (mql.matches) {
smallScreen = true;
chatWidget.style.left = '0';
chatWidget.style.right = '0';
chatWidget.style.width = '100%';
} else {
chatWidget.style.left = 'auto';
chatWidget.style.right = '50px';
chatWidget.style.width = widgetWidth;
}
};
var mql = window.matchMedia('screen and (max-device-width: 480px)');
mediaqueryresponse(mql);
mql.addListener(mediaqueryresponse);
// track user navigation
trackNavigation();
};
if (typeof w.initRocket !== 'undefined') {
console.warn('initRocket is now deprecated. Please update the livechat code.');
init(w.initRocket[0]);
}
if (typeof w.RocketChat.url !== 'undefined') {
init(w.RocketChat.url);
}
var queue = w.RocketChat._;
w.RocketChat = w.RocketChat._.push = function(c) {
c.call(w.RocketChat.livechat);
};
// exports
w.RocketChat.livechat = {
// methods
pageVisited: pageVisited,
setCustomField: setCustomField,
setTheme: setTheme,
setDepartment: setDepartment,
clearDepartment: clearDepartment,
setGuestToken: setGuestToken,
setGuestName: setGuestName,
setGuestEmail: setGuestEmail,
registerGuest: registerGuest,
// callbacks
onChatMaximized: function(fn) { registerCallback('chat-maximized', fn); },
onChatMinimized: function(fn) { registerCallback('chat-minimized', fn); },
onChatStarted: function(fn) { registerCallback('chat-started', fn); },
onChatEnded: function(fn) { registerCallback('chat-ended', fn); },
onPrechatFormSubmit: function(fn) { registerCallback('pre-chat-form-submit', fn); },
onOfflineFormSubmit: function(fn) { registerCallback('offline-form-submit', fn); }
};
// proccess queue
queue.forEach(function(c) {
c.call(w.RocketChat.livechat);
});
}(window));

View File

@@ -0,0 +1,19 @@
Package.describe({
name: 'rocketchat:livechat',
version: '0.0.1',
summary: 'Livechat plugin for Rocket.Chat',
});
Package.registerBuildPlugin({
name: 'Livechat',
use: ['ecmascript'],
sources: ['plugin/build-livechat.js'],
npmDependencies: {
'uglify-js': '2.8.29',
},
});
Package.onUse(function (api) {
// livechat app
api.addAssets('assets/demo.html', 'client');
});

View File

@@ -0,0 +1,23 @@
import path from 'path';
import { execSync } from 'child_process';
import fs from 'fs';
import UglifyJS from 'uglify-js';
const livechatSource = path.resolve('packages', 'rocketchat-livechat', 'assets', 'rocket-livechat.js');
const livechatTarget = path.resolve('packages', 'rocketchat-livechat', 'assets', 'rocketchat-livechat.min.js');
fs.writeFileSync(livechatTarget, UglifyJS.minify(livechatSource).code);
const packagePath = path.join(path.resolve('.'), 'packages', 'rocketchat-livechat');
const pluginPath = path.join(packagePath, 'plugin');
const options = {
env: process.env,
};
if (process.platform === 'win32') {
execSync(`${pluginPath}/build.bat`, options);
} else {
execSync(`sh ${pluginPath}/build.sh`, options);
}

View File

@@ -0,0 +1,21 @@
@echo off
SET NODE_ENV="production"
cd packages/rocketchat-livechat/.app
call meteor npm install --production
call meteor build --headless --directory .meteor/build/
SET LIVECHAT_DIR="../../../public/livechat"
SET BUILD_DIR=".meteor/build/bundle/programs/web.browser"
mkdir ..\public
del /q /s ..\public\*
rmdir /q /s %LIVECHAT_DIR%
mkdir %LIVECHAT_DIR%
xcopy /y %BUILD_DIR%\*.css %LIVECHAT_DIR%\livechat.css*
xcopy /y %BUILD_DIR%\*.js %LIVECHAT_DIR%\livechat.js*
xcopy /y %BUILD_DIR%\head.html ..\public\head.html*
rmdir /s /q .meteor\build\

View File

@@ -0,0 +1,27 @@
# set -x
export NODE_ENV="production"
export LIVECHAT_DIR="./public/livechat"
export LIVECHAT_ASSETS_DIR="./private/livechat"
ROOT=$(pwd)
rm -rf $LIVECHAT_DIR
mkdir -p $LIVECHAT_DIR
rm -rf $LIVECHAT_ASSETS_DIR
mkdir $LIVECHAT_ASSETS_DIR
#NEW LIVECHAT#
echo "Installing Livechat ${LATEST_LIVECHAT_VERSION}..."
cd $LIVECHAT_DIR
cp -a $ROOT/../../packages/livechat/dist/. ./
# Sourcemaps remain in packages/livechat/dist for local debugging / upload
# to error tracking, but should not be baked into the Meteor bundle and
# Docker image (they account for ~360 MB of the shipped widget).
find . -name '*.map' -delete
# change to lowercase so all injected junk from rocket.chat is not sent: https://github.com/meteorhacks/meteor-inject-initial/blob/master/lib/inject-core.js#L10
# this is not harmful since doctype is case-insesitive: https://www.w3.org/TR/html5/syntax.html#the-doctype
meteor node -e 'fs.writeFileSync("index.html", fs.readFileSync("index.html").toString().replace("<!DOCTYPE", "<!doctype"));'
cd $ROOT/$LIVECHAT_ASSETS_DIR
cp ../../public/livechat/index.html .

View File

@@ -0,0 +1,12 @@
Package.describe({
name: 'rocketchat:mongo-config',
version: '0.0.1',
summary: '',
git: '',
});
Package.onUse(function (api) {
api.use(['ecmascript', 'mongo', 'email', 'http']);
api.mainModule('server/index.js', 'server');
});

View File

@@ -0,0 +1,57 @@
import tls from 'tls';
import { PassThrough } from 'stream';
import { Email } from 'meteor/email';
import { Mongo } from 'meteor/mongo';
// we always want Meteor to disable oplog tailing
Package['disable-oplog'] = {};
// FIX For TLS error see more here https://github.com/RocketChat/Rocket.Chat/issues/9316
// TODO: Remove after NodeJS fix it, more information
// https://github.com/nodejs/node/issues/16196
// https://github.com/nodejs/node/pull/16853
// This is fixed in Node 10, but this supports LTS versions
tls.DEFAULT_ECDH_CURVE = 'auto';
const mongoConnectionOptions = {
// add retryWrites=false if not present in MONGO_URL
...(!process.env.MONGO_URL.includes('retryWrites') && { retryWrites: false }),
ignoreUndefined: false,
// TODO ideally we should call isTracingEnabled(), but since this is a Meteor package we can't :/
monitorCommands: ['yes', 'true'].includes(String(process.env.TRACING_ENABLED).toLowerCase()),
};
const mongoOptionStr = process.env.MONGO_OPTIONS;
if (typeof mongoOptionStr !== 'undefined') {
try {
const mongoOptions = JSON.parse(mongoOptionStr);
Object.assign(mongoConnectionOptions, mongoOptions);
} catch (error) {
throw new Error('Invalid MONGO_OPTIONS environment variable: must be valid JSON.', { cause: error });
}
}
if (Object.keys(mongoConnectionOptions).length > 0) {
Mongo.setConnectionOptions(mongoConnectionOptions);
}
process.env.HTTP_FORWARDED_COUNT = process.env.HTTP_FORWARDED_COUNT || '1';
// Just print to logs if in TEST_MODE due to a bug in Meteor 2.5: TypeError: Cannot read property '_syncSendMail' of null
if ((process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api')) {
Email.sendAsync = async function _sendAsync(options) {
console.log('Email.sendAsync', options);
};
} else if (process.env.NODE_ENV !== 'development') {
// Send emails to a "fake" stream instead of print them in console in case MAIL_URL or SMTP is not configured
const stream = new PassThrough();
stream.on('data', () => {});
stream.on('end', () => {});
const { sendAsync } = Email;
Email.sendAsync = function _sendAsync(options) {
return sendAsync.call(this, { stream, ...options });
};
}

View File

@@ -0,0 +1 @@
/.npm

View File

@@ -0,0 +1,180 @@
import { CssTools } from 'meteor/minifier-css';
import postcss from 'postcss';
import postcssrc from 'postcss-load-config';
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
let loaded = false;
let postcssConfigPlugins = [];
let postcssConfigParser = null;
let postcssConfigExcludedPackages = [];
const loadPostcssConfig = async () => {
if (loaded) {
return;
}
try {
const config = await postcssrc({ meteor: true });
postcssConfigPlugins = config.plugins || [];
postcssConfigParser = config.options.parser || null;
postcssConfigExcludedPackages = config.options.excludedPackages || [];
} catch (error) {
if (error.message.indexOf('No PostCSS Config found') < 0) {
throw error;
}
} finally {
loaded = true;
}
};
const isImportFile = ({ _source: { url } }) => /\.import\.css$/.test(url) || /(?:^|\/)imports\//.test(url);
const isInExcludedPackages = (pathInBundle) =>
postcssConfigExcludedPackages.some((packageName) => pathInBundle.indexOf(`packages/${packageName.replace(':', '_')}/`) > -1);
const handleFileError = (file, error) => {
if (error.name === 'CssSyntaxError') {
file.error({
message: error.message,
line: error.line,
column: error.column,
});
return;
}
if (error.reason) {
file.error({
message: error.reason,
line: error.line,
column: error.column,
});
return;
}
file.error({ message: error.message });
};
const getAbstractSyntaxTree = async (file) => {
const filename = file.getPathInBundle();
if (isInExcludedPackages(filename)) {
return Object.assign(
CssTools.parseCss(file.getContentsAsString(), {
source: filename,
position: true,
}),
{ filename },
);
}
try {
const postcssResult = await postcss(postcssConfigPlugins).process(file.getContentsAsString(), {
from: process.cwd() + file._source.url,
parser: postcssConfigParser,
});
postcssResult.warnings().forEach((warn) => {
process.stderr.write(warn.toString());
});
return Object.assign(
CssTools.parseCss(postcssResult.css, {
source: filename,
position: true,
}),
{ filename },
);
} catch (error) {
if (error.name === 'CssSyntaxError') {
error.message = `${error.message}\n\nCss Syntax Error.\n\n${error.message}${error.showSourceCode()}`;
}
handleFileError(file, error);
return {
type: 'stylesheet',
stylesheet: {
rules: [],
},
filename,
};
}
};
const mergeCssFiles = async (files) => {
const cssAsts = await Promise.all(files.map(getAbstractSyntaxTree));
const mergedCssAst = CssTools.mergeCssAsts(cssAsts, (filename, msg) => {
console.warn(`${filename}: warn: ${msg}`);
});
const { code, map } = CssTools.stringifyCss(mergedCssAst, {
sourcemap: true,
inputSourcemaps: false,
});
if (!code) {
return {
code: '',
};
}
const mapFilenameToFile = files.reduce(
(obj, file) => ({
...obj,
[file.getPathInBundle()]: file,
}),
{},
);
map.sourcesContent = map.sources.map((filename) => mapFilenameToFile[filename].getContentsAsString());
// yes, this await is needed
const consumer = await new SourceMapConsumer(map);
const newMap = SourceMapGenerator.fromSourceMap(consumer);
consumer.destroy();
files
.filter((file) => file.getSourceMap())
.forEach((file) => {
newMap.applySourceMap(new SourceMapConsumer(file.getSourceMap()), file.getPathInBundle());
});
return {
code,
sourceMap: newMap.toString(),
};
};
const processFilesForBundle = async (files = [], { minifyMode }) => {
if (!files.length) {
return;
}
await loadPostcssConfig();
const filesToMerge = files.filter((file) => !isImportFile(file));
const { code, sourceMap } = await mergeCssFiles(filesToMerge);
if (minifyMode === 'development') {
files[0].addStylesheet({
data: code,
sourceMap,
path: 'merged-stylesheets.css',
});
return;
}
const minifiedFiles = await CssTools.minifyCssAsync(code);
minifiedFiles.forEach((data) => {
files[0].addStylesheet({ data });
});
};
Plugin.registerMinifier({ extensions: ['css'] }, () => ({
processFilesForBundle,
}));

View File

@@ -0,0 +1,15 @@
Package.describe({
name: 'rocketchat:postcss',
version: '1.0.0',
summary: 'CSS post-processing with PostCSS',
});
Package.registerBuildPlugin({
name: 'postcss',
use: ['ecmascript', 'minifier-css'],
sources: ['build.js'],
});
Package.onUse((api) => {
api.use('isobuild:minifier-plugin@1.0.0');
});

View File

@@ -0,0 +1,16 @@
Package.describe({
name: 'rocketchat:version',
summary: '',
version: '1.0.0',
});
Package.registerBuildPlugin({
name: 'compileVersion',
use: ['ecmascript'],
sources: ['plugin/compile-version.js'],
});
Package.onUse(function (api) {
api.use('ecmascript');
api.use('isobuild:compiler-plugin@1.0.0');
});

View File

@@ -0,0 +1,168 @@
import { exec } from 'child_process';
import os from 'os';
import util from 'util';
import path from 'path';
import fs from 'fs';
import https from 'https';
const execAsync = util.promisify(exec);
class VersionCompiler {
async processFilesForTarget(files) {
const processVersionFile = async function (file) {
const data = await new Promise((resolve, reject) => {
const currentVersion =
JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }))?.version || '';
const type = currentVersion.includes('-rc.') ? 'candidate' : currentVersion.includes('-develop') ? 'develop' : 'stable';
const url = `https://releases.rocket.chat/v2/server/supportedVersions?includeDraftType=${type}&includeDraftTag=${currentVersion}`;
function handleError(err) {
console.error('Error getting supported versions', err);
// TODO remove this when we are ready to fail
if (process.env.NODE_ENV !== 'development') {
reject(err);
return;
}
resolve({});
}
console.log('Getting supported versions from', url);
https
.get(url, function (response) {
let data = '';
response.on('data', function (chunk) {
data += chunk;
});
response.on('end', async function () {
const supportedVersions = JSON.parse(data);
if (!supportedVersions?.signed) {
return handleError(new Error(`Invalid supportedVersions result:\n URL: ${url} \n RESULT: ${data}`));
}
// check if timestamp is inside 1 hour within build
if (Math.abs(new Date().getTime() - new Date(supportedVersions.timestamp).getTime()) > 1000 * 60 * 60) {
return handleError(new Error(`Invalid supportedVersions timestamp:\n URL: ${url} \n RESULT: ${data}`));
}
for await (const version of supportedVersions.versions) {
// check if expiration is after the first rocket.chat release
if (new Date(version.expiration) < new Date('2019-04-01T00:00:00.000Z')) {
return handleError(new Error(`Invalid supportedVersions expiration:\n URL: ${url} \n RESULT: ${data}`));
}
}
resolve(supportedVersions);
});
response.on('error', function (err) {
handleError(err);
});
})
.end();
});
file.addJavaScript({
data: `exports.supportedVersions = ${JSON.stringify(data)}`,
path: `${file.getPathInPackage()}.js`,
});
};
const processFile = async function (file) {
let output = JSON.parse(file.getContentsAsString());
output.build = {
date: new Date().toISOString(),
nodeVersion: process.version,
arch: process.arch,
platform: process.platform,
osRelease: os.release(),
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
cpus: os.cpus().length,
};
output.marketplaceApiVersion = require('@rocket.chat/apps-engine/package.json').version.replace(/^[^0-9]/g, '');
const minimumClientVersions =
JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }))?.rocketchat
?.minimumClientVersions || {};
try {
const result = await execAsync("git log --pretty=format:'%H%n%ad%n%an%n%s' -n 1");
const data = result.stdout.split('\n');
output.commit = {
hash: data.shift(),
date: data.shift(),
author: data.shift(),
subject: data.join('\n'),
};
} catch (e) {
if (process.env.NODE_ENV !== 'development') {
throw e;
}
// no git
}
try {
const tags = await execAsync('git describe --abbrev=0 --tags');
if (output.commit) {
output.commit.tag = tags.stdout.replace('\n', '');
}
} catch (e) {
// no tags
}
try {
const branch = await execAsync('git rev-parse --abbrev-ref HEAD');
if (output.commit) {
output.commit.branch = branch.stdout.replace('\n', '');
}
} catch (e) {
if (process.env.NODE_ENV !== 'development') {
throw e;
}
// no branch
}
file.addJavaScript({
data: `exports.Info = ${JSON.stringify(output, null, 4)};
exports.minimumClientVersions = ${JSON.stringify(minimumClientVersions, null, 4)};`,
path: `${file.getPathInPackage()}.js`,
});
};
for await (const file of files) {
console.log('Processing file', file.getDisplayPath());
switch (true) {
case file.getDisplayPath().endsWith('rocketchat.info'): {
await processFile(file);
break;
}
case file.getDisplayPath().endsWith('rocketchat-supported-versions.info'): {
if (process.env.NODE_ENV === 'development') {
file.addJavaScript({
data: `exports.supportedVersions = {}`,
path: `${file.getPathInPackage()}.js`,
});
break;
}
await processVersionFile(file);
break;
}
default: {
throw new Error(`Unexpected file ${file.getDisplayPath()}`);
}
}
console.log('Processed file', file.getDisplayPath());
}
}
}
Plugin.registerCompiler(
{
extensions: ['info'],
},
function () {
return new VersionCompiler();
},
);