Compare commits

...

1 commit

Author SHA1 Message Date
dd34daae6b Initial commit
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/tests Pipeline failed
2025-03-23 21:20:21 +01:00
8 changed files with 107 additions and 230 deletions

View file

@ -29,11 +29,6 @@ x-django-env: &django-env
EMAIL_USE_SSL:
EMAIL_DEFAULT_FROM:
# Reddit
REDDIT_CLIENT_ID:
REDDIT_CLIENT_SECRET:
REDDIT_CALLBACK_URL:
# Sentry
SENTRY_DSN:

View file

@ -209,16 +209,6 @@ STATICFILES_FINDERS = [
# Email
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
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
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)
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
AXES_HANDLER = "axes.handlers.database.AxesDatabaseHandler"

View file

@ -3,139 +3,136 @@ import { connect } from 'react-redux';
import Cookies from 'js-cookie';
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';
class PostModal extends React.Component {
modalListener = ::this.modalListener;
readTimer = null;
readTimer = null;
componentDidMount() {
const post = { ...this.props.post };
const markPostRead = this.props.markPostRead;
const token = Cookies.get('csrftoken');
componentDidMount() {
const post = { ...this.props.post };
const markPostRead = this.props.markPostRead;
const token = Cookies.get('csrftoken');
if (this.props.autoMarking && this.props.selectedType != SAVED_TYPE && !post.read) {
this.readTimer = setTimeout(markPostRead, 3000, post, token);
}
window.addEventListener('click', this.modalListener);
if (this.props.autoMarking && this.props.selectedType != SAVED_TYPE && !post.read) {
this.readTimer = setTimeout(markPostRead, 3000, post, token);
}
componentWillUnmount() {
if (this.readTimer) {
clearTimeout(this.readTimer);
}
window.addEventListener('click', this.modalListener);
}
this.readTimer = null;
window.removeEventListener('click', this.modalListener);
componentWillUnmount() {
if (this.readTimer) {
clearTimeout(this.readTimer);
}
modalListener(e) {
const targetClassName = e.target.className;
this.readTimer = null;
if (this.props.post && targetClassName == 'modal post-modal') {
this.props.unSelectPost();
}
window.removeEventListener('click', this.modalListener);
}
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() {
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';
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>
let ruleUrl = '';
switch (this.props.rule.type) {
case SUBREDDIT:
ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`;
break;
default:
ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`;
break;
}
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}
<div className="post__buttons">
{this.props.category && (
<span
className="badge post__category"
title={this.props.category.name}
>
<a
href={`${this.props.categoriesUrl}/${this.props.category.id}/`}
target="_blank"
rel="noopener noreferrer"
>
<a
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}
{this.props.category.name}
</a>
</span>
<a
className="post__link"
href={post.url}
target="_blank"
rel="noopener noreferrer"
>
<i className="fas fa-external-link-alt" />
)}
<span className="badge post__rule" title={this.props.rule.name}>
<a href={ruleUrl} target="_blank" rel="noopener noreferrer">
{this.props.rule.name}
</a>
<span
className={savedIconClass}
onClick={() => this.props.toggleSaved(post, token)}
/>
</div>
</span>
<a
className="post__link"
href={post.url}
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>
{/* HTML is sanitized by the collectors */}
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />
</div>
{/* HTML is sanitized by the collectors */}
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />
</div>
);
}
</div>
);
}
}
const mapDispatchToProps = dispatch => ({

View file

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
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 { formatDatetime } from '../../../../utils.js';
@ -18,12 +18,7 @@ class PostItem extends React.Component {
: 'posts__header';
const savedIconClass = post.saved ? 'saved-icon saved-icon--saved' : 'saved-icon';
let ruleUrl = '';
if (rule.type === SUBREDDIT) {
ruleUrl = `${this.props.subredditUrl}/${rule.id}/`;
} else {
ruleUrl = `${this.props.feedUrl}/${rule.id}/`;
}
const ruleUrl = `${this.props.feedUrl}/${rule.id}/`;
return (
<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 SAVED_TYPE = 'SAVED';
export const SUBREDDIT = 'subreddit';
export const FEED = 'feed';

View file

@ -1,4 +1,3 @@
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
@ -84,13 +83,6 @@ class CollectionRule(TimeStampedModel):
@property
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
@property

View file

@ -1,19 +1,13 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.utils.translation import gettext as _
import requests
from celery.exceptions import Reject
from celery.utils.log import get_task_logger
from newsreader.accounts.models import User
from newsreader.celery import app
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.utils import post
from newsreader.utils.celery import MemCacheLock
@ -49,84 +43,6 @@ class FeedTask(app.Task):
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):
name = "FaviconTask"
ignore_result = True
@ -167,5 +83,3 @@ class FaviconTask(app.Task):
FeedTask = app.register_task(FeedTask())
FaviconTask = app.register_task(FaviconTask())
RedditTask = app.register_task(RedditTask())
RedditTokenTask = app.register_task(RedditTokenTask())