Initial commit of TARS bot code
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (DiscordBot)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="identifier.sqlite" uuid="ba9c9cd6-b4ca-45a7-9b22-1fb1bb1d28d8">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.13 (DiscordBot)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (DiscordBot)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/DiscordBot.iml" filepath="$PROJECT_DIR$/.idea/DiscordBot.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
DB_FILE = "tars_bot.db"
|
||||||
|
NWORD_PATTERN = re.compile(r'\bn+[i1!|]+[g9]+[e3]+r+s*\b', re.IGNORECASE)
|
||||||
|
SUICIDE_PATTERNS = [
|
||||||
|
re.compile(r'\bk[i1!|]+ll\s+(yourself|urself|yoself)\b', re.IGNORECASE),
|
||||||
|
re.compile(r'\b(commit|do)\s+suicide\b', re.IGNORECASE),
|
||||||
|
]
|
||||||
|
DRUG_KEYWORDS = ["cocaine", "heroin", "meth", "weed", "marijuana", "lsd", "mdma", "ecstasy", "fent", "xanax"]
|
||||||
|
BANNED_WORDS = [
|
||||||
|
"nigger", "faggot", "retard", "kike", "chink", "spic",
|
||||||
|
"rape", "porn", "sex", "cock", "cum", "slut", "whore", "kys", "nigga"
|
||||||
|
]
|
||||||
|
STAFF_ROLES_FOR_PING = ["Moderator", "Admin", "Senior Admin", "Head Admin", "Co Owner", "Owner"]
|
||||||
|
recent_messages = {}
|
||||||
|
recent_message_timestamps = {}
|
||||||
|
recent_joins = []
|
||||||
|
recent_message_history = {}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
import discord
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import aiosqlite
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from tars import tars_text
|
||||||
|
from config import STAFF_ROLES_FOR_PING, recent_messages, recent_message_timestamps, \
|
||||||
|
NWORD_PATTERN, SUICIDE_PATTERNS, DRUG_KEYWORDS, DB_FILE
|
||||||
|
|
||||||
|
logger = logging.getLogger("tars")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_moderation(message):
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
if any(role.name in STAFF_ROLES_FOR_PING for role in message.author.roles):
|
||||||
|
return
|
||||||
|
text = message.content or ""
|
||||||
|
banned_words = await get_banned_words()
|
||||||
|
for word in banned_words:
|
||||||
|
if re.search(rf"\b{re.escape(word)}\b", text, re.IGNORECASE):
|
||||||
|
count = await get_warnings(str(message.author.id))
|
||||||
|
count += 1
|
||||||
|
await set_warnings(str(message.author.id), count)
|
||||||
|
await add_warn_log(str(message.author.id), f"Use of banned word: {word}")
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete banned word message: {e}")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, watch your language. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(
|
||||||
|
message.author,
|
||||||
|
f"T.A.R.S. Warning {count}/3: Use of banned word (‘{word}’). Message deleted."
|
||||||
|
)
|
||||||
|
await send_mod_log(
|
||||||
|
f"Banned word '{word}' used by {message.author} in {message.channel.mention}. Warnings {count}."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
g = message.guild
|
||||||
|
if len(text.splitlines()) > 10 or len(text) > 500:
|
||||||
|
count = await get_warnings(str(message.author.id))
|
||||||
|
count += 1
|
||||||
|
await set_warnings(str(message.author.id), count)
|
||||||
|
await add_warn_log(str(message.author.id), "Text wall / spam")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, sending large text walls or spam is prohibited. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Text wall or spam detected.")
|
||||||
|
await send_mod_log(g,
|
||||||
|
f"Text wall by {message.author} in {message.channel.mention}: {len(text)} chars / {len(text.splitlines())} lines. Warnings {count}")
|
||||||
|
return
|
||||||
|
ping_count = 0
|
||||||
|
for role in g.roles:
|
||||||
|
if role.name in STAFF_ROLES_FOR_PING and role.mention in text:
|
||||||
|
ping_count += text.count(role.mention)
|
||||||
|
if ping_count > 4:
|
||||||
|
count = await get_warnings(str(message.author.id))
|
||||||
|
count += 1
|
||||||
|
await set_warnings(str(message.author.id), count)
|
||||||
|
await add_warn_log(str(message.author.id), "Excessive staff pinging")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, excessive staff pinging is not allowed. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Excessive staff pinging.")
|
||||||
|
await send_mod_log(g,
|
||||||
|
f"Excessive staff pings by {message.author} in {message.channel.mention}. Count: {ping_count}. Warnings {count}")
|
||||||
|
return
|
||||||
|
uid = str(message.author.id)
|
||||||
|
now = datetime.utcnow()
|
||||||
|
msg_list = recent_messages.get(uid, [])
|
||||||
|
ts_list = recent_message_timestamps.get(uid, [])
|
||||||
|
msg_list.append(text)
|
||||||
|
ts_list.append(now)
|
||||||
|
recent_messages[uid] = msg_list[-6:]
|
||||||
|
recent_message_timestamps[uid] = ts_list[-6:]
|
||||||
|
if len(set(recent_messages[uid])) == 1 and len(recent_messages[uid]) >= 3:
|
||||||
|
count = await get_warnings(uid)
|
||||||
|
count += 1
|
||||||
|
await set_warnings(uid, count)
|
||||||
|
await add_warn_log(uid, "Repeated message spam")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, repeated messages detected. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Repeated message spam.")
|
||||||
|
await send_mod_log(g, f"Repeated messages by {message.author} in {message.channel.mention}. Warnings {count}")
|
||||||
|
return
|
||||||
|
|
||||||
|
links = re.findall(r"https?://\S+", text)
|
||||||
|
if len(links) > 2:
|
||||||
|
count = await get_warnings(uid)
|
||||||
|
count += 1
|
||||||
|
await set_warnings(uid, count)
|
||||||
|
await add_warn_log(uid, "Link spam")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, excessive links detected. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Excessive link posting.")
|
||||||
|
await send_mod_log(g,
|
||||||
|
f"Link spam by {message.author} in {message.channel.mention}. Links: {len(links)}. Warnings {count}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if NWORD_PATTERN.search(text):
|
||||||
|
reaction = random.choice(
|
||||||
|
["Interesting choice of words… not recommended.", "Attempting human chaos detected. Deleting."])
|
||||||
|
count = await helper_warn(message, reaction, uid)
|
||||||
|
await add_warn_log(uid, "Prohibited slur")
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Use of prohibited slur.")
|
||||||
|
await send_mod_log(g,
|
||||||
|
f"Banned word by {message.author} in {message.channel.mention}: \"{text}\". Warnings {count}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for pat in SUICIDE_PATTERNS:
|
||||||
|
if pat.search(text):
|
||||||
|
reaction = random.choice(["Protocol violation. That’s a negative.",
|
||||||
|
"Error detected: inappropriate content. Executing deletion."])
|
||||||
|
count = await helper_warn(message, reaction, uid)
|
||||||
|
await add_warn_log(uid, "Self-harm encouragement")
|
||||||
|
await dm_send_safe(message.author,
|
||||||
|
f"T.A.R.S. Warning {count}/3: Promoting self-harm is prohibited. If you are in danger, contact emergency services or the suicide hotline.")
|
||||||
|
await send_mod_log(g,
|
||||||
|
f"Self-harm phrase by {message.author} in {message.channel.mention}. Warnings {count}. Message: \"{text}\"")
|
||||||
|
return
|
||||||
|
|
||||||
|
if any(w in text for w in DRUG_KEYWORDS):
|
||||||
|
count = await get_warnings(uid)
|
||||||
|
count += 1
|
||||||
|
await set_warnings(uid, count)
|
||||||
|
await add_warn_log(uid, "Drug mention")
|
||||||
|
await message.channel.send(
|
||||||
|
tars_text(f"{message.author.mention}, discussion of drugs is prohibited. [Warning {count}/3]"))
|
||||||
|
await dm_send_safe(message.author, f"T.A.R.S. Warning {count}/3: Discussion of drugs prohibited.")
|
||||||
|
await send_mod_log(g, f"Drug mention by {message.author} in {message.channel.mention}. Warnings {count}.")
|
||||||
|
return
|
||||||
|
current_warnings = await get_warnings(uid)
|
||||||
|
if current_warnings >= 3:
|
||||||
|
try:
|
||||||
|
await message.author.timeout(discord.utils.utcnow() + timedelta(minutes=10),
|
||||||
|
reason="T.A.R.S. automated enforcement")
|
||||||
|
await send_mod_log(g, f"{message.author} was timed out for 10 minutes (3 warnings).")
|
||||||
|
await set_warnings(uid, 0)
|
||||||
|
except Exception as e:
|
||||||
|
await send_mod_log(g, f"Failed to timeout {message.author}: " + str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def get_banned_words():
|
||||||
|
"""Fetch the banned word list from config (SQLite)."""
|
||||||
|
async with aiosqlite.connect(DB_FILE) as db:
|
||||||
|
cur = await db.execute("SELECT value FROM config WHERE key = 'banned_words'")
|
||||||
|
row = await cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return json.loads(row[0])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_warnings(user_id: str) -> int:
|
||||||
|
async with aiosqlite.connect(DB_FILE) as db:
|
||||||
|
cur = await db.execute("SELECT count FROM warnings WHERE user_id = ?", (user_id,))
|
||||||
|
row = await cur.fetchone()
|
||||||
|
return row[0] if row else 0
|
||||||
|
|
||||||
|
|
||||||
|
async def set_warnings(user_id: str, count: int):
|
||||||
|
async with aiosqlite.connect(DB_FILE) as db:
|
||||||
|
await db.execute("INSERT OR REPLACE INTO warnings(user_id, count) VALUES(?,?)", (user_id, count))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def add_warn_log(user_id: str, reason: str, moderator: str = "T.A.R.S."):
|
||||||
|
async with aiosqlite.connect(DB_FILE) as db:
|
||||||
|
await db.execute("INSERT INTO warns_log(user_id, reason, time, moderator) VALUES(?,?,?,?)",
|
||||||
|
(user_id, reason, datetime.utcnow().isoformat(), moderator))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def dm_send_safe(user: discord.User, text: str):
|
||||||
|
try:
|
||||||
|
await user.send(tars_text(text))
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Could not DM user {user}: " + str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def helper_warn(message, reaction, uid):
|
||||||
|
await message.channel.send(tars_text(reaction))
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except Exception as e:
|
||||||
|
e.args = (f"{e.args[0]} (while deleting message)",)
|
||||||
|
logger.exception("Could not delete message: " + str(e))
|
||||||
|
count = await get_warnings(uid)
|
||||||
|
count += 1
|
||||||
|
await set_warnings(uid, count)
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def dm_send_safe(user: discord.User, text: str):
|
||||||
|
try:
|
||||||
|
await user.send(tars_text(text))
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Could not DM user {user}: " + str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def send_mod_log(guild: discord.Guild, message: str):
|
||||||
|
log_channel = discord.utils.get(guild.text_channels, name="╰-︰🤖tars-logs")
|
||||||
|
if not log_channel:
|
||||||
|
if not guild.me.guild_permissions.manage_channels:
|
||||||
|
logger.warning("Bot lacks permission to create log channel.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
overwrites = {
|
||||||
|
guild.default_role: discord.PermissionOverwrite(view_channel=False),
|
||||||
|
guild.me: discord.PermissionOverwrite(view_channel=True)
|
||||||
|
}
|
||||||
|
log_channel = await guild.create_text_channel("tars-logs", overwrites=overwrites)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Could not create log channel: {e}")
|
||||||
|
return
|
||||||
|
mentions = []
|
||||||
|
for rname in ("Moderator", "Admin", "Senior Admin", "Head Admin", "Co Owner", "Owner"):
|
||||||
|
r = discord.utils.get(guild.roles, name=rname)
|
||||||
|
if r:
|
||||||
|
mentions.append(r.mention)
|
||||||
|
mention_text = " ".join(mentions) + ("\n" if mentions else "")
|
||||||
|
await log_channel.send(f"{mention_text}{message}")
|
||||||
Binary file not shown.
@@ -0,0 +1,31 @@
|
|||||||
|
import random
|
||||||
|
def tars_text(text: str, category: str = "default") -> str:
|
||||||
|
variants = {
|
||||||
|
"success": [
|
||||||
|
f"Mission accomplished: {text}",
|
||||||
|
f"Objective complete — {text}",
|
||||||
|
f"Task executed flawlessly: {text}",
|
||||||
|
f"Done. {text}",
|
||||||
|
],
|
||||||
|
"info": [
|
||||||
|
f"Processing complete: {text}",
|
||||||
|
f"Update: {text}",
|
||||||
|
f"Noted. {text}",
|
||||||
|
f"Acknowledged: {text}",
|
||||||
|
],
|
||||||
|
"error": [
|
||||||
|
f"Error detected: {text}",
|
||||||
|
f"Minor malfunction: {text}",
|
||||||
|
f"That didn’t go as planned — {text}",
|
||||||
|
f"Negative. {text}",
|
||||||
|
],
|
||||||
|
"default": [
|
||||||
|
f"{text}",
|
||||||
|
f"{text}.",
|
||||||
|
f"Affirmative — {text}",
|
||||||
|
f"Understood: {text}",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
pool = variants.get(category, variants["default"])
|
||||||
|
line = random.choice(pool)
|
||||||
|
return f"{line}"
|
||||||
+1051
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from tars_bot import bot, DISCORD_TOKEN
|
||||||
|
bot.run(DISCORD_TOKEN)
|
||||||
Reference in New Issue
Block a user