Merge pull request #429 from BeaudanBrown/rss-updated

Rss updated
pull/446/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit 205f03419f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -97,6 +97,7 @@ module.exports = {
updateConversation,
removeConversation,
getAllConversations,
getAllRssFeedConversations,
getAllPublicConversations,
getPubKeysWithFriendStatus,
getAllConversationIds,
@ -784,57 +785,84 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
console.log('updateToLokiSchemaVersion1: starting...');
await instance.run('BEGIN TRANSACTION;');
const publicChatData = {
id: 'publicChat:1@chat.lokinet.org',
await instance.run(
`ALTER TABLE messages
ADD COLUMN serverId STRING;`
);
const initConversation = async data => {
const { id, type, name, friendRequestStatus } = data;
await instance.run(
`INSERT INTO conversations (
id,
json,
type,
members,
name,
friendRequestStatus
) values (
$id,
$json,
$type,
$members,
$name,
$friendRequestStatus
);`,
{
$id: id,
$json: objectToJSON(data),
$type: type,
$members: null,
$name: name,
$friendRequestStatus: friendRequestStatus,
}
);
};
const baseData = {
friendRequestStatus: 4, // Friends
sealedSender: 0,
sessionResetStatus: 0,
swarmNodes: [],
type: 'group',
server: 'https://chat.lokinet.org',
name: 'Loki Public Chat',
channelId: '1',
unlockTimestamp: null,
unreadCount: 0,
verified: 0,
version: 2,
};
const { id, type, name, friendRequestStatus } = publicChatData;
await instance.run(
`ALTER TABLE messages
ADD COLUMN serverId STRING;`
);
await instance.run(
`INSERT INTO conversations (
id,
json,
const publicChatData = {
...baseData,
id: 'publicChat:1@chat.lokinet.org',
server: 'https://chat.lokinet.org',
name: 'Loki Public Chat',
channelId: '1',
};
type,
members,
name,
friendRequestStatus
) values (
$id,
$json,
const newsRssFeedData = {
...baseData,
id: 'rss://loki.network/feed/',
rssFeed: 'https://loki.network/feed/',
closable: true,
name: 'Loki.network News',
profileAvatar: 'images/loki/loki_icon.png',
};
$type,
$members,
$name,
$friendRequestStatus
);`,
{
$id: id,
$json: objectToJSON(publicChatData),
const updatesRssFeedData = {
...baseData,
id: 'rss://loki.network/category/messenger-updates/feed/',
rssFeed: 'https://loki.network/category/messenger-updates/feed/',
closable: false,
name: 'Messenger updates',
profileAvatar: 'images/loki/loki_icon.png',
};
$type: type,
$members: null,
$name: name,
$friendRequestStatus: friendRequestStatus,
}
);
await initConversation(publicChatData);
await initConversation(newsRssFeedData);
await initConversation(updatesRssFeedData);
await instance.run(
`INSERT INTO loki_schema (
@ -1606,6 +1634,17 @@ async function getAllPrivateConversations() {
return map(rows, row => jsonToObject(row.json));
}
async function getAllRssFeedConversations() {
const rows = await db.all(
`SELECT json FROM conversations WHERE
type = 'group' AND
id LIKE 'rss://%'
ORDER BY id ASC;`
);
return map(rows, row => jsonToObject(row.json));
}
async function getAllPublicConversations() {
const rows = await db.all(
`SELECT json FROM conversations WHERE

@ -206,6 +206,15 @@
const initAPIs = async () => {
const ourKey = textsecure.storage.user.getNumber();
const rssFeedConversations = await window.Signal.Data.getAllRssFeedConversations(
{
ConversationCollection: Whisper.ConversationCollection,
}
);
window.feeds = [];
rssFeedConversations.forEach(conversation => {
window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings()));
});
window.lokiMessageAPI = new window.LokiMessageAPI(ourKey);
window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey);
const publicConversations = await window.Signal.Data.getAllPublicConversations(
@ -577,7 +586,7 @@
window.log.info('Cleanup: complete');
window.log.info('listening for registration events');
Whisper.events.on('registration_done', () => {
Whisper.events.on('registration_done', async () => {
window.log.info('handling registration event');
startLocalLokiServer();
@ -591,7 +600,7 @@
// logger: window.log,
// });
initAPIs();
await initAPIs();
connect(true);
});
@ -1424,6 +1433,7 @@
unread: 1,
isP2p: data.isP2p,
isPublic: data.isPublic,
isRss: data.isRss,
};
if (data.friendRequest) {

@ -196,6 +196,12 @@
isPublic() {
return this.id.match(/^publicChat:/);
},
isClosable() {
return !this.isRss() || this.get('closable');
},
isRss() {
return this.id && this.id.match(/^rss:/);
},
isBlocked() {
return BlockedNumberController.isBlocked(this.id);
},
@ -302,6 +308,7 @@
},
async updateProfileAvatar() {
if (this.isRss()) return;
const path = profileImages.getOrCreateImagePath(this.id);
await this.setProfileAvatar(path);
},
@ -437,6 +444,7 @@
color,
type: this.isPrivate() ? 'direct' : 'group',
isMe: this.isMe(),
isClosable: this.isClosable(),
isTyping: typingKeys.length > 0,
lastUpdated: this.get('timestamp'),
name: this.getName(),
@ -453,6 +461,7 @@
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
isRss: this.isRss(),
},
isOnline: this.isOnline(),
hasNickname: !!this.getNickname(),
@ -642,6 +651,11 @@
);
},
updateTextInputState() {
if (this.isRss()) {
// or if we're an rss conversation, disable it
this.trigger('disable:input', true);
return;
}
switch (this.get('friendRequestStatus')) {
case FriendRequestStatusEnum.none:
case FriendRequestStatusEnum.requestExpired:
@ -2031,6 +2045,17 @@
getNickname() {
return this.get('nickname');
},
getRssSettings() {
if (!this.isRss()) {
return null;
}
return {
RSS_FEED: this.get('rssFeed'),
CONVO_ID: this.id,
title: this.get('name'),
closeable: this.get('closable'),
};
},
// maybe "Backend" instead of "Source"?
getPublicSource() {
if (!this.isPublic()) {
@ -2088,6 +2113,23 @@
});
}
},
async setGroupNameAndAvatar(name, avatarPath) {
const currentName = this.get('name');
const profileAvatar = this.get('profileAvatar');
if (profileAvatar !== avatarPath || currentName !== name) {
// only update changed items
if (profileAvatar !== avatarPath) {
this.set({ profileAvatar: avatarPath });
}
if (currentName !== name) {
this.set({ name });
}
// save
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
},
async setProfileAvatar(avatarPath) {
const profileAvatar = this.get('profileAvatar');
if (profileAvatar !== avatarPath) {

@ -674,6 +674,7 @@
expirationTimestamp,
isP2p: !!this.get('isP2p'),
isPublic: !!this.get('isPublic'),
isRss: !!this.get('isRss'),
onCopyText: () => this.copyText(),
onReply: () => this.trigger('reply', this),

@ -118,6 +118,7 @@ module.exports = {
getPubKeysWithFriendStatus,
getAllConversationIds,
getAllPrivateConversations,
getAllRssFeedConversations,
getAllPublicConversations,
getAllGroupsInvolvingId,
@ -741,6 +742,14 @@ async function getAllConversationIds() {
return ids;
}
async function getAllRssFeedConversations({ ConversationCollection }) {
const conversations = await channels.getAllRssFeedConversations();
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
}
async function getAllPublicConversations({ ConversationCollection }) {
const conversations = await channels.getAllPublicConversations();

@ -0,0 +1,134 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-loop-func */
/* global log, window, textsecure */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const PER_MIN = 60 * 1000;
const PER_HR = 60 * PER_MIN;
const RSS_POLL_EVERY = 1 * PER_HR; // once an hour
function xml2json(xml) {
try {
let obj = {};
if (xml.children.length > 0) {
for (let i = 0; i < xml.children.length; i += 1) {
const item = xml.children.item(i);
const { nodeName } = item;
if (typeof obj[nodeName] === 'undefined') {
obj[nodeName] = xml2json(item);
} else {
if (typeof obj[nodeName].push === 'undefined') {
const old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xml2json(item));
}
}
} else {
obj = xml.textContent;
}
return obj;
} catch (e) {
log.error(e.message);
}
return {};
}
class LokiRssAPI extends EventEmitter {
constructor(settings) {
super();
// properties
this.feedUrl = settings.RSS_FEED;
this.groupId = settings.CONVO_ID;
this.feedTitle = settings.title;
this.closeable = settings.closeable;
// non configureable options
this.feedTimer = null;
this.conversationSetup = false;
// initial set up
this.getFeed();
}
async getFeed() {
let response;
let success = true;
try {
response = await nodeFetch(this.feedUrl);
} catch (e) {
log.error('fetcherror', e);
success = false;
}
const responseXML = await response.text();
let feedDOM = {};
try {
feedDOM = await new window.DOMParser().parseFromString(
responseXML,
'text/xml'
);
} catch (e) {
log.error('xmlerror', e);
success = false;
}
if (!success) return;
const feedObj = xml2json(feedDOM);
let receivedAt = new Date().getTime();
if (!feedObj || !feedObj.rss || !feedObj.rss.channel) {
log.error('rsserror', feedObj, feedDOM, responseXML);
return;
}
if (!feedObj.rss.channel.item) {
// no records
return;
}
feedObj.rss.channel.item.reverse().forEach(item => {
// log.debug('item', item)
const pubDate = new Date(item.pubDate);
// if we use group style, we can put the title in the source
const messageData = {
friendRequest: false,
source: this.groupId,
sourceDevice: 1,
timestamp: pubDate.getTime(),
serverTimestamp: pubDate.getTime(),
receivedAt,
isRss: true,
message: {
body: `<h2>${item.title} </h2>${item.description}`,
attachments: [],
group: {
id: this.groupId,
type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
flags: 0,
expireTimer: 0,
profileKey: null,
timestamp: pubDate.getTime(),
received_at: receivedAt,
sent_at: pubDate.getTime(),
quote: null,
contact: [],
preview: [],
profile: null,
},
};
receivedAt += 1; // Ensure different arrival times
this.emit('rssMessage', {
message: messageData,
});
});
function callTimer() {
this.getFeed();
}
this.feedTimer = setTimeout(callTimer, RSS_POLL_EVERY);
}
}
module.exports = LokiRssAPI;

@ -201,6 +201,7 @@
isVerified: this.model.isVerified(),
isKeysPending: !this.model.isFriend(),
isMe: this.model.isMe(),
isClosable: this.model.isClosable(),
isBlocked: this.model.isBlocked(),
isGroup: !this.model.isPrivate(),
isOnline: this.model.isOnline(),

@ -17,6 +17,7 @@
/* global localServerPort: false */
/* global lokiMessageAPI: false */
/* global lokiP2pAPI: false */
/* global feeds: false */
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
@ -76,7 +77,14 @@ MessageReceiver.prototype.extend({
});
this.httpPollingResource.pollServer();
localLokiServer.on('message', this.handleP2pMessage.bind(this));
lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this));
lokiPublicChatAPI.on(
'publicMessage',
this.handleUnencryptedMessage.bind(this)
);
// set up pollers for any RSS feeds
feeds.forEach(feed => {
feed.on('rssMessage', this.handleUnencryptedMessage.bind(this));
});
this.startLocalServer();
// TODO: Rework this socket stuff to work with online messaging
@ -144,7 +152,7 @@ MessageReceiver.prototype.extend({
};
this.httpPollingResource.handleMessage(message, options);
},
handlePublicMessage({ message }) {
handleUnencryptedMessage({ message }) {
const ev = new Event('message');
ev.confirm = function confirmTerm() {};
ev.data = message;

@ -326,6 +326,8 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api');
window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api');
window.LokiRssAPI = require('./js/modules/loki_rss_api');
window.LocalLokiServer = require('./libloki/modules/local_loki_server');
window.localServerPort = config.localServerPort;

@ -21,6 +21,7 @@ export type PropsData = {
type: 'group' | 'direct';
avatarPath?: string;
isMe: boolean;
isClosable?: boolean;
lastUpdated: number;
unreadCount: number;
@ -30,6 +31,7 @@ export type PropsData = {
lastMessage?: {
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
text: string;
isRss: boolean;
};
showFriendRequestIndicator?: boolean;
@ -162,6 +164,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
i18n,
isBlocked,
isMe,
isClosable,
hasNickname,
onDeleteContact,
onDeleteMessages,
@ -190,7 +193,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
) : null}
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{!isMe ? (
{!isMe && isClosable ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu>
@ -213,7 +216,13 @@ export class ConversationListItem extends React.PureComponent<Props> {
if (!lastMessage && !isTyping) {
return null;
}
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
let text = lastMessage && lastMessage.text ? lastMessage.text : '';
// if coming from Rss feed
if (lastMessage && lastMessage.isRss) {
// strip any HTML
text = text.replace(/<[^>]*>?/gm, '');
}
if (isEmpty(text)) {
return null;

@ -26,6 +26,7 @@ interface Props {
isVerified: boolean;
isMe: boolean;
isClosable?: boolean;
isGroup: boolean;
isArchived: boolean;
@ -201,6 +202,7 @@ export class ConversationHeader extends React.Component<Props> {
i18n,
isBlocked,
isMe,
isClosable,
isGroup,
isArchived,
onDeleteMessages,
@ -275,7 +277,7 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
)}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{!isMe ? (
{!isMe && isClosable ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu>

@ -9,6 +9,7 @@ const linkify = LinkifyIt();
interface Props {
text: string;
isRss?: boolean;
/** Allows you to customize now non-links are rendered. Simplest is just a <span>. */
renderNonLink?: RenderTextCallbackType;
}
@ -22,9 +23,28 @@ export class Linkify extends React.Component<Props> {
};
public render() {
const { text, renderNonLink } = this.props;
const matchData = linkify.match(text) || [];
const { text, renderNonLink, isRss } = this.props;
const results: Array<any> = [];
if (isRss && text.indexOf('</') !== -1) {
results.push(
<div
dangerouslySetInnerHTML={{
__html: text
.replace(
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
''
)
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ''),
}}
/>
);
// should already have links
return results;
}
const matchData = linkify.match(text) || [];
let last = 0;
let count = 1;

@ -87,6 +87,7 @@ export interface Props {
expirationTimestamp?: number;
isP2p?: boolean;
isPublic?: boolean;
isRss?: boolean;
onClickAttachment?: (attachment: AttachmentType) => void;
onClickLinkPreview?: (url: string) => void;
@ -676,7 +677,7 @@ export class Message extends React.PureComponent<Props, State> {
}
public renderText() {
const { text, textPending, i18n, direction, status } = this.props;
const { text, textPending, i18n, direction, status, isRss } = this.props;
const contents =
direction === 'incoming' && status === 'error'
@ -700,6 +701,7 @@ export class Message extends React.PureComponent<Props, State> {
>
<MessageBody
text={contents || ''}
isRss={isRss}
i18n={i18n}
textPending={textPending}
/>

@ -9,6 +9,7 @@ import { LocalizerType, RenderTextCallbackType } from '../../types/Util';
interface Props {
text: string;
isRss?: boolean;
textPending?: boolean;
/** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */
disableJumbomoji?: boolean;
@ -73,6 +74,7 @@ export class MessageBody extends React.Component<Props> {
textPending,
disableJumbomoji,
disableLinks,
isRss,
i18n,
} = this.props;
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
@ -93,6 +95,7 @@ export class MessageBody extends React.Component<Props> {
return this.addDownloading(
<Linkify
text={textWithPending}
isRss={isRss}
renderNonLink={({ key, text: nonLinkText }) => {
return renderEmoji({
i18n,

@ -40,10 +40,12 @@ export type ConversationType = {
lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string;
isRss: boolean;
};
phoneNumber: string;
type: 'direct' | 'group';
isMe: boolean;
isClosable?: boolean;
lastUpdated: number;
unreadCount: number;
isSelected: boolean;

@ -136,7 +136,15 @@
// 'as' is nicer than angle brackets.
"prefer-type-cast": false,
// We use || and && shortcutting because we're javascript programmers
"strict-boolean-expressions": false
"strict-boolean-expressions": false,
"react-no-dangerous-html": [
true,
{
"file": "ts/components/conversation/Linkify.tsx",
"method": "render",
"comment": "Usage has been approved by Ryan Tharp on 2019-07-22"
}
]
},
"rulesDirectory": ["node_modules/tslint-microsoft-contrib"]
}

Loading…
Cancel
Save