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-
81from 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
183from robotoff .utils import get_logger , http_session
194
205logger = get_logger (__name__ )
216
227
23- class SlackException (Exception ):
24- pass
25-
26-
278class 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
5324class 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-
12560class 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
15986class 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\n and 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
0 commit comments