Initial commit
This commit is contained in:
parent
1a54fdbcd1
commit
1b0671b34c
8 changed files with 107 additions and 230 deletions
|
|
@ -29,11 +29,6 @@ x-django-env: &django-env
|
||||||
EMAIL_USE_SSL:
|
EMAIL_USE_SSL:
|
||||||
EMAIL_DEFAULT_FROM:
|
EMAIL_DEFAULT_FROM:
|
||||||
|
|
||||||
# Reddit
|
|
||||||
REDDIT_CLIENT_ID:
|
|
||||||
REDDIT_CLIENT_SECRET:
|
|
||||||
REDDIT_CALLBACK_URL:
|
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
SENTRY_DSN:
|
SENTRY_DSN:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -209,16 +209,6 @@ STATICFILES_FINDERS = [
|
||||||
# Email
|
# Email
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
# Reddit integration
|
|
||||||
REDDIT_CLIENT_ID = "CLIENT_ID"
|
|
||||||
REDDIT_CLIENT_SECRET = "CLIENT_SECRET"
|
|
||||||
REDDIT_REDIRECT_URL = (
|
|
||||||
"http://127.0.0.1:8000/accounts/settings/integrations/reddit/callback/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Twitter integration
|
|
||||||
TWITTER_URL = "https://twitter.com"
|
|
||||||
|
|
||||||
# Third party settings
|
# Third party settings
|
||||||
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
|
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
|
||||||
AXES_CACHE = "axes"
|
AXES_CACHE = "axes"
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,6 @@ EMAIL_USE_SSL = bool(os.environ.get("EMAIL_USE_SSL"))
|
||||||
VERSION = get_current_version(debug=False)
|
VERSION = get_current_version(debug=False)
|
||||||
ENVIRONMENT = "production"
|
ENVIRONMENT = "production"
|
||||||
|
|
||||||
# Reddit integration
|
|
||||||
REDDIT_CLIENT_ID = os.environ.get("REDDIT_CLIENT_ID", "")
|
|
||||||
REDDIT_CLIENT_SECRET = os.environ.get("REDDIT_CLIENT_SECRET", "")
|
|
||||||
REDDIT_REDIRECT_URL = os.environ.get("REDDIT_CALLBACK_URL", "")
|
|
||||||
|
|
||||||
# Third party settings
|
# Third party settings
|
||||||
AXES_HANDLER = "axes.handlers.database.AxesDatabaseHandler"
|
AXES_HANDLER = "axes.handlers.database.AxesDatabaseHandler"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,139 +3,136 @@ import { connect } from 'react-redux';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
import { unSelectPost, markPostRead, toggleSaved } from '../actions/posts.js';
|
import { unSelectPost, markPostRead, toggleSaved } from '../actions/posts.js';
|
||||||
import { SAVED_TYPE, SUBREDDIT } from '../constants.js';
|
import { SAVED_TYPE } from '../constants.js';
|
||||||
import { formatDatetime } from '../../../utils.js';
|
import { formatDatetime } from '../../../utils.js';
|
||||||
|
|
||||||
class PostModal extends React.Component {
|
class PostModal extends React.Component {
|
||||||
modalListener = ::this.modalListener;
|
modalListener = ::this.modalListener;
|
||||||
readTimer = null;
|
readTimer = null;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const post = { ...this.props.post };
|
const post = { ...this.props.post };
|
||||||
const markPostRead = this.props.markPostRead;
|
const markPostRead = this.props.markPostRead;
|
||||||
const token = Cookies.get('csrftoken');
|
const token = Cookies.get('csrftoken');
|
||||||
|
|
||||||
if (this.props.autoMarking && this.props.selectedType != SAVED_TYPE && !post.read) {
|
if (this.props.autoMarking && this.props.selectedType != SAVED_TYPE && !post.read) {
|
||||||
this.readTimer = setTimeout(markPostRead, 3000, post, token);
|
this.readTimer = setTimeout(markPostRead, 3000, post, token);
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('click', this.modalListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
window.addEventListener('click', this.modalListener);
|
||||||
if (this.readTimer) {
|
}
|
||||||
clearTimeout(this.readTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.readTimer = null;
|
componentWillUnmount() {
|
||||||
|
if (this.readTimer) {
|
||||||
window.removeEventListener('click', this.modalListener);
|
clearTimeout(this.readTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
modalListener(e) {
|
this.readTimer = null;
|
||||||
const targetClassName = e.target.className;
|
|
||||||
|
|
||||||
if (this.props.post && targetClassName == 'modal post-modal') {
|
window.removeEventListener('click', this.modalListener);
|
||||||
this.props.unSelectPost();
|
}
|
||||||
}
|
|
||||||
|
modalListener(e) {
|
||||||
|
const targetClassName = e.target.className;
|
||||||
|
|
||||||
|
if (this.props.post && targetClassName == 'modal post-modal') {
|
||||||
|
this.props.unSelectPost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const post = this.props.post;
|
||||||
|
const token = Cookies.get('csrftoken');
|
||||||
|
const publicationDate = formatDatetime(post.publicationDate);
|
||||||
|
const titleClassName = post.read ? 'post__title post__title--read' : 'post__title';
|
||||||
|
const readButtonDisabled =
|
||||||
|
post.read || this.props.isUpdating || this.props.selectedType === SAVED_TYPE;
|
||||||
|
const savedIconClass = post.saved
|
||||||
|
? 'post__save post__save--saved saved-icon saved-icon--saved'
|
||||||
|
: 'post__save saved-icon';
|
||||||
|
|
||||||
|
let ruleUrl = '';
|
||||||
|
|
||||||
|
switch (this.props.rule.type) {
|
||||||
|
default:
|
||||||
|
ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const post = this.props.post;
|
<div className="modal post-modal">
|
||||||
const token = Cookies.get('csrftoken');
|
<div className="post">
|
||||||
const publicationDate = formatDatetime(post.publicationDate);
|
<div className="post__container">
|
||||||
const titleClassName = post.read ? 'post__title post__title--read' : 'post__title';
|
<div className="post__header">
|
||||||
const readButtonDisabled =
|
<div className="post__actions">
|
||||||
post.read || this.props.isUpdating || this.props.selectedType === SAVED_TYPE;
|
<button
|
||||||
const savedIconClass = post.saved
|
className={`button read-button ${readButtonDisabled &&
|
||||||
? 'post__save post__save--saved saved-icon saved-icon--saved'
|
'button--disabled'}`}
|
||||||
: 'post__save saved-icon';
|
onClick={() =>
|
||||||
|
!readButtonDisabled && this.props.markPostRead(post, token)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="fas fa-check" /> Mark as read
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button post__close-button"
|
||||||
|
onClick={() => this.props.unSelectPost()}
|
||||||
|
>
|
||||||
|
<i className="fas fa-times"></i> Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="post__heading">
|
||||||
|
<h2 className={titleClassName}>{`${post.title} `}</h2>
|
||||||
|
<div className="post__meta">
|
||||||
|
<div className="post__text">
|
||||||
|
<span className="post__date">{publicationDate}</span>
|
||||||
|
{post.author && <span className="post__author">{post.author}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
let ruleUrl = '';
|
<div className="post__buttons">
|
||||||
|
{this.props.category && (
|
||||||
switch (this.props.rule.type) {
|
<span
|
||||||
case SUBREDDIT:
|
className="badge post__category"
|
||||||
ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`;
|
title={this.props.category.name}
|
||||||
break;
|
>
|
||||||
default:
|
<a
|
||||||
ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`;
|
href={`${this.props.categoriesUrl}/${this.props.category.id}/`}
|
||||||
break;
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal post-modal">
|
|
||||||
<div className="post">
|
|
||||||
<div className="post__container">
|
|
||||||
<div className="post__header">
|
|
||||||
<div className="post__actions">
|
|
||||||
<button
|
|
||||||
className={`button read-button ${readButtonDisabled &&
|
|
||||||
'button--disabled'}`}
|
|
||||||
onClick={() =>
|
|
||||||
!readButtonDisabled && this.props.markPostRead(post, token)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="fas fa-check" /> Mark as read
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button post__close-button"
|
|
||||||
onClick={() => this.props.unSelectPost()}
|
|
||||||
>
|
|
||||||
<i className="fas fa-times"></i> Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="post__heading">
|
|
||||||
<h2 className={titleClassName}>{`${post.title} `}</h2>
|
|
||||||
<div className="post__meta">
|
|
||||||
<div className="post__text">
|
|
||||||
<span className="post__date">{publicationDate}</span>
|
|
||||||
{post.author && <span className="post__author">{post.author}</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="post__buttons">
|
|
||||||
{this.props.category && (
|
|
||||||
<span
|
|
||||||
className="badge post__category"
|
|
||||||
title={this.props.category.name}
|
|
||||||
>
|
>
|
||||||
<a
|
{this.props.category.name}
|
||||||
href={`${this.props.categoriesUrl}/${this.props.category.id}/`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{this.props.category.name}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="badge post__rule" title={this.props.rule.name}>
|
|
||||||
<a href={ruleUrl} target="_blank" rel="noopener noreferrer">
|
|
||||||
{this.props.rule.name}
|
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<a
|
)}
|
||||||
className="post__link"
|
<span className="badge post__rule" title={this.props.rule.name}>
|
||||||
href={post.url}
|
<a href={ruleUrl} target="_blank" rel="noopener noreferrer">
|
||||||
target="_blank"
|
{this.props.rule.name}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<i className="fas fa-external-link-alt" />
|
|
||||||
</a>
|
</a>
|
||||||
<span
|
</span>
|
||||||
className={savedIconClass}
|
<a
|
||||||
onClick={() => this.props.toggleSaved(post, token)}
|
className="post__link"
|
||||||
/>
|
href={post.url}
|
||||||
</div>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i className="fas fa-external-link-alt" />
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
className={savedIconClass}
|
||||||
|
onClick={() => this.props.toggleSaved(post, token)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HTML is sanitized by the collectors */}
|
|
||||||
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* HTML is sanitized by the collectors */}
|
||||||
|
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
import { CATEGORY_TYPE, SAVED_TYPE, SUBREDDIT } from '../../constants.js';
|
import { CATEGORY_TYPE, SAVED_TYPE } from '../../constants.js';
|
||||||
import { selectPost, toggleSaved } from '../../actions/posts.js';
|
import { selectPost, toggleSaved } from '../../actions/posts.js';
|
||||||
import { formatDatetime } from '../../../../utils.js';
|
import { formatDatetime } from '../../../../utils.js';
|
||||||
|
|
||||||
|
|
@ -18,12 +18,7 @@ class PostItem extends React.Component {
|
||||||
: 'posts__header';
|
: 'posts__header';
|
||||||
const savedIconClass = post.saved ? 'saved-icon saved-icon--saved' : 'saved-icon';
|
const savedIconClass = post.saved ? 'saved-icon saved-icon--saved' : 'saved-icon';
|
||||||
|
|
||||||
let ruleUrl = '';
|
const ruleUrl = `${this.props.feedUrl}/${rule.id}/`;
|
||||||
if (rule.type === SUBREDDIT) {
|
|
||||||
ruleUrl = `${this.props.subredditUrl}/${rule.id}/`;
|
|
||||||
} else {
|
|
||||||
ruleUrl = `${this.props.feedUrl}/${rule.id}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="posts__item" ref={this.props.forwardedRef}>
|
<li className="posts__item" ref={this.props.forwardedRef}>
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,4 @@ export const RULE_TYPE = 'RULE';
|
||||||
export const CATEGORY_TYPE = 'CATEGORY';
|
export const CATEGORY_TYPE = 'CATEGORY';
|
||||||
export const SAVED_TYPE = 'SAVED';
|
export const SAVED_TYPE = 'SAVED';
|
||||||
|
|
||||||
export const SUBREDDIT = 'subreddit';
|
|
||||||
export const FEED = 'feed';
|
export const FEED = 'feed';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
@ -84,13 +83,6 @@ class CollectionRule(TimeStampedModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_url(self):
|
def source_url(self):
|
||||||
if self.type == RuleTypeChoices.subreddit:
|
|
||||||
from newsreader.news.collection.reddit import REDDIT_API_URL, REDDIT_URL
|
|
||||||
|
|
||||||
return self.url.replace(REDDIT_API_URL, REDDIT_URL)
|
|
||||||
elif self.type == RuleTypeChoices.twitter_timeline:
|
|
||||||
return f"{settings.TWITTER_URL}/{self.screen_name}"
|
|
||||||
|
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.mail import send_mail
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from celery.exceptions import Reject
|
from celery.exceptions import Reject
|
||||||
from celery.utils.log import get_task_logger
|
from celery.utils.log import get_task_logger
|
||||||
|
|
||||||
from newsreader.accounts.models import User
|
from newsreader.accounts.models import User
|
||||||
from newsreader.celery import app
|
from newsreader.celery import app
|
||||||
from newsreader.news.collection.choices import RuleTypeChoices
|
from newsreader.news.collection.choices import RuleTypeChoices
|
||||||
from newsreader.news.collection.exceptions.stream import StreamException
|
|
||||||
from newsreader.news.collection.feed import FeedCollector
|
from newsreader.news.collection.feed import FeedCollector
|
||||||
from newsreader.news.collection.utils import post
|
|
||||||
from newsreader.utils.celery import MemCacheLock
|
from newsreader.utils.celery import MemCacheLock
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,84 +43,6 @@ class FeedTask(app.Task):
|
||||||
raise Reject(reason="Task already running", requeue=False)
|
raise Reject(reason="Task already running", requeue=False)
|
||||||
|
|
||||||
|
|
||||||
class RedditTask(app.Task):
|
|
||||||
name = "RedditTask"
|
|
||||||
ignore_result = True
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
from newsreader.news.collection.reddit import RedditCollector, RedditScheduler
|
|
||||||
|
|
||||||
with MemCacheLock("reddit-task", self.app.oid) as acquired:
|
|
||||||
if acquired:
|
|
||||||
logger.info("Running reddit task")
|
|
||||||
|
|
||||||
scheduler = RedditScheduler()
|
|
||||||
subreddits = scheduler.get_scheduled_rules()
|
|
||||||
|
|
||||||
collector = RedditCollector()
|
|
||||||
collector.collect(rules=subreddits)
|
|
||||||
else:
|
|
||||||
logger.warning("Cancelling task due to existing lock")
|
|
||||||
|
|
||||||
raise Reject(reason="Task already running", requeue=False)
|
|
||||||
|
|
||||||
|
|
||||||
class RedditTokenTask(app.Task):
|
|
||||||
name = "RedditTokenTask"
|
|
||||||
ignore_result = True
|
|
||||||
|
|
||||||
def run(self, user_pk):
|
|
||||||
from newsreader.news.collection.reddit import REDDIT_URL
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = User.objects.get(pk=user_pk)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
message = f"User {user_pk} does not exist"
|
|
||||||
logger.exception(message)
|
|
||||||
|
|
||||||
raise Reject(reason=message, requeue=False)
|
|
||||||
|
|
||||||
if not user.reddit_refresh_token:
|
|
||||||
raise Reject(reason=f"User {user_pk} has no refresh token", requeue=False)
|
|
||||||
|
|
||||||
client_auth = requests.auth.HTTPBasicAuth(
|
|
||||||
settings.REDDIT_CLIENT_ID, settings.REDDIT_CLIENT_SECRET
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = post(
|
|
||||||
f"{REDDIT_URL}/api/v1/access_token",
|
|
||||||
data={
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": user.reddit_refresh_token,
|
|
||||||
},
|
|
||||||
auth=client_auth,
|
|
||||||
)
|
|
||||||
except StreamException:
|
|
||||||
logger.exception(
|
|
||||||
f"Failed refreshing reddit access token for user {user_pk}"
|
|
||||||
)
|
|
||||||
|
|
||||||
user.reddit_refresh_token = None
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
message = _(
|
|
||||||
"Your Reddit account credentials have expired. Re-authenticate in"
|
|
||||||
" the settings page to keep retrieving Reddit specific information"
|
|
||||||
" from your account."
|
|
||||||
)
|
|
||||||
|
|
||||||
send_mail(
|
|
||||||
"Reddit account needs re-authentication", message, None, [user.email]
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
response_data = response.json()
|
|
||||||
|
|
||||||
user.reddit_access_token = response_data["access_token"]
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
|
|
||||||
class FaviconTask(app.Task):
|
class FaviconTask(app.Task):
|
||||||
name = "FaviconTask"
|
name = "FaviconTask"
|
||||||
ignore_result = True
|
ignore_result = True
|
||||||
|
|
@ -167,5 +83,3 @@ class FaviconTask(app.Task):
|
||||||
|
|
||||||
FeedTask = app.register_task(FeedTask())
|
FeedTask = app.register_task(FeedTask())
|
||||||
FaviconTask = app.register_task(FaviconTask())
|
FaviconTask = app.register_task(FaviconTask())
|
||||||
RedditTask = app.register_task(RedditTask())
|
|
||||||
RedditTokenTask = app.register_task(RedditTokenTask())
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue