feat: Add initial files
All checks were successful
Build Docker / Build Image (push) Successful in 32s
All checks were successful
Build Docker / Build Image (push) Successful in 32s
This commit is contained in:
parent
ffd896271c
commit
ddc0deccef
41
.gitea/workflows/build.yml
Normal file
41
.gitea/workflows/build.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: Build Docker
|
||||||
|
run-name: Build Docker Images
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Build Image:
|
||||||
|
runs-on: [ubuntu-latest, amd]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Docker
|
||||||
|
run : |
|
||||||
|
apt-get install ca-certificates curl gnupg
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
apt-get update
|
||||||
|
apt-get install docker-ce-cli -y
|
||||||
|
- name: Build Docker image
|
||||||
|
run : |
|
||||||
|
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||||
|
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
|
||||||
|
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||||
|
tag=${tag//\//-}
|
||||||
|
tag_num=${GITHUB_RUN_NUMBER}
|
||||||
|
echo "tag_num=$tag_num"
|
||||||
|
|
||||||
|
if [[ "$tag" == "main" ]]; then
|
||||||
|
tag="latest"
|
||||||
|
else
|
||||||
|
tag_num="${tag}-${tag_num}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
docker build -t shaker-bot:$tag_num .
|
||||||
|
docker tag shaker-bot:$tag_num git.woodburn.au/nathanwoodburn/shaker-bot:$tag_num
|
||||||
|
docker push git.woodburn.au/nathanwoodburn/shaker-bot:$tag_num
|
||||||
|
docker tag shaker-bot:$tag_num git.woodburn.au/nathanwoodburn/shaker-bot:$tag
|
||||||
|
docker push git.woodburn.au/nathanwoodburn/shaker-bot:$tag
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
roles.json
|
||||||
|
|
||||||
|
faucet.json
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt /app
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Add mount point for data volume
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3"]
|
||||||
|
CMD ["bot.py"]
|
||||||
|
|
||||||
|
FROM builder as dev-envs
|
159
bot.py
Normal file
159
bot.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
import json
|
||||||
|
from faucet import send_domain
|
||||||
|
import dns.resolver
|
||||||
|
import dns.exception
|
||||||
|
import dns.message
|
||||||
|
import shaker
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||||
|
ADMINID = 0
|
||||||
|
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.members = True
|
||||||
|
intents.guilds = True
|
||||||
|
client = discord.Client(intents=intents)
|
||||||
|
tree = app_commands.CommandTree(client)
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@tree.command(name="faucet", description="Get a free domain")
|
||||||
|
async def faucet(ctx, email:str):
|
||||||
|
# Check if a DM
|
||||||
|
if ctx.guild is None:
|
||||||
|
await ctx.response.send_message("You can not claim from the faucet in DMs")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
roles = {}
|
||||||
|
if os.path.exists('/data/faucet.json'):
|
||||||
|
with open('/data/faucet.json', 'r') as f:
|
||||||
|
roles = json.load(f)
|
||||||
|
if str(ctx.guild.id) in roles:
|
||||||
|
if roles[str(ctx.guild.id)] in [role.id for role in ctx.user.roles]:
|
||||||
|
await ctx.response.send_message("I'll send you a DM when your domain has been sent",ephemeral=True)
|
||||||
|
await send_domain(ctx.user, email)
|
||||||
|
return
|
||||||
|
await ctx.response.send_message("You can't claim from the faucet",ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="faucet-role", description="Change the role that can use the faucet")
|
||||||
|
async def faucetrole(ctx,role:discord.Role):
|
||||||
|
if ctx.user.id != ADMINID:
|
||||||
|
await ctx.response.send_message("You don't have permission to do that",ephemeral=True)
|
||||||
|
return
|
||||||
|
await ctx.response.send_message("Faucet role set to " + role.name + " for server " + ctx.guild.name,ephemeral=True)
|
||||||
|
roles = {}
|
||||||
|
if os.path.exists('/data/faucet.json'):
|
||||||
|
with open('/data/faucet.json', 'r') as f:
|
||||||
|
roles = json.load(f)
|
||||||
|
|
||||||
|
roles[str(ctx.guild.id)] = role.id
|
||||||
|
with open('/data/faucet.json', 'w') as f:
|
||||||
|
json.dump(roles, f)
|
||||||
|
|
||||||
|
@tree.command(name="setverifiedrole", description="Set the role that verified users get")
|
||||||
|
async def setverifiedrole(ctx,role:discord.Role):
|
||||||
|
# Check user has manage guild permission
|
||||||
|
if not ctx.user.guild_permissions.manage_guild:
|
||||||
|
await ctx.response.send_message("You don't have permission to do that",ephemeral=True)
|
||||||
|
return
|
||||||
|
# Verify bot can manage roles
|
||||||
|
if not ctx.guild.me.guild_permissions.manage_roles:
|
||||||
|
await ctx.response.send_message("I don't have permission to do that",ephemeral=True)
|
||||||
|
return
|
||||||
|
# Verify I can manage the role
|
||||||
|
if not ctx.guild.me.top_role > role:
|
||||||
|
await ctx.response.send_message("I don't have permission to do that",ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists('/data/roles.json'):
|
||||||
|
with open('/data/roles.json', 'w') as f:
|
||||||
|
json.dump({}, f)
|
||||||
|
|
||||||
|
with open('/data/roles.json', 'r') as f:
|
||||||
|
roles = json.load(f)
|
||||||
|
|
||||||
|
roles[str(ctx.guild.id)] = role.id
|
||||||
|
with open('/data/roles.json', 'w') as f:
|
||||||
|
json.dump(roles, f)
|
||||||
|
|
||||||
|
await ctx.response.send_message("Verified role set to " + role.name + " for server " + ctx.guild.name,ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="verify", description="Verifies your ownership of a Handshake name and sets your nickname.")
|
||||||
|
async def verify(ctx, domain:str):
|
||||||
|
name_idna = domain.lower().strip().rstrip("/").encode("idna")
|
||||||
|
name_ascii = name_idna.decode("ascii")
|
||||||
|
|
||||||
|
parts = name_ascii.split(".")
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if not re.match(r'[A-Za-z0-9-_]+$', part):
|
||||||
|
return await ctx.response.send_message("Invalid domain",ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
name_rendered = name_idna.decode("idna")
|
||||||
|
except UnicodeError: # don't render invalid punycode
|
||||||
|
name_rendered = name_ascii
|
||||||
|
|
||||||
|
if shaker.check_name(ctx.user.id, name_ascii):
|
||||||
|
try:
|
||||||
|
await ctx.user.edit(nick=name_rendered + "/")
|
||||||
|
# Set role
|
||||||
|
await shaker.handle_role(ctx.user, True)
|
||||||
|
return await ctx.response.send_message("Your nickname has been set to " + name_rendered + "/",ephemeral=True)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
return await ctx.response.send_message("I don't have permission to do that",ephemeral=True)
|
||||||
|
|
||||||
|
records = [{
|
||||||
|
"type": 'TXT',
|
||||||
|
"host": ".".join(["_shaker", "_auth"] + parts[:-1]),
|
||||||
|
"value": str(ctx.user.id),
|
||||||
|
"ttl": 60,
|
||||||
|
}]
|
||||||
|
|
||||||
|
records = json.dumps(records)
|
||||||
|
records = records.encode("utf-8")
|
||||||
|
records = base64.b64encode(records)
|
||||||
|
records = records.decode("utf-8")
|
||||||
|
|
||||||
|
message = f"To verify that you own `{name_rendered}/` please create a TXT record located at `_shaker._auth.{name_ascii}` with the following data: `{ctx.user.id}`.\n\n"
|
||||||
|
message += f"If you use Namebase, you can do this automatically by visiting the following link:\n"
|
||||||
|
message += f"<https://namebase.io/next/domain-manager/{parts[-1]}/records?records={records}>\n\n"
|
||||||
|
message += f"Once the record is set (this may take a few minutes) you can run this command again or manually set your nickname to `{name_rendered}/`."
|
||||||
|
|
||||||
|
await ctx.response.send_message(message,ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# When the bot is ready
|
||||||
|
@client.event
|
||||||
|
async def on_ready():
|
||||||
|
global ADMINID
|
||||||
|
ADMINID = client.application.owner.id
|
||||||
|
await tree.sync()
|
||||||
|
|
||||||
|
# When a member updates their nickname
|
||||||
|
@client.event
|
||||||
|
async def on_member_update(before, after):
|
||||||
|
await shaker.check_member(after)
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_member_join(member) -> None:
|
||||||
|
await shaker.check_member(member)
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_message(message):
|
||||||
|
if message.author == client.user:
|
||||||
|
return
|
||||||
|
if not message.guild:
|
||||||
|
await message.channel.send('Invite this bot into your server by using this link:\nhttps://discord.com/api/oauth2/authorize?client_id=1073940877984153692&permissions=402653184&scope=bot')
|
||||||
|
|
||||||
|
|
||||||
|
client.run(TOKEN)
|
24
faucet.py
Normal file
24
faucet.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
import json
|
||||||
|
from email_validator import validate_email, EmailNotValidError
|
||||||
|
import requests
|
||||||
|
|
||||||
|
async def send_domain(user, email):
|
||||||
|
try:
|
||||||
|
emailinfo = validate_email(email, check_deliverability=False)
|
||||||
|
email = emailinfo.normalized
|
||||||
|
except EmailNotValidError as e:
|
||||||
|
await user.send("Your email is invalid")
|
||||||
|
return
|
||||||
|
|
||||||
|
response = requests.post("https://faucet.woodburn.au/api?email=" + email+"&name="+user.name + "&key=" + os.getenv('FAUCET_KEY'))
|
||||||
|
response = response.json()
|
||||||
|
if response['success']:
|
||||||
|
await user.send("Congratulations! We've sent you a domain to your email")
|
||||||
|
else:
|
||||||
|
await user.send("Sorry, something went wrong. Please try again later")
|
||||||
|
await user.send(response['error'])
|
||||||
|
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
|
email-validator
|
||||||
|
py3dns
|
||||||
|
discord.py
|
64
shaker.py
Normal file
64
shaker.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import dns.resolver
|
||||||
|
import dns.exception
|
||||||
|
import dns.message
|
||||||
|
import discord
|
||||||
|
import json
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
serverIP = os.getenv('DNS_SERVER')
|
||||||
|
resolver.nameservers = [serverIP]
|
||||||
|
resolver.port = int(os.getenv('DNS_PORT'))
|
||||||
|
|
||||||
|
|
||||||
|
def check_name(user_id: int, name: str) -> bool:
|
||||||
|
try:
|
||||||
|
answer = resolver.resolve('_shaker._auth.' + name, 'TXT')
|
||||||
|
for rrset in answer.response.answer:
|
||||||
|
parts = rrset.to_text().split(" ")
|
||||||
|
if str(user_id) in parts[-1]:
|
||||||
|
return True
|
||||||
|
except dns.exception.DNSException as e:
|
||||||
|
print("DNS Exception")
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def handle_role(member: discord.Member, shouldHaveRole: bool):
|
||||||
|
with open('/data/roles.json', 'r') as f:
|
||||||
|
roles = json.load(f)
|
||||||
|
|
||||||
|
key = str(member.guild.id)
|
||||||
|
|
||||||
|
if not key in roles:
|
||||||
|
return
|
||||||
|
|
||||||
|
role_id = roles[key]
|
||||||
|
|
||||||
|
if role_id:
|
||||||
|
guild = member.guild
|
||||||
|
role = guild.get_role(role_id)
|
||||||
|
if role and shouldHaveRole and not role in member.roles:
|
||||||
|
await member.add_roles(role)
|
||||||
|
elif role and not shouldHaveRole and role in member.roles:
|
||||||
|
await member.remove_roles(role)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_member(member: discord.Member) -> bool:
|
||||||
|
if member.display_name[-1] != "/":
|
||||||
|
await handle_role(member, False)
|
||||||
|
return
|
||||||
|
|
||||||
|
if check_name(member.id, member.display_name[0:-1]):
|
||||||
|
await handle_role(member, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
await member.edit(nick=member.display_name[0:-1])
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
await handle_role(member, False)
|
||||||
|
return False
|
Loading…
Reference in New Issue
Block a user