Compare commits
14 Commits
fix-golly-
...
golly-show
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e787962fa | |||
| 782e98e8f2 | |||
| af077bbe9d | |||
| 265e099cec | |||
| 493cef12d9 | |||
| cde1af1ce9 | |||
| 4fe755c781 | |||
| 7f83f21319 | |||
| 50d61c6b34 | |||
| 379c2cc80b | |||
| cb0453a518 | |||
| d6a8244a93 | |||
| dffd5b6a5f | |||
| d4c2c48633 |
2
.github/workflows/golly-star-cup.yml
vendored
2
.github/workflows/golly-star-cup.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
# Every hour at minute 1, Tuesday and Wednesday UTC
|
||||
# 5 AM Pacific = 12:00 UTC (PDT) or 13:00 UTC (PST)
|
||||
# Run every hour Tue-Wed UTC to cover both; script checks Pacific time
|
||||
- cron: '20 * * * 2,3'
|
||||
- cron: '1 * * * 2,3'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.claude
|
||||
.DS_Store
|
||||
__pycache__
|
||||
.worktrees/
|
||||
.worktrees
|
||||
utilities/.environment
|
||||
|
||||
@@ -19,5 +19,6 @@ See [SETUP.md](SETUP.md) for Slack app creation, GitHub secrets, and how to add
|
||||
## Slack TV Channel Directory
|
||||
|
||||
- `waste-land/` - **The Waste Land, One Line Per Hour** — Eliot's poem is 434 lines. At one line per hour, it takes ~18 days to complete, then loops. The line-per-hour pace is critical: each line sits alone in the channel long enough to be read as a standalone fragment, which is how the poem actually works — collage, juxtaposition, voices interrupting each other. The footnotes post as threaded replies.
|
||||
- `dactylic-odyssey` - **The Odyssey in Dactylic Drip** — Post 5 lines of the Fagles (or Lattimore, or Fitzgerald) translation every 4 hours. The Odyssey is ~12,110 lines. At 30 lines/day, the full poem takes ~403 days — just over a year. A 5-line chunk is roughly one complete Homeric image or action beat. The year-long duration mirrors the scale of the journey itself. When it finishes, start the Iliad.
|
||||
- `dactylic-odyssey/` - **The Odyssey in Dactylic Drip** — Post 5 lines of the Fagles (or Lattimore, or Fitzgerald) translation every 4 hours. The Odyssey is ~12,110 lines. At 30 lines/day, the full poem takes ~403 days — just over a year. A 5-line chunk is roughly one complete Homeric image or action beat. The year-long duration mirrors the scale of the journey itself. When it finishes, start the Iliad.
|
||||
- `golly-notifier/` - **Golly Postseason Notifications** — Tracks the [Golly](https://golly.life) postseason and posts game results, series updates, and schedule notifications. Two bot personas: **Star Cup** (Tuesday–Wednesday) and **Hellmouth Cup** (Friday–Sunday). Each checks the Golly API hourly during its window, reporting series starts, daily scores with replay links, series clinches, and cup winners. Scheduling: Star runs 5 AM–1 AM PT Tue, 5 AM–5 PM PT Wed; Hellmouth runs 8 AM–4 PM PT Fri, 8 AM–8 PM PT Sat–Sun.
|
||||
|
||||
|
||||
BIN
golly-notifier/hellmouth-cup-icon.png
Normal file
BIN
golly-notifier/hellmouth-cup-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
53
golly-notifier/notifications_description.txt
Normal file
53
golly-notifier/notifications_description.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
star notifications:
|
||||
|
||||
- loop for League Division Series and League Championship Series starts running at 5 AM Pacific Tuesday, runs every hour for 20 hours
|
||||
- loop for Star Cup Series starts running at 5 AM Pacific Wednesday, runs every hour for 12 hours
|
||||
|
||||
hellmouth notifications:
|
||||
|
||||
- loop for League Division Series starts running at 10 AM Pacific Friday, runs every hour for 8 hours
|
||||
- loop for League Championship Series starts running at 8 AM Pacific Saturday, runs every hour for 12 hours
|
||||
- loop for Hellmouth Cup Series starts running at 8 AM Pacific Sunday, runs every hour for 12 hours
|
||||
|
||||
|
||||
general notification rules:
|
||||
|
||||
- if mode 21:
|
||||
- do nothing
|
||||
|
||||
- if mode 31:
|
||||
- if it has been mode 31 for less than 3600 seconds, send a notification that the League Division Series is starting
|
||||
- otherwise, print two notifications:
|
||||
- zeroth, determine how many hours have elapsed. that is the zero-indexed Day. you must report the one-indexed Day in the notification.
|
||||
- for example, if 3700 seconds have elapsed since mode 31, it is Day 2.
|
||||
- first, determine the outcome of the game from the prior day. create a notification with that score (plus links to those games).
|
||||
- second, post a notification about the ongoing games for the current day (plus links to those games).
|
||||
|
||||
- if mode 22:
|
||||
- if it has been mode 22 for less than 3600 seconds, send a notification of who won the League Division Series
|
||||
- otherwise, do not send any notification at all
|
||||
|
||||
- if mode 32:
|
||||
- if it has been mode 32 for less than 3600 seconds, send a notification that the League Championship Series is starting
|
||||
- otherwise, print two notifications:
|
||||
- zeroth, determine how many hours have elapsed. that is the zero-indexed Day. you must report the one-indexed Day in the notification.
|
||||
- for example, if 3700 seconds have elapsed since mode 32, it is Day 2.
|
||||
- first, determine the outcome of the game from the prior day. create a notification with that score (plus links to those games).
|
||||
- second, post a notification about the ongoing games for the current day (plus links to those games).
|
||||
|
||||
- if mode 23:
|
||||
- if it has been mode 23 for less than 3600 seconds, send a notification of who won the League Championship Series
|
||||
- otherwise, do not send any notification at all
|
||||
|
||||
- if mode 33:
|
||||
- if it has been mode 33 for less than 3600 seconds, send a notification that the Star VII Cup Series is starting
|
||||
- otherwise, send two notifications:
|
||||
- zeroth, determine how many hours have elapsed. that is the zero-indexed Day. you must report the one-indexed Day in the notification.
|
||||
- for example, if 3700 seconds have elapsed since mode 33, it is Day 2.
|
||||
- first, determine the outcome of the game from the prior day. create a notification with that score (plus links to those games).
|
||||
- second, post a notification about the ongoing games for the current day (plus links to those games).
|
||||
|
||||
- if mode 40:
|
||||
- if it has been mode 40 for less than 3600 seconds, send a notification of who won the Cup Series
|
||||
- otherwise, do not send any notification at all
|
||||
|
||||
@@ -14,9 +14,12 @@ CUPS = {
|
||||
"cup_name": "Star Cup",
|
||||
"cup_series_key": "SCS",
|
||||
"bot_username": "Star Cup",
|
||||
"bot_icon_url": "https://git.charlesreid1.com/charlesreid1/slack-tv/raw/branch/main/golly-notifier/icon-star.png",
|
||||
"days": [1, 2], # Tuesday, Wednesday (weekday() values)
|
||||
"start_hour": 5,
|
||||
"bot_icon_url": "https://git.charlesreid1.com/charlesreid1/slack-tv/raw/branch/main/golly-notifier/star-cup-icon.png",
|
||||
# weekday(): 0=Mon, 1=Tue, 2=Wed, 3=Thu, 4=Fri, 5=Sat, 6=Sun
|
||||
"schedule": {
|
||||
1: {"start_hour": 5, "duration": 20}, # Tuesday: LDS+LCS, 5 AM for 20h
|
||||
2: {"start_hour": 5, "duration": 12}, # Wednesday: Cup Series, 5 AM for 12h
|
||||
},
|
||||
},
|
||||
"hellmouth": {
|
||||
"api_base": "https://cloud.vii.golly.life",
|
||||
@@ -24,17 +27,21 @@ CUPS = {
|
||||
"cup_name": "Hellmouth Cup",
|
||||
"cup_series_key": "HCS",
|
||||
"bot_username": "Hellmouth Cup",
|
||||
"bot_icon_url": "https://git.charlesreid1.com/charlesreid1/slack-tv/raw/branch/main/golly-notifier/icon-hellmouth.png",
|
||||
"days": [4, 5, 6], # Friday, Saturday, Sunday
|
||||
"start_hour": 8,
|
||||
"bot_icon_url": "https://git.charlesreid1.com/charlesreid1/slack-tv/raw/branch/main/golly-notifier/hellmouth-cup-icon.png",
|
||||
# weekday(): 0=Mon, 1=Tue, 2=Wed, 3=Thu, 4=Fri, 5=Sat, 6=Sun
|
||||
"schedule": {
|
||||
4: {"start_hour": 8, "duration": 8}, # Friday: LDS, 8 AM for 8h
|
||||
5: {"start_hour": 8, "duration": 12}, # Saturday: LCS, 8 AM for 12h
|
||||
6: {"start_hour": 8, "duration": 12}, # Sunday: Cup Series, 8 AM for 12h
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SERIES_NAMES = {
|
||||
"LDS": "Division Series",
|
||||
"LCS": "League Championship Series",
|
||||
"LCS": "Championship Series",
|
||||
"HCS": "Hellmouth VII Cup",
|
||||
"SCS": "Star Cup Series",
|
||||
"SCS": "Star VII Cup",
|
||||
}
|
||||
|
||||
MODE_TO_SERIES = {
|
||||
@@ -44,19 +51,25 @@ MODE_TO_SERIES = {
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slack helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fetch_json(url):
|
||||
resp = requests.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def send_slack_message(token, channel, text, username, icon_url):
|
||||
def send_slack_message(token, channel, text, username, icon_url, attachments=None):
|
||||
payload = {
|
||||
"channel": channel,
|
||||
"text": text,
|
||||
"username": username,
|
||||
"icon_url": icon_url,
|
||||
}
|
||||
if attachments:
|
||||
payload["attachments"] = attachments
|
||||
response = requests.post(
|
||||
SLACK_API_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -70,14 +83,6 @@ def send_slack_message(token, channel, text, username, icon_url):
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def format_matchup(g):
|
||||
"""Two-line hockey-bot style: bold name line, then abbr/score line."""
|
||||
name_line = f"*{g['team1Name']} vs. {g['team2Name']}*"
|
||||
score_line = f"{g['team1Abbr']}: {g['team1Score']} | {g['team2Abbr']}: {g['team2Score']} | Final"
|
||||
return [name_line, score_line]
|
||||
|
||||
|
||||
def game_link(site_base, game):
|
||||
game_id = game.get("id", "")
|
||||
desc = game.get("description", "")
|
||||
@@ -87,22 +92,251 @@ def game_link(site_base, game):
|
||||
return f"<{url}|{label}>"
|
||||
|
||||
|
||||
def game_link_url(site_base, game):
|
||||
game_id = game.get("id", "")
|
||||
return f"{site_base}/simulator/index.html?gameId={game_id}"
|
||||
|
||||
|
||||
def game_link_label(game):
|
||||
desc = game.get("description", "")
|
||||
match = re.match(r"Game (\d+)", desc)
|
||||
return f"Game {match.group(1)}" if match else "View Simulation"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Block Kit card builders (mimic vii.golly.life game card style)
|
||||
#
|
||||
# Design reference (from the site's HTML/CSS):
|
||||
# - Dark card background (#272b30, Bootswatch Slate)
|
||||
# - Team 1 name + (W-L) record, score right-aligned
|
||||
# - Team 2 name + (W-L) record, score right-aligned
|
||||
# - Winner is bold, loser is normal weight
|
||||
# - Map name + generation count below
|
||||
# - Green "View Simulation" button (#62c462)
|
||||
# - Sidebar color = winning team's color
|
||||
#
|
||||
# Slack constraints:
|
||||
# - No colored text in Block Kit, so we use *bold* for the winner
|
||||
# - Colored sidebar via attachment "color" field
|
||||
# - Section blocks with fields for the two-column team/score layout
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _finished_game_attachment(g, site_base=None):
|
||||
"""Build a Slack attachment with Block Kit blocks for a finished game.
|
||||
|
||||
Mirrors the vii.golly.life finished-game-card layout:
|
||||
Team 1 Name (W-L) score
|
||||
Team 2 Name (W-L) score
|
||||
Map: <map name>
|
||||
<generations> Generations
|
||||
[View Simulation]
|
||||
"""
|
||||
t1_score = g.get("team1Score", 0)
|
||||
t2_score = g.get("team2Score", 0)
|
||||
t1_color = g.get("team1Color", "#aaaaaa")
|
||||
t2_color = g.get("team2Color", "#aaaaaa")
|
||||
|
||||
# Determine winner for bold styling and sidebar color
|
||||
if t1_score > t2_score:
|
||||
winner_color = t1_color
|
||||
t1_fmt = "*{name}*"
|
||||
t2_fmt = "{name}"
|
||||
else:
|
||||
winner_color = t2_color
|
||||
t1_fmt = "{name}"
|
||||
t2_fmt = "*{name}*"
|
||||
|
||||
# Series win-loss records (pre-game, as shown on the card)
|
||||
w1, l1 = g.get("team1SeriesWinLoss", [0, 0])
|
||||
w2, l2 = g.get("team2SeriesWinLoss", [0, 0])
|
||||
t1_record = f" ({w1}-{l1})" if w1 or l1 else ""
|
||||
t2_record = f" ({w2}-{l2})" if w2 or l2 else ""
|
||||
|
||||
t1_label = t1_fmt.format(name=g["team1Name"]) + t1_record
|
||||
t2_label = t2_fmt.format(name=g["team2Name"]) + t2_record
|
||||
|
||||
map_name = g.get("mapName", "Unknown")
|
||||
generations = g.get("generations", 0)
|
||||
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{"type": "mrkdwn", "text": t1_label},
|
||||
{"type": "mrkdwn", "text": f"*{t1_score}*" if t1_score > t2_score else str(t1_score)},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{"type": "mrkdwn", "text": t2_label},
|
||||
{"type": "mrkdwn", "text": f"*{t2_score}*" if t2_score > t1_score else str(t2_score)},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{"type": "mrkdwn", "text": f"Map: {map_name}"},
|
||||
{"type": "mrkdwn", "text": f"{generations} Generations"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
if site_base:
|
||||
url = game_link_url(site_base, g)
|
||||
label = "View Simulation"
|
||||
blocks.append({
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": label},
|
||||
"url": url,
|
||||
"style": "primary",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
# Plain text fallback for notifications / non-Block-Kit clients
|
||||
fallback = (
|
||||
f"{g['team1Name']} {t1_score} - {g['team2Name']} {t2_score} "
|
||||
f"| Map: {map_name} | {generations} Gen"
|
||||
)
|
||||
|
||||
return {
|
||||
"color": winner_color,
|
||||
"blocks": blocks,
|
||||
"fallback": fallback,
|
||||
}
|
||||
|
||||
|
||||
def _upcoming_game_attachment(g, site_base=None):
|
||||
"""Build a Slack attachment for an upcoming/in-progress game (no scores yet).
|
||||
|
||||
Mirrors the vii.golly.life scheduled/in-progress game card:
|
||||
Team 1 Name (W-L)
|
||||
Team 2 Name (W-L)
|
||||
Map: <map name>
|
||||
[View Simulation]
|
||||
"""
|
||||
w1, l1 = g.get("team1SeriesWinLoss", [0, 0])
|
||||
w2, l2 = g.get("team2SeriesWinLoss", [0, 0])
|
||||
t1_record = f" ({w1}-{l1})" if w1 or l1 else ""
|
||||
t2_record = f" ({w2}-{l2})" if w2 or l2 else ""
|
||||
|
||||
t1_label = g["team1Name"] + t1_record
|
||||
t2_label = g["team2Name"] + t2_record
|
||||
|
||||
map_name = g.get("mapName", "Unknown")
|
||||
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": f"*{t1_label}*\n*{t2_label}*"},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{"type": "mrkdwn", "text": f"Map: {map_name}"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
if site_base:
|
||||
url = game_link_url(site_base, g)
|
||||
blocks.append({
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "View Simulation"},
|
||||
"url": url,
|
||||
"style": "primary",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
fallback = f"{g['team1Name']} vs. {g['team2Name']} | Map: {map_name}"
|
||||
|
||||
# Use team1's color for the sidebar on upcoming games
|
||||
sidebar_color = g.get("team1Color", "#3a3f44")
|
||||
|
||||
return {
|
||||
"color": sidebar_color,
|
||||
"blocks": blocks,
|
||||
"fallback": fallback,
|
||||
}
|
||||
|
||||
|
||||
def _header_attachment(header_text):
|
||||
"""Simple styled header as an attachment (for series titles, day headers)."""
|
||||
return {
|
||||
"color": "#3a3f44",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": f"*{header_text}*"},
|
||||
},
|
||||
],
|
||||
"fallback": header_text,
|
||||
}
|
||||
|
||||
|
||||
def _series_clinch_attachment(winner, winner_wl, loser, loser_wl, winner_color):
|
||||
"""Styled announcement that a team has won the series."""
|
||||
text = f"*{winner}* ({winner_wl}) wins the series over {loser} ({loser_wl})"
|
||||
return {
|
||||
"color": winner_color,
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": text},
|
||||
},
|
||||
],
|
||||
"fallback": f"{winner} ({winner_wl}) wins the series over {loser} ({loser_wl})",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification builders
|
||||
#
|
||||
# Each returns a list of message dicts: {"text": str, "attachments": list}
|
||||
# A single Slack API call sends one message dict.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def format_matchup(g, site_base=None):
|
||||
"""Build a single attachment for a finished game result."""
|
||||
return _finished_game_attachment(g, site_base)
|
||||
|
||||
|
||||
def format_current_games(current_games, site_base=None):
|
||||
lines = []
|
||||
"""Build a list of attachments for today's upcoming/in-progress games."""
|
||||
attachments = []
|
||||
for g in sorted(current_games, key=lambda x: x.get("description", "")):
|
||||
w1, l1 = g["team1SeriesWinLoss"]
|
||||
w2, l2 = g["team2SeriesWinLoss"]
|
||||
t1 = f"{g['team1Name']} ({w1}-{l1})" if w1 or l1 else g["team1Name"]
|
||||
t2 = f"{g['team2Name']} ({w2}-{l2})" if w2 or l2 else g["team2Name"]
|
||||
if site_base:
|
||||
link = game_link(site_base, g)
|
||||
lines.append(f"{t1} vs. {t2}: {link}")
|
||||
else:
|
||||
lines.append(f"*{t1} vs. {t2}*")
|
||||
return lines
|
||||
attachments.append(_upcoming_game_attachment(g, site_base))
|
||||
return attachments
|
||||
|
||||
|
||||
def updated_series_wl(game):
|
||||
"""Return (team1_wins, team1_losses, team2_wins, team2_losses) accounting for this game's outcome."""
|
||||
w1, l1 = game["team1SeriesWinLoss"]
|
||||
w2, l2 = game["team2SeriesWinLoss"]
|
||||
if game["team1Score"] > game["team2Score"]:
|
||||
w1 += 1
|
||||
l2 += 1
|
||||
else:
|
||||
w2 += 1
|
||||
l1 += 1
|
||||
return w1, l1, w2, l2
|
||||
|
||||
|
||||
def build_notification(cup_config, mode_data, postseason, current_games=None):
|
||||
"""Build styled Slack messages for the current game state.
|
||||
|
||||
Returns a list of message dicts, each with:
|
||||
- "text": plain-text fallback
|
||||
- "attachments": list of Slack attachments with Block Kit blocks
|
||||
"""
|
||||
mode = mode_data["mode"]
|
||||
elapsed = mode_data.get("elapsed", 0)
|
||||
cup_name = cup_config["cup_name"]
|
||||
@@ -114,35 +348,35 @@ def build_notification(cup_config, mode_data, postseason, current_games=None):
|
||||
return []
|
||||
|
||||
if mode in (31, 32, 33):
|
||||
if not just_entered:
|
||||
return []
|
||||
|
||||
if mode == 33:
|
||||
series_key = cup_series_key
|
||||
else:
|
||||
series_key = MODE_TO_SERIES[mode]
|
||||
|
||||
series_name = SERIES_NAMES.get(series_key, series_key)
|
||||
# hours elapsed = zero-indexed day; report one-indexed day
|
||||
# e.g., 3700s elapsed -> 3700//3600 = 1 -> Day 2
|
||||
series_day = (elapsed // 3600) + 1
|
||||
series_data = postseason.get(series_key, [])
|
||||
|
||||
if just_entered and series_day == 1:
|
||||
if just_entered:
|
||||
site_link = f"<{site_base}|{site_base.replace('https://', '')}>" if site_base else ""
|
||||
lines = [f"{cup_name}: {series_name} is starting now! {site_link}".strip()]
|
||||
header_text = f"{cup_name}: {series_name} is starting now! {site_link}".strip()
|
||||
attachments = [_header_attachment(header_text)]
|
||||
if current_games:
|
||||
lines.extend(format_current_games(current_games))
|
||||
return ["\n".join(lines)]
|
||||
attachments.extend(format_current_games(current_games, site_base))
|
||||
return [{"text": header_text, "attachments": attachments}]
|
||||
|
||||
messages = []
|
||||
|
||||
if just_entered and series_data:
|
||||
yesterday_games = series_data[-1]
|
||||
outcome_lines = [f"*{series_name} Results*"]
|
||||
# Yesterday's results: use day-based index (series_day - 2 for 0-indexed prior day)
|
||||
yesterday_idx = series_day - 2
|
||||
if series_data and 0 <= yesterday_idx < len(series_data):
|
||||
yesterday_games = series_data[yesterday_idx]
|
||||
header = f"{series_name} \u2014 Day {series_day - 1} Results"
|
||||
result_attachments = [_header_attachment(header)]
|
||||
for g in yesterday_games:
|
||||
outcome_lines.extend(format_matchup(g))
|
||||
w1, l1 = g["team1SeriesWinLoss"]
|
||||
w2, l2 = g["team2SeriesWinLoss"]
|
||||
outcome_lines.append(f"Series: {g['team1Name']} {w1}-{l1}, {g['team2Name']} {w2}-{l2}")
|
||||
result_attachments.append(format_matchup(g, site_base))
|
||||
|
||||
today_pairs = set()
|
||||
if current_games:
|
||||
@@ -153,20 +387,26 @@ def build_notification(cup_config, mode_data, postseason, current_games=None):
|
||||
for g in yesterday_games:
|
||||
pair = tuple(sorted([g["team1Name"], g["team2Name"]]))
|
||||
if pair not in today_pairs:
|
||||
w1, _ = g["team1SeriesWinLoss"]
|
||||
w2, _ = g["team2SeriesWinLoss"]
|
||||
w1, l1, w2, l2 = updated_series_wl(g)
|
||||
if w1 > w2:
|
||||
winner, loser, wl = g["team1Name"], g["team2Name"], f"{w1}-{w2}"
|
||||
winner, winner_wl = g["team1Name"], f"{w1}-{l1}"
|
||||
loser, loser_wl = g["team2Name"], f"{w2}-{l2}"
|
||||
winner_color = g.get("team1Color", "#62c462")
|
||||
else:
|
||||
winner, loser, wl = g["team2Name"], g["team1Name"], f"{w2}-{w1}"
|
||||
outcome_lines.append(f"{winner} wins the series over {loser} ({wl})")
|
||||
winner, winner_wl = g["team2Name"], f"{w2}-{l2}"
|
||||
loser, loser_wl = g["team1Name"], f"{w1}-{l1}"
|
||||
winner_color = g.get("team2Color", "#62c462")
|
||||
result_attachments.append(
|
||||
_series_clinch_attachment(winner, winner_wl, loser, loser_wl, winner_color)
|
||||
)
|
||||
|
||||
messages.append("\n".join(outcome_lines))
|
||||
messages.append({"text": header, "attachments": result_attachments})
|
||||
|
||||
if current_games:
|
||||
upcoming_lines = [f"*{series_name} — Today's Games*"]
|
||||
upcoming_lines.extend(format_current_games(current_games))
|
||||
messages.append("\n".join(upcoming_lines))
|
||||
header = f"{series_name} \u2014 Day {series_day} Games"
|
||||
upcoming_attachments = [_header_attachment(header)]
|
||||
upcoming_attachments.extend(format_current_games(current_games, site_base))
|
||||
messages.append({"text": header, "attachments": upcoming_attachments})
|
||||
|
||||
return messages
|
||||
|
||||
@@ -179,7 +419,7 @@ def build_notification(cup_config, mode_data, postseason, current_games=None):
|
||||
if mode == 23:
|
||||
if not just_entered:
|
||||
return []
|
||||
result = announce_series_outcome(postseason, "LCS", "League Championship Series")
|
||||
result = announce_series_outcome(postseason, "LCS", "Championship Series")
|
||||
return [result] if result else []
|
||||
|
||||
if mode == 40:
|
||||
@@ -192,26 +432,35 @@ def build_notification(cup_config, mode_data, postseason, current_games=None):
|
||||
|
||||
|
||||
def announce_series_outcome(postseason, series_key, series_name):
|
||||
"""Build a styled message announcing the final outcome of a series."""
|
||||
series_data = postseason.get(series_key, [])
|
||||
if not series_data:
|
||||
return None
|
||||
|
||||
last_day = series_data[-1]
|
||||
lines = [f"*{series_name} Results*"]
|
||||
for game in last_day:
|
||||
w1, _ = game["team1SeriesWinLoss"]
|
||||
w2, _ = game["team2SeriesWinLoss"]
|
||||
if w1 > w2:
|
||||
winner = game["team1Name"]
|
||||
loser = game["team2Name"]
|
||||
series_wl = f"{w1}-{w2}"
|
||||
else:
|
||||
winner = game["team2Name"]
|
||||
loser = game["team1Name"]
|
||||
series_wl = f"{w2}-{w1}"
|
||||
lines.append(f"{winner} defeats {loser} ({series_wl})")
|
||||
return "\n".join(lines)
|
||||
header = f"{series_name} Results"
|
||||
attachments = [_header_attachment(header)]
|
||||
|
||||
for game in last_day:
|
||||
w1, l1, w2, l2 = updated_series_wl(game)
|
||||
if w1 > w2:
|
||||
winner, winner_wl = game["team1Name"], f"{w1}-{l1}"
|
||||
loser, loser_wl = game["team2Name"], f"{w2}-{l2}"
|
||||
winner_color = game.get("team1Color", "#62c462")
|
||||
else:
|
||||
winner, winner_wl = game["team2Name"], f"{w2}-{l2}"
|
||||
loser, loser_wl = game["team1Name"], f"{w1}-{l1}"
|
||||
winner_color = game.get("team2Color", "#62c462")
|
||||
attachments.append(
|
||||
_series_clinch_attachment(winner, winner_wl, loser, loser_wl, winner_color)
|
||||
)
|
||||
|
||||
return {"text": header, "attachments": attachments}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run(cup_key):
|
||||
token = os.getenv("SLACK_BOT_TOKEN")
|
||||
@@ -231,12 +480,22 @@ def run(cup_key):
|
||||
print(f"[{now_pt.strftime('%Y-%m-%d %H:%M PT')}] Running {cup['cup_name']} notifier")
|
||||
|
||||
weekday = now_pt.weekday()
|
||||
if weekday not in cup["days"]:
|
||||
schedule = cup.get("schedule", {})
|
||||
if weekday not in schedule:
|
||||
print(f" Not a {cup['cup_name']} day (weekday={weekday}). Skipping.")
|
||||
return
|
||||
|
||||
if now_pt.hour < cup["start_hour"]:
|
||||
print(f" Too early (hour={now_pt.hour}, start={cup['start_hour']}). Skipping.")
|
||||
day_config = schedule[weekday]
|
||||
start_hour = day_config["start_hour"]
|
||||
duration = day_config["duration"]
|
||||
end_hour = start_hour + duration
|
||||
|
||||
if now_pt.hour < start_hour:
|
||||
print(f" Too early (hour={now_pt.hour}, start={start_hour}). Skipping.")
|
||||
return
|
||||
|
||||
if now_pt.hour >= end_hour:
|
||||
print(f" Past window (hour={now_pt.hour}, end={end_hour}). Skipping.")
|
||||
return
|
||||
|
||||
api_base = cup["api_base"]
|
||||
@@ -253,8 +512,10 @@ def run(cup_key):
|
||||
|
||||
if messages:
|
||||
for i, msg in enumerate(messages, 1):
|
||||
print(f" Sending message {i}/{len(messages)}: {msg[:80]}")
|
||||
send_slack_message(token, channel, msg, cup["bot_username"], cup["bot_icon_url"])
|
||||
text = msg["text"]
|
||||
attachments = msg.get("attachments")
|
||||
print(f" Sending message {i}/{len(messages)}: {text[:80]}")
|
||||
send_slack_message(token, channel, text, cup["bot_username"], cup["bot_icon_url"], attachments)
|
||||
print(f" Sent message {i}.")
|
||||
else:
|
||||
print(" No notification to send.")
|
||||
|
||||
BIN
golly-notifier/star-cup-icon.png
Normal file
BIN
golly-notifier/star-cup-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
4
utilities/.environment.example
Normal file
4
utilities/.environment.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copy this file to .environment and fill in real values.
|
||||
# Then: source .environment
|
||||
|
||||
SLACK_BOT_TOKEN=xoxb-your-token-here
|
||||
147
utilities/README.md
Normal file
147
utilities/README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Utilities — Message Cleanup
|
||||
|
||||
Scripts for deleting messages posted by the Slack TV bot. Useful when testing a new channel, resetting after a bad deploy, or cleaning up a channel before archiving it.
|
||||
|
||||
Both scripts default to **dry-run mode** — they show what would happen without touching anything. Pass `--real` to actually delete.
|
||||
|
||||
---
|
||||
|
||||
## Before you start: expand the app's permissions
|
||||
|
||||
The channel notifiers only need `chat:write` and `chat:write.customize`. These utilities need two additional scopes so they can read channel history and identify bot messages.
|
||||
|
||||
### 1. Add the new scopes
|
||||
|
||||
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and select the **Slack TV** app
|
||||
2. In the left sidebar, click **OAuth & Permissions**
|
||||
3. Scroll to **Bot Token Scopes** and add:
|
||||
|
||||
| Scope | Why |
|
||||
|-------|-----|
|
||||
| `channels:history` | Read message history in public channels (`conversations.history`) |
|
||||
| `groups:history` | Read message history in private channels (only needed if any Slack TV channel is private) |
|
||||
|
||||
The app should now have four (or five) scopes total:
|
||||
|
||||
| Scope | Used by |
|
||||
|-------|---------|
|
||||
| `chat:write` | notifiers (post messages) + utilities (delete messages) |
|
||||
| `chat:write.customize` | notifiers (override bot name and icon per message) |
|
||||
| `channels:history` | utilities (list messages in public channels) |
|
||||
| `groups:history` | utilities (list messages in private channels) — optional |
|
||||
|
||||
### 2. Reinstall the app
|
||||
|
||||
After adding scopes, Slack requires you to reinstall:
|
||||
|
||||
1. Scroll to the top of the **OAuth & Permissions** page
|
||||
2. Click **Reinstall to Workspace** → **Allow**
|
||||
3. The **Bot User OAuth Token** (`xoxb-...`) stays the same — no secrets or config to update
|
||||
|
||||
That's it. The existing notifiers are unaffected; the new scopes are additive.
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
utilities/
|
||||
├── delete_message.py # Delete a single message by URL
|
||||
├── clear_channel.py # Delete all bot messages in a channel
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .environment.example # Template — copy to .environment and fill in
|
||||
├── .environment # Your real tokens (git-ignored)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
These scripts are meant to be run locally on a developer's machine, not in CI. You need to set one variable:
|
||||
|
||||
| Variable | Value | Where to find it |
|
||||
|----------|-------|-------------------|
|
||||
| `SLACK_BOT_TOKEN` | `xoxb-...` | [api.slack.com/apps](https://api.slack.com/apps) → Slack TV → **OAuth & Permissions** → **Bot User OAuth Token** |
|
||||
|
||||
A `.environment.example` template is checked in. Copy it, fill in your token, and source it:
|
||||
|
||||
```bash
|
||||
cp .environment.example .environment
|
||||
# edit .environment — paste your real xoxb-... token
|
||||
```
|
||||
|
||||
The real `.environment` file is git-ignored so tokens never get committed.
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd utilities
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
source .environment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## delete_message.py
|
||||
|
||||
Delete a single message by its Slack URL. Get the URL by right-clicking a message in Slack → **Copy link**.
|
||||
|
||||
```bash
|
||||
# Dry run (default) — shows what would be deleted
|
||||
python delete_message.py "https://myworkspace.slack.com/archives/C012AB3CD/p1234567890123456"
|
||||
|
||||
# Actually delete
|
||||
python delete_message.py "https://myworkspace.slack.com/archives/C012AB3CD/p1234567890123456" --real
|
||||
```
|
||||
|
||||
The script parses the channel ID and message timestamp from the URL automatically. It can only delete messages posted by the bot (Slack enforces this — bots cannot delete other users' messages).
|
||||
|
||||
---
|
||||
|
||||
## clear_channel.py
|
||||
|
||||
Delete **all** messages posted by the bot in a given channel. Takes a channel ID, not a channel name.
|
||||
|
||||
```bash
|
||||
# Dry run (default) — lists every bot message that would be deleted
|
||||
python clear_channel.py C012AB3CD
|
||||
|
||||
# Actually delete (prompts for confirmation)
|
||||
python clear_channel.py C012AB3CD --real
|
||||
|
||||
# Actually delete (skip confirmation prompt)
|
||||
python clear_channel.py C012AB3CD --real --force
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
1. Calls `auth.test` to learn the bot's own user ID
|
||||
2. Pages through `conversations.history` to find all messages in the channel
|
||||
3. Filters to messages where `user` matches the bot's user ID
|
||||
4. In dry-run mode, prints the list and stops
|
||||
5. In real mode, deletes each message via `chat.delete` with a 1.2-second delay between calls to stay within Slack's rate limits (~50 requests/minute for Tier 3 methods)
|
||||
|
||||
### Safety rails
|
||||
|
||||
- **Dry run by default** — you have to explicitly opt in with `--real`
|
||||
- **Confirmation prompt** — in `--real` mode, prints a loud warning and requires typing `y` before proceeding
|
||||
- **`--force` flag** — skips the confirmation prompt for scripted use, but still requires `--real`
|
||||
- **Bot messages only** — the script identifies and deletes only messages posted by the bot token you're using; other users' messages are untouched
|
||||
|
||||
---
|
||||
|
||||
## Finding the channel ID
|
||||
|
||||
Slack's API uses channel IDs, not names. To find a channel's ID:
|
||||
|
||||
- Open Slack in a browser, navigate to the channel, and grab the ID from the URL:
|
||||
```
|
||||
https://app.slack.com/client/T.../C012AB3CD
|
||||
^^^^^^^^^^^ channel ID
|
||||
```
|
||||
- Or: right-click the channel name → **View channel details** → scroll to the bottom of the **About** tab
|
||||
206
utilities/clear_channel.py
Normal file
206
utilities/clear_channel.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Delete ALL messages posted by this bot in a Slack channel.
|
||||
|
||||
Usage:
|
||||
python clear_channel.py <channel_id> # dry run (default)
|
||||
python clear_channel.py <channel_id> --dry-run # explicit dry run
|
||||
python clear_channel.py <channel_id> --real # actually delete (prompts for confirmation)
|
||||
python clear_channel.py <channel_id> --real --force # actually delete (skip confirmation)
|
||||
|
||||
Environment:
|
||||
SLACK_BOT_TOKEN — Bot User OAuth Token (xoxb-...)
|
||||
|
||||
Required Slack scopes:
|
||||
channels:history — read message history in public channels
|
||||
groups:history — read message history in private channels (if needed)
|
||||
chat:write — delete messages posted by this bot
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
SLACK_HISTORY_URL = "https://slack.com/api/conversations.history"
|
||||
SLACK_DELETE_URL = "https://slack.com/api/chat.delete"
|
||||
SLACK_AUTH_TEST_URL = "https://slack.com/api/auth.test"
|
||||
|
||||
|
||||
def get_bot_user_id(token):
|
||||
"""Get the bot's own user ID via auth.test."""
|
||||
response = requests.post(
|
||||
SLACK_AUTH_TEST_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not data.get("ok"):
|
||||
raise RuntimeError(f"Slack API error: {data.get('error')}")
|
||||
return data["user_id"]
|
||||
|
||||
|
||||
def fetch_bot_messages(token, channel, bot_user_id):
|
||||
"""Fetch all messages in a channel posted by the bot.
|
||||
|
||||
Pages through conversations.history (newest-first, 200 per page) and
|
||||
collects messages where the user field matches the bot's user ID.
|
||||
"""
|
||||
messages = []
|
||||
cursor = None
|
||||
|
||||
while True:
|
||||
params = {"channel": channel, "limit": 200}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
|
||||
response = requests.get(
|
||||
SLACK_HISTORY_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if not data.get("ok"):
|
||||
raise RuntimeError(f"Slack API error: {data.get('error')}")
|
||||
|
||||
for msg in data.get("messages", []):
|
||||
if msg.get("user") == bot_user_id:
|
||||
messages.append(msg)
|
||||
|
||||
# Pagination
|
||||
metadata = data.get("response_metadata", {})
|
||||
cursor = metadata.get("next_cursor")
|
||||
if not cursor:
|
||||
break
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def delete_message(token, channel, ts):
|
||||
"""Call chat.delete to remove a message. Returns True on success."""
|
||||
response = requests.post(
|
||||
SLACK_DELETE_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"channel": channel, "ts": ts},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not data.get("ok"):
|
||||
error = data.get("error")
|
||||
if error == "message_not_found":
|
||||
# Already deleted or ephemeral — not a failure
|
||||
return False
|
||||
raise RuntimeError(f"Slack API error: {error}")
|
||||
return True
|
||||
|
||||
|
||||
def confirm_destructive_action(channel, count):
|
||||
"""Print a loud warning and require 'y' + Enter to proceed."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" WARNING: DESTRUCTIVE ACTION")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f" You are about to permanently delete {count} message(s)")
|
||||
print(f" from channel {channel}.")
|
||||
print()
|
||||
print(" This CANNOT be undone.")
|
||||
print()
|
||||
print("=" * 60)
|
||||
print()
|
||||
answer = input(" Type 'y' and press Enter to continue: ")
|
||||
return answer.strip().lower() == "y"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
||||
print(__doc__.strip())
|
||||
sys.exit(0)
|
||||
|
||||
channel = sys.argv[1]
|
||||
flags = sys.argv[2:]
|
||||
|
||||
real_mode = "--real" in flags
|
||||
force = "--force" in flags
|
||||
|
||||
if "--dry-run" in flags and "--real" in flags:
|
||||
print("Error: --dry-run and --real are mutually exclusive.")
|
||||
sys.exit(1)
|
||||
|
||||
if force and not real_mode:
|
||||
print("Error: --force only makes sense with --real.")
|
||||
sys.exit(1)
|
||||
|
||||
token = os.getenv("SLACK_BOT_TOKEN")
|
||||
if not token:
|
||||
print("Error: SLACK_BOT_TOKEN environment variable is not set.")
|
||||
sys.exit(1)
|
||||
|
||||
# Identify who we are
|
||||
print("Identifying bot user...")
|
||||
bot_user_id = get_bot_user_id(token)
|
||||
print(f"Bot user ID: {bot_user_id}")
|
||||
print()
|
||||
|
||||
# Fetch messages
|
||||
print(f"Fetching messages from channel {channel}...")
|
||||
bot_messages = fetch_bot_messages(token, channel, bot_user_id)
|
||||
print(f"Found {len(bot_messages)} message(s) from this bot.")
|
||||
|
||||
if not bot_messages:
|
||||
print("Nothing to delete.")
|
||||
return
|
||||
|
||||
# Show preview
|
||||
print()
|
||||
print("Messages to delete:")
|
||||
print("-" * 60)
|
||||
for msg in bot_messages:
|
||||
ts = msg["ts"]
|
||||
text = msg.get("text", "")
|
||||
preview = text[:70].replace("\n", " ")
|
||||
if len(text) > 70:
|
||||
preview += "..."
|
||||
print(f" [{ts}] {preview}")
|
||||
print("-" * 60)
|
||||
print()
|
||||
|
||||
if not real_mode:
|
||||
print(f"[DRY RUN] Would delete {len(bot_messages)} message(s) from channel {channel}.")
|
||||
print("Pass --real to actually delete.")
|
||||
return
|
||||
|
||||
# Confirmation gate
|
||||
if not force:
|
||||
if not confirm_destructive_action(channel, len(bot_messages)):
|
||||
print("Aborted.")
|
||||
sys.exit(0)
|
||||
|
||||
# Delete
|
||||
deleted = 0
|
||||
skipped = 0
|
||||
for i, msg in enumerate(bot_messages, 1):
|
||||
ts = msg["ts"]
|
||||
text_preview = msg.get("text", "")[:50].replace("\n", " ")
|
||||
sys.stdout.write(f"\r Deleting {i}/{len(bot_messages)}: [{ts}] {text_preview}")
|
||||
sys.stdout.flush()
|
||||
if delete_message(token, channel, ts):
|
||||
deleted += 1
|
||||
else:
|
||||
skipped += 1
|
||||
# Slack rate limit: ~50 requests per minute for chat.delete (Tier 3)
|
||||
# Sleep briefly between deletions to stay well within limits.
|
||||
time.sleep(1.2)
|
||||
|
||||
print()
|
||||
print()
|
||||
print(f"Done. Deleted {deleted}, skipped {skipped} (already gone).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
106
utilities/delete_message.py
Normal file
106
utilities/delete_message.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Delete a single Slack message by its message URL.
|
||||
|
||||
Usage:
|
||||
python delete_message.py <message_url> # dry run (default)
|
||||
python delete_message.py <message_url> --dry-run # explicit dry run
|
||||
python delete_message.py <message_url> --real # actually delete
|
||||
|
||||
Message URL format (copy from Slack via "Copy link"):
|
||||
https://yourworkspace.slack.com/archives/C012AB3CD/p1234567890123456
|
||||
|
||||
Environment:
|
||||
SLACK_BOT_TOKEN — Bot User OAuth Token (xoxb-...)
|
||||
|
||||
Required Slack scopes:
|
||||
chat:write — delete messages posted by this bot
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import requests
|
||||
|
||||
|
||||
SLACK_DELETE_URL = "https://slack.com/api/chat.delete"
|
||||
|
||||
|
||||
def parse_message_url(url):
|
||||
"""Extract channel ID and message timestamp from a Slack message URL.
|
||||
|
||||
Slack message URLs look like:
|
||||
https://workspace.slack.com/archives/C012AB3CD/p1234567890123456
|
||||
https://workspace.slack.com/archives/C012AB3CD/p1234567890123456?thread_ts=...
|
||||
|
||||
The 'p' prefix on the timestamp is Slack's URL encoding — the actual ts
|
||||
is the digits with a dot inserted: p1234567890123456 -> 1234567890.123456
|
||||
"""
|
||||
pattern = r"archives/([A-Z0-9]+)/p(\d{10})(\d{6})"
|
||||
match = re.search(pattern, url)
|
||||
if not match:
|
||||
return None, None
|
||||
channel_id = match.group(1)
|
||||
ts = f"{match.group(2)}.{match.group(3)}"
|
||||
return channel_id, ts
|
||||
|
||||
|
||||
def delete_message(token, channel, ts):
|
||||
"""Call chat.delete to remove a message."""
|
||||
response = requests.post(
|
||||
SLACK_DELETE_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"channel": channel, "ts": ts},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not data.get("ok"):
|
||||
raise RuntimeError(f"Slack API error: {data.get('error')}")
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
||||
print(__doc__.strip())
|
||||
sys.exit(0)
|
||||
|
||||
url = sys.argv[1]
|
||||
flags = sys.argv[2:]
|
||||
|
||||
real_mode = "--real" in flags
|
||||
# --dry-run is the default, but accept it explicitly too
|
||||
if "--dry-run" in flags and "--real" in flags:
|
||||
print("Error: --dry-run and --real are mutually exclusive.")
|
||||
sys.exit(1)
|
||||
|
||||
channel_id, ts = parse_message_url(url)
|
||||
if not channel_id or not ts:
|
||||
print(f"Error: Could not parse message URL: {url}")
|
||||
print()
|
||||
print("Expected format:")
|
||||
print(" https://yourworkspace.slack.com/archives/C012AB3CD/p1234567890123456")
|
||||
sys.exit(1)
|
||||
|
||||
token = os.getenv("SLACK_BOT_TOKEN")
|
||||
if not token:
|
||||
print("Error: SLACK_BOT_TOKEN environment variable is not set.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Channel: {channel_id}")
|
||||
print(f"Message: {ts}")
|
||||
print()
|
||||
|
||||
if not real_mode:
|
||||
print("[DRY RUN] Would delete message {ts} from channel {channel_id}.".format(
|
||||
ts=ts, channel_id=channel_id,
|
||||
))
|
||||
print("Pass --real to actually delete.")
|
||||
return
|
||||
|
||||
print(f"Deleting message {ts} from channel {channel_id}...")
|
||||
delete_message(token, channel_id, ts)
|
||||
print("Deleted.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
utilities/requirements.txt
Normal file
1
utilities/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests==2.32.3
|
||||
Reference in New Issue
Block a user