Show discussion avatar (#14053)

* Show discussion avatar

* Create room avatar endpoint

* Fix livechat room avatar

* Fix avatar when showing names

* Only get room avatars for discussions

* Split avatar endpoints into multiple files

* Better router files organization

* Better logic for requesting discussion's avatar

* Fix avatar autocomplete for channels

* Better avatar cache invalidation

* Don't use Session on server
pull/14185/head
Diego Sampaio 6 years ago committed by GitHub
parent 4d160bf9dc
commit 535324e94a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/discussion/client/views/creationDialog/CreateDiscussion.html
  2. 6
      app/discussion/client/views/creationDialog/CreateDiscussion.js
  3. 4
      app/importer-slack/server/importer.js
  4. 5
      app/lib/lib/roomTypes/direct.js
  5. 21
      app/lib/lib/roomTypes/private.js
  6. 7
      app/lib/lib/roomTypes/public.js
  7. 4
      app/lib/server/functions/attachMessage.js
  8. 5
      app/livechat/lib/LivechatRoomType.js
  9. 6
      app/message-pin/server/pinMessage.js
  10. 4
      app/oembed/server/jumpToMessage.js
  11. 8
      app/slackbridge/server/SlackAdapter.js
  12. 36
      app/ui-account/client/avatar/avatar.js
  13. 25
      app/ui-sidenav/client/chatRoomItem.js
  14. 4
      app/ui-sidenav/client/sidebarItem.html
  15. 23
      app/ui-utils/client/lib/avatar.js
  16. 2
      app/ui/client/components/popupList.html
  17. 4
      app/ui/client/lib/notification.js
  18. 2
      app/utils/client/index.js
  19. 3
      app/utils/lib/RoomTypeConfig.js
  20. 10
      app/utils/lib/getAvatarURL.js
  21. 21
      app/utils/lib/getAvatarUrlFromUsername.js
  22. 13
      app/utils/lib/getRoomAvatarURL.js
  23. 18
      app/utils/lib/getUserAvatarURL.js
  24. 2
      app/utils/server/index.js
  25. 2
      server/main.js
  26. 9
      server/routes/avatar/index.js
  27. 15
      server/routes/avatar/middlewares/auth.js
  28. 5
      server/routes/avatar/middlewares/index.js
  29. 41
      server/routes/avatar/room.js
  30. 78
      server/routes/avatar/user.js
  31. 93
      server/routes/avatar/utils.js
  32. 155
      server/startup/avatar.js

@ -42,7 +42,7 @@
{{> icon block="rc-input__icon-svg" icon="discussion"}}
</div>
<input name="discussion_name" id="discussion_name" class="rc-input__element" placeholder="{{_ 'New_discussion_name'}}"
maxlength="{{maxMessageLength}}" value="{{channelName}}"/>
maxlength="{{maxMessageLength}}" value="{{nameSuggestion}}"/>
</div>
</label>
</div>

@ -82,7 +82,7 @@ Template.CreateDiscussion.helpers({
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`;
};
},
channelName() {
nameSuggestion() {
return Template.instance().discussionName.get();
},
});
@ -126,7 +126,7 @@ Template.CreateDiscussion.onRendered(function() {
this.find(this.data.rid ? '#discussion_name' : '#parentChannel').focus();
});
const suggestName = (name, msg) => [name, msg].filter((e) => e).join(' - ').substr(0, 140);
const suggestName = (msg = '') => msg.substr(0, 140);
Template.CreateDiscussion.onCreated(function() {
const { rid, message: msg } = this.data;
@ -141,7 +141,7 @@ Template.CreateDiscussion.onCreated(function() {
}
const roomName = room && roomTypes.getRoomName(room.t, room);
this.discussionName = new ReactiveVar(suggestName(roomName, msg && msg.msg));
this.discussionName = new ReactiveVar(suggestName(msg && msg.msg));
this.pmid = msg && msg._id;

@ -8,7 +8,7 @@ import {
SelectionUser,
} from '../../importer/server';
import { RocketChatFile } from '../../file';
import { getAvatarUrlFromUsername } from '../../utils';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import { Users, Rooms, Messages } from '../../models';
import { sendMessage } from '../../lib';
@ -378,7 +378,7 @@ export class SlackImporter extends Base {
attachments: [{
text: this.convertSlackMessageToRocketChat(message.attachments[0].text),
author_name : message.attachments[0].author_subname,
author_icon : getAvatarUrlFromUsername(message.attachments[0].author_subname),
author_icon : getUserAvatarURL(message.attachments[0].author_subname),
}],
};
Messages.createWithTypeRoomIdMessageAndUser('message_pinned', room._id, '', this.getRocketUser(message.user), msgObj);

@ -5,6 +5,7 @@ import { openRoom } from '../../../ui-utils';
import { getUserPreference, RoomTypeConfig, RoomTypeRouteConfig, RoomSettingsEnum, UiTextContext } from '../../../utils';
import { hasPermission, hasAtLeastOnePermission } from '../../../authorization';
import { settings } from '../../../settings';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
export class DirectMessageRoomRoute extends RoomTypeRouteConfig {
constructor() {
@ -149,4 +150,8 @@ export class DirectMessageRoomType extends RoomTypeConfig {
return { title, text };
}
getAvatarPath(roomData) {
return getUserAvatarURL(roomData.name || this.roomName(roomData));
}
}

@ -4,6 +4,9 @@ import { openRoom } from '../../../ui-utils';
import { settings } from '../../../settings';
import { hasAtLeastOnePermission, hasPermission } from '../../../authorization';
import { getUserPreference, RoomSettingsEnum, RoomTypeConfig, RoomTypeRouteConfig, UiTextContext } from '../../../utils';
import { getRoomAvatarURL } from '../../../utils/lib/getRoomAvatarURL';
import { getAvatarURL } from '../../../utils/lib/getAvatarURL';
import { roomTypes } from '../../../utils';
export class PrivateRoomRoute extends RoomTypeRouteConfig {
constructor() {
@ -108,4 +111,22 @@ export class PrivateRoomType extends RoomTypeConfig {
return '';
}
}
getAvatarPath(roomData) {
// TODO: change to always get avatar from _id when rooms have avatars
// if room is not a discussion, returns the avatar for its name
if (!roomData.prid) {
return getAvatarURL({ username: `@${ this.roomName(roomData) }` });
}
// if discussion's parent room is known, get his avatar
const proom = ChatRoom.findOne({ _id: roomData.prid }, { reactive: false });
if (proom) {
return roomTypes.getConfig(proom.t).getAvatarPath(proom);
}
// otherwise gets discussion's avatar via _id
return getRoomAvatarURL(roomData.prid);
}
}

@ -4,6 +4,7 @@ import { ChatRoom, ChatSubscription } from '../../../models';
import { settings } from '../../../settings';
import { hasAtLeastOnePermission } from '../../../authorization';
import { getUserPreference, RoomTypeConfig, RoomTypeRouteConfig, RoomSettingsEnum, UiTextContext } from '../../../utils';
import { getAvatarURL } from '../../../utils/lib/getAvatarURL';
export class PublicRoomRoute extends RoomTypeRouteConfig {
constructor() {
@ -117,4 +118,10 @@ export class PublicRoomType extends RoomTypeConfig {
return '';
}
}
getAvatarPath(roomData) {
// TODO: change to always get avatar from _id when rooms have avatars
return getAvatarURL({ username: `@${ this.roomName(roomData) }` });
}
}

@ -1,12 +1,12 @@
import { getAvatarUrlFromUsername } from '../../../utils';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
import { roomTypes } from '../../../utils';
export const attachMessage = function(message, room) {
const { msg, u: { username }, ts, attachments, _id } = message;
return {
text: msg,
author_name: username,
author_icon: getAvatarUrlFromUsername(username),
author_icon: getUserAvatarURL(username),
message_link: `${ roomTypes.getRouteLink(room.t, room) }?msg=${ _id }`,
attachments,
ts,

@ -5,6 +5,7 @@ import { hasPermission } from '../../authorization';
import { openRoom } from '../../ui-utils';
import { RoomSettingsEnum, UiTextContext, RoomTypeRouteConfig, RoomTypeConfig } from '../../utils';
import { LivechatInquiry } from './LivechatInquiry';
import { getAvatarURL } from '../../utils/lib/getAvatarURL';
class LivechatRoomRoute extends RoomTypeRouteConfig {
constructor() {
@ -85,4 +86,8 @@ export default class LivechatRoomType extends RoomTypeConfig {
return '';
}
}
getAvatarPath(roomData) {
return getAvatarURL({ username: `@${ this.roomName(roomData) }` });
}
}

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { isTheLastMessage } from '../../lib';
import { getAvatarUrlFromUsername } from '../../utils';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import { hasPermission } from '../../authorization';
import { Subscriptions, Messages, Users, Rooms } from '../../models';
@ -100,9 +100,7 @@ Meteor.methods({
{
text: originalMessage.msg,
author_name: originalMessage.u.username,
author_icon: getAvatarUrlFromUsername(
originalMessage.u.username
),
author_icon: getUserAvatarURL(originalMessage.u.username),
ts: originalMessage.ts,
attachments: recursiveRemove(attachments),
},

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Messages } from '../../models';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { getAvatarUrlFromUsername } from '../../utils';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import _ from 'underscore';
import URL from 'url';
import QueryString from 'querystring';
@ -39,7 +39,7 @@ callbacks.add('beforeSaveMessage', (msg) => {
text: jumpToMessage.msg,
translations: jumpToMessage.translations,
author_name: jumpToMessage.alias || jumpToMessage.u.username,
author_icon: getAvatarUrlFromUsername(jumpToMessage.u.username),
author_icon: getUserAvatarURL(jumpToMessage.u.username),
message_link: item.url,
attachments: jumpToMessage.attachments || [],
ts: jumpToMessage.ts,

@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { getAvatarUrlFromUsername } from '../../utils';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import { Messages, Rooms, Users } from '../../models';
import { settings } from '../../settings';
import {
@ -628,7 +628,7 @@ export default class SlackAdapter {
postMessage(slackChannel, rocketMessage) {
if (slackChannel && slackChannel.id) {
let iconUrl = getAvatarUrlFromUsername(rocketMessage.u && rocketMessage.u.username);
let iconUrl = getUserAvatarURL(rocketMessage.u && rocketMessage.u.username);
if (iconUrl) {
iconUrl = Meteor.absoluteUrl().replace(/\/$/, '') + iconUrl;
}
@ -906,7 +906,7 @@ export default class SlackAdapter {
attachments: [{
text : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text),
author_name : slackMessage.attachments[0].author_subname,
author_icon : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname),
author_icon : getUserAvatarURL(slackMessage.attachments[0].author_subname),
ts : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000),
}],
};
@ -1117,7 +1117,7 @@ export default class SlackAdapter {
attachments: [{
text : this.rocket.convertSlackMsgTxtToRocketTxtFormat(pin.message.text),
author_name : user.username,
author_icon : getAvatarUrlFromUsername(user.username),
author_icon : getUserAvatarURL(user.username),
ts : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000),
}],
};

@ -1,28 +1,30 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { getAvatarUrlFromUsername } from '../../../utils';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
Template.avatar.helpers({
src() {
let { url } = Template.instance().data;
if (!url) {
let { username } = this;
if (username == null && this.userId != null) {
const user = Meteor.users.findOne(this.userId);
username = user && user.username;
}
if (username == null) {
return;
}
Session.get(`avatar_random_${ username }`);
const { url } = Template.instance().data;
if (url) {
return url;
}
if (this.roomIcon) {
username = `@${ username }`;
}
let { username } = this;
if (username == null && this.userId != null) {
const user = Meteor.users.findOne(this.userId);
username = user && user.username;
}
if (!username) {
return;
}
url = getAvatarUrlFromUsername(username);
Session.get(`avatar_random_${ username }`);
if (this.roomIcon) {
username = `@${ username }`;
}
return url;
return getUserAvatarURL(username);
},
});

@ -1,11 +1,12 @@
import { Tracker } from 'meteor/tracker';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { t } from '../../utils';
import { settings } from '../../settings';
import { roomTypes } from '../../utils';
import { Rooms } from '../../models';
import { callbacks } from '../../callbacks';
import { t } from '../../utils/client';
import { settings } from '../../settings/client';
import { roomTypes } from '../../utils/client';
import { Rooms } from '../../models/client';
import { callbacks } from '../../callbacks/client';
Template.chatRoomItem.helpers({
roomData() {
@ -15,22 +16,21 @@ Template.chatRoomItem.helpers({
// unread = this.unread;
// }
const roomType = roomTypes.getConfig(this.t);
const active = [this.rid, this._id].includes((id) => id === openedRoom);
const archivedClass = this.archived ? 'archived' : false;
const icon = this.t !== 'd' && roomTypes.getIcon(this);
const avatar = !icon;
const name = roomTypes.getRoomName(this.t, this);
const roomData = {
...this,
icon,
avatar,
avatar: roomType.getAvatarPath(this),
username : this.name,
route: roomTypes.getRouteLink(this.t, this),
name,
name: roomType.roomName(this),
unread,
active,
archivedClass,
@ -38,11 +38,6 @@ Template.chatRoomItem.helpers({
};
roomData.username = roomData.username || roomData.name;
// hide icon for discussions
if (this.prid) {
roomData.darken = true;
}
if (!this.lastMessage && settings.get('Store_Last_Message')) {
const room = Rooms.findOne(this.rid || this._id, { fields: { lastMessage: 1 } });
roomData.lastMessage = (room && room.lastMessage) || { msg: t('No_messages_yet') };

@ -9,7 +9,7 @@
</template>
<template name="sidebarItem">
<li class="sidebar-item{{#if showUnread }} sidebar-item--unread{{/if}}{{#if active}} sidebar-item--active{{/if}}{{#if toolbar}} popup-item{{/if}}" data-id="{{_id}}">
<li class="sidebar-item{{#if showUnread }} sidebar-item--unread{{/if}}{{#if active}} sidebar-item--active{{/if}}{{#if toolbar}} popup-item{{/if}} js-sidebar-type-{{t}}" data-id="{{_id}}">
<a class="sidebar-item__link" href="{{#if route}}{{route}}{{else}}{{pathFor pathSection group=pathGroup}}{{/if}}" aria-label="{{name}}">
{{#unless isLivechatQueue}}
@ -22,7 +22,7 @@
{{/if}}
{{else}}
<div class="sidebar-item__user-thumb">
{{> avatar username=username roomIcon=icon lazy=true}}
{{> avatar url=avatar roomIcon=icon lazy=true}}
</div>
{{/if}}
</div>

@ -1,13 +1,13 @@
import { Blaze } from 'meteor/blaze';
import { Session } from 'meteor/session';
import { getAvatarUrlFromUsername } from '../../../utils';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
import { RoomManager } from './RoomManager';
Blaze.registerHelper('avatarUrlFromUsername', getAvatarUrlFromUsername);
Blaze.registerHelper('avatarUrlFromUsername', getUserAvatarURL);
export const getAvatarAsPng = function(username, cb) {
const image = new Image;
image.src = getAvatarUrlFromUsername(username);
image.src = getUserAvatarURL(username);
image.onload = function() {
const canvas = document.createElement('canvas');
@ -27,13 +27,16 @@ export const getAvatarAsPng = function(username, cb) {
};
export const updateAvatarOfUsername = function(username) {
const key = `avatar_random_${ username }`;
Session.set(key, Math.round(Math.random() * 1000));
Session.set(`avatar_random_${ username }`, Date.now());
const url = getUserAvatarURL(username);
// force reload of avatars of messages
$(Object.values(RoomManager.openedRooms).map((room) => room.dom))
.find(`.message[data-username='${ username }'] .avatar-image`).attr('src', url);
// force reload of avatar on sidenav
$(`.sidebar-item.js-sidebar-type-d .sidebar-item__link[aria-label='${ username }'] .avatar-image`)
.attr('src', url);
Object.keys(RoomManager.openedRooms).forEach((key) => {
const room = RoomManager.openedRooms[key];
const url = getAvatarUrlFromUsername(username);
$(room.dom).find(`.message[data-username='${ username }'] .avatar-image`).css('background-image', `url(${ url })`);
});
return true;
};

@ -30,7 +30,7 @@
<template name="popupList_item_channel">
<li class="rc-popup-list__item">
<span class="rc-popup-list__item-image">
{{>avatar username=item.name}}
{{>avatar username=item.name roomIcon=true}}
</span>
<span class="rc-popup-list__item-name">{{{modifier item.name}}}</span>
</li>

@ -10,7 +10,7 @@ import s from 'underscore.string';
import { e2e } from '../../../e2e/client';
import { Users, ChatSubscription } from '../../../models';
import { getUserPreference } from '../../../utils';
import { getAvatarUrlFromUsername } from '../../../utils';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
import { getAvatarAsPng } from '../../../ui-utils';
import { promises } from '../../../promises/client';
@ -34,7 +34,7 @@ export const KonchatNotification = {
const message = { rid: (notification.payload != null ? notification.payload.rid : undefined), msg: notification.text, notification: true };
return promises.run('onClientMessageReceived', message).then(function(message) {
const n = new Notification(notification.title, {
icon: notification.icon || getAvatarUrlFromUsername(notification.payload.sender.username),
icon: notification.icon || getUserAvatarURL(notification.payload.sender.username),
body: s.stripTags(message.msg),
tag: notification.payload._id,
silent: true,

@ -9,7 +9,7 @@ export { fileUploadMediaWhiteList, fileUploadIsValidContentType } from '../lib/f
export { roomTypes } from './lib/roomTypes';
export { RoomTypeRouteConfig, RoomTypeConfig, RoomSettingsEnum, UiTextContext } from '../lib/RoomTypeConfig';
export { RoomTypesCommon } from '../lib/RoomTypesCommon';
export { getAvatarUrlFromUsername } from '../lib/getAvatarUrlFromUsername';
export { getUserAvatarURL } from '../lib/getUserAvatarURL';
export { slashCommands } from '../lib/slashCommand';
export { getUserNotificationPreference } from '../lib/getUserNotificationPreference';
export { applyCustomTranslations } from './lib/CustomTranslations';

@ -275,4 +275,7 @@ export class RoomTypeConfig {
return {};
}
getAvatarPath(/* roomData */) {
return '';
}
}

@ -0,0 +1,10 @@
import { getURL } from './getURL';
export const getAvatarURL = ({ username, roomId, cache }) => {
if (username) {
return getURL(`/avatar/${ encodeURIComponent(username) }${ cache ? `?_dc=${ cache }` : '' }`);
}
if (roomId) {
return getURL(`/avatar/room/${ encodeURIComponent(roomId) }${ cache ? `?_dc=${ cache }` : '' }`);
}
};

@ -1,21 +0,0 @@
import { Session } from 'meteor/session';
import { settings } from '../../settings';
export const getAvatarUrlFromUsername = function(username) {
const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, '');
if (externalSource !== '') {
return externalSource.replace('{username}', username);
}
const key = `avatar_random_${ username }`;
const random = typeof Session !== 'undefined' && typeof Session.keys[key] !== 'undefined' ? Session.keys[key] : 0;
if (username == null) {
return;
}
const cdnPrefix = (settings.get('CDN_PREFIX') || '').trim().replace(/\/$/, '');
const pathPrefix = (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '').trim().replace(/\/$/, '');
let path = pathPrefix;
if (cdnPrefix) {
path = cdnPrefix + pathPrefix;
}
return `${ path }/avatar/${ encodeURIComponent(username) }?_dc=${ random }`;
};

@ -0,0 +1,13 @@
import { settings } from '../../settings';
import { getAvatarURL } from './getAvatarURL';
export const getRoomAvatarURL = function(roomId) {
const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, '');
if (externalSource !== '') {
return externalSource.replace('{roomId}', roomId);
}
if (!roomId) {
return;
}
return getAvatarURL({ roomId });
};

@ -0,0 +1,18 @@
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { settings } from '../../settings';
import { getAvatarURL } from './getAvatarURL';
export const getUserAvatarURL = function(username) {
const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, '');
if (externalSource !== '') {
return externalSource.replace('{username}', username);
}
if (username == null) {
return;
}
const key = `avatar_random_${ username }`;
const cache = Tracker.nonreactive(() => Session && Session.get(key)); // there is no Session on server
return getAvatarURL({ username, cache });
};

@ -7,7 +7,7 @@ export { roomTypes } from './lib/roomTypes';
export { RoomTypeRouteConfig, RoomTypeConfig, RoomSettingsEnum, UiTextContext } from '../lib/RoomTypeConfig';
export { RoomTypesCommon } from '../lib/RoomTypesCommon';
export { isDocker } from './functions/isDocker';
export { getAvatarUrlFromUsername } from '../lib/getAvatarUrlFromUsername';
export { getUserAvatarURL } from '../lib/getUserAvatarURL';
export { slashCommands } from '../lib/slashCommand';
export { getUserNotificationPreference } from '../lib/getUserNotificationPreference';
export { getAvatarColor } from '../lib/getAvatarColor';

@ -8,7 +8,6 @@ import './lib/cordova';
import './lib/roomFiles';
import './startup/migrations';
import './startup/appcache';
import './startup/avatar';
import './startup/cron';
import './startup/initialData';
import './startup/presence';
@ -78,5 +77,6 @@ import './publications/subscription';
import './publications/userAutocomplete';
import './publications/userChannels';
import './publications/userData';
import './routes/avatar';
import './stream/messages';
import './stream/streamBroadcast';

@ -0,0 +1,9 @@
import { WebApp } from 'meteor/webapp';
import { roomAvatar } from './room';
import { userAvatar } from './user';
import './middlewares';
WebApp.connectHandlers.use('/avatar/room/', roomAvatar);
WebApp.connectHandlers.use('/avatar/', userAvatar);

@ -0,0 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { userCanAccessAvatar } from '../utils';
// protect all avatar endpoints
export const protectAvatars = Meteor.bindEnvironment((req, res, next) => {
if (!userCanAccessAvatar(req)) {
res.writeHead(403);
res.write('Forbidden');
res.end();
return;
}
return next();
});

@ -0,0 +1,5 @@
import { WebApp } from 'meteor/webapp';
import { protectAvatars } from './auth';
WebApp.connectHandlers.use('/avatar/', protectAvatars);

@ -0,0 +1,41 @@
import { Meteor } from 'meteor/meteor';
import { Rooms } from '../../../app/models/server';
import { roomTypes } from '../../../app/utils';
import {
renderSVGLetters,
serveAvatar,
wasFallbackModified,
setCacheAndDispositionHeaders,
} from './utils';
const getRoom = (roomId) => {
const room = Rooms.findOneById(roomId, { fields: { t: 1, prid: 1, name: 1, fname: 1 } });
// if it is a discussion, returns the parent room
if (room.prid) {
return Rooms.findOneById(room.prid, { fields: { t: 1, name: 1, fname: 1 } });
}
return room;
};
export const roomAvatar = Meteor.bindEnvironment(function(req, res/* , next*/) {
const roomId = req.url.substr(1);
const room = getRoom(roomId);
const roomName = roomTypes.getConfig(room.t).roomName(room);
setCacheAndDispositionHeaders(req, res);
const reqModifiedHeader = req.headers['if-modified-since'];
if (!wasFallbackModified(reqModifiedHeader, res)) {
res.writeHead(304);
res.end();
return;
}
const svg = renderSVGLetters(roomName, req.query.size && parseInt(req.query.size));
return serveAvatar(svg, req.query.format, res);
});

@ -0,0 +1,78 @@
import { Meteor } from 'meteor/meteor';
import { FileUpload } from '../../../app/file-upload';
import { settings } from '../../../app/settings/server';
import { Users, Avatars } from '../../../app/models/server';
import {
renderSVGLetters,
serveAvatar,
wasFallbackModified,
setCacheAndDispositionHeaders,
} from './utils';
// request /avatar/@name forces returning the svg
export const userAvatar = Meteor.bindEnvironment(function(req, res) {
const requestUsername = decodeURIComponent(req.url.substr(1).replace(/\?.*$/, ''));
if (!requestUsername) {
res.writeHead(404);
res.end();
return;
}
const avatarSize = req.query.size && parseInt(req.query.size);
setCacheAndDispositionHeaders(req, res);
// if request starts with @ always return the svg letters
if (requestUsername[0] === '@') {
const svg = renderSVGLetters(requestUsername.substr(1), avatarSize);
serveAvatar(svg, req.query.format, res);
return;
}
const reqModifiedHeader = req.headers['if-modified-since'];
const file = Avatars.findOneByName(requestUsername);
if (file) {
res.setHeader('Content-Security-Policy', 'default-src \'none\'');
if (reqModifiedHeader && reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) {
res.setHeader('Last-Modified', reqModifiedHeader);
res.writeHead(304);
res.end();
return;
}
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);
return FileUpload.get(file, req, res);
}
// if still using "letters fallback"
if (!wasFallbackModified(reqModifiedHeader, res)) {
res.writeHead(304);
res.end();
return;
}
let svg = renderSVGLetters(requestUsername, avatarSize);
if (settings.get('UI_Use_Name_Avatar')) {
const user = Users.findOneByUsername(requestUsername, {
fields: {
name: 1,
},
});
if (user && user.name) {
svg = renderSVGLetters(user.name, avatarSize);
}
}
serveAvatar(svg, req.query.format, res);
return;
});

@ -0,0 +1,93 @@
import sharp from 'sharp';
import { throttle } from 'underscore';
import { Cookies } from 'meteor/ostrio:cookies';
import { Users } from '../../../app/models/server';
import { getAvatarColor } from '../../../app/utils';
import { settings } from '../../../app/settings/server';
const FALLBACK_LAST_MODIFIED = 'Thu, 01 Jan 2015 00:00:00 GMT';
const cookie = new Cookies();
export const serveAvatar = (avatar, format, res) => {
res.setHeader('Last-Modified', FALLBACK_LAST_MODIFIED);
if (['png', 'jpg', 'jpeg'].includes(format)) {
res.setHeader('Content-Type', `image/${ format }`);
sharp(new Buffer(avatar))
.toFormat(format)
.pipe(res);
return;
}
res.setHeader('Content-Type', 'image/svg+xml');
res.write(avatar);
res.end();
};
export const wasFallbackModified = (reqModifiedHeader) => {
if (!reqModifiedHeader || reqModifiedHeader !== FALLBACK_LAST_MODIFIED) {
return true;
}
return false;
};
function isUserAuthenticated({ headers, query }) {
let { rc_uid, rc_token } = query;
if (!rc_uid && headers.cookie) {
rc_uid = cookie.get('rc_uid', headers.cookie) ;
rc_token = cookie.get('rc_token', headers.cookie);
}
const userFound = Users.findOneByIdAndLoginToken(rc_uid, rc_token, { fields: { _id: 1 } });
return !!rc_uid && !!rc_token && !!userFound;
}
const warnUnauthenticatedAccess = throttle(() => {
console.warn('The server detected an unauthenticated access to an user avatar. This type of request will soon be blocked by default.');
}, 60000 * 30); // 30 minutes
export function userCanAccessAvatar({ headers = {}, query = {} }) {
const isAuthenticated = isUserAuthenticated({ headers, query });
if (settings.get('Accounts_AvatarBlockUnauthenticatedAccess') === true) {
return isAuthenticated;
}
if (!isAuthenticated) {
warnUnauthenticatedAccess();
}
return true;
}
const getFirstLetter = (name) => name.replace(/[^A-Za-z0-9]/g, '').substr(0, 1).toUpperCase();
export const renderSVGLetters = (username, viewSize = 200) => {
let color = '';
let initials = '';
if (username === '?') {
color = '#000';
initials = username;
} else {
color = getAvatarColor(username);
initials = getFirstLetter(username);
}
const fontSize = viewSize / 1.6;
return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${ viewSize } ${ viewSize }\">\n<rect width=\"100%\" height=\"100%\" fill=\"${ color }\"/>\n<text x=\"50%\" y=\"50%\" dy=\"0.36em\" text-anchor=\"middle\" pointer-events=\"none\" fill=\"#ffffff\" font-family=\"'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'\" font-size="${ fontSize }">\n${ initials }\n</text>\n</svg>`;
};
const getCacheTime = (cacheTime) => cacheTime || settings.get('Accounts_AvatarCacheTime');
export function setCacheAndDispositionHeaders(req, res) {
const cacheTime = getCacheTime(req.query.cacheTime);
res.setHeader('Cache-Control', `public, max-age=${ cacheTime }`);
res.setHeader('Content-Disposition', 'inline');
}

@ -1,155 +0,0 @@
import { WebApp } from 'meteor/webapp';
import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
import sharp from 'sharp';
import { Cookies } from 'meteor/ostrio:cookies';
import { FileUpload } from '../../app/file-upload';
import { getAvatarColor } from '../../app/utils';
import { Users, Avatars } from '../../app/models';
import { settings } from '../../app/settings';
const cookie = new Cookies();
function isUserAuthenticated(req) {
const headers = req.headers || {};
const query = req.query || {};
let { rc_uid, rc_token } = query;
if (!rc_uid && headers.cookie) {
rc_uid = cookie.get('rc_uid', headers.cookie) ;
rc_token = cookie.get('rc_token', headers.cookie);
}
if (!rc_uid || !rc_token || !Users.findOneByIdAndLoginToken(rc_uid, rc_token)) {
return false;
}
return true;
}
const warnUnauthenticatedAccess = _.debounce(() => {
console.warn('The server detected an unauthenticated access to an user avatar. This type of request will soon be blocked by default.');
}, 60000 * 30); // 30 minutes
function userCanAccessAvatar(req) {
if (settings.get('Accounts_AvatarBlockUnauthenticatedAccess') === true) {
return isUserAuthenticated(req);
}
if (!isUserAuthenticated(req)) {
warnUnauthenticatedAccess();
}
return true;
}
Meteor.startup(function() {
WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/* , next*/) {
const params = {
username: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')),
};
const cacheTime = req.query.cacheTime || settings.get('Accounts_AvatarCacheTime');
if (_.isEmpty(params.username) || !userCanAccessAvatar(req)) {
res.writeHead(403);
res.write('Forbidden');
res.end();
return;
}
const match = /^\/([^?]*)/.exec(req.url);
if (match[1]) {
let username = decodeURIComponent(match[1]);
let file;
username = username.replace(/\.jpg$/, '');
if (username[0] !== '@') {
file = Avatars.findOneByName(username);
}
if (file) {
res.setHeader('Content-Security-Policy', 'default-src \'none\'');
const reqModifiedHeader = req.headers['if-modified-since'];
if (reqModifiedHeader && reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) {
res.setHeader('Last-Modified', reqModifiedHeader);
res.writeHead(304);
res.end();
return;
}
res.setHeader('Cache-Control', `public, max-age=${ cacheTime }`);
res.setHeader('Expires', '-1');
res.setHeader('Content-Disposition', 'inline');
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);
return FileUpload.get(file, req, res);
} else {
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', `public, max-age=${ cacheTime }`);
res.setHeader('Expires', '-1');
res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT');
const reqModifiedHeader = req.headers['if-modified-since'];
if (reqModifiedHeader) {
if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') {
res.writeHead(304);
res.end();
return;
}
}
if (settings.get('UI_Use_Name_Avatar')) {
const user = Users.findOneByUsername(username, {
fields: {
name: 1,
},
});
if (user && user.name) {
username = user.name;
}
}
let color = '';
let initials = '';
if (username === '?') {
color = '#000';
initials = username;
} else {
color = getAvatarColor(username);
initials = username.replace(/[^A-Za-z0-9]/g, '').substr(0, 1).toUpperCase();
}
const viewSize = parseInt(req.query.size) || 200;
const fontSize = viewSize / 1.6;
const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${ viewSize } ${ viewSize }\">\n<rect width=\"100%\" height=\"100%\" fill=\"${ color }\"/>\n<text x=\"50%\" y=\"50%\" dy=\"0.36em\" text-anchor=\"middle\" pointer-events=\"none\" fill=\"#ffffff\" font-family=\"'Helvetica', 'Arial', 'Lucida Grande', 'sans-serif'\" font-size="${ fontSize }">\n${ initials }\n</text>\n</svg>`;
if (['png', 'jpg', 'jpeg'].includes(req.query.format)) {
res.setHeader('Content-Type', `image/${ req.query.format }`);
sharp(new Buffer(svg))
.toFormat(req.query.format)
.pipe(res);
return;
}
res.write(svg);
res.end();
return;
}
}
res.writeHead(404);
res.end();
return;
}));
});
Loading…
Cancel
Save