Skip to content

Commit

Permalink
add bot handling
Browse files Browse the repository at this point in the history
  • Loading branch information
escattone committed Jan 11, 2025
1 parent 452d3bc commit 45f333c
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 10 deletions.
1 change: 1 addition & 0 deletions kitsune/community/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def top_contributors_l10n(

users = (
User.objects.filter(created_revisions__in=revisions, is_active=True)
.exclude(profile__is_bot=True)
.annotate(query_count=Count("created_revisions"))
.order_by(F("query_count").desc(nulls_last=True))
.select_related("profile")
Expand Down
2 changes: 1 addition & 1 deletion kitsune/l10n/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def get_l10n_bot():
username="sumo-l10n-bot", defaults=dict(email="sumodev@mozilla.com")
)
if created:
Profile.objects.create(user=user, name="SUMO Localization Bot")
Profile.objects.create(user=user, name="SUMO Localization Bot", is_bot=True)
return user


Expand Down
10 changes: 8 additions & 2 deletions kitsune/sumo/form_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ def to_python(self, value):
to_objects = {}
for key, value in key_value_pairs:
# check if the value is a valid username in the database
if key.lower() == "user" and not User.objects.filter(username=value).exists():
raise ValidationError(_("{name} is not a valid username.").format(name=value))
if key.lower() == "user":
if not User.objects.filter(username=value).exists():
raise ValidationError(_(f"{value} is not a valid username."))
if User.objects.filter(username=value, profile__is_bot=True).exists():
raise ValidationError(
_(f"{value} is a bot. You cannot send messages to bots.")
)

to_objects.setdefault(f"{key.lower()}s", []).append(value)

return to_objects
Expand Down
9 changes: 7 additions & 2 deletions kitsune/sumo/static/sumo/scss/layout/_contributors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
width: 100%;
max-width: none;
margin: 0 0 p.$spacing-md 0;

&[type="checkbox"] {
width: auto;
margin-right: p.$spacing-md;
margin-left: p.$spacing-xs;
}
}

img {
Expand All @@ -86,7 +92,6 @@
select,
input {
margin: 0 10px;

}
}
}
Expand Down Expand Up @@ -263,4 +268,4 @@ ol.headings-list {
}
}
}
}
}
3 changes: 2 additions & 1 deletion kitsune/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ProfileAdmin(admin.ModelAdmin):
"fxa_refresh_token",
"zendesk_id",
"fxa_avatar",
"is_bot",
],
},
),
Expand Down Expand Up @@ -79,7 +80,7 @@ class ProfileAdmin(admin.ModelAdmin):
list_filter = ["is_fxa_migrated", "country"]
search_fields = ["user__username", "user__email", "name", "fxa_uid"]
autocomplete_fields = ["user"]
readonly_fields = ["fxa_refresh_token", "zendesk_id"]
readonly_fields = ["fxa_refresh_token", "zendesk_id", "is_bot"]

def get_products(self, obj):
"""Get a list of products that a user is subscribed to."""
Expand Down
1 change: 1 addition & 0 deletions kitsune/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def usernames(request):

users = (
User.objects.filter(is_active=True)
.exclude(profile__is_bot=True)
.exclude(profile__is_fxa_migrated=False)
.filter(Q(username__istartswith=pre) | Q(profile__name__istartswith=pre))
.select_related("profile")
Expand Down
6 changes: 4 additions & 2 deletions kitsune/users/jinja2/users/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ <h1 class="sumo-page-heading">{{ _('Your Account') }}</h1>
<hr class="section-break"/>
{% endif %}

{% if request.user.is_authenticated and request.user != profile.user %}
{% if not profile.is_bot and request.user.is_authenticated and (request.user != profile.user) %}
{% if user.has_perm('users.change_profile') %}
<div id="admin-actions">
<a class="edit" rel="nofollow" href="{{ url('users.edit_profile', profile.user.username) }}">
Expand Down Expand Up @@ -114,7 +114,7 @@ <h2 class="sumo-page-subheading location">
</section>


{% if user.id != profile.user.id and user.has_perm('users.deactivate_users') %}
{% if not profile.is_bot and (user.id != profile.user.id) and user.has_perm('users.deactivate_users') %}
{% if profile.user.is_active %}
<form class="deactivate" method="post" action="{{ url('users.deactivate') }}">
{% csrf_token %}
Expand Down Expand Up @@ -195,6 +195,7 @@ <h2 class="sumo-page-subheading mt-lg">{{ _('Badges') }}</h2>
</section>
{% endif %}

{% if not profile.is_bot %}
<div class="pm-or-signout sumo-button-wrap extra-pad-top">
{% if profile.user == user %}
<form id="sign-out" action="{{ url('users.logout') }}" method="post">
Expand All @@ -212,5 +213,6 @@ <h2 class="sumo-page-subheading mt-lg">{{ _('Badges') }}</h2>
{{ private_message(profile.user) }}
{% endif %}
</div>
{% endif %}
</article>
{% endblock %}
18 changes: 18 additions & 0 deletions kitsune/users/migrations/0030_profile_is_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-18 14:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0029_remove_profile_avatar"),
]

operations = [
migrations.AddField(
model_name="profile",
name="is_bot",
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions kitsune/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Profile(ModelBase):
user = models.OneToOneField(
User, on_delete=models.CASCADE, primary_key=True, verbose_name=_lazy("User")
)
is_bot = models.BooleanField(default=False)
name = models.CharField(
max_length=255, null=True, blank=True, verbose_name=_lazy("Display name")
)
Expand Down
7 changes: 6 additions & 1 deletion kitsune/users/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from kitsune.messages.models import InboxMessage, OutboxMessage
from kitsune.sumo import email_utils
from kitsune.tidings.models import Watch
from kitsune.users.models import ContributionAreas, Deactivation, Setting
from kitsune.users.models import ContributionAreas, Deactivation, Profile, Setting

log = logging.getLogger("k.users")

Expand Down Expand Up @@ -159,3 +159,8 @@ def user_is_contributor(user):
user.is_authenticated
and user.groups.filter(name__in=ContributionAreas.get_groups()).exists()
)


def user_is_bot(user):
"""Return whether the user is a bot."""
return Profile.objects.filter(user=user, is_bot=True).exists()
33 changes: 32 additions & 1 deletion kitsune/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
anonymize_user,
deactivate_user,
get_oidc_fxa_setting,
user_is_bot,
)
from kitsune.wiki.models import user_documents, user_redirects

Expand Down Expand Up @@ -146,6 +147,10 @@ def profile(request, username):
@login_required
@require_POST
def close_account(request):
# Forbid this for bots.
if user_is_bot(request.user):
return HttpResponseForbidden()

anonymize_user(request.user)

# Log the user out
Expand All @@ -158,6 +163,11 @@ def close_account(request):
@permission_required("users.deactivate_users")
def deactivate(request, mark_spam=False):
user = get_object_or_404(User, id=request.POST["user_id"], is_active=True)

# Forbid this for bots.
if user_is_bot(user):
return HttpResponseForbidden()

deactivate_user(user, request.user)

if mark_spam:
Expand Down Expand Up @@ -246,6 +256,10 @@ def documents_contributed(request, username):
@require_http_methods(["GET", "POST"])
def edit_settings(request):
"""Edit user settings"""
# Forbid this for bots.
if user_is_bot(request.user):
return HttpResponseForbidden()

template = "users/edit_settings.html"
if request.method == "POST":
settings_form = SettingsForm(request.POST)
Expand Down Expand Up @@ -275,7 +289,11 @@ def edit_settings(request):
@login_required
@require_http_methods(["GET", "POST"])
def edit_contribution_area(request):
"""Edit user settings"""
"""Edit the user's contribution area."""
# Forbid this for bots.
if user_is_bot(request.user):
return HttpResponseForbidden()

template = "users/edit_contributions.html"
contribution_form = ContributionAreaForm(request.POST or None, request=request)

Expand All @@ -290,6 +308,10 @@ def edit_contribution_area(request):
@require_http_methods(["GET", "POST"])
def edit_watch_list(request):
"""Edit watch list"""
# Forbid this for bots.
if user_is_bot(request.user):
return HttpResponseForbidden()

watches = Watch.objects.filter(user=request.user).order_by("content_type")

watch_list = []
Expand Down Expand Up @@ -328,6 +350,7 @@ def edit_profile(request, username=None):
# Make sure the auth'd user has permission:
if not request.user.has_perm("users.change_profile"):
return HttpResponseForbidden()

if not user:
user = request.user

Expand All @@ -338,6 +361,10 @@ def edit_profile(request, username=None):
# a profile. We can remove this fallback.
user_profile = Profile.objects.create(user=user)

# Forbid this for bots.
if user_profile.is_bot:
return HttpResponseForbidden()

profile_form = ProfileForm(request.POST or None, request.FILES or None, instance=user_profile)
user_form = UserForm(request.POST or None, instance=user_profile.user)

Expand Down Expand Up @@ -371,6 +398,10 @@ def edit_profile(request, username=None):
@require_http_methods(["POST"])
def make_contributor(request):
"""Adds the logged in user to the contributor group"""
# Forbid this for bots.
if user_is_bot(request.user):
return HttpResponseForbidden()

add_to_contributors(request.user, request.LANGUAGE_CODE)

if "return_to" in request.POST:
Expand Down
1 change: 1 addition & 0 deletions kitsune/wiki/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,5 +426,6 @@ class RevisionFilterForm(forms.Form):

locale = forms.ChoiceField(label=_lazy("Locale:"), choices=languages, required=False)
users = MultiUsernameField(label=_lazy("Users:"), required=False)
include_bots = forms.BooleanField(label=_lazy("Bots:"), required=False)
start = forms.DateField(label=_lazy("Start:"), required=False)
end = forms.DateField(label=_lazy("End:"), required=False)
6 changes: 6 additions & 0 deletions kitsune/wiki/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,9 @@ def _show_revision_warning(document, revision):
def recent_revisions(request):
request.GET = request.GET.copy()
fragment = request.GET.pop("fragment", None)
if not fragment:
request.GET.setdefault("include_bots", "on")

form = RevisionFilterForm(request.GET)

# Validate the form to populate cleaned_data, even with invalid usernames.
Expand All @@ -1706,6 +1709,9 @@ def recent_revisions(request):
if form.cleaned_data.get("users"):
filters.update(creator__in=form.cleaned_data["users"])

if not form.cleaned_data.get("include_bots"):
filters.update(creator__profile__is_bot=False)

start = form.cleaned_data.get("start")
end = form.cleaned_data.get("end")
if start or end:
Expand Down

0 comments on commit 45f333c

Please sign in to comment.