Infomate 1.2 release
This commit is contained in:
@@ -19,3 +19,5 @@ ADD . /app
|
||||
|
||||
RUN pip install --no-cache-dir -e . \
|
||||
&& pip install --no-cache-dir -r $requirements
|
||||
|
||||
RUN python -c "import nltk; nltk.download('punkt')"
|
||||
|
||||
@@ -122,6 +122,10 @@ boards:
|
||||
rss: http://feeds.feedburner.com/TechCrunch/
|
||||
url: https://techcrunch.com
|
||||
is_parsable: false # do not try to parse pages, show RSS content only
|
||||
conditions:
|
||||
- type: not_in
|
||||
field: title
|
||||
word: Trump # exclude articles with a word "Trump" in title
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
name = 'auth'
|
||||
@@ -1,7 +0,0 @@
|
||||
from auth.helpers import authorized_user
|
||||
|
||||
|
||||
def me(request):
|
||||
return {
|
||||
"me": authorized_user(request)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def authorized_user(request):
|
||||
token = request.COOKIES.get(settings.AUTH_COOKIE_NAME)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
||||
except (jwt.DecodeError, jwt.ExpiredSignatureError):
|
||||
return None
|
||||
|
||||
if datetime.utcfromtimestamp(payload["exp"]) < datetime.utcnow():
|
||||
return None
|
||||
|
||||
return payload
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-05 13:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('token', models.CharField(max_length=256, unique=True)),
|
||||
('user_id', models.IntegerField()),
|
||||
('user_name', models.CharField(max_length=32, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
],
|
||||
options={
|
||||
'db_table': 'sessions',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-07 20:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='session',
|
||||
name='token',
|
||||
field=models.CharField(max_length=1024, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -1,15 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
token = models.CharField(max_length=1024, unique=True)
|
||||
user_id = models.IntegerField() # original id of a club user (we don't store profiles)
|
||||
user_name = models.CharField(max_length=32, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
db_table = "sessions"
|
||||
@@ -1,55 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from auth.models import Session
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def login(request):
|
||||
return redirect(f"{settings.AUTH_REDIRECT_URL}?redirect={settings.APP_HOST}/auth/club_callback/")
|
||||
|
||||
|
||||
def club_callback(request):
|
||||
token = request.GET.get("jwt")
|
||||
if not token:
|
||||
return render(request, "message.html", {
|
||||
"title": "Что-то пошло не так",
|
||||
"message": "При авторизации потерялся токен. Попробуйте войти еще раз."
|
||||
})
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
||||
except (jwt.DecodeError, jwt.ExpiredSignatureError) as ex:
|
||||
log.error(f"JWT token error: {ex}")
|
||||
return render(request, "message.html", {
|
||||
"title": "Что-то сломалось",
|
||||
"message": "Неправильный ключ. Наверное, что-то сломалось. Либо ты ХАКИР!!11"
|
||||
})
|
||||
|
||||
Session.objects.get_or_create(
|
||||
token=token[:1024],
|
||||
defaults=dict(
|
||||
user_id=payload["user_id"],
|
||||
user_name=str(payload.get("user_name") or "")[:32],
|
||||
expires_at=datetime.utcfromtimestamp(payload["exp"])
|
||||
)
|
||||
)
|
||||
|
||||
response = redirect("index")
|
||||
response.set_cookie(settings.AUTH_COOKIE_NAME, token, max_age=settings.AUTH_COOKIE_MAX_AGE)
|
||||
return response
|
||||
|
||||
|
||||
def logout(request):
|
||||
token = request.COOKIES.get(settings.AUTH_COOKIE_NAME)
|
||||
if token:
|
||||
Session.objects.filter(token=token).delete()
|
||||
|
||||
response = redirect("index")
|
||||
response.delete_cookie(settings.AUTH_COOKIE_NAME)
|
||||
return response
|
||||
244
boards.yml
244
boards.yml
@@ -4,7 +4,7 @@ boards:
|
||||
is_visible: true
|
||||
is_private: false
|
||||
curator:
|
||||
name: Главные новости
|
||||
name: Новости
|
||||
title: События в мире
|
||||
avatar: https://i.vas3k.ru/63cd2ebddba4422aa684b2bd754c636eb061ef0555f542042c31850525d2f5bb.png
|
||||
bio: Подборка основных новостных изданий, чтобы следить за событиями в России и в мире. Пока бета-версия. Предложения новых источников присылайте в личку
|
||||
@@ -19,6 +19,7 @@ boards:
|
||||
url: https://tass.ru/
|
||||
rss: https://tass.ru/rss/v2.xml
|
||||
icon: https://i.vas3k.ru/aca2f29518b01b25b3a40d63109d45dde74be15e47877aaa89553ff567b05151.png
|
||||
is_parsable: false
|
||||
- name: TJ
|
||||
url: https://tjournal.ru
|
||||
rss: https://tjournal.ru/rss/all
|
||||
@@ -36,13 +37,14 @@ boards:
|
||||
- name: Дождь
|
||||
url: https://tvrain.ru/news/
|
||||
rss: https://tvrain.ru/export/rss/all.xml
|
||||
is_parsable: false
|
||||
- name: Lenta.ru
|
||||
url: https://lenta.ru/
|
||||
rss: https://lenta.ru/rss
|
||||
- name: BBC Россия
|
||||
url: https://www.bbc.com/russian
|
||||
rss: https://feeds.bbci.co.uk/russian/rss.xml
|
||||
- name: Телеграм-каналы
|
||||
- name: Телеграм
|
||||
slug: tg
|
||||
feeds:
|
||||
- name: Varlamov News
|
||||
@@ -64,34 +66,60 @@ boards:
|
||||
url: https://t.me/rtvimain
|
||||
rss: https://infomate.club/parsing/telegram/rtvimain?only=text
|
||||
icon: https://i.vas3k.ru/de4d266b4744ca0cb8bc9ded3842989593fdd54ec78484c7c982b8951db08617.jpg
|
||||
- name: Expresso
|
||||
url: https://t.me/expressotoday
|
||||
rss: https://infomate.club/parsing/telegram/expressotoday?only=text
|
||||
icon: https://i.vas3k.ru/8ecf3ff7c82f89b0bfdde441e32082b063d7bf33e7b3b4b13a77dd35b38aa744.jpg
|
||||
# - name: Expresso
|
||||
# url: https://t.me/expressotoday
|
||||
# rss: https://infomate.club/parsing/telegram/expressotoday?only=text
|
||||
# icon: https://i.vas3k.ru/8ecf3ff7c82f89b0bfdde441e32082b063d7bf33e7b3b4b13a77dd35b38aa744.jpg
|
||||
- name: США
|
||||
slug: us
|
||||
feeds:
|
||||
- name: Reddit News
|
||||
url: https://www.reddit.com/r/news/
|
||||
rss: https://www.reddit.com/r/news.rss
|
||||
- name: CNN
|
||||
url: https://cnn.com/
|
||||
rss: http://rss.cnn.com/rss/edition.rss
|
||||
icon: https://i.vas3k.ru/d2d88f4a1d1646bf3a70f76d6b585c472ee1735abd73dd30719d7f2f42f5743a.png
|
||||
- name: NYT
|
||||
url: https://www.nytimes.com/
|
||||
rss: https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
- name: Axios
|
||||
url: https://www.axios.com/
|
||||
rss: https://api.axios.com/feed/
|
||||
icon: https://i.vas3k.ru/17f55ad102b80a85c618d5e56c61f24c17c20d12f8c960a87902845154a5bdfc.jpg
|
||||
- name: Reuters
|
||||
url: https://www.reuters.com/news/world
|
||||
rss: https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com&ceid=US:en&hl=en-US&gl=US
|
||||
- name: Bloomberg
|
||||
url: https://www.bloomberg.com/
|
||||
rss: http://www.bloomberg.com/politics/feeds/site.xml
|
||||
icon: https://i.vas3k.ru/35c6ae6df0fe47166ed5c656bde6faa974ae1beca949c89443f0aed0b86e0806.png
|
||||
- name: Reuters
|
||||
url: https://www.reuters.com/news/world
|
||||
rss: https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com&ceid=US:en&hl=en-US&gl=US
|
||||
- name: Reddit News
|
||||
url: https://www.reddit.com/r/news/
|
||||
rss: https://www.reddit.com/r/news.rss
|
||||
is_parsable: false
|
||||
- name: NYT
|
||||
url: https://www.nytimes.com/
|
||||
rss: https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml
|
||||
is_parsable: false
|
||||
- name: POLITICO
|
||||
url: https://www.politico.com/
|
||||
rss: https://www.politico.com/rss/politicopicks.xml
|
||||
icon: https://i.vas3k.ru/0281ddd9b3bd890e1476666d5ea74688bc5fcf313500a5fc166127bc433b1287.jpg
|
||||
is_parsable: false
|
||||
- name: 🏳️🌈 Левые
|
||||
slug: us_left
|
||||
view: "blocks/two.html"
|
||||
feeds:
|
||||
- view: "feeds/favicons.html"
|
||||
is_parsable: false
|
||||
mix:
|
||||
- http://rss.cnn.com/rss/edition.rss
|
||||
- https://www.buzzfeed.com/index.xml
|
||||
- https://www.huffpost.com/section/front-page/feed
|
||||
- https://www.newyorker.com/feed/news
|
||||
- https://www.msnbc.com/feed
|
||||
- name: 💰 Правые
|
||||
slug: us_right
|
||||
view: "blocks/two.html"
|
||||
feeds:
|
||||
- view: "feeds/favicons.html"
|
||||
is_parsable: false
|
||||
mix:
|
||||
- http://feeds.feedburner.com/foxnews/latest
|
||||
- https://spectator.org/feed
|
||||
- https://www.washingtontimes.com/rss/headlines/news/
|
||||
- https://www.dailymail.co.uk/ushome/index.rss
|
||||
- name: Европа
|
||||
slug: eu
|
||||
feeds:
|
||||
@@ -127,7 +155,7 @@ boards:
|
||||
is_private: false
|
||||
curator:
|
||||
name: Технологии
|
||||
title: Основные СМИ
|
||||
title: Главные новости
|
||||
avatar: https://i.vas3k.ru/229b722cc79faca1f148c66b1e7240488e7405704f94b8d7f0fadddcf66212f0.jpg
|
||||
bio: Подборка мейнстримовых новостей о технологиях на русском и английском языках
|
||||
blocks:
|
||||
@@ -208,12 +236,11 @@ boards:
|
||||
- name: addmeto
|
||||
url: https://t.me/addmeto
|
||||
rss: https://infomate.club/parsing/telegram/addmeto
|
||||
icon: https://i.vas3k.ru/cb1fe74c1a42fbe9d145c8538ed9230b7512633d06f680b96464fc4b355b23ef.jpg
|
||||
- name: Rozetked
|
||||
url: https://t.me/rozetked
|
||||
rss: https://infomate.club/parsing/telegram/rozetked
|
||||
- name: Rozetked
|
||||
url: https://t.me/rozetked
|
||||
rss: https://infomate.club/parsing/telegram/rozetked
|
||||
icon: https://i.vas3k.ru/abe55f96279f22704cd1cc5009be2f6527c8d205f289dc1c7c328a03314f3d5d.jpg
|
||||
- name: Мейнстрим
|
||||
slug: mainstream
|
||||
feeds:
|
||||
@@ -274,6 +301,14 @@ boards:
|
||||
url: https://www.reddit.com/r/technology
|
||||
rss: https://www.reddit.com/r/technology.rss
|
||||
is_parsable: false
|
||||
- name: "Pinboard: Popular"
|
||||
url: https://pinboard.in/popular/
|
||||
rss: http://feeds.pinboard.in/rss/popular/
|
||||
icon: https://i.vas3k.ru/adfe6b6f09b2be1398df020bdd8d1b8dade25b139c88c68c0177d26e5ae0bce0.jpg
|
||||
conditions:
|
||||
- type: not_in
|
||||
field: title
|
||||
word: Trump
|
||||
- name: ZDNet
|
||||
rss: https://www.zdnet.com/news/rss.xml
|
||||
url: https://www.zdnet.com
|
||||
@@ -284,100 +319,21 @@ boards:
|
||||
- name: Slashdot
|
||||
rss: http://rss.slashdot.org/Slashdot/slashdotMain
|
||||
url: https://slashdot.org
|
||||
icon: https://i.vas3k.ru/2da938e66d63ca719a8854ead38a09d17d6ab17725aaf15fa68f401aa937340e.png
|
||||
- name: 'Medium: Technology'
|
||||
icon: https://i.vas3k.ru/fhb.png
|
||||
url: https://medium.com/topic/technology
|
||||
rss: https://medium.com/feed/topic/technology
|
||||
is_parsable: false
|
||||
- name: Hacker Noon
|
||||
url: https://hackernoon.com/
|
||||
rss: https://hackernoon.com/feed
|
||||
- name: Европейское айти
|
||||
slug: eu
|
||||
feeds:
|
||||
- name: "EU-startups"
|
||||
url: https://www.eu-startups.com/
|
||||
rss: https://www.eu-startups.com/feed/
|
||||
icon: https://i.vas3k.ru/fkp.jpg
|
||||
- name: Tech.eu
|
||||
url: https://tech.eu/
|
||||
rss: https://tech.eu/feed/
|
||||
icon: https://i.vas3k.ru/fl9.jpg
|
||||
- name: "TechCrunch: Europe"
|
||||
url: https://techcrunch.com/europe/
|
||||
rss: http://feeds.feedburner.com/Techcrunch/europe
|
||||
is_parsable: false
|
||||
- name: Инди-разработка
|
||||
slug: make
|
||||
feeds:
|
||||
- name: Show HN
|
||||
url: https://news.ycombinator.com/show
|
||||
rss: https://hnrss.org/show
|
||||
- name: Starter Story
|
||||
url: https://www.starterstory.com
|
||||
rss: https://www.starterstory.com/feed?format=rss
|
||||
- name: 'Reddit: /r/SideProject'
|
||||
url: https://www.reddit.com/r/SideProject/
|
||||
rss: https://www.reddit.com/r/SideProject.rss
|
||||
is_parsable: false
|
||||
- name: Путешествия
|
||||
slug: travel
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
- name: Vandrouki
|
||||
url: https://vandrouki.ru
|
||||
rss: https://feeds.feedburner.com/vandroukiru
|
||||
icon: https://i.vas3k.ru/fer.jpg
|
||||
- name: Secret Flying
|
||||
url: https://www.secretflying.com
|
||||
rss: https://www.secretflying.com/feed/
|
||||
- name: 'Atlas Obscura: Stories'
|
||||
url: https://www.atlasobscura.com/articles
|
||||
rss: https://www.atlasobscura.com/feeds/latest
|
||||
- name: "T—Ж"
|
||||
url: https://journal.tinkoff.ru/chemodan/
|
||||
rss: https://journal.tinkoff.ru/feed/
|
||||
- name: Geeky Explorer
|
||||
url: https://www.geekyexplorer.com
|
||||
rss: https://www.geekyexplorer.com/feed/
|
||||
- name: Микс из блогов крупных компаний
|
||||
slug: engineering
|
||||
feeds:
|
||||
- url: http://www.rssmix.com/1/
|
||||
rss: http://www.rssmix.com/u/10536474/rss.xml
|
||||
columns: 3
|
||||
mix:
|
||||
- http://blog.stackoverflow.com/feed/
|
||||
- https://machinelearning.apple.com/feed.xml
|
||||
- http://nerds.airbnb.com/feed/
|
||||
- http://www.ebaytechblog.com/feed/
|
||||
- http://blog.agilebits.com/feed/
|
||||
- http://research.microsoft.com/rss/news.xml
|
||||
- http://blog.keras.io/feeds/all.atom.xml
|
||||
- http://techblog.netflix.com/feeds/posts/default
|
||||
- http://nickcraver.com/blog/feed.xml
|
||||
- http://googlerussiablog.blogspot.com/atom.xml
|
||||
- https://eng.uber.com/feed/
|
||||
- http://web.mit.edu/newsoffice/topic/mitcomputers-rss.xml
|
||||
- http://feeds.feedburner.com/corner-squareup-com
|
||||
- http://googleresearch.blogspot.com/atom.xml
|
||||
- http://githubengineering.com/atom.xml
|
||||
- http://labs.spotify.com/feed/
|
||||
- http://blog.kaggle.com/feed/
|
||||
- http://clubs.ya.ru/company/rss/posts.xml
|
||||
- name: Блоги людей
|
||||
slug: people
|
||||
feeds:
|
||||
- url: http://www.rssmix.com/2/
|
||||
rss: http://www.rssmix.com/u/10538572/rss.xml
|
||||
columns: 3
|
||||
- columns: 3
|
||||
view: "feeds/favicons.html"
|
||||
mix:
|
||||
- http://nedbatchelder.com/blog/rss.xml
|
||||
- http://rasskazov.pro/blog/?go=rss/
|
||||
- http://vas3k.ru/rss/
|
||||
- http://www.dmitriysivak.com/feed/
|
||||
- http://nl.livejournal.com/data/rss
|
||||
- http://sashavolkova.ru/rss/
|
||||
- http://alexmak.net/blog/feed/
|
||||
@@ -410,6 +366,81 @@ boards:
|
||||
- http://feeds.feedburner.com/codinghorror/
|
||||
- http://theoatmeal.com/feed/rss
|
||||
- https://waitbutwhy.com/feed
|
||||
- name: Инди-разработка
|
||||
slug: make
|
||||
feeds:
|
||||
- name: Show HN
|
||||
url: https://news.ycombinator.com/show
|
||||
rss: https://hnrss.org/show
|
||||
- name: Starter Story
|
||||
url: https://www.starterstory.com
|
||||
rss: https://www.starterstory.com/feed?format=rss
|
||||
- name: 'Reddit: /r/SideProject'
|
||||
url: https://www.reddit.com/r/SideProject/
|
||||
rss: https://www.reddit.com/r/SideProject.rss
|
||||
is_parsable: false
|
||||
- name: Путешествия
|
||||
slug: travel
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
- name: Vandrouki
|
||||
url: https://vandrouki.ru
|
||||
rss: https://feeds.feedburner.com/vandroukiru
|
||||
icon: https://i.vas3k.ru/fer.jpg
|
||||
- name: Secret Flying
|
||||
url: https://www.secretflying.com
|
||||
rss: https://www.secretflying.com/feed/
|
||||
- name: 'Atlas Obscura: Stories'
|
||||
url: https://www.atlasobscura.com/articles
|
||||
rss: https://www.atlasobscura.com/feeds/latest
|
||||
icon: https://i.vas3k.ru/345139fb86cb52076134880d1b4ef700d6354c4cf4639ebdfaf1f9891115f7ad.jpg
|
||||
- name: "T—Ж"
|
||||
url: https://journal.tinkoff.ru/chemodan/
|
||||
rss: https://journal.tinkoff.ru/feed/
|
||||
- name: Geeky Explorer
|
||||
url: https://www.geekyexplorer.com
|
||||
rss: https://www.geekyexplorer.com/feed/
|
||||
- name: Европейское айти
|
||||
slug: eu
|
||||
feeds:
|
||||
- name: "EU-startups"
|
||||
url: https://www.eu-startups.com/
|
||||
rss: https://www.eu-startups.com/feed/
|
||||
icon: https://i.vas3k.ru/fkp.jpg
|
||||
- name: Tech.eu
|
||||
url: https://tech.eu/
|
||||
rss: https://tech.eu/feed/
|
||||
icon: https://i.vas3k.ru/fl9.jpg
|
||||
- name: "TechCrunch: Europe"
|
||||
url: https://techcrunch.com/europe/
|
||||
rss: http://feeds.feedburner.com/Techcrunch/europe
|
||||
is_parsable: false
|
||||
- name: Микс из блогов крупных компаний
|
||||
slug: engineering
|
||||
feeds:
|
||||
- columns: 3
|
||||
view: "feeds/favicons.html"
|
||||
mix:
|
||||
- http://blog.stackoverflow.com/feed/
|
||||
- https://machinelearning.apple.com/feed.xml
|
||||
- http://nerds.airbnb.com/feed/
|
||||
- http://www.ebaytechblog.com/feed/
|
||||
- http://blog.agilebits.com/feed/
|
||||
- http://research.microsoft.com/rss/news.xml
|
||||
- http://blog.keras.io/feeds/all.atom.xml
|
||||
- http://techblog.netflix.com/feeds/posts/default
|
||||
- http://nickcraver.com/blog/feed.xml
|
||||
- http://googlerussiablog.blogspot.com/atom.xml
|
||||
- https://eng.uber.com/feed/
|
||||
- http://web.mit.edu/newsoffice/topic/mitcomputers-rss.xml
|
||||
- http://feeds.feedburner.com/corner-squareup-com
|
||||
- http://googleresearch.blogspot.com/atom.xml
|
||||
- http://githubengineering.com/atom.xml
|
||||
- http://labs.spotify.com/feed/
|
||||
- http://blog.kaggle.com/feed/
|
||||
- http://clubs.ya.ru/company/rss/posts.xml
|
||||
- name: Фотография
|
||||
slug: photo
|
||||
feeds:
|
||||
@@ -629,6 +660,7 @@ boards:
|
||||
- name: "DeepMind"
|
||||
url: https://www.deepmind.com/blog
|
||||
rss: https://www.deepmind.com/blog/feed/basic/
|
||||
icon: https://i.vas3k.ru/aff485d139d37ac7236f0bdf831812a2ef2419972ca3996c66885f229dccf7e2.jpg
|
||||
- name: "Google"
|
||||
url: https://ai.googleblog.com/
|
||||
rss: http://rssmix.com/u/10966870/rss.xml
|
||||
@@ -721,6 +753,7 @@ boards:
|
||||
- name: "DeepMind: The Podcast"
|
||||
url: https://deepmind.com/blog/article/welcome-to-the-deepmind-podcast
|
||||
rss: https://feeds.simplecast.com/JT6pbPkg
|
||||
icon: https://i.vas3k.ru/aff485d139d37ac7236f0bdf831812a2ef2419972ca3996c66885f229dccf7e2.jpg
|
||||
- name: "Microsoft Research Podcast"
|
||||
icon: https://i.vas3k.ru/i0t.png
|
||||
url: https://www.microsoft.com/en-us/research/blog/category/podcast/
|
||||
@@ -2140,4 +2173,3 @@ boards:
|
||||
- name: "QA Mania (Украиноязычный канал)"
|
||||
url: https://t.me/qamania
|
||||
rss: https://infomate.club/parsing/telegram/qamania
|
||||
|
||||
|
||||
@@ -4,3 +4,25 @@ DOMAIN_ICONS = {
|
||||
"reddit.com": "fa:fab fa-reddit-alien",
|
||||
"github.com": "fa:fab fa-github",
|
||||
}
|
||||
|
||||
DOMAIN_FAVICONS = {
|
||||
"bbc.com": "https://i.vas3k.ru/635c5e5828a4868b73bdb777611084a3459873b628f3f7f9752a34e1516fc505.png",
|
||||
"bbc.co.uk": "https://i.vas3k.ru/635c5e5828a4868b73bdb777611084a3459873b628f3f7f9752a34e1516fc505.png",
|
||||
"cnn.com": "https://i.vas3k.ru/d2d88f4a1d1646bf3a70f76d6b585c472ee1735abd73dd30719d7f2f42f5743a.png",
|
||||
"cnn.it": "https://i.vas3k.ru/d2d88f4a1d1646bf3a70f76d6b585c472ee1735abd73dd30719d7f2f42f5743a.png",
|
||||
"foxnews.com": "https://i.vas3k.ru/46b5aabb4269c34ff22c90afeae1cf4b2fc64078efb0611dbddd53d336395ea4.png",
|
||||
"vox.com": "https://i.vas3k.ru/1a7ab1f00077e12d840d236e4438db9cc47a21b2b25c9cedea68ea9b167bb7d3.png",
|
||||
"buzzfeednews.com": "https://i.vas3k.ru/ebe634861320fccd85c2fe96a5c93efd2b6643c2e191e3c8ee4b2ac0fb5b5e1c.png",
|
||||
"buzzfeed.com": "https://i.vas3k.ru/ebe634861320fccd85c2fe96a5c93efd2b6643c2e191e3c8ee4b2ac0fb5b5e1c.png",
|
||||
"huffpost.com": "https://i.vas3k.ru/a357095a972b2846476d95fb045de8880fe6e16f77fb34f7e07cbb13f196cb0b.png",
|
||||
"newyorker.com": "https://i.vas3k.ru/b807c76f8514e36216d3593d9585bfe0335ff1514a5913e475a97fcc52e1fe21.png",
|
||||
"dailymail.co.uk": "https://i.vas3k.ru/16371b2d1711d17cae30aa943e337b1486442d8ea471b08e2f3bd29f14239a81.png",
|
||||
"dailymail.com": "https://i.vas3k.ru/16371b2d1711d17cae30aa943e337b1486442d8ea471b08e2f3bd29f14239a81.png",
|
||||
"washingtontimes.com": "https://i.vas3k.ru/6da045463763191ed01c6bced5321f47d10d6804d244baee335b7702efbd8bc0.png",
|
||||
"msnbc.com": "https://i.vas3k.ru/1b87330e504abdd0ffa6767827f9a2a3475c72ecc1eb4c80e3cd73e99b5d348a.png",
|
||||
"vas3k.ru": "https://i.vas3k.ru/90e7cad4728678bf11dd2379dbcbda94f485b92ccd229d492a1bf2da31441c7c.png",
|
||||
"vas3k.com": "https://i.vas3k.ru/90e7cad4728678bf11dd2379dbcbda94f485b92ccd229d492a1bf2da31441c7c.png",
|
||||
"maximilyahov.ru": "https://i.vas3k.ru/5a40caa8b7c7ebeab10516ebc7402151d4c98cd96704b9149d4869f6028bf642.jpg",
|
||||
"skaplichniy.ru": "https://i.vas3k.ru/c0c5e651be84c1a84b6b819448605def65db8373607fea958bc53a27ea7d3902.jpg",
|
||||
"mikeozornin.ru": "https://i.vas3k.ru/31344c5d6928534842caf0b0f173205556baaea370ee0966853260c4713004b1.png",
|
||||
}
|
||||
|
||||
34
boards/migrations/0009_auto_20200905_1246.py
Normal file
34
boards/migrations/0009_auto_20200905_1246.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 2.2.13 on 2020-09-05 12:46
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0008_boardfeed_is_parsable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='boardblock',
|
||||
name='view',
|
||||
field=models.CharField(default='blocks/three.html', max_length=256),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='mix',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='view',
|
||||
field=models.CharField(default='feeds/simple.html', max_length=256),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='url',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,7 @@ from django.db import models
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from slugify import slugify
|
||||
|
||||
from boards.icons import DOMAIN_ICONS
|
||||
from boards.icons import DOMAIN_ICONS, DOMAIN_FAVICONS
|
||||
|
||||
|
||||
class Board(models.Model):
|
||||
@@ -57,11 +57,15 @@ class Board(models.Model):
|
||||
|
||||
|
||||
class BoardBlock(models.Model):
|
||||
DEFAULT_VIEW = "blocks/three.html"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
board = models.ForeignKey(Board, related_name="blocks", on_delete=models.CASCADE, db_index=True)
|
||||
name = models.CharField(max_length=512, null=True)
|
||||
slug = models.SlugField()
|
||||
|
||||
view = models.CharField(max_length=256, default=DEFAULT_VIEW, null=False)
|
||||
|
||||
created_at = models.DateTimeField(db_index=True)
|
||||
updated_at = models.DateTimeField()
|
||||
|
||||
@@ -71,6 +75,12 @@ class BoardBlock(models.Model):
|
||||
db_table = "board_blocks"
|
||||
ordering = ["index"]
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
if self.view and self.view.endswith(".html"):
|
||||
return self.view
|
||||
return self.DEFAULT_VIEW
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.utcnow()
|
||||
@@ -84,14 +94,19 @@ class BoardBlock(models.Model):
|
||||
|
||||
|
||||
class BoardFeed(models.Model):
|
||||
DEFAULT_VIEW = "feeds/simple.html"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
board = models.ForeignKey(Board, related_name="feeds", on_delete=models.CASCADE, db_index=True)
|
||||
block = models.ForeignKey(BoardBlock, related_name="feeds", on_delete=models.CASCADE, db_index=True)
|
||||
name = models.CharField(max_length=512, null=True)
|
||||
comment = models.TextField(null=True)
|
||||
url = models.URLField(max_length=512)
|
||||
url = models.TextField()
|
||||
icon = models.URLField(max_length=512, null=True)
|
||||
rss = models.URLField(max_length=512, null=True)
|
||||
mix = JSONField(null=True)
|
||||
|
||||
view = models.CharField(max_length=256, default=DEFAULT_VIEW, null=False)
|
||||
|
||||
created_at = models.DateTimeField(db_index=True)
|
||||
last_article_at = models.DateTimeField(null=True)
|
||||
@@ -109,6 +124,12 @@ class BoardFeed(models.Model):
|
||||
db_table = "board_feeds"
|
||||
ordering = ["index"]
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
if self.view and self.view.endswith(".html"):
|
||||
return self.view
|
||||
return self.DEFAULT_VIEW
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.utcnow()
|
||||
@@ -167,6 +188,13 @@ class Article(models.Model):
|
||||
|
||||
return f"""<img src="{article_icon}" alt="{self.domain}" class="icon"> """
|
||||
|
||||
def favicon(self):
|
||||
if not self.domain:
|
||||
return None
|
||||
|
||||
favicon_domain = self.domain.lower().replace("www.", "")
|
||||
return DOMAIN_FAVICONS.get(favicon_domain)
|
||||
|
||||
def natural_created_at(self):
|
||||
if not self.created_at:
|
||||
return None
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.shortcuts import render, get_object_or_404
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import last_modified
|
||||
|
||||
from auth.helpers import authorized_user
|
||||
from boards.cache import board_last_modified_at
|
||||
from boards.models import Board, BoardBlock, BoardFeed
|
||||
|
||||
@@ -23,13 +22,6 @@ def index(request):
|
||||
def board(request, board_slug):
|
||||
board = get_object_or_404(Board, slug=board_slug)
|
||||
|
||||
if board.is_private:
|
||||
me = authorized_user(request)
|
||||
if not me:
|
||||
return render(request, "board_no_access.html", {
|
||||
"board": board
|
||||
}, status=401)
|
||||
|
||||
cached_page = cache.get(f"board_{board.slug}")
|
||||
if cached_page and board.refreshed_at and board.refreshed_at <= \
|
||||
datetime.utcnow() - timedelta(seconds=settings.BOARD_CACHE_SECONDS):
|
||||
@@ -46,15 +38,6 @@ def board(request, board_slug):
|
||||
return result
|
||||
|
||||
|
||||
@cache_page(settings.STATIC_PAGE_CACHE_SECONDS)
|
||||
def export(request, board_slug):
|
||||
board = get_object_or_404(Board, slug=board_slug)
|
||||
|
||||
return render(request, "export.html", {
|
||||
"board": board,
|
||||
})
|
||||
|
||||
|
||||
@cache_page(settings.STATIC_PAGE_CACHE_SECONDS)
|
||||
def what(request):
|
||||
return render(request, "what.html")
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=infomate
|
||||
ports:
|
||||
- 5432
|
||||
- "54321:5432"
|
||||
|
||||
migrate_and_init:
|
||||
<<: *app
|
||||
|
||||
@@ -16,7 +16,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"django_bleach",
|
||||
"auth",
|
||||
"boards",
|
||||
"parsing"
|
||||
]
|
||||
@@ -39,7 +38,6 @@ TEMPLATES = [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"boards.context_processors.settings_processor",
|
||||
"auth.context_processors.me",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.urls import path
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
from auth.views import login, logout, club_callback
|
||||
from boards.views import index, board, privacy_policy, what, export
|
||||
from boards.views import index, board, privacy_policy, what
|
||||
from infomate import settings
|
||||
from parsing.views import TelegramChannelFeed
|
||||
|
||||
@@ -12,12 +11,7 @@ urlpatterns = [
|
||||
|
||||
path("docs/privacy_policy/", privacy_policy, name="privacy_policy"),
|
||||
|
||||
path("auth/login/", login, name="login"),
|
||||
path("auth/club_callback/", club_callback, name="club_callback"),
|
||||
path("auth/logout/", logout, name="logout"),
|
||||
|
||||
path("<slug:board_slug>/", board, name="board"),
|
||||
path("<slug:board_slug>/export/", export, name="export"),
|
||||
|
||||
path("parsing/telegram/<str:channel>/",
|
||||
cache_page(settings.TELEGRAM_CACHE_SECONDS)(TelegramChannelFeed()),
|
||||
|
||||
@@ -8,7 +8,6 @@ beautifulsoup4==4.6.2
|
||||
pyyaml==5.2
|
||||
feedparser==5.2.1
|
||||
sentry-sdk==0.14.1
|
||||
pyjwt>=1.7.1
|
||||
nltk==3.4.5
|
||||
newspaper3k>=0.2.8
|
||||
telethon==1.10.10
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from time import mktime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from requests import RequestException
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
import requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_REQUEST_HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 "
|
||||
@@ -13,3 +23,86 @@ MAX_PARSABLE_CONTENT_LENGTH = 15 * 1024 * 1024 # 15Mb
|
||||
|
||||
socket.setdefaulttimeout(DEFAULT_REQUEST_TIMEOUT)
|
||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||
|
||||
|
||||
def resolve_url(entry_link):
|
||||
url = str(entry_link)
|
||||
content_type = None
|
||||
content_length = MAX_PARSABLE_CONTENT_LENGTH + 1 # don't parse null content-types
|
||||
depth = 10
|
||||
while depth > 0:
|
||||
depth -= 1
|
||||
|
||||
try:
|
||||
response = requests.head(url, timeout=DEFAULT_REQUEST_TIMEOUT, verify=False, stream=True)
|
||||
except RequestException:
|
||||
log.warning(f"Failed to resolve URL: {url}")
|
||||
return None, content_type, content_length
|
||||
|
||||
if 300 < response.status_code < 400:
|
||||
url = response.headers["location"] # follow redirect
|
||||
else:
|
||||
content_type = response.headers.get("content-type")
|
||||
content_length = int(response.headers.get("content-length") or 0)
|
||||
break
|
||||
|
||||
return url, content_type, content_length
|
||||
|
||||
|
||||
def parse_domain(url):
|
||||
domain = urlparse(url).netloc
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
return domain
|
||||
|
||||
|
||||
def parse_datetime(entry):
|
||||
published_time = entry.get("published_parsed") or entry.get("updated_parsed")
|
||||
if published_time:
|
||||
return datetime.fromtimestamp(mktime(published_time))
|
||||
return datetime.utcnow()
|
||||
|
||||
|
||||
def parse_title(entry):
|
||||
title = entry.get("title") or entry.get("description") or entry.get("summary")
|
||||
return re.sub("<[^<]+?>", "", title).strip()
|
||||
|
||||
|
||||
def parse_link(entry):
|
||||
if entry.get("link"):
|
||||
return entry["link"]
|
||||
|
||||
if entry.get("links"):
|
||||
return entry["links"][0]["href"]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss_image(entry):
|
||||
if entry.get("media_content"):
|
||||
images = [m["url"] for m in entry["media_content"] if m.get("medium") == "image" and m.get("url")]
|
||||
if images:
|
||||
return images[0]
|
||||
|
||||
if entry.get("image"):
|
||||
if isinstance(entry["image"], dict):
|
||||
return entry["image"].get("href")
|
||||
return entry["image"]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss_text_and_image(entry):
|
||||
if not entry.get("summary"):
|
||||
return "", ""
|
||||
|
||||
bs = BeautifulSoup(entry["summary"], features="lxml")
|
||||
text = re.sub(r"\s\s+", " ", bs.text or "").strip()
|
||||
|
||||
img_tags = bs.findAll("img")
|
||||
for img_tag in img_tags:
|
||||
src = img_tag.get("src", None)
|
||||
if src:
|
||||
return text, src
|
||||
|
||||
return text, ""
|
||||
@@ -11,12 +11,12 @@ from urllib.parse import urljoin
|
||||
import click
|
||||
import requests
|
||||
import yaml
|
||||
import feedparser
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from boards.models import Board, BoardFeed, BoardBlock
|
||||
from boards.icons import DOMAIN_FAVICONS
|
||||
from utils.images import upload_image_from_url
|
||||
from scripts.common import DEFAULT_REQUEST_HEADERS
|
||||
from scripts.common import DEFAULT_REQUEST_HEADERS, parse_domain
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -79,76 +79,84 @@ def initialize(config, board_slug, upload_favicons, always_yes):
|
||||
slug=block_config["slug"],
|
||||
defaults=dict(
|
||||
name=block_name,
|
||||
index=block_index
|
||||
index=block_index,
|
||||
view=block_config.get("view") or BoardBlock.DEFAULT_VIEW,
|
||||
)
|
||||
)
|
||||
|
||||
if not is_created:
|
||||
block.index = block_index
|
||||
block.name = block_name
|
||||
block.view = block_config.get("view") or BoardBlock.DEFAULT_VIEW
|
||||
block.save()
|
||||
|
||||
if not block_config.get("feeds"):
|
||||
continue
|
||||
|
||||
updated_feed_urls = set()
|
||||
|
||||
for feed_index, feed_config in enumerate(block_config.get("feeds") or []):
|
||||
feed_name = feed_config.get("name")
|
||||
feed_url = feed_config["url"]
|
||||
feed_mix = feed_config.get("mix")
|
||||
if feed_mix:
|
||||
feed_url = feed_config.get("url") or f"mix:{'|'.join(feed_mix)}"
|
||||
feed_rss = None
|
||||
else:
|
||||
feed_url = feed_config["url"]
|
||||
feed_rss = feed_config["rss"]
|
||||
|
||||
updated_feed_urls.add(feed_url)
|
||||
|
||||
print(f"Creating or updating feed: {feed_name}...")
|
||||
print(f"Creating or updating feed {feed_name} ({feed_url})...")
|
||||
|
||||
feed, is_created = BoardFeed.objects.get_or_create(
|
||||
board=board,
|
||||
block=block,
|
||||
url=feed_config["url"],
|
||||
url=feed_url,
|
||||
defaults=dict(
|
||||
rss=feed_config.get("rss"),
|
||||
rss=feed_rss,
|
||||
mix=feed_mix,
|
||||
name=feed_name,
|
||||
comment=feed_config.get("comment"),
|
||||
icon=feed_config.get("icon"),
|
||||
index=feed_index,
|
||||
columns=feed_config.get("columns") or 1,
|
||||
conditions=feed_config.get("conditions"),
|
||||
is_parsable=feed_config.get("is_parsable", True)
|
||||
is_parsable=feed_config.get("is_parsable", True),
|
||||
view=feed_config.get("view") or BoardFeed.DEFAULT_VIEW,
|
||||
)
|
||||
)
|
||||
|
||||
if not is_created:
|
||||
feed.rss = feed_config.get("rss")
|
||||
feed.rss = feed_rss
|
||||
feed.mix = feed_mix
|
||||
feed.name = feed_name
|
||||
feed.comment = feed_config.get("comment")
|
||||
feed.index = feed_index
|
||||
feed.icon = feed.icon or feed_config.get("icon")
|
||||
feed.columns = feed_config.get("columns") or 1
|
||||
feed.conditions = feed_config.get("conditions")
|
||||
feed.is_parsable = feed_config.get("is_parsable", True)
|
||||
feed.view = feed_config.get("view") or BoardFeed.DEFAULT_VIEW
|
||||
|
||||
html = None
|
||||
|
||||
if not feed.rss:
|
||||
html = html or load_page_html(feed_url)
|
||||
rss_url = feed_config.get("rss")
|
||||
if not rss_url:
|
||||
rss_url = find_rss_feed(feed_url, html)
|
||||
if not rss_url:
|
||||
print(f"RSS feed for '{feed_name}' not found. "
|
||||
f"Please specify 'rss' key.")
|
||||
exit(1)
|
||||
print(f"- found RSS: {rss_url}")
|
||||
if not feed.mix:
|
||||
if not feed.icon:
|
||||
feed.icon = DOMAIN_FAVICONS.get(parse_domain(feed_url))
|
||||
|
||||
feed.rss = rss_url
|
||||
if not feed.icon:
|
||||
html = html or load_page_html(feed_url)
|
||||
icon = feed_config.get("icon")
|
||||
if not icon:
|
||||
icon = find_favicon(feed_url, html)
|
||||
print(f"- found favicon: {icon}")
|
||||
|
||||
if not feed.icon:
|
||||
html = html or load_page_html(feed_url)
|
||||
icon = feed_config.get("icon")
|
||||
if not icon:
|
||||
icon = find_favicon(feed_url, html)
|
||||
print(f"- found favicon: {icon}")
|
||||
if upload_favicons:
|
||||
icon = upload_image_from_url(icon)
|
||||
print(f"- uploaded favicon: {icon}")
|
||||
|
||||
if upload_favicons:
|
||||
icon = upload_image_from_url(icon)
|
||||
print(f"- uploaded favicon: {icon}")
|
||||
|
||||
feed.icon = icon
|
||||
feed.icon = icon
|
||||
|
||||
feed.save()
|
||||
|
||||
@@ -157,7 +165,7 @@ def initialize(config, board_slug, upload_favicons, always_yes):
|
||||
board=board,
|
||||
block=block
|
||||
).exclude(
|
||||
url__in={feed["url"] for feed in block_config.get("feeds") or []}
|
||||
url__in=updated_feed_urls
|
||||
).delete()
|
||||
|
||||
# delete unused blocks
|
||||
@@ -180,32 +188,32 @@ def load_page_html(url):
|
||||
).text
|
||||
|
||||
|
||||
def find_rss_feed(url, html):
|
||||
bs = BeautifulSoup(html, features="lxml")
|
||||
possible_feeds = set()
|
||||
|
||||
feed_urls = bs.findAll("link", rel="alternate")
|
||||
for feed_url in feed_urls:
|
||||
t = feed_url.get("type", None)
|
||||
if t:
|
||||
if "rss" in t or "xml" in t:
|
||||
href = feed_url.get("href", None)
|
||||
if href:
|
||||
possible_feeds.add(urljoin(url, href))
|
||||
|
||||
a_tags = bs.findAll("a")
|
||||
for a in a_tags:
|
||||
href = a.get("href", None)
|
||||
if href:
|
||||
if "xml" in href or "rss" in href or "feed" in href:
|
||||
possible_feeds.add(urljoin(url, href))
|
||||
|
||||
for feed_url in possible_feeds:
|
||||
feed = feedparser.parse(feed_url)
|
||||
if feed.entries:
|
||||
return feed_url
|
||||
|
||||
return None
|
||||
# def find_rss_feed(url, html):
|
||||
# bs = BeautifulSoup(html, features="lxml")
|
||||
# possible_feeds = set()
|
||||
#
|
||||
# feed_urls = bs.findAll("link", rel="alternate")
|
||||
# for feed_url in feed_urls:
|
||||
# t = feed_url.get("type", None)
|
||||
# if t:
|
||||
# if "rss" in t or "xml" in t:
|
||||
# href = feed_url.get("href", None)
|
||||
# if href:
|
||||
# possible_feeds.add(urljoin(url, href))
|
||||
#
|
||||
# a_tags = bs.findAll("a")
|
||||
# for a in a_tags:
|
||||
# href = a.get("href", None)
|
||||
# if href:
|
||||
# if "xml" in href or "rss" in href or "feed" in href:
|
||||
# possible_feeds.add(urljoin(url, href))
|
||||
#
|
||||
# for feed_url in possible_feeds:
|
||||
# feed = feedparser.parse(feed_url)
|
||||
# if feed.entries:
|
||||
# return feed_url
|
||||
#
|
||||
# return None
|
||||
|
||||
|
||||
def find_favicon(url, html):
|
||||
|
||||
@@ -6,27 +6,25 @@ sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "infomate.settings")
|
||||
django.setup()
|
||||
|
||||
import re
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from urllib.parse import urlparse
|
||||
from time import mktime
|
||||
import threading
|
||||
import queue
|
||||
|
||||
import requests
|
||||
import click
|
||||
import feedparser
|
||||
from bs4 import BeautifulSoup
|
||||
from requests import RequestException
|
||||
from newspaper import Article as NewspaperArticle, ArticleException
|
||||
|
||||
from boards.models import BoardFeed, Article, Board
|
||||
from scripts.common import DEFAULT_REQUEST_HEADERS, DEFAULT_REQUEST_TIMEOUT, MAX_PARSABLE_CONTENT_LENGTH
|
||||
from scripts.common import DEFAULT_REQUEST_HEADERS, DEFAULT_REQUEST_TIMEOUT, MAX_PARSABLE_CONTENT_LENGTH, resolve_url, \
|
||||
parse_domain, parse_datetime, parse_title, parse_link, parse_rss_image, parse_rss_text_and_image
|
||||
|
||||
DEFAULT_NUM_WORKER_THREADS = 5
|
||||
DEFAULT_ENTRIES_LIMIT = 100
|
||||
DEFAULT_ENTRIES_LIMIT = 30
|
||||
MIN_REFRESH_DELTA = timedelta(minutes=30)
|
||||
DELETE_OLD_ARTICLES_DELTA = timedelta(days=300)
|
||||
|
||||
log = logging.getLogger()
|
||||
queue = queue.Queue()
|
||||
@@ -57,6 +55,7 @@ def update(num_workers, force, feed):
|
||||
"board_id": feed.board_id,
|
||||
"name": feed.name,
|
||||
"rss": feed.rss,
|
||||
"mix": feed.mix,
|
||||
"conditions": feed.conditions,
|
||||
"is_parsable": feed.is_parsable,
|
||||
})
|
||||
@@ -78,6 +77,9 @@ def update(num_workers, force, feed):
|
||||
updated_boards = {feed.board_id for feed in need_to_update_feeds}
|
||||
Board.objects.filter(id__in=updated_boards).update(refreshed_at=datetime.utcnow())
|
||||
|
||||
# remove old data
|
||||
Article.objects.filter(created_at__lte=datetime.now() - DELETE_OLD_ARTICLES_DELTA).delete()
|
||||
|
||||
# stop workers
|
||||
for i in range(num_workers):
|
||||
queue.put(None)
|
||||
@@ -103,7 +105,28 @@ def worker():
|
||||
|
||||
def refresh_feed(item):
|
||||
print(f"Updating feed {item['name']}...")
|
||||
feed = feedparser.parse(item['rss'])
|
||||
if item["mix"]:
|
||||
for rss in item["mix"]:
|
||||
fetch_rss(item, rss)
|
||||
else:
|
||||
fetch_rss(item, item["rss"])
|
||||
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
frequency = Article.objects.filter(feed_id=item["id"], created_at__gte=week_ago).count()
|
||||
last_article = Article.objects.filter(feed_id=item["id"]).order_by("-created_at").first()
|
||||
|
||||
BoardFeed.objects.filter(id=item["id"]).update(
|
||||
refreshed_at=datetime.utcnow(),
|
||||
last_article_at=last_article.created_at if last_article else None,
|
||||
frequency=frequency or 0
|
||||
)
|
||||
|
||||
|
||||
def fetch_rss(item, rss):
|
||||
print(f"Parsing RSS: {rss}")
|
||||
|
||||
feed = feedparser.parse(rss)
|
||||
|
||||
print(f"Entries found: {len(feed.entries)}")
|
||||
for entry in feed.entries[:DEFAULT_ENTRIES_LIMIT]:
|
||||
entry_title = parse_title(entry)
|
||||
@@ -113,14 +136,14 @@ def refresh_feed(item):
|
||||
continue
|
||||
|
||||
print(f"- article: '{entry_title}' {entry_link}")
|
||||
|
||||
conditions = item.get("conditions")
|
||||
|
||||
conditions = item.get("conditions")
|
||||
if conditions:
|
||||
is_valid = check_conditions(conditions, entry)
|
||||
if not is_valid:
|
||||
print(f"Condition {conditions} does not match. Skipped")
|
||||
continue
|
||||
|
||||
|
||||
article, is_created = Article.objects.get_or_create(
|
||||
board_id=item["board_id"],
|
||||
feed_id=item["id"],
|
||||
@@ -171,16 +194,6 @@ def refresh_feed(item):
|
||||
|
||||
article.save()
|
||||
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
frequency = Article.objects.filter(feed_id=item["id"], created_at__gte=week_ago).count()
|
||||
last_article = Article.objects.filter(feed_id=item["id"]).order_by("-created_at").first()
|
||||
|
||||
BoardFeed.objects.filter(id=item["id"]).update(
|
||||
refreshed_at=datetime.utcnow(),
|
||||
last_article_at=last_article.created_at if last_article else None,
|
||||
frequency=frequency or 0
|
||||
)
|
||||
|
||||
|
||||
def check_conditions(conditions, entry):
|
||||
if not conditions:
|
||||
@@ -188,95 +201,16 @@ def check_conditions(conditions, entry):
|
||||
|
||||
for condition in conditions:
|
||||
if condition["type"] == "in":
|
||||
if condition["in"] not in entry[condition["field"]]:
|
||||
if condition["word"] not in entry[condition["field"]]:
|
||||
return False
|
||||
|
||||
if condition["type"] == "not_in":
|
||||
if condition["word"] in entry[condition["field"]]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def resolve_url(entry_link):
|
||||
url = str(entry_link)
|
||||
content_type = None
|
||||
content_length = MAX_PARSABLE_CONTENT_LENGTH + 1 # don't parse null content-types
|
||||
depth = 10
|
||||
while depth > 0:
|
||||
depth -= 1
|
||||
|
||||
try:
|
||||
response = requests.head(url, timeout=DEFAULT_REQUEST_TIMEOUT, verify=False, stream=True)
|
||||
except RequestException:
|
||||
log.warning(f"Failed to resolve URL: {url}")
|
||||
return None, content_type, content_length
|
||||
|
||||
if 300 < response.status_code < 400:
|
||||
url = response.headers["location"] # follow redirect
|
||||
else:
|
||||
content_type = response.headers.get("content-type")
|
||||
content_length = int(response.headers.get("content-length") or 0)
|
||||
break
|
||||
|
||||
return url, content_type, content_length
|
||||
|
||||
|
||||
def parse_domain(url):
|
||||
domain = urlparse(url).netloc
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
return domain
|
||||
|
||||
|
||||
def parse_datetime(entry):
|
||||
published_time = entry.get("published_parsed") or entry.get("updated_parsed")
|
||||
if published_time:
|
||||
return datetime.fromtimestamp(mktime(published_time))
|
||||
return datetime.utcnow()
|
||||
|
||||
|
||||
def parse_title(entry):
|
||||
title = entry.get("title") or entry.get("description") or entry.get("summary")
|
||||
return re.sub("<[^<]+?>", "", title).strip()
|
||||
|
||||
|
||||
def parse_link(entry):
|
||||
if entry.get("link"):
|
||||
return entry["link"]
|
||||
|
||||
if entry.get("links"):
|
||||
return entry["links"][0]["href"]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss_image(entry):
|
||||
if entry.get("media_content"):
|
||||
images = [m["url"] for m in entry["media_content"] if m.get("medium") == "image" and m.get("url")]
|
||||
if images:
|
||||
return images[0]
|
||||
|
||||
if entry.get("image"):
|
||||
if isinstance(entry["image"], dict):
|
||||
return entry["image"].get("href")
|
||||
return entry["image"]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_rss_text_and_image(entry):
|
||||
if not entry.get("summary"):
|
||||
return "", ""
|
||||
|
||||
bs = BeautifulSoup(entry["summary"], features="lxml")
|
||||
text = re.sub(r"\s\s+", " ", bs.text or "").strip()
|
||||
|
||||
img_tags = bs.findAll("img")
|
||||
for img_tag in img_tags:
|
||||
src = img_tag.get("src", None)
|
||||
if src:
|
||||
return text, src
|
||||
|
||||
return text, ""
|
||||
|
||||
|
||||
def load_page_safe(url):
|
||||
try:
|
||||
response = requests.get(
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
.curator-bio {
|
||||
font-size: 120%;
|
||||
padding-top: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.curator-info a {
|
||||
@@ -256,10 +256,19 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article-favicon {
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
padding-right: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.article-tooltip {
|
||||
visibility: hidden;
|
||||
transition: visibility 0.1s;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
left: 0;
|
||||
|
||||
@@ -46,7 +46,10 @@
|
||||
}
|
||||
|
||||
.board {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
max-width: var(--max-content-width);
|
||||
margin: 0 auto;
|
||||
@@ -56,21 +59,51 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.block-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block-50 {
|
||||
width: 49%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 570px) {
|
||||
.block-100, .block-50 {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.feeds {
|
||||
display: grid;
|
||||
grid-template-columns: 33% 33% 33%;
|
||||
grid-template-rows: auto auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.feed {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.feeds-3 .feed {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.feeds-2 .feed {
|
||||
width: 49%;
|
||||
}
|
||||
|
||||
.feed .article-tooltip {
|
||||
/* Shift tooltip to right */
|
||||
left: 85%;
|
||||
left: 250px;
|
||||
top: -100px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.feed:nth-child(3n) .article-tooltip {
|
||||
.feeds-3 .feed:nth-child(3n) .article-tooltip,
|
||||
.feeds-2 .feed:nth-child(2n) .article-tooltip {
|
||||
/* Shift tooltip to left */
|
||||
left: -105%;
|
||||
top: -100px;
|
||||
|
||||
13
templates/blocks/three.html
Normal file
13
templates/blocks/three.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="block block-100">
|
||||
{% if block.name %}
|
||||
<div class="block-header">{{ block.name }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="feeds feeds-3">
|
||||
{% for feed in feeds %}
|
||||
{% if feed.block == block %}
|
||||
{% include feed.template %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
13
templates/blocks/two.html
Normal file
13
templates/blocks/two.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="block block-50">
|
||||
{% if block.name %}
|
||||
<div class="block-header">{{ block.name }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="feeds">
|
||||
{% for feed in feeds %}
|
||||
{% if feed.block == block %}
|
||||
{% include feed.template %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load text_filters %}
|
||||
{% load static %}
|
||||
{% load bleach_tags %}
|
||||
|
||||
{% block title %}{{ board.curator_name }}{% if board.curator_title %} | {{ board.curator_title }}{% endif %} | {{ block.super }}{% endblock %}
|
||||
|
||||
@@ -46,57 +45,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for block in blocks %}
|
||||
<div class="block">
|
||||
{% if block.name %}
|
||||
<div class="block-header">{{ block.name }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="feeds">
|
||||
{% for feed in feeds %}
|
||||
{% if feed.block == block %}
|
||||
{% for column, articles in feed.articles_by_column %}
|
||||
<div class="feed {% if column > 1 %}hide-on-iphone{% endif %}">
|
||||
{% if feed.name %}
|
||||
<div class="feed-title {% if column != 0 %}feed-title-hidden{% endif %}">
|
||||
{% if feed.icon %}
|
||||
<img src="{{ feed.icon }}" alt="{{ feed.name }}">
|
||||
{% endif %}
|
||||
<a href="{{ feed.url }}" target="_blank">{{ feed.name }}</a><br>
|
||||
<small>последний пост {{ feed.natural_last_article_at }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="articles feed-articles">
|
||||
{% for article in articles %}
|
||||
<div class="article {% if article.is_fresh %}is-article-fresh{% endif %}">
|
||||
<div class="article-title">{{ article.icon|safe }}<a href="{{ article.url }}" target="_blank" class="article-link">{{ article.title|bleach }}</a></div>
|
||||
<a href="{{ article.url }}" class="article-tooltip" target="_blank">
|
||||
{% if article.image %}
|
||||
<img src="{{ article.image }}" alt="{{ article.title|striptags }}" class="article-tooltip-image">
|
||||
{% endif %}
|
||||
|
||||
<span class="article-tooltip-title">{{ article.title|striptags|truncatechars:300 }}</span>
|
||||
|
||||
{% if article.description or article.summary %}
|
||||
<span class="article-tooltip-description">
|
||||
{% if feed.is_parsable and article.summary %}
|
||||
{{ article.summary|striptags|truncatechars:700|escape|nl2p|safe }}
|
||||
{% else %}
|
||||
{{ article.description|striptags|truncatechars:700|escape|nl2p|safe }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="article-tooltip-info">{{ article.natural_created_at }} @ {{ article.domain }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% include block.template %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
@@ -110,10 +59,6 @@
|
||||
{% if board.natural_refreshed_at %}
|
||||
<br><small>Обновлено {{ board.natural_refreshed_at }}</small>
|
||||
{% endif %}
|
||||
|
||||
<div class="board-footer-export">
|
||||
<a href="{% url "export" board.slug %}" class="button export-button">Экспортировать подборку себе</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
{% extends "board.html" %}
|
||||
|
||||
{% block board %}
|
||||
<div class="block is-block-blurred">
|
||||
<div class="blocker"></div>
|
||||
<div class="block-header">Привет, друг! Тоже думал, что самый умный? Штош...</div>
|
||||
<div class="feeds">
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="https://i.vas3k.ru/fhm.png" alt="twitter">
|
||||
<a href="#" target="_blank">Twitter</a><br>
|
||||
<small>последний недушный пост: никогда</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="https://i.vas3k.ru/fer.jpg" alt="twitter">
|
||||
<a href="#" target="_blank">Facebook</a><br>
|
||||
<small>верни мои данные!</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="https://i.vas3k.ru/fhc.png" alt="twitter">
|
||||
<a href="#" target="_blank">Одноклассники</a><br>
|
||||
<small>все скорее туда</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="https://i.vas3k.ru/fhf.png" alt="twitter">
|
||||
<a href="#" target="_blank">Телеграм</a><br>
|
||||
<small>чо, многому научились из каналов?</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="https://i.vas3k.ru/fhe.png" alt="twitter">
|
||||
<a href="#" target="_blank">Вконтакте</a><br>
|
||||
<small>верни стену</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-login">
|
||||
<div class="block-login-window">
|
||||
<span class="block-login-title">Только для своих</span>
|
||||
<span class="block-login-description">
|
||||
Эта подборка доступна только членам <a href="https://vas3k.ru/club/" target="_blank">Вастрик.Клуба</a>.
|
||||
Присоединяйтесь и заходите!
|
||||
</span>
|
||||
<a href="{% url "login" %}?redirect={{ settings.APP_HOST }}{% url "board" board.slug %}" class="button block-login-button">Войти по клубной карте</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,21 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load text_filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Экспорт | {{ board.curator_name }} | {{ board.curator_title }} | {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="export">
|
||||
<div class="coming-soon">
|
||||
<h2>Экспорт скоро будет!</h2>
|
||||
|
||||
<p>
|
||||
Эта идея появилась совсем недавно и мы пока думаем как её сделать удобнее всего.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Подпишитесь на мой канал ⭐️ <a href="https://t.me/vas3k_channel">Вастрик.Пынь</a> чтобы узнать сразу когда он появится.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
templates/feeds/favicons.html
Normal file
27
templates/feeds/favicons.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% load static %}
|
||||
{% load text_filters %}
|
||||
{% load bleach_tags %}
|
||||
|
||||
{% for column, articles in feed.articles_by_column %}
|
||||
<div class="feed {% if column > 1 %}hide-on-iphone{% endif %}">
|
||||
{% if feed.name %}
|
||||
<div class="feed-title {% if column != 0 %}feed-title-hidden{% endif %}">
|
||||
{% if feed.icon %}
|
||||
<img src="{{ feed.icon }}" alt="{{ feed.name }}">
|
||||
{% endif %}
|
||||
<a href="{{ feed.url }}" target="_blank">{{ feed.name }}</a><br>
|
||||
<small>последний пост {{ feed.natural_last_article_at }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="articles feed-articles">
|
||||
{% for article in articles %}
|
||||
<div class="article {% if article.is_fresh %}is-article-fresh{% endif %}">
|
||||
<div class="article-title">
|
||||
{% if article.favicon %}<img src="{{ article.favicon }}" class="article-favicon" alt=">">{% endif %}<a href="{{ article.url }}" rel="noopener noreferrer nofollow" target="_blank" class="article-link">{{ article.title|bleach }}</a>
|
||||
</div>
|
||||
{% include "tooltips/simple.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
27
templates/feeds/simple.html
Normal file
27
templates/feeds/simple.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% load static %}
|
||||
{% load text_filters %}
|
||||
{% load bleach_tags %}
|
||||
|
||||
{% for column, articles in feed.articles_by_column %}
|
||||
<div class="feed {% if column > 1 %}hide-on-iphone{% endif %}">
|
||||
{% if feed.name %}
|
||||
<div class="feed-title {% if column != 0 %}feed-title-hidden{% endif %}">
|
||||
{% if feed.icon %}
|
||||
<img src="{{ feed.icon }}" alt="{{ feed.name }}">
|
||||
{% endif %}
|
||||
<a href="{{ feed.url }}" target="_blank">{{ feed.name }}</a><br>
|
||||
<small>последний пост {{ feed.natural_last_article_at }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="articles feed-articles">
|
||||
{% for article in articles %}
|
||||
<div class="article {% if article.is_fresh %}is-article-fresh{% endif %}">
|
||||
<div class="article-title">
|
||||
{{ article.icon|safe }}<a href="{{ article.url }}" rel="noopener noreferrer nofollow" target="_blank" class="article-link">{{ article.title|bleach }}</a>
|
||||
</div>
|
||||
{% include "tooltips/simple.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -41,11 +41,8 @@
|
||||
|
||||
{% block footer %}
|
||||
<div class="footer">
|
||||
Сделал <a href="https://vas3k.ru">Вастрик</a>. Код проекта <a href="https://github.com/vas3k/infomate.club">открыт</a>.<br><br>
|
||||
Сайт использует <a href="https://ru.wikipedia.org/wiki/Cookie" target="_blank">куки</a> для авторизации<br> и собирает <a href="{% url "privacy_policy" %}">анонимные данные</a> для статистики.
|
||||
{% if me %}
|
||||
<br><a href="{% url "logout" %}" class="button logout-button">Выйти</a>
|
||||
{% endif %}
|
||||
Проект <a href="https://vas3k.club">Вастрик.Клуба</a>. Код <a href="https://github.com/vas3k/infomate.club">открыт</a>.<br>
|
||||
Идеи и предложения новых источников присылайте на гитхаб.<br><br>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
21
templates/tooltips/simple.html
Normal file
21
templates/tooltips/simple.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% load text_filters %}
|
||||
|
||||
<a href="{{ article.url }}" class="article-tooltip" rel="noopener noreferrer nofollow" target="_blank">
|
||||
{% if article.image %}
|
||||
<img src="{{ article.image }}" alt="{{ article.title|striptags }}" class="article-tooltip-image">
|
||||
{% endif %}
|
||||
|
||||
<span class="article-tooltip-title">{{ article.title|striptags|truncatechars:300 }}</span>
|
||||
|
||||
{% if article.description or article.summary %}
|
||||
<span class="article-tooltip-description">
|
||||
{% if feed.is_parsable and article.summary %}
|
||||
{{ article.summary|striptags|truncatechars:700|escape|nl2p|safe }}
|
||||
{% else %}
|
||||
{{ article.description|striptags|truncatechars:700|escape|nl2p|safe }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="article-tooltip-info">{{ article.natural_created_at }} @ {{ article.domain }}</span>
|
||||
</a>
|
||||
Reference in New Issue
Block a user