Skip to content

Commit

Permalink
Fix two notifications sent
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaudDauce committed Feb 7, 2024
1 parent 249e026 commit 2379b7e
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 30 deletions.
3 changes: 3 additions & 0 deletions udata/core/discussions/api.py
Expand Up @@ -7,6 +7,7 @@
from udata.auth import admin_permission
from udata.api import api, API, fields
from udata.core.spam.api import SpamAPI
from udata.core.spam.fields import spam_fields
from udata.utils import id_or_404
from udata.core.user.api_fields import user_ref_fields

Expand All @@ -25,6 +26,7 @@
'posted_by': fields.Nested(user_ref_fields,
description='The message author'),
'posted_on': fields.ISODateTime(description='The message posting date'),
'spam': fields.Nested(spam_fields),
})

discussion_fields = api.model('Discussion', {
Expand All @@ -44,6 +46,7 @@
'url': fields.UrlFor('api.discussion',
description='The discussion API URI'),
'extras': fields.Raw(description='Extra attributes as key-value pairs'),
'spam': fields.Nested(spam_fields),
})

start_discussion_fields = api.model('DiscussionStart', {
Expand Down
19 changes: 13 additions & 6 deletions udata/core/spam/api.py
@@ -1,25 +1,32 @@
from udata.api import api, API
from udata.utils import id_or_404
from udata.auth import admin_permission
from udata.utils import id_or_404


class SpamAPI(API):
'''
"""
Base Spam Model API.
'''
"""
model = None

def get_model(self, id):
'''This function returns the base model and the spamable model which can be different. The base model is the model stored inside Mongo and the spamable model is the embed document (for exemple a comment inside a discussion)'''
"""
This function returns the base model and the spamable model which can be different. The base model is the
model stored inside Mongo and the spamable model is the embed document (for example a comment inside a
discussion)
"""
model = self.model.objects.get_or_404(id=id_or_404(id))
return model, model

@api.secure(admin_permission)
def delete(self, **kwargs):
'''Mark a potentiel spam as no spam'''
"""
Mark a potential spam as no spam
"""
base_model, model = self.get_model(**kwargs)

if not model.is_spam():
return {}, 200

model.mark_as_no_spam(base_model)
return {}, 200
60 changes: 39 additions & 21 deletions udata/core/spam/models.py
Expand Up @@ -8,14 +8,19 @@
POTENTIAL_SPAM = 'potential_spam'
NO_SPAM = 'no_spam'

SPAM_STATUS_CHOICES = [NOT_CHECKED, POTENTIAL_SPAM, NO_SPAM]


class SpamInfo(db.EmbeddedDocument):
status = db.StringField(choices=[NOT_CHECKED, POTENTIAL_SPAM, NO_SPAM], default=NOT_CHECKED)
status = db.StringField(choices=SPAM_STATUS_CHOICES, default=NOT_CHECKED)
callbacks = db.DictField(default={})


class SpamMixin(object):
spam = db.EmbeddedDocumentField(SpamInfo)

attributes_before = None
detect_spam_enabled: bool = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -27,24 +32,35 @@ def __init__(self, *args, **kwargs):
@staticmethod
def spam_words():
return current_app.config.get('SPAM_WORDS', [])

@staticmethod
def allowed_langs():
return current_app.config.get('SPAM_ALLOWED_LANGS', [])

def clean(self):
super().clean()

# We do not want to check embeded document here, they will be checked
# We do not want to check embedded document here, they will be checked
# during the clean of their parents.
if isinstance(self, db.Document):
self.detect_spam()

def detect_spam(self, breadcrumb = None):
def save_without_spam_detection(self):
"""
Allow to save a model without doing the spam detection (useful when saving the callbacks for exemple)
"""
self.detect_spam_enabled = False
self.save()
self.detect_spam_enabled = True

def detect_spam(self, breadcrumb=None):
"""
This is the main function doing the spam detection.
This function set a flag POTENTIAL_SPAM if a model is suspicious.
"""
if not self.detect_spam_enabled:
return

# During initialisation some models can have no spam associated
if not self.spam:
self.spam = SpamInfo(status=NOT_CHECKED, callbacks={})
Expand All @@ -60,10 +76,10 @@ def detect_spam(self, breadcrumb = None):
if not text:
continue

# We do not want to re-run the spam detection if the texts didn't changed from the initialisation. If we don't do this,
# a potential spam marked as no spam will be reflag as soon as we make change in the model (for exemple to set the spam status,
# or to add a new message).
# If the model is new, the texts haven't changed since the init but we still want to do the spam check.
# We do not want to re-run the spam detection if the texts didn't change from the initialisation. If we
# don't do this, a potential spam marked as no spam will be re-flag as soon as we make change in the model
# (for example to set the spam status, or to add a new message). If the model is new, the texts haven't
# changed since the init, but we still want to do the spam check.
if before == text and not self.is_new():
continue

Expand All @@ -82,7 +98,8 @@ def detect_spam(self, breadcrumb = None):
return

for embed in self.embeds_to_check_for_spam():
# We need to copy to avoid adding multiple time (in each loop iteration) a new element to the shared breadcrumb list
# We need to copy to avoid adding multiple time (in each loop iteration) a new element to the shared
# breadcrumb list
embed.detect_spam(breadcrumb.copy())

def is_new(self):
Expand Down Expand Up @@ -112,24 +129,26 @@ def mark_as_no_spam(self, base_model):

def is_spam(self):
return self.spam and self.spam.status == POTENTIAL_SPAM

def texts_to_check_for_spam(self):
raise NotImplementedError("Please implement the `texts_to_check_for_spam` method. Should return a list of strings to check.")
raise NotImplementedError(
"Please implement the `texts_to_check_for_spam` method. Should return a list of strings to check.")

def embeds_to_check_for_spam(self):
return []

def spam_report_title(self):
return type(self).__name__

def spam_report_link(self):
return None

def _report(self, text, breadcrumb, reason):
# Note that all the chain should be a SpamMixin, maybe we could filter out if it's not the case here…
title = " → ".join(map(lambda o: o.spam_report_title(), breadcrumb))

# Select the first link in the embed list (for exemple message doesn't have a link, so check if discussion have one)
# Select the first link in the embed list (for example message doesn't have a link, so check if discussion
# have one)
for object in reversed(breadcrumb):
link = object.spam_report_link()
if link:
Expand All @@ -147,6 +166,7 @@ def spam_protected(get_model_to_check=None):
on an embed document. The class method should always take a `self` as a first argument which is the base
model to allow saving the callbacks back into Mongo (we cannot .save() an embed document).
"""

def decorator(f):
def protected_function(*args, **kwargs):
base_model = args[0]
Expand All @@ -156,20 +176,18 @@ def protected_function(*args, **kwargs):
model_to_check = base_model

if not isinstance(model_to_check, SpamMixin):
raise ValueError("@spam_protected should be called within a SpamMixin. " + type(model_to_check).__name__ + " given.")
raise ValueError(
"@spam_protected should be called within a SpamMixin. " + type(model_to_check).__name__ + " given.")

if model_to_check.is_spam():
model_to_check.spam.callbacks[f.__name__] = {
'args': args[1:],
'kwargs': kwargs
}
# Here we call save() on the base model because we cannot save an embed document.
# `save()` call `clean()` so we recall `detect_spam()`, but there is a check inside `detect_spam()` to not do nothing
# if we didn't change the texts to check.
base_model.save()
base_model.save_without_spam_detection()
else:
f(*args, **kwargs)

return protected_function
return decorator

return decorator
2 changes: 1 addition & 1 deletion udata/core/spam/signals.py
Expand Up @@ -2,5 +2,5 @@

namespace = Namespace()

#: Trigerred when a spam is detected
#: Triggered when a spam is detected
on_new_potential_spam = namespace.signal('on-new-potential-spam')
4 changes: 2 additions & 2 deletions udata/notifications/mattermost.py
@@ -1,8 +1,8 @@
import requests
from udata.tasks import connect
from udata.core.spam.signals import on_new_potential_spam
from flask import current_app


@on_new_potential_spam.connect
def notify_potential_spam(sender, **kwargs):
title = kwargs.get('title', 'no title')
Expand Down Expand Up @@ -38,4 +38,4 @@ def send_message(text: str):
data = {'text': text}

r = requests.post(webhook, json=data)
r.raise_for_status()
r.raise_for_status()

0 comments on commit 2379b7e

Please sign in to comment.