aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site/apps/api')
-rw-r--r--pydis_site/apps/api/models/bot/metricity.py53
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py30
-rw-r--r--pydis_site/apps/api/tests/test_users.py16
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py16
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py7
5 files changed, 111 insertions, 11 deletions
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 25b42fa2..cae630f1 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -1,5 +1,12 @@
from django.db import connections
+BLOCK_INTERVAL = 10 * 60 # 10 minute blocks
+
+EXCLUDE_CHANNELS = [
+ "267659945086812160", # Bot commands
+ "607247579608121354" # SeasonalBot commands
+]
+
class NotFound(Exception):
"""Raised when an entity cannot be found."""
@@ -21,7 +28,8 @@ class Metricity:
def user(self, user_id: str) -> dict:
"""Query a user's data."""
- columns = ["verified_at"]
+ # TODO: Swap this back to some sort of verified at date
+ columns = ["joined_at"]
query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'"
self.cursor.execute(query, [user_id])
values = self.cursor.fetchone()
@@ -33,7 +41,48 @@ class Metricity:
def total_messages(self, user_id: str) -> int:
"""Query total number of messages for a user."""
- self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id])
+ self.cursor.execute(
+ """
+ SELECT
+ COUNT(*)
+ FROM messages
+ WHERE
+ author_id = '%s'
+ AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
+ """,
+ [user_id, EXCLUDE_CHANNELS]
+ )
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return values[0]
+
+ def total_message_blocks(self, user_id: str) -> int:
+ """
+ Query number of 10 minute blocks during which the user has been active.
+
+ This metric prevents users from spamming to achieve the message total threshold.
+ """
+ self.cursor.execute(
+ """
+ SELECT
+ COUNT(*)
+ FROM (
+ SELECT
+ (floor((extract('epoch' from created_at) / %s )) * %s) AS interval
+ FROM messages
+ WHERE
+ author_id='%s'
+ AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
+ GROUP BY interval
+ ) block_query;
+ """,
+ [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS]
+ )
values = self.cursor.fetchone()
if not values:
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index 93ef8171..82b497aa 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -512,6 +512,36 @@ class CreationTests(APISubdomainTestCase):
)
+class InfractionDeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = User.objects.create(
+ id=9876,
+ name='Unknown user',
+ discriminator=9876,
+ )
+
+ cls.warning = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='warning',
+ active=False
+ )
+
+ def test_delete_unknown_infraction_returns_404(self):
+ url = reverse('bot:infraction-detail', args=('something',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_delete_known_infraction_returns_204(self):
+ url = reverse('bot:infraction-detail', args=(self.warning.id,), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 204)
+ self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id)
+
+
class ExpandedTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 72ffcb3c..69bbfefc 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -407,9 +407,10 @@ class UserMetricityTests(APISubdomainTestCase):
def test_get_metricity_data(self):
# Given
- verified_at = "foo"
+ joined_at = "foo"
total_messages = 1
- self.mock_metricity_user(verified_at, total_messages)
+ total_blocks = 1
+ self.mock_metricity_user(joined_at, total_messages, total_blocks)
# When
url = reverse('bot:user-metricity-data', args=[0], host='api')
@@ -418,9 +419,10 @@ class UserMetricityTests(APISubdomainTestCase):
# Then
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
- "verified_at": verified_at,
+ "joined_at": joined_at,
"total_messages": total_messages,
"voice_banned": False,
+ "activity_blocks": total_blocks
})
def test_no_metricity_user(self):
@@ -440,7 +442,7 @@ class UserMetricityTests(APISubdomainTestCase):
{'exception': ObjectDoesNotExist, 'voice_banned': False},
]
- self.mock_metricity_user("foo", 1)
+ self.mock_metricity_user("foo", 1, 1)
for case in cases:
with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']):
@@ -453,13 +455,14 @@ class UserMetricityTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
- def mock_metricity_user(self, verified_at, total_messages):
+ def mock_metricity_user(self, joined_at, total_messages, total_blocks):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
self.addCleanup(patcher.stop)
self.metricity = self.metricity.return_value.__enter__.return_value
- self.metricity.user.return_value = dict(verified_at=verified_at)
+ self.metricity.user.return_value = dict(joined_at=joined_at)
self.metricity.total_messages.return_value = total_messages
+ self.metricity.total_message_blocks.return_value = total_blocks
def mock_no_metricity_user(self):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
@@ -468,3 +471,4 @@ class UserMetricityTests(APISubdomainTestCase):
self.metricity = self.metricity.return_value.__enter__.return_value
self.metricity.user.side_effect = NotFound()
self.metricity.total_messages.side_effect = NotFound()
+ self.metricity.total_message_blocks.side_effect = NotFound()
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index edec0a1e..423e806e 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -5,6 +5,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.mixins import (
CreateModelMixin,
+ DestroyModelMixin,
ListModelMixin,
RetrieveModelMixin
)
@@ -18,7 +19,13 @@ from pydis_site.apps.api.serializers import (
)
-class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet):
+class InfractionViewSet(
+ CreateModelMixin,
+ RetrieveModelMixin,
+ ListModelMixin,
+ GenericViewSet,
+ DestroyModelMixin
+):
"""
View providing CRUD operations on infractions for Discord users.
@@ -108,6 +115,13 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
- 400: if a field in the request body is invalid or disallowed
- 404: if an infraction with the given `id` could not be found
+ ### DELETE /bot/infractions/<id:int>
+ Delete the infraction with the given `id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a infraction with the given `id` does not exist
+
### Expanded routes
All routes support expansion of `user` and `actor` in responses. To use an expanded route,
append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`.
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 5205dc97..829e2694 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -109,8 +109,10 @@ class UserViewSet(ModelViewSet):
#### Response format
>>> {
- ... "verified_at": "2020-10-06T21:54:23.540766",
- ... "total_messages": 2
+ ... "joined_at": "2020-10-06T21:54:23.540766",
+ ... "total_messages": 2,
+ ... "voice_banned": False,
+ ... "activity_blocks": 1
...}
#### Status codes
@@ -255,6 +257,7 @@ class UserViewSet(ModelViewSet):
data = metricity.user(user.id)
data["total_messages"] = metricity.total_messages(user.id)
data["voice_banned"] = voice_banned
+ data["activity_blocks"] = metricity.total_message_blocks(user.id)
return Response(data, status=status.HTTP_200_OK)
except NotFound:
return Response(dict(detail="User not found in metricity"),