Skip to content

Commit 0b762ef

Browse files
authored
feat: remove Slack notifications (#1646)
Slack notifications were not working since 31st of March, due to Slack moving away from legacy bots. It's a good opportunity to remove it entirely, as we now rely on Nutripatrol for moderation, and as it generated (for other notifications) many (unread) message. Solves #1576.
1 parent 35c76b1 commit 0b762ef

26 files changed

Lines changed: 38 additions & 495 deletions

.env

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ TAG=latest
1919
# (`dev` by default).
2020
# If `prod` is used, openfoodfacts.org domain will be used by default,
2121
# and openfoodfacts.net if `dev` is used.
22-
# Messages to Slack are only enabled if `ROBOTOFF_INSTANCE=prod`.
2322
ROBOTOFF_INSTANCE=dev
2423

2524
# Overwrites the Product Opener domain used. If empty, the domain will

.github/workflows/container-deploy.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ jobs:
150150
echo "INFLUXDB_PORT=${{ env.INFLUXDB_PORT }}" >> .env
151151
echo "INFLUXDB_BUCKET=off_metrics" >> .env
152152
echo "INFLUXDB_AUTH_TOKEN=${{ secrets.INFLUXDB_AUTH_TOKEN }}" >> .env
153-
echo "SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}" >> .env
154153
echo "GUNICORN_NUM_WORKERS=4" >> .env
155154
echo "EVENTS_API_URL=https://event.openfoodfacts.${{ env.ROBOTOFF_TLD }}" >> .env
156155
echo "IMAGE_MODERATION_SERVICE_URL=${{ env.IMAGE_MODERATION_SERVICE_URL }}" >> .env

data/ocr/store_notify.txt

Whitespace-only changes.

doc/how-to-guides/test-and-debug.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ Remember to put quotes especially if you have multiple arguments.
6464

6565
Write test cases every time you write a new feature, to test a feature or to understand the working of an existing function. Automated testing really helps to prevent future bugs as we introduce new features or refactor the code.
6666

67-
There are even cases where automated tests are your only chance to test you code. For example: when you write code to post notifications on Slack channel you can only test them by writing a unit test case.
68-
6967
There are instances when Robotoff tries to connect to MongoDB via Open Food Facts server. To disable this
7068
feature (this is disabled by default on local environments), set `ENABLE_MONGODB_ACCESS=0` in your `.env`.
7169

doc/introduction/notifications.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ x-robotoff-base-env:
4141
INFLUXDB_PORT:
4242
INFLUXDB_BUCKET:
4343
INFLUXDB_AUTH_TOKEN:
44-
SLACK_TOKEN:
4544
SENTRY_DSN:
4645
ELASTIC_HOST:
4746
ELASTIC_PASSWORD:

robotoff/logos.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
)
2323
from robotoff.models import Prediction as PredictionModel
2424
from robotoff.models import ProductInsight, db
25-
from robotoff.notifier import NotifierFactory
2625
from robotoff.off import OFFAuthentication
2726
from robotoff.types import (
2827
ElasticSearchIndex,
@@ -362,7 +361,6 @@ def import_logo_insights(
362361
thresholds: dict[LogoLabelType, float],
363362
server_type: ServerType,
364363
default_threshold: float = 0.2,
365-
notify: bool = True,
366364
) -> InsightImportResult:
367365
"""Generate and import insights from logos.
368366
@@ -377,8 +375,6 @@ class is not defined, default threshold is used
377375
:param server_type: the server type (project) associated with the logos
378376
:param default_threshold: the default confidence threshold to use,
379377
defaults to 0.2
380-
:param notify: whether to send a notification for each logo selected to
381-
generate an insight, defaults to True
382378
:return: the result from the insight import
383379
"""
384380
selected_logos = []
@@ -420,10 +416,6 @@ class is not defined, default threshold is used
420416
predictions = predict_logo_predictions(selected_logos, logo_probs, server_type)
421417
import_result = import_insights(predictions, server_type)
422418

423-
if notify:
424-
for logo, probs in zip(selected_logos, logo_probs):
425-
NotifierFactory.get_notifier().send_logo_notification(logo, probs)
426-
427419
return import_result
428420

429421

@@ -632,7 +624,7 @@ def refresh_nearest_neighbors(
632624
else:
633625
logos = [embedding.logo for embedding in logo_embeddings]
634626
import_logo_insights(
635-
logos, thresholds=thresholds, server_type=server_type, notify=False
627+
logos, thresholds=thresholds, server_type=server_type
636628
)
637629

638630
logger.info("refresh of logo nearest neighbors finished")

robotoff/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class ProductInsight(BaseModel):
102102
# above.
103103
# NOTE: there is no 1:1 mapping between the type and the JSON format
104104
# provided here, for example for type==label, the data here could be:
105-
# {"logo_id":X,"bounding_box":Y}, or {"text":X,"notify":Y}
105+
# {"logo_id":X,"bounding_box":Y}, or {"text":X}
106106
data = BinaryJSONField(default=dict)
107107

108108
# Timestamp is the timestamp of when this insight was imported into the DB.

robotoff/notifier.py

Lines changed: 3 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,13 @@
1-
import json
2-
import operator
3-
4-
import requests
5-
from requests.exceptions import ConnectionError as RequestConnectionError
6-
from requests.exceptions import JSONDecodeError
7-
81
from robotoff import settings
9-
from robotoff.models import ImageModel, LogoAnnotation, ProductInsight, crop_image_url
10-
from robotoff.types import (
11-
InsightType,
12-
JSONType,
13-
LogoLabelType,
14-
Prediction,
15-
ProductIdentifier,
16-
ServerType,
17-
)
2+
from robotoff.types import Prediction, ProductIdentifier
183
from robotoff.utils import get_logger, http_session
194

205
logger = get_logger(__name__)
216

227

23-
class SlackException(Exception):
24-
pass
25-
26-
278
class NotifierInterface:
28-
"""NotifierInterface is an interface for posting
29-
Robotoff-related alerts and notifications
30-
to various channels.
31-
"""
9+
"""NotifierInterface is an interface for sending notifications
10+
to external services."""
3211

3312
# Note: we do not use abstract methods,
3413
# for a notifier might choose to only implements a few
@@ -41,14 +20,6 @@ def notify_image_flag(
4120
):
4221
pass
4322

44-
def notify_automatic_processing(self, insight: ProductInsight):
45-
pass
46-
47-
def send_logo_notification(
48-
self, logo: LogoAnnotation, probs: dict[LogoLabelType, float]
49-
):
50-
pass
51-
5223

5324
class NotifierFactory:
5425
"""NotifierFactory is responsible for creating a notifier to post
@@ -57,12 +28,6 @@ class NotifierFactory:
5728
@staticmethod
5829
def get_notifier() -> NotifierInterface:
5930
notifiers: list[NotifierInterface] = []
60-
token = settings.slack_token()
61-
if token == "":
62-
# use a Noop notifier to get logs for tests and dev
63-
notifiers.append(NoopSlackNotifier())
64-
else:
65-
notifiers.append(SlackNotifier(token))
6631
moderation_service_url: str | None = settings.IMAGE_MODERATION_SERVICE_URL
6732
if moderation_service_url:
6833
notifiers.append(ImageModerationNotifier(moderation_service_url))
@@ -92,36 +57,6 @@ def get_notifier() -> NotifierInterface:
9257
}
9358

9459

95-
def _sensitive_image(flag_type: str, flagged_label: str) -> bool:
96-
"""Determines whether the given flagged image should be considered as
97-
sensitive."""
98-
is_human = flagged_label in HUMAN_FLAG_LABELS
99-
return (
100-
is_human and flag_type == "label_annotation"
101-
) or flag_type == "safe_search_annotation"
102-
103-
104-
def _slack_message_block(
105-
message_text: str, with_image: str | None = None
106-
) -> list[dict]:
107-
"""Formats given parameters into a Slack message block."""
108-
block = {
109-
"type": "section",
110-
"text": {
111-
"type": "mrkdwn",
112-
"text": message_text,
113-
},
114-
}
115-
116-
if with_image:
117-
block["accessory"] = {
118-
"type": "image",
119-
"image_url": with_image,
120-
"alt_text": "-",
121-
}
122-
return [block]
123-
124-
12560
class MultiNotifier(NotifierInterface):
12661
"""Aggregate multiple notifiers in one instance
12762
@@ -147,14 +82,6 @@ def notify_image_flag(
14782
):
14883
self._dispatch("notify_image_flag", predictions, source_image, product_id)
14984

150-
def notify_automatic_processing(self, insight: ProductInsight):
151-
self._dispatch("notify_automatic_processing", insight)
152-
153-
def send_logo_notification(
154-
self, logo: LogoAnnotation, probs: dict[LogoLabelType, float]
155-
):
156-
self._dispatch("send_logo_notification", logo, probs)
157-
15885

15986
class ImageModerationNotifier(NotifierInterface):
16087
"""Notifier to dispatch to image moderation server
@@ -230,152 +157,3 @@ def notify_image_flag(
230157
"barcode": product_id.barcode,
231158
},
232159
)
233-
234-
235-
class SlackNotifier(NotifierInterface):
236-
"""Notifier to send messages on specific slack channels"""
237-
238-
# Slack channel IDs.
239-
ROBOTOFF_ALERT_CHANNEL = "CGKPALRCG" # robotoff-alerts-annotations
240-
241-
BASE_URL = "https://slack.com/api"
242-
POST_MESSAGE_URL = BASE_URL + "/chat.postMessage"
243-
COLLAPSE_LINKS_PARAMS = {
244-
"unfurl_links": False,
245-
"unfurl_media": False,
246-
}
247-
248-
def __init__(self, slack_token: str):
249-
"""Should not be called directly, use the NotifierFactory instead."""
250-
self.slack_token = slack_token
251-
252-
def notify_automatic_processing(self, insight: ProductInsight):
253-
server_type = ServerType[insight.server_type]
254-
product_url = (
255-
f"{settings.BaseURLProvider.world(server_type)}/product/{insight.barcode}"
256-
)
257-
258-
if insight.source_image:
259-
if insight.data and "bounding_box" in insight.data:
260-
image_url = crop_image_url(
261-
server_type,
262-
insight.source_image,
263-
insight.data.get("bounding_box"),
264-
)
265-
else:
266-
image_url = settings.BaseURLProvider.image_url(
267-
server_type, insight.source_image
268-
)
269-
metadata_text = f"(<{product_url}|product>, <{image_url}|source image>)"
270-
else:
271-
metadata_text = f"(<{product_url}|product>)"
272-
273-
value = insight.value or insight.value_tag
274-
275-
if insight.type in {
276-
InsightType.product_weight.name,
277-
InsightType.expiration_date.name,
278-
}:
279-
text = f"The {insight.type} `{value}` (match: `{insight.data['raw']}`) was automatically added to product {insight.barcode}"
280-
else:
281-
text = f"The `{value}` {insight.type} was automatically added to product {insight.barcode}"
282-
283-
message = _slack_message_block(f"{text} {metadata_text}")
284-
self._post_message(
285-
message, self.ROBOTOFF_ALERT_CHANNEL, **self.COLLAPSE_LINKS_PARAMS
286-
)
287-
288-
def _get_base_params(self) -> JSONType:
289-
return {
290-
"username": "robotoff-bot",
291-
"token": self.slack_token,
292-
"icon_url": "https://s3-us-west-2.amazonaws.com/slack-files2/"
293-
"bot_icons/2019-03-01/565595869687_48.png",
294-
}
295-
296-
def send_logo_notification(
297-
self, logo: LogoAnnotation, probs: dict[LogoLabelType, float]
298-
):
299-
crop_url = logo.get_crop_image_url()
300-
prob_text = "\n".join(
301-
(
302-
f"{label[0]} - {label[1]}: {prob:.2g}"
303-
for label, prob in sorted(
304-
probs.items(), key=operator.itemgetter(1), reverse=True
305-
)
306-
)
307-
)
308-
image_model: ImageModel = logo.image_prediction.image
309-
product_id = image_model.get_product_id()
310-
base_off_url = settings.BaseURLProvider.world(product_id.server_type)
311-
text = (
312-
f"Prediction for <{crop_url}|image> "
313-
f"(<https://hunger.openfoodfacts.org/logos?logo_id={logo.id}|annotate logo>, "
314-
f"<{base_off_url}/product/{product_id.barcode}|product>):\n{prob_text}"
315-
)
316-
self._post_message(_slack_message_block(text), self.ROBOTOFF_ALERT_CHANNEL)
317-
318-
def _post_message(
319-
self,
320-
blocks: list[dict],
321-
channel: str,
322-
**kwargs,
323-
):
324-
try:
325-
params: JSONType = {
326-
**(self._get_base_params()),
327-
"channel": channel,
328-
"blocks": json.dumps(blocks),
329-
**kwargs,
330-
}
331-
332-
r = http_session.post(self.POST_MESSAGE_URL, data=params)
333-
response_json = _get_slack_json(r)
334-
return response_json
335-
except (RequestConnectionError, JSONDecodeError) as e:
336-
logger.info(
337-
"An exception occurred when sending a Slack notification", exc_info=e
338-
)
339-
except Exception as e:
340-
logger.error(
341-
"An exception occurred when sending a Slack notification", exc_info=e
342-
)
343-
344-
345-
class NoopSlackNotifier(SlackNotifier):
346-
"""NoopSlackNotifier is a NOOP SlackNotifier used in dev/local executions
347-
of Robotoff."""
348-
349-
def __init__(self):
350-
super().__init__("")
351-
352-
def _post_message(
353-
self,
354-
blocks: list[dict],
355-
channel: str,
356-
**kwargs,
357-
):
358-
"""Overrides the actual posting to Slack with logging of the args that
359-
would've been posted."""
360-
logger.info(
361-
"Alerting on slack channel '%s', with message:\n%s\nand additional args:\n%s",
362-
channel,
363-
blocks,
364-
kwargs,
365-
)
366-
367-
368-
def _get_slack_json(response: requests.Response) -> JSONType:
369-
json_data = response.json()
370-
371-
if not response.ok:
372-
raise SlackException(
373-
"Non-200 status code from Slack: "
374-
"{}, response: {}"
375-
"".format(response.status_code, json_data)
376-
)
377-
378-
if not json_data.get("ok", False):
379-
raise SlackException("Non-ok response: {}".format(json_data))
380-
381-
return json_data

robotoff/prediction/ocr/brand.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from robotoff import settings
77
from robotoff.brands import get_brand_blacklist, keep_brand_from_taxonomy
8-
from robotoff.types import Prediction, PredictionType
8+
from robotoff.types import JSONType, Prediction, PredictionType
99
from robotoff.utils import get_logger, text_file_iter
1010
from robotoff.utils.cache import function_cache_register
1111
from robotoff.utils.text import KeywordProcessor, get_tag
@@ -78,7 +78,7 @@ def extract_brands(
7878
text, span_info=True
7979
):
8080
match_str = text[span_start:span_end]
81-
data = {"text": match_str, "notify": False}
81+
data: JSONType = {"text": match_str}
8282
if (
8383
bounding_box := get_match_bounding_box(content, span_start, span_end)
8484
) is not None:
@@ -113,7 +113,6 @@ def extract_brands_google_cloud_vision(ocr_result: OCRResult) -> list[Prediction
113113
value_tag=get_tag(brand),
114114
automatic_processing=False,
115115
predictor="google-cloud-vision",
116-
data={"notify": False},
117116
confidence=logo_annotation.score,
118117
predictor_version=PREDICTOR_VERSION,
119118
)

0 commit comments

Comments
 (0)