Skip to content

Commit

Permalink
feat: Adds education role (#380)
Browse files Browse the repository at this point in the history
* feat: Adds education role
  • Loading branch information
db0 authored Feb 27, 2024
1 parent ae3bb6b commit afadc3d
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

# 4.32.0

* Add education role

# 4.31.4

* Fix worker picking up testing models because they were missing customizer role
Expand Down
1 change: 1 addition & 0 deletions README_return_codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ The errors returned by the AI horde are always in this json format
| TilingMismatch | Tiling cannot be used in combination with this model |
| ControlNetMismatch | ControlNet cannot be used in combination with this model |
| HiResMismatch | HiRes fix cannot be used in combination with this model |
| EducationCannotSendKudos | Education accounts cannot transfer kudos out |
23 changes: 19 additions & 4 deletions horde/apis/limiter_api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
from flask import request

from datetime import datetime, timedelta
from loguru import logger
from horde.consts import WHITELISTED_SERVICE_IPS
from horde.utils import hash_api_key

class DynamicIPWhitelist():
# Marks IPs to dynamically whitelist for 1 day
# Those IPs will have lower limits during API calls
whitelisted_ips = {}

def whitelist_ip(self, ipaddr):
self.whitelisted_ips[ipaddr] = datetime.now() + timedelta(days=1)

def is_ip_whitelisted(self,ipaddr):
if ipaddr not in self.whitelisted_ips:
return False
return self.whitelisted_ips[ipaddr] > datetime.now()

dynamic_ip_whitelist = DynamicIPWhitelist()

# Used to for the flask limiter, to limit requests per url paths
def get_request_path():
Expand All @@ -11,19 +26,19 @@ def get_request_path():


def get_request_90min_limit_per_ip():
if request.remote_addr in WHITELISTED_SERVICE_IPS:
if request.remote_addr in WHITELISTED_SERVICE_IPS or dynamic_ip_whitelist.is_ip_whitelisted(request.remote_addr):
return "300/minute"
return "90/minute"


def get_request_90hour_limit_per_ip():
if request.remote_addr in WHITELISTED_SERVICE_IPS:
if request.remote_addr in WHITELISTED_SERVICE_IPS or dynamic_ip_whitelist.is_ip_whitelisted(request.remote_addr):
return "600/hour"
return "90/hour"


def get_request_2sec_limit_per_ip():
if request.remote_addr in WHITELISTED_SERVICE_IPS:
if request.remote_addr in WHITELISTED_SERVICE_IPS or dynamic_ip_whitelist.is_ip_whitelisted(request.remote_addr):
return "10/second"
return "2/second"

Expand Down
13 changes: 13 additions & 0 deletions horde/apis/models/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ def __init__(self, api):
default=0,
description="The amount of Kudos this user has given to other users.",
),
"donated": fields.Float(
default=0,
description="The amount of Kudos this user has donated to public goods accounts like education.",
),
"admin": fields.Float(
default=0,
description="The amount of Kudos this user has been given by the Horde admins.",
Expand Down Expand Up @@ -895,6 +899,10 @@ def __init__(self, api):
example=False,
description="This is a service account used by a horde proxy.",
),
"education": fields.Boolean(
example=False,
description="This is an education account used schools and universities.",
),
"special": fields.Boolean(
example=False,
description="(Privileged) This user has been given the Special role.",
Expand Down Expand Up @@ -986,6 +994,10 @@ def __init__(self, api):
example=False,
description="When set to true, the user is considered a service account proxying the requests for other users.",
),
"education": fields.Boolean(
example=False,
description="When set to true, the user is considered an education account and some options become more restrictive.",
),
"special": fields.Boolean(
example=False,
description="When set to true, The user can send special payloads.",
Expand Down Expand Up @@ -1041,6 +1053,7 @@ def __init__(self, api):
"customizer": fields.Boolean(description="The user's new customizer status."),
"vpn": fields.Boolean(description="The user's new vpn status."),
"service": fields.Boolean(description="The user's new service status."),
"education": fields.Boolean(description="The user's new education status."),
"special": fields.Boolean(description="The user's new special status."),
"new_suspicion": fields.Integer(description="The user's new suspiciousness rating."),
"contact": fields.String(example="[email protected]", description="The new contact details."),
Expand Down
20 changes: 18 additions & 2 deletions horde/apis/v2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ def validate(self):
raise e.InvalidAPIKey("generation")
if not self.user.service and self.args["proxied_account"]:
raise e.BadRequest("Only service accounts can provide a proxied_account value.")
if self.user.education or self.user.trusted:
lim.dynamic_ip_whitelist.whitelist_ip(self.user_ip)
self.username = self.user.get_unique_alias()
# logger.warning(datetime.utcnow())
if self.args["prompt"] == "":
Expand Down Expand Up @@ -237,9 +239,9 @@ def validate(self):
prompt_replaced = False
if prompt_suspicion >= 2 and self.gentype != "text":
# if replacement filter mode is enabled AND prompt is short enough, do that instead
if self.args.replacement_filter:
if self.args.replacement_filter or self.user.education:
if not prompt_checker.check_prompt_replacement_length(self.args.prompt):
raise e.BadRequest("Prompt has to be below 1000 chars when replacement filter is on")
raise e.BadRequest("Prompt has to be below 7000 chars when replacement filter is on")
self.args.prompt = prompt_checker.apply_replacement_filter(self.args.prompt)
# If it returns None, it means it replaced everything with an empty string
if self.args.prompt is not None:
Expand Down Expand Up @@ -1272,6 +1274,7 @@ def get(self, user_id=""):
location="json",
)
parser.add_argument("service", type=bool, required=False, location="json")
parser.add_argument("education", type=bool, required=False, location="json")
parser.add_argument(
"special",
type=bool,
Expand Down Expand Up @@ -1366,6 +1369,19 @@ def put(self, user_id=""):
raise e.NotModerator(admin.get_unique_alias(), "PUT UserSingle")
user.set_service(self.args.service)
ret_dict["service"] = user.service
if self.args.education is not None:
if not admin.moderator:
raise e.NotModerator(admin.get_unique_alias(), "PUT UserSingle")
if self.args.concurrency is None and self.args.education is True and user.concurrency < 200:
user.concurrency = 200
ret_dict["concurrency"] = user.concurrency
# The commit() will happen in set_education()
if self.args.concurrency is None and self.args.education is False and user.concurrency == 200:
user.concurrency = 30
ret_dict["concurrency"] = user.concurrency
# The commit() will happen in set_education()
user.set_education(self.args.education)
ret_dict["education"] = user.education
if self.args.special is not None:
if not admin.moderator:
raise e.NotModerator(admin.get_unique_alias(), "PUT UserSingle")
Expand Down
28 changes: 27 additions & 1 deletion horde/classes/base/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,25 @@ def service(cls):
)
return cls.id == subquery

@hybrid_property
def education(self) -> bool:
user_role = UserRole.query.filter_by(user_id=self.id, user_role=UserRoleTypes.EDUCATION).first()
return user_role is not None and user_role.value

@education.expression
def education(cls):
subquery = (
db.session.query(UserRole.user_id)
.filter(
UserRole.user_role == UserRoleTypes.EDUCATION,
UserRole.value == True, # noqa E712
UserRole.user_id == cls.id,
)
.correlate(cls)
.as_scalar()
)
return cls.id == subquery

@hybrid_property
def special(self) -> bool:
user_role = UserRole.query.filter_by(user_id=self.id, user_role=UserRoleTypes.SPECIAL).first()
Expand Down Expand Up @@ -470,7 +489,6 @@ def set_user_role(self, role, value):
db.session.add(new_role)
db.session.commit()
return
logger.debug(user_role)
if user_role.value is False:
user_role.value = True
db.session.commit()
Expand Down Expand Up @@ -513,6 +531,11 @@ def set_service(self, is_service):
return
self.set_user_role(UserRoleTypes.SERVICE, is_service)

def set_education(self, is_service):
if self.is_anon():
return
self.set_user_role(UserRoleTypes.EDUCATION, is_service)

def set_special(self, is_special):
if self.is_anon():
return
Expand Down Expand Up @@ -733,6 +756,8 @@ def count_sharedkeys(self):
def max_sharedkeys(self):
if self.trusted:
return 10
if self.education:
return 1000
return 3

def is_suspicious(self):
Expand Down Expand Up @@ -815,6 +840,7 @@ def get_details(self, details_privilege=0):
"worker_count": self.count_workers(),
"account_age": (datetime.utcnow() - self.created).total_seconds(),
"service": self.service,
"education": self.education,
# unnecessary information, since the workers themselves wil be visible
# "public_workers": self.public_workers,
}
Expand Down
2 changes: 2 additions & 0 deletions horde/classes/base/waiting_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ def get_model_names(self):
# These are typically horde-specific so they will be defined in the specific class for this horde type
def extract_params(self):
# logger.debug(self.params)
if self.user.education:
self.nsfw = False
self.n = self.params.pop("n", 1)
# We store the original amount of jobs requested as well
self.jobs = self.n
Expand Down
6 changes: 6 additions & 0 deletions horde/classes/stable/waiting_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ def extract_params(self):
# logger.debug([self.prompt,self.params['width'],self.params['sampler_name']])
self.things = self.width * self.height * self.get_accurate_steps()
self.total_usage = round(self.things * self.n / hv.thing_divisors["image"], 2)
# Education accounts get some settings hardcoded regardless of the request
if self.user.education:
self.nsfw = False
self.censor_nsfw = True
self.trusted_workers = True
self.shared = False
self.prepare_job_payload(self.params)
self.set_job_ttl()
# Commit will happen in prepare_job_payload()
Expand Down
2 changes: 1 addition & 1 deletion horde/consts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
HORDE_VERSION = "4.31.4"
HORDE_VERSION = "4.32.0"

WHITELISTED_SERVICE_IPS = {
"212.227.227.178", # Turing Bot
Expand Down
11 changes: 10 additions & 1 deletion horde/database/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ def transfer_kudos(source_user, dest_user, amount):
"The target account has been flagged for suspicious activity and tranferring kudos to them is blocked.",
"SourceAccountFlagged",
]
if source_user.education:
return [
0,
"Education accounts cannot transfer kudos away",
"EducationCannotSendKudos",
]
if dest_user.is_suspicious():
return [
0,
Expand All @@ -417,7 +423,10 @@ def transfer_kudos(source_user, dest_user, amount):
)
db.session.add(transfer_log)
db.session.commit()
source_user.modify_kudos(-amount, "gifted")
transfer_type = "gifted"
if source_user.education:
transfer_type = "donated"
source_user.modify_kudos(-amount, transfer_type)
dest_user.modify_kudos(amount, "received")
logger.info(f"{source_user.get_unique_alias()} transfered {amount} kudos to {dest_user.get_unique_alias()}")
return [amount, "OK"]
Expand Down
2 changes: 1 addition & 1 deletion horde/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def check_csam_triggers(self, prompt):
def check_prompt_replacement_length(self, prompt):
if "###" in prompt:
prompt, negprompt = prompt.split("###", 1)
return len(prompt) <= 1000
return len(prompt) <= 7000

# this function takes a prompt input, and returns a filtered prompt instead
# when a prompt is sanitized this way, additional negative prompts are also added
Expand Down
1 change: 1 addition & 0 deletions horde/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ class UserRoleTypes(enum.Enum):
VPN = 5
SPECIAL = 6
SERVICE = 7
EDUCATION = 8
1 change: 1 addition & 0 deletions horde/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"SpecialFieldNeedsSpecialUser",
"Img2ImgMismatch",
"TilingMismatch",
"EducationCannotSendKudos",
]


Expand Down
1 change: 1 addition & 0 deletions sql_statements/4.32.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE userroletypes ADD VALUE 'EDUCATION';
1 change: 1 addition & 0 deletions tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ def test_simple_image_gen(api_key: str, HORDE_URL: str, CIVERSION: str) -> None:


if __name__ == "__main__":
# "ci/cd#12285"
test_simple_image_gen("2bc5XkMeLAWiN9O5s7bhfg", "dev.stablehorde.net", "0.1.1")

0 comments on commit afadc3d

Please sign in to comment.