Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes are recorded here, newest release first. Versions follow
[Semantic Versioning](https://semver.org/). Dates use America/Los_Angeles.
The history before this initial versioned snapshot remains in Git.

## [1.0.8] - 2026-10-17

### Fixed

- Derive stable anniversary dates from successfully sent posts instead of the
statistics collection start; allow explicit dates and handle February 29.
Skip missing history and isolate invalid channels rather than aborting all sends.
- Deliver annual introductions featuring the main feed and community, with a
deterministic paginated channel directory and direct text birthday notices.
The daily sweep no longer depends on Reddit listings or media availability.
- Persist annually unique message plans, acknowledged Telegram IDs, and run
outcomes independently of the queue cleaner. Resume rejected/unsent parts on
later sweeps; hold interrupted or ambiguous sends for review rather than risk
duplicates. Expose a read-only report/preview and keep test sends isolated.
- Propagate supply failures after reporting them so partial anniversary runs
cannot be recorded as successful tasks. Preserve the original error if
developer-chat reporting fails. No migration or historical replay is required.

## [1.0.7] - 2026-10-17

### Fixed
Expand Down
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,54 @@ R2T_LIVE_TESTS=1 .venv/bin/python -m unittest tests/test_live_resources.py -v
```

Leave `R2T_LIVE_TESTS` unset during offline test runs.

Anniversary announcements
------------------------

`tech_anniversary` runs daily at 11:55 UTC without fetching a Reddit listing.
On a channel's anniversary, its introduction features `@reddit2telegram`, then
the `@r_channels` community, followed by the birthday greeting and a paginated
directory of other scheduled channels. A short birthday notice also goes to
the main channel, community, and developer chat. Identical destinations are
deduplicated. Announcements use text, so unavailable Reddit media cannot block
them.

The default anniversary date is the earliest **successfully sent** post in
`urls`, never the start of statistics collection. This is an earliest-known-post
date, not a verified Telegram creation date. It is saved once in `settings`
under `_id: r2t_anniversary_date:<lowercase channel>` so history cleanup cannot
move it. A channel document's optional `anniversary_date` (`YYYY-MM-DD` or a
BSON datetime) overrides that default. Channels without either date are skipped
and reported; one missing or invalid channel does not prevent other sends.
February 29 anniversaries are observed on February 28 in non-leap years.

Annual delivery records use `_id: r2t_anniversary_delivery:<channel>:<UTC year>`
in `settings`. Their message text and directory order are frozen when queued;
each acknowledged Telegram message ID is saved before the next send. Daily
runs resume outstanding messages from earlier days and never repeat completed
parts. Channels removed from the schedule are not sent queued retries. Every
invocation also leaves a `kind: r2t_anniversary_run` record containing its start,
finish, outcome, due/completed/pending counts, missing history, and errors. The
next lease owner marks a crashed sweep as interrupted. The hourly task cleaner
does not remove any of these records. No historical
anniversaries are backfilled on installation.

Inspect dates, upcoming/due channels, a complete message preview, pending parts,
and the last seven days of run history without writing state or sending messages:

```bash
.venv/bin/python scripts/anniversary_report.py
.venv/bin/python scripts/anniversary_report.py --date 2027-02-12 --days 30
```

A sweep lease prevents overlapping workers. Telegram API rejections are safe to
retry, while timeouts and interrupted requests are recorded as `uncertain`.
Telegram does not provide an idempotency key for sends, so these parts require
checking the destination before another attempt. After that check, an operator
can conditionally update the event in `settings`: select its `_id` and
`deliveries.<index>.state: uncertain`; set that part's state to `sent` and its
`message_ids` to the verified IDs if it arrived, or reset it to `pending` if it
did not. The next scheduled run resumes the remaining parts. A partial run is
reported as a supply failure, with the retained run ID for diagnosis. The
existing `supplier.py --test --sub tech_anniversary` path sends only a sample to
`@r_channels_test` and leaves production anniversary records untouched.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.7
1.0.8
74 changes: 13 additions & 61 deletions reddit2telegram/channels/tech_anniversary/app.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,24 @@
#encoding:utf-8
"""Daily anniversary sweep; no Reddit listing is needed to send announcements."""

import utils
from utils import SupplyResult
from utils.tech import is_birthday_today, get_all_public_channels, get_dev_channel
from utils.tech import generate_list_of_channels, default_ending, chunker, get_all_public_submodules
from utils.tech import short_sleep, long_sleep
from utils.setup import get_config
from supplier import send_to_channel_from_subreddit
from channels.reddit2telegram.app import make_nice_submission
from utils.anniversaries import AnniversaryAnnouncements, announcement_messages
from utils.tech import get_dev_channel


subreddit = 'all'
t_channel = get_dev_channel()
requires_submission = False


def send_post(submission, r2t):
channels_list = get_all_public_channels(r2t)
# supplier --test sets the initial destination. Never turn a test invocation
# into production sends or update production anniversary delivery records.
if r2t.t_channel == '@r_channels_test':
for text in announcement_messages('@r_channels_test', 1, ['@reddit2telegram', '@r_channels']):
r2t.send_text(text, parse_mode='HTML', disable_web_page_preview=True)
return SupplyResult.STOP_THIS_SUPPLY

for submodule_name in get_all_public_submodules():
submodule = utils.channels_stuff.import_submodule(submodule_name)
channel = submodule.t_channel
bd_party, years = is_birthday_today(r2t, channel)
if bd_party and years > 0:
plural = 's' if years != 1 else ''
# To the @r_channels
long_sleep()
r2t.t_channel = '@r_channels'
cakes = '🎂' * years
text_to_send = '{cake}\n🎁 Today {channel} is {years_cnt} year{s} old.\n🎉 Congratulations! 🎈'.format(
channel=channel,
years_cnt=years,
s=plural,
cake=cakes
)
r2t.send_text(text_to_send)
# To the dev channel
long_sleep()
r2t.t_channel = get_dev_channel()
r2t.send_text(text_to_send)
# To the channels itself
long_sleep()
r2t.t_channel = channel
text1_to_send = text_to_send
list_of_channels = generate_list_of_channels(channels_list, random_permutation=True)
text3_to_send = default_ending()
r2t.send_text(text1_to_send)
short_sleep()
text2_to_send = 'Other @reddit2telegram channels powered by @r_channels:\n'
for l in chunker(list_of_channels, 100):
text2_to_send += '\n'.join(l)
r2t.send_text(text2_to_send)
text2_to_send = ''
short_sleep()
r2t.send_text(text3_to_send)
long_sleep()
if channel != '@reddit2telegram':
config = get_config()
send_to_channel_from_subreddit(
how_to_post=make_nice_submission,
channel_to_post='@reddit2telegram',
subreddit=submodule.subreddit,
submodule_name_to_promte=submodule_name,
submissions_ranking='top',
submissions_limit=1000,
config=config,
extra_args=True,
extra_ending=text_to_send
)
long_sleep()
# It's not a proper supply, so just stop.
result = AnniversaryAnnouncements(r2t).run()
if result['status'] in ('failed', 'partial'):
raise RuntimeError('Anniversary sweep needs attention; run {}'.format(result['_id']))
return SupplyResult.STOP_THIS_SUPPLY
12 changes: 9 additions & 3 deletions reddit2telegram/reporting_stuff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import sys
import os
from functools import wraps

import yaml
import sentry_sdk
Expand Down Expand Up @@ -60,13 +61,18 @@ def send_report_to_dev_chat(exc):


def report_error(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
try:
fn(*args, **kwargs)
return fn(*args, **kwargs)
except Exception as e:
if client: # has sentry instance
client.capture_exception(e)
else:
logging.exception('Exception Ignored.')
send_report_to_dev_chat(e)
logging.exception('Supply failed.')
try:
send_report_to_dev_chat(e)
except Exception:
logging.exception('Failed to report supply error to the developer chat.')
raise
return wrapper
Loading
Loading