commit c22dade3d55dc89a3c23db6d184eb4d38bae0ea0 Author: aeru Date: Mon Oct 26 02:21:57 2026 +0100 initial commit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82ca48e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +whitelist.json +__pycache__/ +venv/ +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..807c028 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Disclaimer and Assumption of Responsibility + +This repository is provided strictly for informational, educational, and illustrative purposes. All code is supplied "AS IS", without warranties or guarantees of any kind, whether express or implied, to the fullest extent permitted by applicable law. + +Any reference to Discord or any other third-party platform is for illustrative purposes only. This project is not affiliated with, endorsed by, or sponsored by Discord. It is not intended to authorize, encourage, or guarantee compliance with any third-party platform's Terms of Service, policies, or rules. + +**All use, deployment, adaptation, modification, and operation of this code are undertaken solely at the user's own initiative and risk.** The user is exclusively responsible for independently assessing the legality, safety, suitability, and compliance of any intended use, as well as for obtaining any necessary permissions. + +To the fullest extent permitted by applicable law, the author disclaims responsibility for consequences arising from third-party use, modification, deployment, or misuse of this code, including account suspensions, bans, restrictions, revoked access, service disruptions, data loss, damages, or claims arising from violations of third-party terms or applicable law. + +The publication of this code does not constitute an offer to operate a service, provide technical support, maintain the software, authorize third-party integrations, or participate in any deployment or activity carried out by users. + +The author makes no representation that the code is suitable for any particular purpose, compliant with any third-party platform's policies, or free from defects, vulnerabilities, or operational failures. + +Nothing in this disclaimer excludes or limits any liability, right, or obligation that cannot lawfully be excluded or limited under applicable law. \ No newline at end of file diff --git a/cmds/cat.py b/cmds/cat.py new file mode 100644 index 0000000..d181e79 --- /dev/null +++ b/cmds/cat.py @@ -0,0 +1,11 @@ +import io,time +import aiohttp,discord +from discord.ext import commands +class cat(commands.Cog): + def __init__(s,bot):s.bot=bot + @commands.command() + async def cat(s,ctx): + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as http,http.get("https://cataas.com/cat") as r: + r.raise_for_status();data=await r.read();ext=r.content_type.split('/')[-1].replace('jpeg','jpg') + await ctx.send(file=discord.File(io.BytesIO(data),filename=f"cat-{int(time.time())}.{ext}")) +async def setup(bot):await bot.add_cog(cat(bot)) diff --git a/cmds/help.py b/cmds/help.py new file mode 100644 index 0000000..53d3ba1 --- /dev/null +++ b/cmds/help.py @@ -0,0 +1,8 @@ +from discord.ext import commands +class help(commands.Cog): + def __init__(s,bot):s.bot=bot + @commands.command(name="help") + async def help(s,ctx): + names=sorted(c.qualified_name for c in s.bot.walk_commands() if not c.hidden) + await ctx.send("```\n"+"\n".join(";"+n for n in names)+"\n```") +async def setup(bot):await bot.add_cog(help(bot)) diff --git a/cmds/perm.py b/cmds/perm.py new file mode 100644 index 0000000..565808a --- /dev/null +++ b/cmds/perm.py @@ -0,0 +1,41 @@ +import asyncio,re +from discord.ext import commands +def uid(s): + m=re.fullmatch(r"<@!?(\d+)>|(\d+)",s.strip()) + return int(m[1] or m[2]) if m else None +class perm(commands.Cog): + def __init__(s,bot): + s.bot=bot;s.names={};s.sem=asyncio.Semaphore(2) + async def cog_check(s,ctx):return ctx.author.id==s.bot.user.id + async def name(s,i): + if i in s.names:return s.names[i] + u=s.bot.get_user(i) + if u is None: + async with s.sem: + try:u=await s.bot.fetch_user(i) + except Exception:u=None + await asyncio.sleep(.6) + n=(u.global_name or u.name) if u else "unknown" + if u:s.names[i]=n + return n + @commands.group(invoke_without_command=True) + async def perm(s,ctx):await ctx.send("`;perm list|add |remove `") + @perm.command(name="list") + async def _list(s,ctx): + ids=sorted(s.bot.wl.ids) + if not ids:return await ctx.send("whitelist empty") + n=await asyncio.gather(*(s.name(i) for i in ids)) + await ctx.send("\n".join(f"{i} - {x}" for i,x in zip(ids,n))) + @perm.command(name="add") + async def _add(s,ctx,target:str): + i=uid(target) + if i is None:return await ctx.send("invalid id") + await s.bot.wl.mutate(i,True);s.bot.hook.put(f"whitelist: {i} added by {ctx.author.id}") + await ctx.send(f"{i} added ({await s.name(i)})") + @perm.command(name="remove") + async def _remove(s,ctx,target:str): + i=uid(target) + if i is None:return await ctx.send("invalid id") + await s.bot.wl.mutate(i,False);s.bot.hook.put(f"whitelist: {i} removed by {ctx.author.id}") + await ctx.send(f"{i} removed") +async def setup(bot):await bot.add_cog(perm(bot)) diff --git a/cmds/ping.py b/cmds/ping.py new file mode 100644 index 0000000..9c1dbf7 --- /dev/null +++ b/cmds/ping.py @@ -0,0 +1,26 @@ +import asyncio,re +import aiohttp +from discord.ext import commands +URL="https://discordstatus.com/metrics-display/5k2rt9f7pmny/day.json" +async def local(): + try: + p=await asyncio.create_subprocess_exec("ping","-c","1","-W","5","discord.com",stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.STDOUT) + out,_=await asyncio.wait_for(p.communicate(),8) + m=re.search(r"time[=<]\s*([\d.]+)\s*ms",out.decode(),re.I) + return round(float(m[1])) if m else None + except Exception:return None +async def glob(): + try: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as h,h.get(URL) as r: + return int((await r.json(content_type=None))["summary"]["last"]) + except Exception:return None +class ping(commands.Cog): + def __init__(s,bot):s.bot=bot + @commands.command() + async def ping(s,ctx): + l,g=await asyncio.gather(local(),glob()) + f=lambda v:f"{v}ms" if v is not None else "n/a" + w=s.bot.latency + w=round(w*1000) if w==w and w!=float("inf") else None + await ctx.send(f"websocket ping: {f(w)}\nlocal discord ping: {f(l)}\nglobal discord ping: {f(g)}") +async def setup(bot):await bot.add_cog(ping(bot)) diff --git a/cmds/q.py b/cmds/q.py new file mode 100644 index 0000000..94d97cc --- /dev/null +++ b/cmds/q.py @@ -0,0 +1,119 @@ +import io,unicodedata,urllib.request +from pathlib import Path +import discord +from discord.ext import commands +from PIL import Image,ImageDraw,ImageFilter,ImageFont,ImageOps +from pilmoji import Pilmoji +from core.threads import run_blocking +W,H,AV,PAD,MAXC=1200,480,268,42,420 +BG0,BG1,CARD,STROKE,ACC,TXT,SUB,MARK=(7,9,17),(15,20,36),(18,24,44),(46,64,108),(82,116,214),(236,242,255),(128,150,192),(52,80,160) +FONT=Path(__file__).resolve().parents[1]/"font"/"NotoSans-Regular.ttf" +def norm(t):return unicodedata.normalize("NFKC",t) +def font(n): + try:return ImageFont.truetype(str(FONT),n) + except Exception:return ImageFont.load_default() +def fetch(url): + with urllib.request.urlopen(urllib.request.Request(url,headers={"User-Agent":"Mozilla/5.0"}),timeout=20) as r:return r.read() +def names(t,users): + for u in {u.id:u for u in users}.values(): + n="@"+(getattr(u,"display_name",None) or u.name);t=t.replace(f"<@{u.id}>",n).replace(f"<@!{u.id}>",n) + return t +def gradient(): + im=Image.new("RGB",(W,H));d=ImageDraw.Draw(im) + for y in range(H): + t=y/(H-1);d.line((0,y,W,y),fill=tuple(int(a+(b-a)*t) for a,b in zip(BG0,BG1))) + return im.convert("RGBA") +def glow(im): + o=Image.new("RGBA",im.size,(0,0,0,0));d=ImageDraw.Draw(o);cx,cy,m=int(W*.74),int(H*.2),int(H*.7) + for i in range(18): + r=int(m*(1-i/18));d.ellipse((cx-r,cy-r,cx+r,cy+r),fill=(100,130,215,int(52*(1-i/18)))) + o=o.filter(ImageFilter.GaussianBlur(12));im.paste(o,(0,0),o) +def tw(d,t,f): + l,_,r,_=d.textbbox((0,0),t,font=f);return r-l +def chop(d,w,f,mw): + out=[];cur="" + for c in w: + if cur and tw(d,cur+c,f)>mw:out.append(cur);cur=c + else:cur+=c + return out+([cur] if cur else []) +def wrap(d,t,f,mw): + lines=[] + for p in t.splitlines() or [t]: + ws=p.split() + if not ws: + if lines:lines.append("") + continue + cur="" + for w in ws: + for part in (chop(d,w,f,mw) if tw(d,w,f)>mw else [w]): + x=part if not cur else cur+" "+part + if tw(d,x,f)<=mw:cur=x + else: + if cur:lines.append(cur) + cur=part + if cur:lines.append(cur) + return lines +def lh(d,f): + _,t,_,b=d.textbbox((0,0),"Ag",font=f);h=b-t;return max(h+6,int(h*1.26)) +def fit(d,t,mw,mh): + last=(["..."],font(15),lh(d,font(15))) + for s in range(110,14,-2): + f=font(s);l=wrap(d,t,f,mw);h=lh(d,f) + if len(l)*h<=mh:return l or ["..."],f,h + last=(l,f,h) + l,f,h=last;return (l or ["..."])[:max(1,mh//max(1,h))],f,h +def avatar(b): + a=ImageOps.fit(Image.open(io.BytesIO(b)).convert("RGB"),(AV,AV));m=Image.new("L",(AV,AV),0) + ImageDraw.Draw(m).ellipse((0,0,AV,AV),fill=255);r=Image.new("RGBA",(AV,AV),(0,0,0,0));r.paste(a,(0,0),m);return r +def build(ab,text,sig): + im=gradient();glow(im);d=ImageDraw.Draw(im) + ax,ay=PAD,(H-AV)//2;cl,ct,cr,cb=ax+AV+18,24,W-24,H-24 + sh=Image.new("RGBA",im.size,(0,0,0,0));ImageDraw.Draw(sh).rounded_rectangle((cl+7,ct+10,cr+7,cb+10),radius=28,fill=(0,0,0,105)) + sh=sh.filter(ImageFilter.GaussianBlur(10));im.paste(sh,(0,0),sh) + d.rounded_rectangle((cl,ct,cr,cb),radius=28,fill=CARD,outline=STROKE,width=2) + gd=AV+28;g=Image.new("RGBA",(gd,gd),(0,0,0,0));gg=ImageDraw.Draw(g);c=gd//2 + for i in range(10): + r=c-i*4 + if r<=0:break + gg.ellipse((c-r,c-r,c+r,c+r),fill=(*ACC,int(55*(1-i/10)))) + g=g.filter(ImageFilter.GaussianBlur(4));im.paste(g,(ax-14,ay-14),g) + rd=AV+8;ring=Image.new("RGBA",(rd,rd),(0,0,0,0));ImageDraw.Draw(ring).ellipse((0,0,rd-1,rd-1),outline=(*ACC,175),width=3);im.paste(ring,(ax-4,ay-4),ring) + a=avatar(ab);im.paste(a,(ax,ay),a) + qx,zt,tr=cl+52,ct+44,cr-32;sf=font(27);sig=f"\u2014 {sig}";_,_,_,sh_=d.textbbox((0,0),sig,font=sf) + sy=cb-sh_-30;dy=sy-15;zh=max(48,dy-zt-16) + lines,qf,h=fit(d,text,tr-qx,zh) + d.text((cl+10,ct+6),"\u201c",font=font(max(54,min(100,int(h*1.55)))),fill=MARK) + cx=(qx+tr)//2;y=zt+max(0,(zh-len(lines)*h)//2) + with Pilmoji(im) as p: + for l in lines: + p.text((cx-tw(d,l,qf)//2,y),l,font=qf,fill=TXT,emoji_scale_factor=1.08,emoji_position_offset=(0,-2));y+=h + d.rectangle((qx,dy,tr,dy+1),fill=STROKE) + p.text((qx,sy),sig,font=sf,fill=SUB,emoji_scale_factor=1.0,emoji_position_offset=(0,-1)) + out=io.BytesIO();im.convert("RGB").save(out,format="PNG");out.seek(0);return out +class q(commands.Cog): + def __init__(s,bot):s.bot=bot + @commands.command(name="q") + async def q(s,ctx,*args): + m=ctx.message;target=m.author;text="" + ref=m.reference + if not args and ref and ref.message_id: + r=ref.resolved if isinstance(ref.resolved,discord.Message) else None + if r is None: + try:r=await m.channel.fetch_message(ref.message_id) + except Exception:r=None + if r: + text=names((r.content or "").strip(),r.mentions) + if text:target=r.author + if not text: + rest=list(args) + if rest and rest[0].startswith("<@") and m.mentions: + target=m.mentions[0];rest=[x for x in rest if x not in(f"<@{target.id}>",f"<@!{target.id}>")] + text=names(" ".join(rest).strip(),m.mentions) + if not text:return await ctx.send("usage: ;q text | ;q @user text | reply with ;q") + text=norm(text[:MAXC]) + url=str(target.display_avatar.replace(size=512,format="png").url) + ab=await run_blocking(fetch,url) + sig=norm(f"{getattr(target,'global_name',None) or target.display_name} (@{target.name})") + img=await run_blocking(build,ab,text,sig) + await ctx.send(file=discord.File(img,filename="quote.png")) +async def setup(bot):await bot.add_cog(q(bot)) diff --git a/cmds/status.py b/cmds/status.py new file mode 100644 index 0000000..bc35ba0 --- /dev/null +++ b/cmds/status.py @@ -0,0 +1,93 @@ +import datetime as dt,os,platform,time +import psutil +from discord.ext import commands +from core.threads import run_blocking +def h(n): + n=float(n) + for u in("b","kib","mib","gib","tib"): + if abs(n)<1024:return f"{n:.2f} {u}" + n/=1024 + return f"{n:.2f} pib" +def kv(d):return "\n".join(f"{k}: {v}" for k,v in d.items()) +def rd(p): + try: + with open(p) as f:return f.read().strip() + except OSError:return "" +def system(): + u=platform.uname();r={} + try:r=platform.freedesktop_os_release() + except Exception:pass + b=psutil.boot_time() + return kv({"system":u.system,"kernel":u.release,"version":u.version,"machine":u.machine,"platform":platform.platform(),"distro":r.get("PRETTY_NAME"),"distro id":r.get("ID"),"distro version":r.get("VERSION_ID"),"libc":" ".join(platform.libc_ver()),"python":f"{platform.python_implementation()} {platform.python_version()}","compiler":platform.python_compiler(),"build":" ".join(platform.python_build()),"boot":dt.datetime.fromtimestamp(b).isoformat(),"uptime":str(dt.timedelta(seconds=int(time.time()-b))),"entropy":rd("/proc/sys/kernel/random/entropy_avail"),"max pid":rd("/proc/sys/kernel/pid_max"),"max threads":rd("/proc/sys/kernel/threads-max"),"file max":rd("/proc/sys/fs/file-max"),"file nr":rd("/proc/sys/fs/file-nr"),"swappiness":rd("/proc/sys/vm/swappiness"),"overcommit":rd("/proc/sys/vm/overcommit_memory"),"tainted":rd("/proc/sys/kernel/tainted"),"cpu vulns":len(os.listdir("/sys/devices/system/cpu/vulnerabilities")) if os.path.isdir("/sys/devices/system/cpu/vulnerabilities") else 0}) +def clock(): + n=dt.datetime.now().astimezone() + return kv({"local":n.isoformat(),"utc":dt.datetime.now(dt.timezone.utc).isoformat(),"tz":n.tzname(),"offset":n.utcoffset(),"time.tzname":time.tzname,"timezone":time.timezone,"altzone":time.altzone,"daylight":time.daylight,"iso week":n.isocalendar()[:2],"day of year":n.timetuple().tm_yday,"epoch":time.time(),"monotonic":time.monotonic(),"perf_counter":time.perf_counter(),"process time":time.process_time(),"clock res":time.get_clock_info("monotonic").resolution}) +def cpu(): + pc=psutil.cpu_percent(interval=1,percpu=True);fr=psutil.cpu_freq(percpu=True) or [] + info={} + for l in rd("/proc/cpuinfo").splitlines(): + if ":" in l: + k,v=l.split(":",1);info.setdefault(k.strip(),v.strip()) + cores="\n".join(f" core {i:>2}: {p:5.1f}%"+(f" @ {fr[i].current:.0f} mhz ({fr[i].min:.0f}-{fr[i].max:.0f})" if i2000 else [text] + else:parts=split(text) if text is not None and len(text)>2000 else [text] + m=None + for i,p in enumerate(parts): + k=kw if i==len(parts)-1 else {"mention_author":False} + try:m=await s.message.reply(p,**k) + except discord.HTTPException: + k.pop("mention_author",None);m=await s.channel.send(p,**k) + return m + reply=send diff --git a/core/hook.py b/core/hook.py new file mode 100644 index 0000000..f151503 --- /dev/null +++ b/core/hook.py @@ -0,0 +1,28 @@ +import asyncio,time,aiohttp +from core.text import split +class Hook: + def __init__(s,url): + s.url=url;s.q=asyncio.Queue();s.task=None;s.http=None + async def start(s): + s.http=aiohttp.ClientSession();s.task=asyncio.create_task(s.run()) + def put(s,text): + line=f"`{time.strftime('%d/%m %H:%M:%S')}` {text}" + print(line) + if s.url: + for c in split(line,1900):s.q.put_nowait(c) + async def run(s): + while True: + item=await s.q.get() + for i in range(6): + try: + async with s.http.post(s.url,json={"content":item,"allowed_mentions":{"parse":[]}}) as r: + if r.status==429: + await asyncio.sleep(float((await r.json()).get("retry_after",1))+.1);continue + if r.status<500:break + await asyncio.sleep(1+i) + except Exception:await asyncio.sleep(1+i) + s.q.task_done();await asyncio.sleep(.25) + async def close(s): + try:await asyncio.wait_for(s.q.join(),15) + except asyncio.TimeoutError:pass + s.task.cancel();await s.http.close() diff --git a/core/limiter.py b/core/limiter.py new file mode 100644 index 0000000..7bc348c --- /dev/null +++ b/core/limiter.py @@ -0,0 +1,26 @@ +import asyncio,time +BASE,MAX,STEP,CALM,CAP=0.5,2.0,0.25,5.0,20 +class State: + __slots__=("slot","interval","last","tasks") + def __init__(s):s.slot=0.0;s.interval=BASE;s.last=0.0;s.tasks=set() +class Limiter: + def __init__(s):s.users={} + def admit(s,uid): + now=time.monotonic();st=s.users.get(uid) + if st is None:st=s.users[uid]=State() + if len(st.tasks)>=CAP:return st,None,True + if now-st.last>CALM:st.interval=BASE + wait=max(0.0,st.slot-now) + if wait>0:st.interval=min(MAX,st.interval+STEP) + st.slot=max(now,st.slot)+st.interval;st.last=now + return st,wait,False + def flush(s,st): + n=len(st.tasks) + for t in list(st.tasks):t.cancel() + st.tasks.clear();st.slot=0.0;st.interval=BASE;return n + def track(s,uid,st,task): + st.tasks.add(task) + def done(t): + st.tasks.discard(t) + if not st.tasks and time.monotonic()-st.last>CALM*2 and s.users.get(uid) is st:s.users.pop(uid,None) + task.add_done_callback(done) diff --git a/core/loader.py b/core/loader.py new file mode 100644 index 0000000..2af8b9d --- /dev/null +++ b/core/loader.py @@ -0,0 +1,8 @@ +from pathlib import Path +ROOT=Path(__file__).resolve().parent.parent +async def load_all(bot): + for d in ("cmds","events"): + for f in sorted((ROOT/d).glob("*.py")): + if f.name.startswith("_"):continue + try:await bot.load_extension(f"{d}.{f.stem}") + except Exception as e:bot.hook.put(f"module load failed: {d}.{f.stem}: {e!r}") diff --git a/core/presence.py b/core/presence.py new file mode 100644 index 0000000..febf53c --- /dev/null +++ b/core/presence.py @@ -0,0 +1,10 @@ +import asyncio,random,time +async def presence(bot,interval=45): + await bot.wait_until_ready() + since=int(time.time()*1000) + while True: + payload={"op":3,"d":{"status":"idle","since":since,"afk":True,"activities":[]}} + try: + if bot.ws:await bot.ws.send_as_json(payload) + except Exception as e:bot.hook.put(f"presence update failed: {e!r}") + await asyncio.sleep(interval+random.uniform(-5,5)) diff --git a/core/text.py b/core/text.py new file mode 100644 index 0000000..7f8e296 --- /dev/null +++ b/core/text.py @@ -0,0 +1,10 @@ +def split(t,n=1990): + out=[];op=False + while t: + pre="```\n" if op else "";room=n-len(pre)-4 + if len(t)<=room:out.append(pre+t);break + cut=t.rfind("\n",0,room);cut=cut if cut>room//2 else room + chunk=t[:cut];t=t[cut:].lstrip("\n") + o=op^(chunk.count("```")%2==1) + out.append(pre+chunk+("\n```" if o else ""));op=o + return out or [""] diff --git a/core/threads.py b/core/threads.py new file mode 100644 index 0000000..4c8e799 --- /dev/null +++ b/core/threads.py @@ -0,0 +1,22 @@ +import asyncio +import functools +from concurrent.futures import ThreadPoolExecutor +_pool = ThreadPoolExecutor(max_workers=32, thread_name_prefix="dfu") +async def run_blocking(func, /, *args, **kwargs): + loop = asyncio.get_running_loop() + return await loop.run_in_executor(_pool, functools.partial(func, *args, **kwargs)) +def threaded(func): + @functools.wraps(func) + async def wrapper(*args, **kwargs): + return await run_blocking(func, *args, **kwargs) + return wrapper +def spawn(coro, *, name=None): + task = asyncio.create_task(coro, name=name) + def _done(t): + if not t.cancelled() and t.exception(): + print(f"task {name or ''} error: {t.exception()!r}") + + task.add_done_callback(_done) + return task +def shutdown(): + _pool.shutdown(wait=False, cancel_futures=True) diff --git a/core/whitelist.py b/core/whitelist.py new file mode 100644 index 0000000..d5ea5f6 --- /dev/null +++ b/core/whitelist.py @@ -0,0 +1,35 @@ +import asyncio,json,os +from pathlib import Path +from core.threads import run_blocking +def _read(p): + if not p.exists():p.write_text("[]") + return p.stat().st_mtime_ns,frozenset(int(x) for x in json.loads(p.read_text())) +def _write(p,ids): + t=p.with_suffix(".tmp");t.write_text(json.dumps(sorted(ids),indent=2));os.replace(t,p);return p.stat().st_mtime_ns +class Whitelist: + def __init__(s,path,hook): + s.path=Path(path);s.hook=hook;s.ids=frozenset();s.m=None;s.lock=asyncio.Lock();s.task=None + def __contains__(s,i):return i in s.ids + async def load(s): + try:m,ids=await run_blocking(_read,s.path) + except Exception as e: + s.hook.put(f"whitelist unreadable: {e!r}, keeping previous list") + try:s.m=(await run_blocking(s.path.stat)).st_mtime_ns + except OSError:pass + return + if s.m is not None and ids!=s.ids:s.hook.put(f"whitelist reloaded ({len(ids)} ids)") + s.ids=ids;s.m=m + async def watch(s): + while True: + await asyncio.sleep(1) + try:m=(await run_blocking(s.path.stat)).st_mtime_ns + except OSError:m=None + if m!=s.m:await s.load() + async def start(s): + await s.load();s.task=asyncio.create_task(s.watch()) + async def mutate(s,i,add): + async with s.lock: + n=set(s.ids) + if add:n.add(i) + else:n.discard(i) + s.m=await run_blocking(_write,s.path,n);s.ids=frozenset(n) diff --git a/dfu.py b/dfu.py new file mode 100644 index 0000000..c57d3d8 --- /dev/null +++ b/dfu.py @@ -0,0 +1,42 @@ +import asyncio,signal +from pathlib import Path +import discord,dotenv +from discord.ext import commands +from discord.http import Route +from core import threads +from core.hook import Hook +from core.loader import load_all +from core.presence import presence +from core.whitelist import Whitelist +dotenv.load_dotenv() +env=dotenv.find_dotenv() +token=dotenv.get_key(env,"TOKEN");log=dotenv.get_key(env,"LOG") +bot=commands.Bot(command_prefix=";",self_bot=True,help_command=None) +def banned(e): + return isinstance(e,discord.LoginFailure) or (isinstance(e,discord.ConnectionClosed) and e.code==4004) or (isinstance(e,discord.HTTPException) and e.status in(401,403)) +async def guard(): + await bot.wait_until_ready() + if bot.user.id not in bot.wl: + await bot.wl.mutate(bot.user.id,True);bot.hook.put(f"owner {bot.user.id} added to whitelist") + bot.hook.put(f"started as {bot.user} ({bot.user.id}), {len(bot.guilds)} guilds, whitelist {len(bot.wl.ids)}") + while True: + await asyncio.sleep(60) + try:await bot.http.request(Route("GET","/users/@me")) + except Exception as e: + if banned(e):return f"account banned, disabled or token revoked: {e!r}" +async def main(): + hook=Hook(log);await hook.start();bot.hook=hook;bot.wl=Whitelist(Path(__file__).resolve().parent/"whitelist.json",hook);await bot.wl.start() + stop=asyncio.Event();loop=asyncio.get_running_loop() + for s in(signal.SIGINT,signal.SIGTERM):loop.add_signal_handler(s,stop.set) + await load_all(bot) + run=asyncio.create_task(bot.start(token));g=asyncio.create_task(guard());w=asyncio.create_task(stop.wait());p=asyncio.create_task(presence(bot)) + done,_=await asyncio.wait({run,g,w},return_when=asyncio.FIRST_COMPLETED) + if run in done: + e=run.exception() if not run.cancelled() else None + r=f"account banned or invalid token: {e!r}" if banned(e) else f"unexpected stop: {e!r}" + elif g in done:r=f"guard stopped: {g.exception()!r}" if g.exception() else g.result() + else:r="stopped by signal" + hook.put(r) + for t in(g,w,p,bot.wl.task):t.cancel() + await bot.close();await hook.close();threads.shutdown() +if __name__=="__main__":asyncio.run(main()) diff --git a/events/on_message.py b/events/on_message.py new file mode 100644 index 0000000..bb34590 --- /dev/null +++ b/events/on_message.py @@ -0,0 +1,38 @@ +import asyncio,discord +from discord.ext import commands +from core.ctx import Ctx +from core.limiter import Limiter +async def setup(bot): + lim=Limiter() + async def run(ctx,st,wait,uid): + try: + if wait>0:await asyncio.sleep(wait) + st.tasks.discard(asyncio.current_task()) + await bot.invoke(ctx) + except asyncio.CancelledError:pass + except Exception as e:bot.hook.put(f"dispatch failed for {uid}: {e!r}"[:1900]) + async def handler(m): + if m.author.bot or not m.content.startswith(await bot.get_prefix(m)):return + uid=m.author.id + if uid!=bot.user.id and uid not in bot.wl:return + ctx=await bot.get_context(m,cls=Ctx) + if ctx.command is None:return + st,wait,full=lim.admit(uid) + if full: + n=lim.flush(st) + bot.hook.put(f"{m.author} ({uid}) flooded, {n} pending commands dropped") + try:await m.add_reaction("\u274c") + except Exception:pass + try:await m.reply(f"too many pending commands, {n} dropped",mention_author=False) + except Exception:pass + return + ctx.limited=wait>0 + where=f"#{getattr(m.channel,'name',None) or 'dm'} ({m.guild.name if m.guild else 'dm'})" + bot.hook.put(f"{m.author} ({uid}) ran `{m.content[:200]}` in {where}") + lim.track(uid,st,asyncio.create_task(run(ctx,st,wait,uid))) + async def on_command_error(ctx,e): + if isinstance(e,(commands.CommandNotFound,commands.CheckFailure)):return + bot.hook.put(f"error in `{ctx.command}` by {ctx.author.id}: `{e!r}`"[:1900]) + try:await ctx.send(f"error: `{e}`"[:1900]) + except Exception:pass + bot.on_message=handler;bot.on_command_error=on_command_error diff --git a/font/NotoSans-Regular.ttf b/font/NotoSans-Regular.ttf new file mode 100644 index 0000000..7e9c169 Binary files /dev/null and b/font/NotoSans-Regular.ttf differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..684e086 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +python-dotenv +aiohttp +Pillow +psutil +git+https://github.com/dolfies/discord.py-self.git +pilmoji