mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Earlier, we were immediately enqueueing event in 'do_remove_alert_words' which can lead to a situation, if any db operation is added after enqueueing event in future, where the action function fails at a later stage. Events should not be sent until we know we're not rolling back. Fixes part of #30489.
		
			
				
	
	
		
			83 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			83 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections.abc import Iterable
 | 
						|
 | 
						|
import ahocorasick
 | 
						|
from django.db import transaction
 | 
						|
 | 
						|
from zerver.lib.cache import (
 | 
						|
    cache_with_key,
 | 
						|
    realm_alert_words_automaton_cache_key,
 | 
						|
    realm_alert_words_cache_key,
 | 
						|
)
 | 
						|
from zerver.models import AlertWord, Realm, UserProfile
 | 
						|
from zerver.models.alert_words import flush_realm_alert_words
 | 
						|
 | 
						|
 | 
						|
@cache_with_key(lambda realm: realm_alert_words_cache_key(realm.id), timeout=3600 * 24)
 | 
						|
def alert_words_in_realm(realm: Realm) -> dict[int, list[str]]:
 | 
						|
    user_ids_and_words = AlertWord.objects.filter(realm=realm, user_profile__is_active=True).values(
 | 
						|
        "user_profile_id", "word"
 | 
						|
    )
 | 
						|
    user_ids_with_words: dict[int, list[str]] = {}
 | 
						|
    for id_and_word in user_ids_and_words:
 | 
						|
        user_ids_with_words.setdefault(id_and_word["user_profile_id"], [])
 | 
						|
        user_ids_with_words[id_and_word["user_profile_id"]].append(id_and_word["word"])
 | 
						|
    return user_ids_with_words
 | 
						|
 | 
						|
 | 
						|
@cache_with_key(lambda realm: realm_alert_words_automaton_cache_key(realm.id), timeout=3600 * 24)
 | 
						|
def get_alert_word_automaton(realm: Realm) -> ahocorasick.Automaton:
 | 
						|
    user_id_with_words = alert_words_in_realm(realm)
 | 
						|
    alert_word_automaton = ahocorasick.Automaton()
 | 
						|
    for user_id, alert_words in user_id_with_words.items():
 | 
						|
        for alert_word in alert_words:
 | 
						|
            alert_word_lower = alert_word.lower()
 | 
						|
            if alert_word_automaton.exists(alert_word_lower):
 | 
						|
                (key, user_ids_for_alert_word) = alert_word_automaton.get(alert_word_lower)
 | 
						|
                user_ids_for_alert_word.add(user_id)
 | 
						|
            else:
 | 
						|
                alert_word_automaton.add_word(alert_word_lower, (alert_word_lower, {user_id}))
 | 
						|
    alert_word_automaton.make_automaton()
 | 
						|
    # If the kind is not AHOCORASICK after calling make_automaton, it means there is no key present
 | 
						|
    # and hence we cannot call items on the automaton yet. To avoid it we return None for such cases
 | 
						|
    # where there is no alert-words in the realm.
 | 
						|
    # https://pyahocorasick.readthedocs.io/en/latest/#make-automaton
 | 
						|
    if alert_word_automaton.kind != ahocorasick.AHOCORASICK:
 | 
						|
        return None
 | 
						|
    return alert_word_automaton
 | 
						|
 | 
						|
 | 
						|
def user_alert_words(user_profile: UserProfile) -> list[str]:
 | 
						|
    return list(AlertWord.objects.filter(user_profile=user_profile).values_list("word", flat=True))
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def add_user_alert_words(user_profile: UserProfile, new_words: Iterable[str]) -> list[str]:
 | 
						|
    existing_words_lower = {word.lower() for word in user_alert_words(user_profile)}
 | 
						|
 | 
						|
    # Keeping the case, use a dictionary to get the set of
 | 
						|
    # case-insensitive distinct, new alert words
 | 
						|
    word_dict: dict[str, str] = {}
 | 
						|
    for word in new_words:
 | 
						|
        if word.lower() in existing_words_lower:
 | 
						|
            continue
 | 
						|
        word_dict[word.lower()] = word
 | 
						|
 | 
						|
    AlertWord.objects.bulk_create(
 | 
						|
        AlertWord(user_profile=user_profile, word=word, realm=user_profile.realm)
 | 
						|
        for word in word_dict.values()
 | 
						|
    )
 | 
						|
    # Django bulk_create operations don't flush caches, so we need to do this ourselves.
 | 
						|
    flush_realm_alert_words(user_profile.realm_id)
 | 
						|
 | 
						|
    return user_alert_words(user_profile)
 | 
						|
 | 
						|
 | 
						|
@transaction.atomic(savepoint=False)
 | 
						|
def remove_user_alert_words(user_profile: UserProfile, delete_words: Iterable[str]) -> list[str]:
 | 
						|
    # TODO: Ideally, this would be a bulk query, but Django doesn't have a `__iexact`.
 | 
						|
    # We can clean this up if/when PostgreSQL has more native support for case-insensitive fields.
 | 
						|
    # If we turn this into a bulk operation, we will need to call flush_realm_alert_words() here.
 | 
						|
    for delete_word in delete_words:
 | 
						|
        AlertWord.objects.filter(user_profile=user_profile, word__iexact=delete_word).delete()
 | 
						|
    return user_alert_words(user_profile)
 |