loki.network rss feed

pull/429/head
Ryan Tharp 6 years ago committed by Beaudan Brown
parent b006e87941
commit c57571fdfd

@ -228,6 +228,7 @@
window.log.warn(`Could not set up channel for ${conversation.id}`);
}
});
window.lokiRssAPI = new window.LokiRssAPI();
window.lokiP2pAPI = new window.LokiP2pAPI(ourKey);
window.lokiP2pAPI.on('pingContact', pubKey => {
const isPing = true;
@ -1424,6 +1425,7 @@
unread: 1,
isP2p: data.isP2p,
isPublic: data.isPublic,
isRss: data.isRss,
};
if (data.friendRequest) {

@ -196,6 +196,9 @@
isPublic() {
return this.id.match(/^publicChat:/);
},
isRss() {
return this.id.match(/^rss:/);
},
isBlocked() {
return BlockedNumberController.isBlocked(this.id);
},
@ -453,6 +456,7 @@
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
isRss: this.isRss(),
},
isOnline: this.isOnline(),
hasNickname: !!this.getNickname(),
@ -642,6 +646,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:
@ -2088,6 +2097,20 @@
});
}
},
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),

@ -0,0 +1,163 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-loop-func */
/* global log, window, textsecure, ConversationController */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const RSS_FEED = 'https://loki.network/feed/';
const CONVO_ID = 'rss://loki.network/feed/';
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.nodeName;
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 {};
}
// hate duplicating this here...
const friendRequestStatusEnum = Object.freeze({
// New conversation, no messages sent or received
none: 0,
// This state is used to lock the input early while sending
pendingSend: 1,
// Friend request sent, awaiting response
requestSent: 2,
// Friend request received, awaiting user input
requestReceived: 3,
// We did it!
friends: 4,
// Friend Request sent but timed out
requestExpired: 5,
});
class LokiRssAPI extends EventEmitter {
constructor() {
super();
// properties
this.groupId = CONVO_ID;
this.feedTimer = null;
this.conversationSetup = false;
// initial set up
this.getFeed();
}
setupConversation() {
// only run once
if (this.conversationSetup) return;
// wait until conversations are loaded
if (ConversationController._initialFetchComplete) {
const conversation = ConversationController.getOrCreate(
this.groupId,
'group'
);
conversation.setFriendRequestStatus(friendRequestStatusEnum.friends);
conversation.setGroupNameAndAvatar(
'Loki.network News',
'images/loki/loki_icon.png'
);
conversation.updateTextInputState();
this.conversationSetup = true; // prevent running again
}
}
async getFeed() {
let response;
let success = true;
try {
response = await nodeFetch(RSS_FEED);
} 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();
// make sure conversation is set up properly
// (delay to after the network response intentionally)
this.setupConversation();
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;

@ -17,6 +17,7 @@
/* global localServerPort: false */
/* global lokiMessageAPI: false */
/* global lokiP2pAPI: false */
/* global lokiRssAPI: false */
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
@ -77,6 +78,7 @@ MessageReceiver.prototype.extend({
this.httpPollingResource.pollServer();
localLokiServer.on('message', this.handleP2pMessage.bind(this));
lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this));
lokiRssAPI.on('rssMessage', this.handlePublicMessage.bind(this));
this.startLocalServer();
// TODO: Rework this socket stuff to work with online messaging

@ -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;

@ -30,6 +30,7 @@ export type PropsData = {
lastMessage?: {
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
text: string;
isRss: boolean;
};
showFriendRequestIndicator?: boolean;
@ -213,7 +214,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;

@ -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,25 @@ 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)/gi, '')
.replace(/(<? *script)/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,6 +40,7 @@ export type ConversationType = {
lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string;
isRss: boolean;
};
phoneNumber: string;
type: 'direct' | 'group';

@ -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