Discord bots

Make a Discord bot with the Public API

This site's data is public (see Public API), so you can bring it into your own Discord server. Two ways, from easy to more work:

  1. A webhook poster - posts in a channel when someone takes #1 or a moon starts. No bot account needed.
  2. A real bot - slash commands like /top Power and /player 24SE that anyone in your server can use.

Both are small Python scripts. You need Python 3 on a computer that stays on while the bot should run (your PC, a Raspberry Pi or a cheap server).

Please: ask the API at most once a minute, and show "Data: OP Ninja Statistics (opninja.nyvix.net)" where people see it. Never share your webhook address or bot token - anyone who has them can post as your bot.

1. Webhook poster

  1. In Discord: Server Settings > Integrations > Webhooks > New Webhook, pick the channel, then Copy Webhook URL.
  2. Install the one library it uses: pip install requests
  3. Save this as opninja_poster.py, paste your webhook URL into WEBHOOK, and run it with python opninja_poster.py
import time
import requests

WEBHOOK = "paste your webhook URL here"
FEED = "https://ingest.nyvix.net"
CREDIT = "-# Data: OP Ninja Statistics (opninja.nyvix.net)"

def post(text):
    requests.post(WEBHOOK, json={"content": text + "\n" + CREDIT}, timeout=20)

last_top, last_moon = {}, None
while True:
    try:
        boards = requests.get(FEED + "/board.json", timeout=20).json()["boards"]
        for name, board in boards.items():
            top = board["rows"][0]
            if last_top.get(name) and last_top[name] != top["name"]:
                post(f"👑 **{top['name']}** took #1 on {name} from {last_top[name]} ({top['value']})")
            last_top[name] = top["name"]
        moon = requests.get(FEED + "/status.json", timeout=20).json().get("moon")
        kind = moon["kind"] if moon else None
        if kind and kind != last_moon:
            post(f"🌕 **{moon['name']}** is up: {moon['effects']}")
        last_moon = kind
    except Exception as e:
        print("could not check the feed:", e)
    time.sleep(60)   # once a minute is plenty: the boards change every few minutes

It stays quiet on the first check (nothing to compare with yet), then posts every change it sees.

2. A bot with slash commands

  1. Go to the Discord Developer Portal, New Application, give it a name.
  2. Bot page: Reset Token and copy the token (shown once - keep it secret).
  3. OAuth2 > URL Generator: tick bot and applications.commands, under Bot Permissions tick Send Messages and Embed Links, open the generated link and add the bot to your server.
  4. Install the libraries: pip install discord.py aiohttp
  5. Save this as opninja_bot.py, paste your token into TOKEN, and run it with python opninja_bot.py
import time
import aiohttp
import discord
from discord import app_commands

TOKEN = "paste your bot token here"
FEED = "https://ingest.nyvix.net"
SITE = "https://opninja.nyvix.net"

client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)
cache = {}

async def get(path):
    # the API, remembered for a minute (a busy server doesn't ask more often)
    hit = cache.get(path)
    if hit and time.time() - hit[0] < 60:
        return hit[1]
    async with aiohttp.ClientSession() as session:
        async with session.get(FEED + path, timeout=aiohttp.ClientTimeout(total=20)) as r:
            data = await r.json(content_type=None)
    cache[path] = (time.time(), data)
    return data

def credit(embed):
    embed.set_footer(text="Data: OP Ninja Statistics - opninja.nyvix.net")
    return embed

@tree.command(description="The top 10 of a leaderboard")
@app_commands.choices(board=[app_commands.Choice(name="Power", value="Power"),
                             app_commands.Choice(name="Honor", value="Honor")])
async def top(interaction: discord.Interaction, board: app_commands.Choice[str]):
    rows = (await get("/board.json"))["boards"][board.value]["rows"][:10]
    lines = [f"**#{r['rank']}** {r['name']} - {r['value']} ({r['gains'].get('24h') or 'n/a'} in 24 h)" for r in rows]
    embed = discord.Embed(title=f"{board.value} top 10", url=SITE, description="\n".join(lines))
    await interaction.response.send_message(embed=credit(embed))

@tree.command(description="Look up a player")
async def player(interaction: discord.Interaction, username: str):
    players = (await get("/players.json"))["players"]
    p = next((x for x in players if x["name"].lower() == username.lower()), None)
    if not p:
        await interaction.response.send_message(f"No player called {username} on the leaderboards yet.")
        return
    def line(board):
        v = p[board]
        return f"#{v['rank']} - {v['value']}" if v["rank"] else f"not on the board (last {v['value'] or '-'})"
    embed = discord.Embed(title=p["name"], url=f"{SITE}/player/{p['id']}")
    embed.add_field(name="Power", value=line("Power"))
    embed.add_field(name="Honor", value=line("Honor"))
    embed.set_thumbnail(url=f"{FEED}/avatar/{p['uid']}.png?s=150")
    await interaction.response.send_message(embed=credit(embed))

@tree.command(description="Is there a moon right now?")
async def moon(interaction: discord.Interaction):
    m = (await get("/status.json")).get("moon")
    text = f"🌕 **{m['name']}** is up: {m['effects']}" if m else "No moon right now."
    await interaction.response.send_message(text)

@client.event
async def on_ready():
    await tree.sync()   # new commands can take a few minutes to show up in Discord
    print("Logged in as", client.user)

client.run(TOKEN)

Ideas to build on

  • /rank <username>: their rank on both boards and how far they are from the rank above (gap in /board.json).
  • A daily summary: post /recap.json (biggest gain, biggest climb, most time at #1) every morning.
  • Moon alerts with a role ping: add <@&ROLE_ID> to the message so people can opt in.

Everything the API offers is listed on Public API. Prefer JavaScript? discord.js works the same way: fetch the same addresses and reply with the numbers.

Parts of this wiki use information from the OP Ninja Simulator Roblox Wiki on Fandom, by its contributors, licensed under CC BY-SA. Text adapted from it is shared under the same license; each page lists its sources. A big thank-you to Deadshot123def456 for keeping the Fandom wiki up to date.

Made with Baserow