Skip to content

Commit

Permalink
new command /(de)?activate_subs; auto deactivate feed if error_coun…
Browse files Browse the repository at this point in the history
…t >= 100

Signed-off-by: Rongrong <15956627+Rongronggg9@users.noreply.github.com>
  • Loading branch information
Rongronggg9 committed Nov 30, 2021
1 parent ff94971 commit f460bef
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 61 deletions.
48 changes: 46 additions & 2 deletions src/command/inner/sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,29 @@ async def sub(user_id: int, feed_url: str, lang: Optional[str] = None) -> Dict[s

if feed:
_sub = await db.Sub.get_or_none(user=user_id, feed=feed)
else:
if not feed or feed.state == 0:
d = await web.feed_get(feed_url, lang=lang)
rss_d = d['rss_d']
ret['status'] = d['status']
ret['msg'] = d['msg']
feed_url_original = feed_url
ret['url'] = feed_url = d['url'] # get the redirected url

if rss_d is None:
logger.warning(f'Sub {feed_url} for {user_id} failed')
return ret

if feed_url_original != feed_url:
logger.info(f'Sub {feed_url_original} redirected to {feed_url}')
if feed:
await migrate_to_new_url(feed, feed_url)

# need to use get_or_create because we've changed feed_url to the redirected one
feed, created_new_feed = await db.Feed.get_or_create(defaults={'title': rss_d.feed.title}, link=feed_url)
if created_new_feed:
if created_new_feed or feed.state == 0:
feed.state = 1
feed.error_count = 0
feed.next_check_time = None
http_caching_d = get_http_caching_headers(d['headers'])
feed.etag = http_caching_d['ETag']
feed.last_modified = http_caching_d['Last-Modified']
Expand Down Expand Up @@ -186,3 +196,37 @@ async def export_opml(user_id: int) -> Optional[bytes]:
return None
logger.info('Exported feed(s).')
return opml.prettify().encode()


async def migrate_to_new_url(feed: db.Feed, new_url: str) -> Union[bool, db.Feed]:
"""
Migrate feed's link to new url, useful when a feed is redirected to a new url.
:param feed:
:param new_url:
:return:
"""
if feed.link == new_url:
return False

logger.info(f'Migrating {feed.link} to {new_url}')
new_url_feed = await db.Feed.get_or_none(link=new_url)
if new_url_feed is None: # new_url not occupied
feed.link = new_url
await feed.save()
return True

# new_url has been occupied by another feed
new_url_feed.state = 1
new_url_feed.title = feed.title
new_url_feed.entry_hashes = feed.entry_hashes
new_url_feed.etag = feed.etag
new_url_feed.last_modified = feed.last_modified
new_url_feed.error_count = 0
new_url_feed.next_check_time = None
await new_url_feed.save()

await feed.subs.all().update(feed=new_url_feed) # migrate all subs to the new feed

await update_interval(new_url_feed)
await feed.delete() # delete the old feed
return new_url_feed
110 changes: 98 additions & 12 deletions src/command/inner/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import AnyStr, Iterable, Tuple, Any, Union, Optional, Mapping, Dict
import asyncio
from typing import AnyStr, Iterable, Tuple, Any, Union, Optional, Mapping, Dict, List
from datetime import datetime
from email.utils import parsedate_to_datetime
from zlib import crc32
Expand Down Expand Up @@ -55,27 +56,35 @@ async def get_sub_choosing_buttons(user_id: int,
page: int,
callback: str,
get_page_callback: str,
lang: Optional[str] = None) -> Tuple[Tuple[KeyboardButtonCallback, ...], ...]:
lang: Optional[str] = None,
rows: int = 12,
columns: int = 2,
*args, **kwargs) -> Optional[Tuple[Tuple[KeyboardButtonCallback, ...], ...]]:
"""
:param user_id: user id
:param page: page number (1-based)
:param callback: callback data header
:param get_page_callback: callback data header for getting another page
:param lang: language code
:param rows: the number of rows
:param columns: the number of columns
:param args: args for `list_sub`
:param kwargs: kwargs for `list_sub`
:return: ReplyMarkup
"""
if page <= 0:
raise IndexError('Page number must be positive.')

max_row_count = 12
max_column_count = 2
subs_count_per_page = max_column_count * max_row_count
user_sub_list = await list_sub(user_id, *args, **kwargs)
if not user_sub_list:
return None

subs_count_per_page = columns * rows
page_start = (page - 1) * subs_count_per_page
page_end = page_start + subs_count_per_page
user_sub_list = await list_sub(user_id)
buttons_to_arrange = tuple(Button.inline(_sub.feed.title, data=f'{callback}_{_sub.id}|{page}')
for _sub in user_sub_list[page_start:page_end])
buttons = arrange_grid(to_arrange=buttons_to_arrange, columns=max_column_count, rows=max_row_count)
buttons = arrange_grid(to_arrange=buttons_to_arrange, columns=columns, rows=rows)

rest_subs_count = len(user_sub_list[page * subs_count_per_page:])
page_buttons = []
Expand All @@ -101,18 +110,95 @@ async def update_interval(feed: Union[db.Feed, int], new_interval: Optional[int]
curr_interval = feed.interval or default_interval

if not new_interval:
intervals = await feed.subs.all().values_list('interval', flat=True)
if not intervals: # no sub subs the feed, del the feed
sub_exist = await feed.subs.all().exists()
intervals = await feed.subs.filter(state=1).values_list('interval', flat=True)
if not sub_exist: # no sub subs the feed, del the feed
await feed.delete()
db.effective_utils.EffectiveTasks.delete(feed.id)
return
if not intervals: # no active sub subs the feed, deactivate the feed
await deactivate_feed(feed)
return
new_interval = min(intervals, key=lambda _: default_interval if _ is None else _) or default_interval

if curr_interval != new_interval:
if new_interval <= curr_interval:
feed.interval = new_interval
await feed.save()
db.effective_utils.EffectiveTasks.update(feed.id, new_interval)
return

if not db.effective_utils.EffectiveTasks.exist(feed.id):
db.effective_utils.EffectiveTasks.update(feed.id, new_interval)


async def list_sub(user_id: int, *args, **kwargs) -> List[db.Sub]:
return await db.Sub.filter(user=user_id, *args, **kwargs).prefetch_related('feed')


async def have_subs(user_id: int) -> bool:
return await db.Sub.filter(user=user_id).exists()


async def activate_feed(feed: db.Feed) -> db.Feed:
if feed.state == 1:
return feed

feed.state = 1
feed.error_count = 0
feed.next_check_time = None
await feed.save()
await update_interval(feed)
return feed


async def list_sub(user_id: int):
return await db.Sub.filter(user=user_id).prefetch_related('feed')
async def deactivate_feed(feed: db.Feed) -> db.Feed:
db.effective_utils.EffectiveTasks.delete(feed.id)

subs = await feed.subs.all()
if not subs:
await feed.delete()
return feed

feed.state = 0
feed.next_check_time = None
await feed.save()
await asyncio.gather(
*(activate_or_deactivate_sub(sub.user_id, sub, activate=False, _update_interval=False) for sub in subs)
)

return feed


async def activate_or_deactivate_sub(user_id: int, sub_: Union[db.Sub, int], activate: bool,
_update_interval: bool = True) -> Optional[db.Sub]:
"""
:param user_id: user id
:param sub_: `db.Sub` or sub id
:param activate: activate the sub if `Ture`, deactivate if `False`
:param _update_interval: update interval or not?
:return: the updated sub, `None` if the sub does not exist
"""
if isinstance(sub_, int):
sub_ = await db.Sub.get_or_none(id=sub_, user_id=user_id).prefetch_related('feed')
if not sub_:
return None
elif sub_.user_id != user_id:
return None

sub_.state = 1 if activate else 0
await sub_.save()
await activate_feed(sub_.feed)
interval = sub_.interval or db.effective_utils.EffectiveOptions.get('default_interval')
if _update_interval:
await update_interval(sub_.feed, new_interval=interval if activate else None)
return sub_


async def activate_or_deactivate_all_subs(user_id: int, activate: bool) -> Tuple[Optional[db.Sub], ...]:
"""
:param user_id: user id
:param activate: activate all subs if `Ture`, deactivate if `False`
:return: the updated sub, `None` if the sub does not exist
"""
subs_ = await list_sub(user_id, state=0 if activate else 1)
return await asyncio.gather(*(activate_or_deactivate_sub(user_id, sub_, activate=activate) for sub_ in subs_))
90 changes: 85 additions & 5 deletions src/command/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

from src import env, web, db
from src.i18n import i18n, ALL_LANGUAGES
from .utils import permission_required, parse_command, logger, escape_html, set_bot_commands, get_commands_list
from .utils import permission_required, command_parser, logger, escape_html, set_bot_commands, get_commands_list, \
callback_data_with_page_parser
from . import inner
from ..parsing.post import get_post_from_entry


@permission_required(only_manager=False)
async def cmd_start(event: Union[events.NewMessage.Event, Message], lang=None, *args, **kwargs):
async def cmd_start(event: Union[events.NewMessage.Event, Message], *args, lang=None, **kwargs):
if lang is None:
await cmd_lang.__wrapped__(event)
return
Expand Down Expand Up @@ -42,7 +43,84 @@ async def callback_set_lang(event: events.CallbackQuery.Event, *args, **kwargs):


@permission_required(only_manager=False)
async def cmd_help(event: Union[events.NewMessage.Event, Message], lang: Optional[str] = None, *args, **kwargs):
async def cmd_activate_or_deactivate_subs(event: Union[events.NewMessage.Event, Message],
activate: bool,
*args,
lang: Optional[str] = None,
**kwargs): # cmd: activate_subs | deactivate_subs
await callback_get_activate_or_deactivate_page.__wrapped__(event, activate, lang=lang, page=1)


@permission_required(only_manager=False)
async def callback_get_activate_or_deactivate_page(event: Union[events.CallbackQuery.Event,
events.NewMessage.Event,
Message],
activate: bool,
*args,
lang: Optional[str] = None,
page: Optional[int] = None,
**kwargs): # callback data: get_(activate|deactivate)_page_{page}
event_is_msg = not isinstance(event, events.CallbackQuery.Event)
origin_msg = None # placeholder
if not event_is_msg:
origin_msg = (await event.get_message()).text
if page is None:
page = int(event.data.decode().strip().split('_')[-1]) if not event_is_msg else 1
have_subs = await inner.utils.have_subs(event.chat_id)
if not have_subs:
no_subscription_msg = i18n[lang]['no_subscription']
await (event.respond(no_subscription_msg) if event_is_msg
else event.edit(no_subscription_msg if not no_subscription_msg == origin_msg else None))
return
sub_buttons = await inner.utils.get_sub_choosing_buttons(
event.chat_id,
page=page,
callback='activate_sub' if activate else 'deactivate_sub',
get_page_callback='get_activate_page' if activate else 'get_deactivate_page',
lang=lang,
rows=11,
state=0 if activate else 1
)
msg = i18n[lang]['choose_sub_to_be_activated' if sub_buttons else 'all_subs_are_activated'] if activate \
else i18n[lang]['choose_sub_to_be_deactivated' if sub_buttons else 'all_subs_are_deactivated']
activate_or_deactivate_all_subs_str = 'activate_all_subs' if activate else 'deactivate_all_subs'
buttons = (
(
(Button.inline(i18n[lang][activate_or_deactivate_all_subs_str],
data=activate_or_deactivate_all_subs_str),),
)
+ sub_buttons
) if sub_buttons else None
await (event.respond(msg, buttons=buttons) if event_is_msg
else event.edit(msg if not msg == origin_msg else None, buttons=buttons))


@permission_required(only_manager=False)
async def callback_activate_or_deactivate_all_subs(event: events.CallbackQuery.Event,
activate: bool,
*args,
lang: Optional[str] = None,
**kwargs): # callback data: (activate|deactivate)_all_subs
await inner.utils.activate_or_deactivate_all_subs(event.chat_id, activate=activate)
await callback_get_activate_or_deactivate_page.__wrapped__(event, activate, lang=lang, page=1)


@permission_required(only_manager=False)
async def callback_activate_or_deactivate_sub(event: events.CallbackQuery.Event,
activate: bool,
*args,
lang: Optional[str] = None,
**kwargs): # callback data: (activate|deactivate)_sub_{id}|{page}
sub_id, page = callback_data_with_page_parser(event.data)
unsub_res = await inner.utils.activate_or_deactivate_sub(event.chat_id, sub_id, activate=activate)
if unsub_res is None:
await event.answer('ERROR: ' + i18n[lang]['subscription_not_exist'], alert=True)
return
await callback_get_activate_or_deactivate_page.__wrapped__(event, activate, lang=lang, page=page)


@permission_required(only_manager=False)
async def cmd_help(event: Union[events.NewMessage.Event, Message], *args, lang: Optional[str] = None, **kwargs):
await event.respond(
f"<a href='https://github.com/Rongronggg9/RSS-to-Telegram-Bot'>{escape_html(i18n[lang]['rsstt_slogan'])}</a>\n"
f"\n"
Expand All @@ -53,6 +131,8 @@ async def cmd_help(event: Union[events.NewMessage.Event, Message], lang: Optiona
f"<b>/list</b>: {escape_html(i18n[lang]['cmd_description_list'])}\n"
f"<b>/import</b>: {escape_html(i18n[lang]['cmd_description_import'])}\n"
f"<b>/export</b>: {escape_html(i18n[lang]['cmd_description_export'])}\n"
f"<b>/activate_subs</b>: {escape_html(i18n[lang]['cmd_description_activate_subs'])}\n"
f"<b>/deactivate_subs</b>: {escape_html(i18n[lang]['cmd_description_deactivate_subs'])}\n"
f"<b>/version</b>: {escape_html(i18n[lang]['cmd_description_version'])}\n"
f"<b>/lang</b>: {escape_html(' / '.join(i18n[_lang]['cmd_description_lang'] for _lang in ALL_LANGUAGES))}\n"
f"<b>/help</b>: {escape_html(i18n[lang]['cmd_description_help'])}\n\n",
Expand All @@ -61,8 +141,8 @@ async def cmd_help(event: Union[events.NewMessage.Event, Message], lang: Optiona


@permission_required(only_manager=True)
async def cmd_test(event: Union[events.NewMessage.Event, Message], lang: Optional[str] = None, *args, **kwargs):
args = parse_command(event.text)
async def cmd_test(event: Union[events.NewMessage.Event, Message], *args, lang: Optional[str] = None, **kwargs):
args = command_parser(event.text)
if len(args) < 2:
await event.respond('ERROR: ' + i18n[lang]['test_command_usage_prompt'])
return
Expand Down
Loading

0 comments on commit f460bef

Please sign in to comment.