Scripting examples.
Aliases, auto-responders, filters, lookups and op tooling, written for bIRC’s birc API. Each is a complete script you can paste in.
The API reference lists every function and event. This page shows how they combine.
On this page
Using an example
- Open the Scripts window (
⌘⌥S), click New, paste the code, and save — or download the file and use Import. The filename becomes the script’s name. - Examples marked network call
birc.fetch. Turn on that script’s Allow network toggle or the fetch is refused (with a line in the server window saying so). - Edit the constants at the top — channel names, trusted nicks, keywords — to taste. Saving re-checks the script and reloads it. Errors show in the script’s Console and as a line in the server window, never as a crash.
- Scripts are global with a per-profile Enabled toggle, so a script that should only run on one network can be limited to that profile.
Commands & aliases
The first thing most people script: a /command that saves typing. birc.onCommand gives you the text after the command as one string, and birc.target is the window it was typed in.
/shrug, /wii, /q
Three small aliases. A script command never overrides a built-in, so /slap and /cycle (which bIRC already has) would be ignored — pick free names. Download aliases.js
birc.onCommand('shrug', function (args) {
if (!birc.target) { birc.print('/shrug needs a channel or private message window'); return; }
const text = (args || '').trim();
birc.say(birc.target, (text ? text + ' ' : '') + '¯\\_(ツ)_/¯');
});
birc.onCommand('wii', function (args) {
const nick = (args || '').trim();
if (!nick) { birc.print('usage: /wii <nick>'); return; }
birc.command('whois ' + nick + ' ' + nick);
});
birc.onCommand('q', function (args) {
const nick = (args || '').trim().split(/\s+/)[0];
if (!nick) { birc.print('usage: /q <nick>'); return; }
birc.command('query ' + nick);
});/remind — a timer that prints back where you typed
birc.setTimeout with browser semantics. The callback runs on the connection the command came from, so birc.printTo lands in the right window even if you have moved on. Download remind.js
const UNITS = { s: 1, m: 60, h: 3600 };
birc.onCommand('remind', function (args) {
const m = /^(\d+)([smh])\s+(.+)$/i.exec((args || '').trim());
if (!m) { birc.print('usage: /remind <n>s|m|h <text> e.g. /remind 10m stretch'); return; }
const ms = parseInt(m[1], 10) * UNITS[m[2].toLowerCase()] * 1000;
const text = m[3];
const target = birc.target;
birc.setTimeout(function () {
if (target) birc.printTo(target, '⏰ ' + text);
else birc.print('⏰ ' + text);
}, ms);
birc.print('Reminder set for ' + m[1] + m[2].toLowerCase() + ': ' + text);
});/uptime — a sysinfo one-liner
birc.uptime.system / .app / .server and birc.os.* are live properties. A script formats them however it likes. Download uptime.js
function human(seconds) {
const s = parseInt(seconds, 10);
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
return (d ? d + 'd ' : '') + (h ? h + 'h ' : '') + m + 'm';
}
birc.onCommand('uptime', function (args) {
const parts = ['system ' + human(birc.uptime.system), birc.appName + ' ' + human(birc.uptime.app)];
if (birc.uptime.server) parts.push(birc.network + ' ' + human(birc.uptime.server));
const line = 'uptime: ' + parts.join(' · ') + ' — ' + birc.os.name + ' ' + birc.os.version
+ ', ' + birc.appName + ' ' + birc.version;
if ((args || '').trim() === 'here' || !birc.target) birc.print(line);
else birc.say(birc.target, line);
});/seen — when someone was last around
Several event hooks feeding one table, persisted with birc.store (JSON in, JSON out). Note the e.isMe / e.isBacklog guards — replayed history should not count as “just now”. Download seen.js
const MAX_ENTRIES = 2000;
let seen = JSON.parse(birc.store.get('seen') || '{}');
let dirty = false;
function save() {
if (!dirty) return;
birc.store.set('seen', JSON.stringify(seen));
dirty = false;
}
birc.setInterval(save, 30000);
birc.on('unload', save);
function note(e, what, where) {
if (!e.nick || e.isMe || e.isBacklog) return;
seen[e.network + '/' + e.nick.toLowerCase()] = { when: Date.now(), what: what, where: where || '' };
const keys = Object.keys(seen);
if (keys.length > MAX_ENTRIES) {
keys.sort((a, b) => seen[a].when - seen[b].when)
.slice(0, keys.length - MAX_ENTRIES)
.forEach((k) => { delete seen[k]; });
}
dirty = true;
}
const place = (e) => birc.sameNick(e.target, birc.nick) ? null : e.target;
const quote = (e) => place(e) ? ' "' + birc.strip(e.text) + '"' : ' you privately';
birc.on('message', (e) => note(e, 'saying' + quote(e), place(e)));
birc.on('action', (e) => note(e, 'doing' + quote(e), place(e)));
birc.on('join', (e) => note(e, 'joining', e.channel));
birc.on('part', (e) => note(e, 'leaving', e.channel));
birc.on('quit', (e) => note(e, 'quitting' + (e.reason ? ' (' + e.reason + ')' : '')));
birc.on('nickchange', (e) => note(e, 'changing nick to ' + e.newNick));
birc.onCommand('seen', function (args) {
const nick = (args || '').trim();
if (!nick) { birc.print('usage: /seen <nick>'); return; }
const hit = seen[birc.network + '/' + nick.toLowerCase()];
if (!hit) { birc.print('seen: no sign of ' + nick + ' on ' + birc.network); return; }
const minutes = Math.round((Date.now() - hit.when) / 60000);
const ago = minutes < 1 ? 'just now' : minutes < 120 ? minutes + ' min ago' : Math.round(minutes / 60) + ' h ago';
birc.print('seen: ' + nick + ' ' + hit.what + (hit.where ? ' in ' + hit.where : '') + ', ' + ago);
});/top — most active in this window
birc.history reads the transcript lines currently in memory (not the stored history). Server lines have no nick, so they drop out of the count. Download top.js
birc.onCommand('top', function () {
const target = birc.target;
if (!target) { birc.print('/top needs a channel or private message window'); return; }
const counts = {};
birc.history(target, 500).forEach((line) => {
if (line.nick) counts[line.nick] = (counts[line.nick] || 0) + 1;
});
const top = Object.keys(counts).sort((a, b) => counts[b] - counts[a]).slice(0, 5);
if (!top.length) { birc.print('top: nothing to count yet'); return; }
birc.print('top in ' + target + ': ' + top.map((n) => n + ' (' + counts[n] + ')').join(', '));
});Reacting to events
Hooks that act on their own: rejoin, follow an invite, hand out voice, answer a private message. Every event object carries the network it came from. Channel events carry e.channel. Message events carry e.target, which in a private message is your own nick.
Rejoin after a kick, follow trusted invites
birc.sameNick compares nicks the way the network does (CASEMAPPING-aware), so use it instead of ===. Download rejoin.js
const REJOIN_DELAY_MS = 3000;
const TRUSTED_INVITERS = ['alice', 'bob'];
birc.on('kick', function (e) {
if (!birc.sameNick(e.target, birc.nick)) return;
const channel = e.channel;
birc.setTimeout(() => birc.command('join ' + channel), REJOIN_DELAY_MS);
});
birc.on('invite', function (e) {
if (TRUSTED_INVITERS.some((n) => birc.sameNick(n, e.nick))) birc.command('join ' + e.channel);
});Auto-voice friends by services account
e.account comes from IRCv3 extended-join, and birc.userInfo tells the script whether you hold ops before it tries. birc.command runs the same /mode you would type. Download autovoice.js
const FRIENDS = { '#mychannel': ['alice', 'bob'] };
birc.on('join', function (e) {
if (e.isMe) return;
const wanted = FRIENDS[e.channel.toLowerCase()];
if (!wanted || !e.account) return;
if (!wanted.some((a) => a.toLowerCase() === e.account.toLowerCase())) return;
const me = birc.userInfo(e.channel, birc.nick);
if (!me || !/[~&@]/.test(me.prefix)) return;
birc.command('mode ' + e.channel + ' +v ' + e.nick);
});Away auto-reply for private messages
Replies with a notice (the polite convention), rate-limited per person so a chatty friend does not get twenty of them. The text lives in birc.store and survives a relaunch. Download autoreply.js
const ONCE_PER_MS = 60 * 60 * 1000;
const replied = {};
birc.onCommand('autoreply', function (args) {
const text = (args || '').trim();
if (!text || text === 'off') { birc.store.delete('text'); birc.print('autoreply: off'); return; }
birc.store.set('text', text);
birc.print('autoreply: on — "' + text + '"');
});
birc.on('message', function (e) {
const text = birc.store.get('text');
if (!text || e.isMe || e.isBacklog) return;
if (!birc.sameNick(e.target, birc.nick)) return;
const key = e.network + '/' + e.nick.toLowerCase();
if (replied[key] && Date.now() - replied[key] < ONCE_PER_MS) return;
replied[key] = Date.now();
birc.notice(e.nick, text);
});Forward mentions to another network
birc.to(name) is a context bound to another connected network — its .say / .notice / .action / .command / .print target that network. An unknown name shows an error in the server window rather than failing silently. Download mentions.js
const FORWARD_TO = { network: 'Bouncer', target: '#my-mentions' };
birc.on('message', function (e) {
if (e.isMe || e.isBacklog || e.network === FORWARD_TO.network) return;
const text = birc.strip(e.text);
if (text.toLowerCase().indexOf(birc.nick.toLowerCase()) < 0) return;
const where = birc.sameNick(e.target, birc.nick) ? 'DM' : e.target;
birc.to(FORWARD_TO.network).say(FORWARD_TO.target,
'[' + e.network + ' ' + where + '] <' + e.nick + '> ' + text);
});Filtering & rewriting
A hook can hide a line (return birc.EAT or false) or change how a message displays (return {text}). The output hook does the same for what you send. Rewrites of incoming text change only the transcript, never what the server sent.
/mute — hide someone in one channel
A soft, per-channel ignore. The same filter function is registered for five event types. For join and part it reads e.channel, for messages e.target. Download mute.js
let mutes = JSON.parse(birc.store.get('mutes') || '{}');
const key = (network, channel) => network + '/' + channel.toLowerCase();
function save() { birc.store.set('mutes', JSON.stringify(mutes)); }
birc.onCommand('mute', function (args) {
const nick = (args || '').trim();
if (!nick || !birc.channel) { birc.print('usage (in a channel): /mute <nick>'); return; }
const k = key(birc.network, birc.channel);
const list = mutes[k] || (mutes[k] = []);
if (!list.some((n) => birc.sameNick(n, nick))) list.push(nick);
save();
birc.print('muted ' + nick + ' in ' + birc.channel);
});
birc.onCommand('unmute', function (args) {
const nick = (args || '').trim();
if (!nick || !birc.channel) { birc.print('usage (in a channel): /unmute <nick>'); return; }
const k = key(birc.network, birc.channel);
mutes[k] = (mutes[k] || []).filter((n) => !birc.sameNick(n, nick));
save();
birc.print('unmuted ' + nick + ' in ' + birc.channel);
});
function filter(e) {
if (!e.nick) return;
const list = mutes[key(e.network, e.channel || e.target || '')];
if (list && list.some((n) => birc.sameNick(n, e.nick))) return birc.EAT;
}
['message', 'action', 'notice', 'join', 'part'].forEach((type) => birc.on(type, filter));Hide bot part/quit noise
Uses the IRCv3 bot flag through birc.userInfo, which bIRC learns from the bot tag on a bot’s messages. A quit has no channel, so it checks every channel you share with the nick. A join can only be hidden once the nick is already known as a bot. Download botquiet.js
function isBot(e) {
const channels = e.channel ? [e.channel] : birc.channels();
return channels.some((c) => {
const info = birc.userInfo(c, e.nick);
return info && info.isBot;
});
}
['join', 'part', 'quit'].forEach((type) => birc.on(type, (e) => {
if (e.nick && isBot(e)) return birc.EAT;
}));Highlight your keywords in bold red
An incoming rewrite that inserts mIRC formatting codes — the transcript renders them like any other formatted message. Download keywords.js
const KEYWORDS = ['swift', 'release', 'bug'];
const RE = new RegExp('\\b(' + KEYWORDS.join('|') + ')\\b', 'gi');
const BOLD = '\x02', COLOR = '\x03';
function emphasize(e) {
if (e.isMe || e.text.search(RE) < 0) return;
return { text: e.text.replace(RE, BOLD + COLOR + '04$1' + COLOR + BOLD) };
}
birc.on('message', emphasize);
birc.on('action', emphasize);s/typo/fix/ — correct your last line
An output hook that remembers your last message per window and, when you type an s/// line, sends the corrected message instead. Returning birc.EAT from output cancels the send. Download sed.js
const last = {};
birc.on('output', function (e) {
if (e.kind !== 'message') return;
const key = e.network + '/' + e.target.toLowerCase();
const m = /^s\/([^\/]+)\/([^\/]*)\/?$/.exec(e.text);
if (!m) { last[key] = e.text; return; }
if (!last[key]) { birc.print('s///: nothing to correct yet'); return birc.EAT; }
let fixed;
try { fixed = last[key].replace(new RegExp(m[1], 'g'), m[2]); }
catch (err) { birc.print('s///: bad pattern — ' + err); return birc.EAT; }
if (fixed === last[key]) { birc.print('s///: no match in "' + last[key] + '"'); return birc.EAT; }
last[key] = fixed;
return { text: fixed };
});Text snippets with Tab completion
birc.onComplete must return its candidates synchronously, because Tab completion runs inline. The semicolon prefix keeps it clear of bIRC’s own :emoji: completion. Download snippets.js
const SNIPPETS = {
shrug: '¯\\_(ツ)_/¯',
tm: '™',
flip: '(╯°□°)╯︵ ┻━┻',
sig: 'sent from bIRC — https://birc.app'
};
birc.on('output', function (e) {
const text = e.text.replace(/(^|\s);(\w+)\b/g, (all, lead, name) =>
SNIPPETS.hasOwnProperty(name) ? lead + SNIPPETS[name] : all);
if (text !== e.text) return { text: text };
});
birc.onComplete(function (word) {
if (word.charAt(0) !== ';') return [];
const typed = word.slice(1).toLowerCase();
return Object.keys(SNIPPETS).filter((k) => k.indexOf(typed) === 0).map((k) => ';' + k);
});Lookups over https
birc.fetch is a GET that returns a Promise of {status, text}. It is off until you flip the script’s “Allow network” toggle in the Scripts window, https-only, size- and time-capped, and it goes through the connection’s proxy — so a Tor profile stays on Tor. Always attach a .catch: an unhandled rejection in a script is silent.
Page titles for posted linksnetwork
Prints the title under the message, in the window it arrived in. The fetch callback runs on the originating connection, so birc.printTo is enough. Download urltitles.js
const URL_RE = /https:\/\/[^\s<>"')\]]+/;
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'", apos: "'" };
const decode = (s) => s.replace(/&(amp|lt|gt|quot|#39|apos);/g, (m, k) => ENTITIES[k]);
birc.on('message', function (e) {
if (e.isMe || e.isBacklog) return;
const m = URL_RE.exec(e.text);
if (!m) return;
const where = birc.sameNick(e.target, birc.nick) ? e.nick : e.target;
birc.fetch(m[0])
.then((r) => {
if (r.status !== 200) return;
const t = /<title[^>]*>([^<]{1,200})/i.exec(r.text);
if (t) birc.printTo(where, '↪ ' + decode(t[1].replace(/\s+/g, ' ').trim()));
})
.catch((err) => console.warn('title fetch failed:', m[0], err));
});/geoip — locate an IP or hostnamenetwork
Two chained fetches (DNS over https, then a geo lookup). Captures birc.network and birc.target up front and prints through birc.to(net) — the explicit way to pin output to where the command was typed. Download geoip.js
const IPV4_RE = /^\d{1,3}(\.\d{1,3}){3}$/;
const IPV6_RE = /^[0-9a-f]*:[0-9a-f:]+$/i;
birc.onCommand('geoip', function (args) {
const net = birc.network;
const target = birc.target;
const out = (text) => birc.to(net).print(text, target);
const query = (args || '').trim();
const locate = (ip, label) =>
birc.fetch('https://ipwho.is/' + ip)
.then((r) => {
const d = JSON.parse(r.text);
if (!d.success) { out('geoip: ' + label + ' — ' + (d.message || 'lookup failed')); return; }
const place = [d.city, d.region, d.country].filter(Boolean).join(', ');
const isp = d.connection && d.connection.isp;
const asn = d.connection && d.connection.asn ? 'AS' + d.connection.asn : null;
out('geoip: ' + label + ' → ' + [d.ip, place, isp, asn].filter(Boolean).join(' · '));
})
.catch((e) => out('geoip: lookup failed — ' + e));
if (!query) {
locate('', 'this client');
} else if (IPV4_RE.test(query) || IPV6_RE.test(query)) {
locate(query, query);
} else {
birc.fetch('https://dns.google/resolve?name=' + encodeURIComponent(query) + '&type=A')
.then((r) => {
const answer = (JSON.parse(r.text).Answer || []).find((a) => a.type === 1);
if (!answer) { out('geoip: no A record for ' + query); return; }
locate(answer.data, query + ' (' + answer.data + ')');
})
.catch((e) => out('geoip: DNS failed — ' + e));
}
});Operator tooling & debugging
Bulk mode changes, and a way to watch the raw server traffic from a script.
/voiceall and /devoiceall
birc.members returns each nick with its highest prefix, so the script strips it before building MODE lines. bIRC’s outbound flood control paces the burst. Download voiceall.js
const PER_LINE = 4;
function massMode(sign) {
const channel = birc.channel;
if (!channel) { birc.print('run this in a channel'); return; }
const nicks = birc.members(channel)
.filter((n) => sign === '+' ? !/^[~&@%+]/.test(n) : n.charAt(0) === '+')
.map((n) => n.replace(/^[~&@%+]+/, ''))
.filter((n) => !birc.sameNick(n, birc.nick));
for (let i = 0; i < nicks.length; i += PER_LINE) {
const chunk = nicks.slice(i, i + PER_LINE);
birc.command('mode ' + channel + ' ' + sign + 'v'.repeat(chunk.length) + ' ' + chunk.join(' '));
}
birc.print(sign + 'v for ' + nicks.length + ' in ' + channel);
}
birc.onCommand('voiceall', () => massMode('+'));
birc.onCommand('devoiceall', () => massMode('-'));/rawlog — log matching wire lines to the Console
The raw hook sees every inbound line as {command, numeric?, params, prefix, nick, tags, raw}. It is observe-only, and console.log goes to the script’s own Console, not the chat. Download rawlog.js
let pattern = null;
birc.onCommand('rawlog', function (args) {
const p = (args || '').trim();
if (!p || p === 'off') { pattern = null; birc.print('rawlog: off'); return; }
try { pattern = new RegExp(p, 'i'); }
catch (err) { birc.print('rawlog: bad pattern — ' + err); return; }
birc.print('rawlog: logging lines matching /' + p + '/i to the script Console');
});
birc.on('raw', function (e) {
if (pattern && pattern.test(e.raw)) {
console.log('[' + e.network + '] ' + (e.numeric ? 'numeric ' + e.numeric + ' ' : '') + e.raw);
}
});The API reference is in the documentation.