Initial commit

This commit is contained in:
Sonny Bakker 2025-03-23 21:20:21 +01:00
parent 1a54fdbcd1
commit 1b0671b34c
8 changed files with 107 additions and 230 deletions

View file

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

View file

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

View file

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

View file

@ -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 => ({

View file

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

View file

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

View file

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

View file

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