mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-13 14:30:17 +02:00
Use Django's cache framework for service group names (#857)
Use django-redis-cache backend for locking get_or_set No longer require group-related tasks to be locked to one simultaneous execution. Remove legacy service group cache models. Truncate Discord nicknames to 32 characters Correct Discourse group name extension using only valid leading characters. Prevent name slicing from ending with illegal character Closes #801 Closes #847 Closes #835 Closes #852
This commit is contained in:
parent
27c9b09116
commit
8987cf2199
@ -211,10 +211,10 @@ MESSAGE_TAGS = {
|
|||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"BACKEND": "redis_cache.RedisCache",
|
||||||
"LOCATION": "redis://127.0.0.1:6379/1",
|
"LOCATION": "localhost:6379",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
"DB": 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ django>=1.10,<2.0
|
|||||||
django-bootstrap-form
|
django-bootstrap-form
|
||||||
django-navhelper
|
django-navhelper
|
||||||
django-bootstrap-pagination
|
django-bootstrap-pagination
|
||||||
django-redis>=4.4
|
django-redis-cache>=1.7.1
|
||||||
django-recaptcha
|
django-recaptcha
|
||||||
django-celery-beat
|
django-celery-beat
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
from django.contrib import admin
|
|
||||||
from services.models import GroupCache
|
|
||||||
|
|
||||||
admin.site.register(GroupCache)
|
|
18
services/migrations/0003_delete_groupcache.py
Normal file
18
services/migrations/0003_delete_groupcache.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-09-02 06:07
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('services', '0002_auto_20161016_0135'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='GroupCache',
|
||||||
|
),
|
||||||
|
]
|
@ -1,18 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class GroupCache(models.Model):
|
|
||||||
SERVICE_CHOICES = (
|
|
||||||
("discourse", "discourse"),
|
|
||||||
("discord", "discord"),
|
|
||||||
)
|
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
groups = models.TextField(default={})
|
|
||||||
service = models.CharField(max_length=254, choices=SERVICE_CHOICES, unique=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.service
|
|
@ -3,14 +3,13 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from services.models import GroupCache
|
|
||||||
from requests_oauthlib import OAuth2Session
|
from requests_oauthlib import OAuth2Session
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
from django.utils import timezone
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -20,9 +19,13 @@ EVE_IMAGE_SERVER = "https://image.eveonline.com"
|
|||||||
AUTH_URL = "https://discordapp.com/api/oauth2/authorize"
|
AUTH_URL = "https://discordapp.com/api/oauth2/authorize"
|
||||||
TOKEN_URL = "https://discordapp.com/api/oauth2/token"
|
TOKEN_URL = "https://discordapp.com/api/oauth2/token"
|
||||||
|
|
||||||
# needs administrator, since Discord can't get their permissions system to work
|
"""
|
||||||
# was kick members, manage roles, manage nicknames
|
Previously all we asked for was permission to kick members, manage roles, and manage nicknames.
|
||||||
#BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
|
Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
|
||||||
|
It's almost fixed the problem.
|
||||||
|
"""
|
||||||
|
# kick members, manage roles, manage nicknames
|
||||||
|
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
|
||||||
BOT_PERMISSIONS = 0x00000008
|
BOT_PERMISSIONS = 0x00000008
|
||||||
|
|
||||||
# get user ID, accept invite
|
# get user ID, accept invite
|
||||||
@ -31,7 +34,7 @@ SCOPES = [
|
|||||||
'guilds.join',
|
'guilds.join',
|
||||||
]
|
]
|
||||||
|
|
||||||
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30)
|
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default
|
||||||
|
|
||||||
|
|
||||||
class DiscordApiException(Exception):
|
class DiscordApiException(Exception):
|
||||||
@ -150,10 +153,14 @@ class DiscordOAuthManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_name(name):
|
||||||
|
return re.sub('[^\w.-]', '', name)[:32]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_groupname(name):
|
def _sanitize_groupname(name):
|
||||||
name = name.strip(' _')
|
name = name.strip(' _')
|
||||||
return re.sub('[^\w.-]', '', name)
|
return DiscordOAuthManager._sanitize_name(name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_bot_add_url():
|
def generate_bot_add_url():
|
||||||
@ -198,8 +205,9 @@ class DiscordOAuthManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def update_nickname(user_id, nickname):
|
def update_nickname(user_id, nickname):
|
||||||
try:
|
try:
|
||||||
|
nickname = DiscordOAuthManager._sanitize_name(nickname)
|
||||||
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||||
data = {'nick': nickname, }
|
data = {'nick': nickname}
|
||||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
||||||
r = requests.patch(path, headers=custom_headers, json=data)
|
r = requests.patch(path, headers=custom_headers, json=data)
|
||||||
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
|
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
|
||||||
@ -230,7 +238,7 @@ class DiscordOAuthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_groups():
|
def _get_groups():
|
||||||
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
|
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
|
||||||
r = requests.get(path, headers=custom_headers)
|
r = requests.get(path, headers=custom_headers)
|
||||||
@ -239,41 +247,20 @@ class DiscordOAuthManager:
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __update_group_cache():
|
def _generate_cache_role_key(name):
|
||||||
GroupCache.objects.filter(service="discord").delete()
|
return 'DISCORD_ROLE_NAME__%s' % md5(str(name).encode('utf-8')).hexdigest()
|
||||||
cache = GroupCache.objects.create(service="discord")
|
|
||||||
cache.groups = json.dumps(DiscordOAuthManager.__get_groups())
|
|
||||||
cache.save()
|
|
||||||
return cache
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_group_cache():
|
def _group_name_to_id(name):
|
||||||
if not GroupCache.objects.filter(service="discord").exists():
|
name = DiscordOAuthManager._sanitize_groupname(name)
|
||||||
DiscordOAuthManager.__update_group_cache()
|
|
||||||
cache = GroupCache.objects.get(service="discord")
|
|
||||||
age = timezone.now() - cache.created
|
|
||||||
if age > GROUP_CACHE_MAX_AGE:
|
|
||||||
logger.debug("Group cache has expired. Triggering update.")
|
|
||||||
cache = DiscordOAuthManager.__update_group_cache()
|
|
||||||
return json.loads(cache.groups)
|
|
||||||
|
|
||||||
@staticmethod
|
def get_or_make_role():
|
||||||
def __group_name_to_id(name):
|
groups = DiscordOAuthManager._get_groups()
|
||||||
cache = DiscordOAuthManager.__get_group_cache()
|
for g in groups:
|
||||||
for g in cache:
|
if g['name'] == name:
|
||||||
if g['name'] == name:
|
return g['id']
|
||||||
return g['id']
|
return DiscordOAuthManager._create_group(name)['id']
|
||||||
logger.debug("Group %s not found on Discord. Creating" % name)
|
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
|
||||||
DiscordOAuthManager.__create_group(name)
|
|
||||||
return DiscordOAuthManager.__group_name_to_id(name)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __group_id_to_name(id):
|
|
||||||
cache = DiscordOAuthManager.__get_group_cache()
|
|
||||||
for g in cache:
|
|
||||||
if g['id'] == id:
|
|
||||||
return g['name']
|
|
||||||
raise KeyError("Group ID %s not found on Discord" % id)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __generate_role():
|
def __generate_role():
|
||||||
@ -300,16 +287,15 @@ class DiscordOAuthManager:
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __create_group(name):
|
def _create_group(name):
|
||||||
role = DiscordOAuthManager.__generate_role()
|
role = DiscordOAuthManager.__generate_role()
|
||||||
DiscordOAuthManager.__edit_role(role['id'], name)
|
return DiscordOAuthManager.__edit_role(role['id'], name)
|
||||||
DiscordOAuthManager.__update_group_cache()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@api_backoff
|
@api_backoff
|
||||||
def update_groups(user_id, groups):
|
def update_groups(user_id, groups):
|
||||||
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||||
group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
|
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
|
||||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
||||||
data = {'roles': group_ids}
|
data = {'roles': group_ids}
|
||||||
r = requests.patch(path, headers=custom_headers, json=data)
|
r = requests.patch(path, headers=custom_headers, json=data)
|
||||||
|
@ -10,7 +10,6 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from eveonline.managers import EveManager
|
from eveonline.managers import EveManager
|
||||||
from notifications import notify
|
from notifications import notify
|
||||||
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
|
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
|
||||||
from services.tasks import only_one
|
|
||||||
from .models import DiscordUser
|
from .models import DiscordUser
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -351,7 +351,7 @@ class DiscordManagerTestCase(TestCase):
|
|||||||
# Assert
|
# Assert
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
|
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
|
||||||
@requests_mock.Mocker()
|
@requests_mock.Mocker()
|
||||||
def test_update_groups(self, group_cache, m):
|
def test_update_groups(self, group_cache, m):
|
||||||
from . import manager
|
from . import manager
|
||||||
@ -385,7 +385,7 @@ class DiscordManagerTestCase(TestCase):
|
|||||||
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
|
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.manager.cache')
|
@mock.patch(MODULE_PATH + '.manager.cache')
|
||||||
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
|
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
|
||||||
@requests_mock.Mocker()
|
@requests_mock.Mocker()
|
||||||
def test_update_groups_backoff(self, group_cache, djcache, m):
|
def test_update_groups_backoff(self, group_cache, djcache, m):
|
||||||
from . import manager
|
from . import manager
|
||||||
@ -420,7 +420,7 @@ class DiscordManagerTestCase(TestCase):
|
|||||||
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
|
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.manager.cache')
|
@mock.patch(MODULE_PATH + '.manager.cache')
|
||||||
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
|
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
|
||||||
@requests_mock.Mocker()
|
@requests_mock.Mocker()
|
||||||
def test_update_groups_global_backoff(self, group_cache, djcache, m):
|
def test_update_groups_global_backoff(self, group_cache, djcache, m):
|
||||||
from . import manager
|
from . import manager
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.core.cache import cache
|
||||||
from services.models import GroupCache
|
from hashlib import md5
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours
|
||||||
|
|
||||||
|
|
||||||
class DiscourseError(Exception):
|
class DiscourseError(Exception):
|
||||||
def __init__(self, endpoint, errors):
|
def __init__(self, endpoint, errors):
|
||||||
@ -21,12 +19,13 @@ class DiscourseError(Exception):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
||||||
|
|
||||||
|
|
||||||
# not exhaustive, only the ones we need
|
# not exhaustive, only the ones we need
|
||||||
ENDPOINTS = {
|
ENDPOINTS = {
|
||||||
'groups': {
|
'groups': {
|
||||||
'list': {
|
'list': {
|
||||||
'path': "/admin/groups.json",
|
'path': "/admin/groups.json",
|
||||||
'method': requests.get,
|
'method': 'get',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -34,7 +33,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'create': {
|
'create': {
|
||||||
'path': "/admin/groups",
|
'path': "/admin/groups",
|
||||||
'method': requests.post,
|
'method': 'post',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['name'],
|
'required': ['name'],
|
||||||
'optional': ['visible'],
|
'optional': ['visible'],
|
||||||
@ -42,7 +41,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'add_user': {
|
'add_user': {
|
||||||
'path': "/admin/groups/%s/members.json",
|
'path': "/admin/groups/%s/members.json",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['usernames'],
|
'required': ['usernames'],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -50,7 +49,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'remove_user': {
|
'remove_user': {
|
||||||
'path': "/admin/groups/%s/members.json",
|
'path': "/admin/groups/%s/members.json",
|
||||||
'method': requests.delete,
|
'method': 'delete',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['username'],
|
'required': ['username'],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -58,7 +57,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'delete': {
|
'delete': {
|
||||||
'path': "/admin/groups/%s.json",
|
'path': "/admin/groups/%s.json",
|
||||||
'method': requests.delete,
|
'method': 'delete',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -68,7 +67,7 @@ ENDPOINTS = {
|
|||||||
'users': {
|
'users': {
|
||||||
'create': {
|
'create': {
|
||||||
'path': "/users",
|
'path': "/users",
|
||||||
'method': requests.post,
|
'method': 'post',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['name', 'email', 'password', 'username'],
|
'required': ['name', 'email', 'password', 'username'],
|
||||||
'optional': ['active'],
|
'optional': ['active'],
|
||||||
@ -76,7 +75,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'update': {
|
'update': {
|
||||||
'path': "/users/%s.json",
|
'path': "/users/%s.json",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['params'],
|
'required': ['params'],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -84,7 +83,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'get': {
|
'get': {
|
||||||
'path': "/users/%s.json",
|
'path': "/users/%s.json",
|
||||||
'method': requests.get,
|
'method': 'get',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -92,7 +91,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'activate': {
|
'activate': {
|
||||||
'path': "/admin/users/%s/activate",
|
'path': "/admin/users/%s/activate",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -100,7 +99,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'set_email': {
|
'set_email': {
|
||||||
'path': "/users/%s/preferences/email",
|
'path': "/users/%s/preferences/email",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['email'],
|
'required': ['email'],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -108,7 +107,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'suspend': {
|
'suspend': {
|
||||||
'path': "/admin/users/%s/suspend",
|
'path': "/admin/users/%s/suspend",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': ['duration', 'reason'],
|
'required': ['duration', 'reason'],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -116,7 +115,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'unsuspend': {
|
'unsuspend': {
|
||||||
'path': "/admin/users/%s/unsuspend",
|
'path': "/admin/users/%s/unsuspend",
|
||||||
'method': requests.put,
|
'method': 'put',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -124,7 +123,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'logout': {
|
'logout': {
|
||||||
'path': "/admin/users/%s/log_out",
|
'path': "/admin/users/%s/log_out",
|
||||||
'method': requests.post,
|
'method': 'post',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -132,7 +131,7 @@ ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
'external': {
|
'external': {
|
||||||
'path': "/users/by-external/%s.json",
|
'path': "/users/by-external/%s.json",
|
||||||
'method': requests.get,
|
'method': 'get',
|
||||||
'args': {
|
'args': {
|
||||||
'required': [],
|
'required': [],
|
||||||
'optional': [],
|
'optional': [],
|
||||||
@ -146,8 +145,7 @@ class DiscourseManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30)
|
REVOKED_EMAIL = 'revoked@localhost'
|
||||||
REVOKED_EMAIL = 'revoked@' + settings.DOMAIN
|
|
||||||
SUSPEND_DAYS = 99999
|
SUSPEND_DAYS = 99999
|
||||||
SUSPEND_REASON = "Disabled by auth."
|
SUSPEND_REASON = "Disabled by auth."
|
||||||
|
|
||||||
@ -171,7 +169,8 @@ class DiscourseManager:
|
|||||||
for arg in kwargs:
|
for arg in kwargs:
|
||||||
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
|
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
|
||||||
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
||||||
r = endpoint['method'](settings.DISCOURSE_URL + endpoint['parsed_url'], params=params, json=data)
|
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], params=params,
|
||||||
|
json=data)
|
||||||
try:
|
try:
|
||||||
if 'errors' in r.json() and not silent:
|
if 'errors' in r.json() and not silent:
|
||||||
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
||||||
@ -190,67 +189,59 @@ class DiscourseManager:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __generate_random_pass():
|
def _get_groups():
|
||||||
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_groups():
|
|
||||||
endpoint = ENDPOINTS['groups']['list']
|
endpoint = ENDPOINTS['groups']['list']
|
||||||
data = DiscourseManager.__exc(endpoint)
|
data = DiscourseManager.__exc(endpoint)
|
||||||
return [g for g in data if not g['automatic']]
|
return [g for g in data if not g['automatic']]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __update_group_cache():
|
def _create_group(name):
|
||||||
GroupCache.objects.filter(service="discourse").delete()
|
|
||||||
cache = GroupCache.objects.create(service="discourse")
|
|
||||||
cache.groups = json.dumps(DiscourseManager.__get_groups())
|
|
||||||
cache.save()
|
|
||||||
return cache
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_group_cache():
|
|
||||||
if not GroupCache.objects.filter(service="discourse").exists():
|
|
||||||
DiscourseManager.__update_group_cache()
|
|
||||||
cache = GroupCache.objects.get(service="discourse")
|
|
||||||
age = timezone.now() - cache.created
|
|
||||||
if age > DiscourseManager.GROUP_CACHE_MAX_AGE:
|
|
||||||
logger.debug("Group cache has expired. Triggering update.")
|
|
||||||
cache = DiscourseManager.__update_group_cache()
|
|
||||||
return json.loads(cache.groups)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __create_group(name):
|
|
||||||
endpoint = ENDPOINTS['groups']['create']
|
endpoint = ENDPOINTS['groups']['create']
|
||||||
DiscourseManager.__exc(endpoint, name=name[:20], visible=True)
|
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
|
||||||
DiscourseManager.__update_group_cache()
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_cache_group_name_key(name):
|
||||||
|
return 'DISCOURSE_GROUP_NAME__%s' % md5(name.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_cache_group_id_key(g_id):
|
||||||
|
return 'DISCOURSE_GROUP_ID__%s' % g_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __group_name_to_id(name):
|
def __group_name_to_id(name):
|
||||||
cache = DiscourseManager.__get_group_cache()
|
name = DiscourseManager._sanitize_groupname(name)
|
||||||
for g in cache:
|
|
||||||
if g['name'] == name[0:20]:
|
def get_or_create_group():
|
||||||
return g['id']
|
groups = DiscourseManager._get_groups()
|
||||||
logger.debug("Group %s not found on Discourse. Creating" % name)
|
for g in groups:
|
||||||
DiscourseManager.__create_group(name)
|
if g['name'] == name:
|
||||||
return DiscourseManager.__group_name_to_id(name)
|
return g['id']
|
||||||
|
return DiscourseManager._create_group(name)['id']
|
||||||
|
|
||||||
|
return cache.get_or_set(DiscourseManager._generate_cache_group_name_key(name), get_or_create_group,
|
||||||
|
GROUP_CACHE_MAX_AGE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __group_id_to_name(id):
|
def __group_id_to_name(g_id):
|
||||||
cache = DiscourseManager.__get_group_cache()
|
def get_group_name():
|
||||||
for g in cache:
|
groups = DiscourseManager._get_groups()
|
||||||
if g['id'] == id:
|
for g in groups:
|
||||||
return g['name']
|
if g['id'] == g_id:
|
||||||
raise KeyError("Group ID %s not found on Discourse" % id)
|
return g['name']
|
||||||
|
raise KeyError("Group ID %s not found on Discourse" % g_id)
|
||||||
|
|
||||||
|
return cache.get_or_set(DiscourseManager._generate_cache_group_id_key(g_id), get_group_name,
|
||||||
|
GROUP_CACHE_MAX_AGE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __add_user_to_group(id, username):
|
def __add_user_to_group(g_id, username):
|
||||||
endpoint = ENDPOINTS['groups']['add_user']
|
endpoint = ENDPOINTS['groups']['add_user']
|
||||||
DiscourseManager.__exc(endpoint, id, usernames=[username])
|
DiscourseManager.__exc(endpoint, g_id, usernames=[username])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __remove_user_from_group(id, username):
|
def __remove_user_from_group(g_id, username):
|
||||||
endpoint = ENDPOINTS['groups']['remove_user']
|
endpoint = ENDPOINTS['groups']['remove_user']
|
||||||
DiscourseManager.__exc(endpoint, id, username=username)
|
DiscourseManager.__exc(endpoint, g_id, username=username)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __generate_group_dict(names):
|
def __generate_group_dict(names):
|
||||||
@ -269,10 +260,6 @@ class DiscourseManager:
|
|||||||
data = DiscourseManager.__get_user(name, silent=silent)
|
data = DiscourseManager.__get_user(name, silent=silent)
|
||||||
return data['user']['id']
|
return data['user']['id']
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __user_id_to_name(id):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user(username, silent=False):
|
def __get_user(username, silent=False):
|
||||||
endpoint = ENDPOINTS['users']['get']
|
endpoint = ENDPOINTS['users']['get']
|
||||||
@ -281,14 +268,14 @@ class DiscourseManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __activate_user(username):
|
def __activate_user(username):
|
||||||
endpoint = ENDPOINTS['users']['activate']
|
endpoint = ENDPOINTS['users']['activate']
|
||||||
id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
DiscourseManager.__exc(endpoint, id)
|
DiscourseManager.__exc(endpoint, u_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __update_user(username, **kwargs):
|
def __update_user(username, **kwargs):
|
||||||
endpoint = ENDPOINTS['users']['update']
|
endpoint = ENDPOINTS['users']['update']
|
||||||
id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
DiscourseManager.__exc(endpoint, id, params=kwargs)
|
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __create_user(username, email, password):
|
def __create_user(username, email, password):
|
||||||
@ -300,21 +287,21 @@ class DiscourseManager:
|
|||||||
try:
|
try:
|
||||||
DiscourseManager.__user_name_to_id(username, silent=True)
|
DiscourseManager.__user_name_to_id(username, silent=True)
|
||||||
return True
|
return True
|
||||||
except:
|
except DiscourseError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __suspend_user(username):
|
def __suspend_user(username):
|
||||||
id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
endpoint = ENDPOINTS['users']['suspend']
|
endpoint = ENDPOINTS['users']['suspend']
|
||||||
return DiscourseManager.__exc(endpoint, id, duration=DiscourseManager.SUSPEND_DAYS,
|
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
|
||||||
reason=DiscourseManager.SUSPEND_REASON)
|
reason=DiscourseManager.SUSPEND_REASON)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __unsuspend(username):
|
def __unsuspend(username):
|
||||||
id = DiscourseManager.__user_name_to_id(username)
|
u_id = DiscourseManager.__user_name_to_id(username)
|
||||||
endpoint = ENDPOINTS['users']['unsuspend']
|
endpoint = ENDPOINTS['users']['unsuspend']
|
||||||
return DiscourseManager.__exc(endpoint, id)
|
return DiscourseManager.__exc(endpoint, u_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __set_email(username, email):
|
def __set_email(username, email):
|
||||||
@ -322,47 +309,53 @@ class DiscourseManager:
|
|||||||
return DiscourseManager.__exc(endpoint, username, email=email)
|
return DiscourseManager.__exc(endpoint, username, email=email)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __logout(id):
|
def __logout(u_id):
|
||||||
endpoint = ENDPOINTS['users']['logout']
|
endpoint = ENDPOINTS['users']['logout']
|
||||||
return DiscourseManager.__exc(endpoint, id)
|
return DiscourseManager.__exc(endpoint, u_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user_by_external(id):
|
def __get_user_by_external(u_id):
|
||||||
endpoint = ENDPOINTS['users']['external']
|
endpoint = ENDPOINTS['users']['external']
|
||||||
return DiscourseManager.__exc(endpoint, id)
|
return DiscourseManager.__exc(endpoint, u_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __user_id_by_external_id(id):
|
def __user_id_by_external_id(u_id):
|
||||||
data = DiscourseManager.__get_user_by_external(id)
|
data = DiscourseManager.__get_user_by_external(u_id)
|
||||||
return data['user']['id']
|
return data['user']['id']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_name(name):
|
||||||
|
name = name.replace(' ', '_')
|
||||||
|
name = name.replace("'", '')
|
||||||
|
name = name.lstrip(' _')
|
||||||
|
name = name[:20]
|
||||||
|
name = name.rstrip(' _')
|
||||||
|
return name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_username(username):
|
def _sanitize_username(username):
|
||||||
sanitized = username.replace(" ", "_")
|
return DiscourseManager._sanitize_name(username)
|
||||||
sanitized = sanitized.strip(' _')
|
|
||||||
sanitized = sanitized.replace("'", "")
|
|
||||||
return sanitized
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_groupname(name):
|
def _sanitize_groupname(name):
|
||||||
name = name.strip(' _')
|
|
||||||
name = re.sub('[^\w]', '', name)
|
name = re.sub('[^\w]', '', name)
|
||||||
|
name = DiscourseManager._sanitize_name(name)
|
||||||
if len(name) < 3:
|
if len(name) < 3:
|
||||||
name = name + "".join('_' for i in range(3-len(name)))
|
name = "Group " + name
|
||||||
return name[:20]
|
return name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_groups(user):
|
def update_groups(user):
|
||||||
groups = []
|
groups = []
|
||||||
for g in user.groups.all():
|
for g in user.groups.all():
|
||||||
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20]))
|
groups.append(DiscourseManager._sanitize_groupname(str(g)))
|
||||||
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
||||||
group_dict = DiscourseManager.__generate_group_dict(groups)
|
group_dict = DiscourseManager.__generate_group_dict(groups)
|
||||||
inv_group_dict = {v: k for k, v in group_dict.items()}
|
inv_group_dict = {v: k for k, v in group_dict.items()}
|
||||||
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
|
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
|
||||||
user_groups = DiscourseManager.__get_user_groups(username)
|
user_groups = DiscourseManager.__get_user_groups(username)
|
||||||
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
||||||
rem_groups = [x for x in user_groups if not x in inv_group_dict]
|
rem_groups = [x for x in user_groups if x not in inv_group_dict]
|
||||||
if add_groups or rem_groups:
|
if add_groups or rem_groups:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))
|
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))
|
||||||
|
@ -5,8 +5,6 @@ from django.contrib.auth.models import User
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from notifications import notify
|
from notifications import notify
|
||||||
|
|
||||||
from services.tasks import only_one
|
|
||||||
|
|
||||||
from .manager import DiscourseManager
|
from .manager import DiscourseManager
|
||||||
from .models import DiscourseUser
|
from .models import DiscourseUser
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user