Skip to content

Instantly share code, notes, and snippets.

@notanimposter
Created June 15, 2020 07:26
Show Gist options
  • Save notanimposter/1e5a2375fa1c3e1df037ccec255753f4 to your computer and use it in GitHub Desktop.
Save notanimposter/1e5a2375fa1c3e1df037ccec255753f4 to your computer and use it in GitHub Desktop.
A work-in-progress IRC/Twitch chat library in Vala
namespace TwitchChat {
public class Chat {
public string name;
public string streamer {
owned get {
return name.substring(1);
}
}
public string[] users;
public bool justNamesed = false;
public Chat(string name) {
this.name = name;
users = new string[0];
}
}
public bool isMod(string tags) {
switch (IRCConnection.searchTag(tags, "user-type")) {
case "mod":
case "admin":
case "global_mod":
return true;
}
var badges = IRCConnection.searchTag(tags, "badges");
if (badges.index_of("broadcaster") >= 0) return true;
return false;
}
public class IRCConnection {
public bool printThings = true;
private void log(string whatever) {
if (printThings) print(whatever);
}
public signal void onDoneConnecting();
public signal void onMessage(Chat where, string who, string what, string tags);
public signal void onMessageSelf(Chat where, string who, string what, string tags);
public signal void onAction(Chat where, string who, string what, string tags);
public signal void onNotice(string notice);
public signal void onError(string error);
public signal void onDoneJoining(Chat where);
public signal void onUserJoin(Chat where, string who);
public signal void onUserPart(Chat where, string who);
public signal void onUserQuit(Chat where, string who, string what);
public signal void onDoneNamesing(Chat where);
//onMode
public signal void onTwitchNotice(Chat where, string notice, string tags);
public signal void onUserBanned(Chat where, string who, string tags);
public signal void onRoomState(Chat where, string tags);
public signal void onUserState(Chat where, string who, string tags);
public signal void onUserSubbed(Chat where, string who, string what, string tags);
public List<Chat> chats;
public string username;
string password;
GLib.SocketConnection connection;
GLib.DataInputStream response;
public bool connected;
Queue<string> toSend;
private Chat getChatByName(string name) {
foreach (Chat c in chats) {
if (c.name == name) return c;
}
error("fix this later");
}
public static string searchTag(string tags, string tagName) {
try {
MatchInfo match;
if (new Regex(tagName+"=([^;\\s]*)").match(tags, 0, out match))
return match.fetch(1);
} catch (RegexError e) {} //my regex is fine, thanks
return "";
}
public IRCConnection(string username, string password) {
this.username = username;
this.password = password;
chats = new List<Chat>();
connected = false;
toSend = new Queue<string>();
onDoneConnecting.connect(() => {
string cmd;
while ((cmd = toSend.pop_head()) != null) sendRaw(cmd);
cap_req("twitch.tv/membership");
cap_req("twitch.tv/tags");
cap_req("twitch.tv/commands");
});
}
~IRCConnection() {
log("Exiting\n");
quit();
}
public async void connect() {
try {
var resolver = Resolver.get_default();
log("Resolving irc.chat.twitch.tv...\n");
var addresses = yield resolver.lookup_by_name_async("irc.chat.twitch.tv", null);
var address = addresses.nth_data(0);
log("Resolved irc.chat.twitch.tv to %s\n".printf(address.to_string()));
var client = new SocketClient();
connection = yield client.connect_async(new InetSocketAddress(address, 6667));
log("Connected to port 6667\n");
this.connected = true;
response = new DataInputStream(connection.input_stream);
sendRaw("PASS %s".printf(password));
sendRaw("NICK %s".printf(username));
log("Logging in...\n");
wait.begin();
} catch (Error e) {
error("Could not connect: %s".printf(e.message));
}
}
public async void wait() {
print(""); //necessary. don't ask.
try {
var line = yield response.read_line_async();
if (line != null) parseLine(line);
} catch (Error e) {
log("Error waiting: %s\n".printf(e.message));
}
if (connected) wait.begin();
}
private void parseLine(string input) {
log("RECV %s\n".printf(input));
string line = input;
string tags = "";
if (line[0] == '@') {
tags = line.split(" ")[0];
line = line.substring(line.index_of(" ")+1);
}
if (line[0] != ':') {
var cmd = line.split(" ")[0].strip();
var msg = line.split(" :")[1].strip();
switch(cmd.up()) {
case "PING":
sendRaw(line.strip().replace("PING","PONG"));
break;
case "NOTICE":
onNotice(msg);
break;
case "ERROR":
onError(msg);
break;
default:
log("Unhandled server command: %s\n".printf(line));
break;
}
return;
}
if ((line.split(" ")[1][0]).isdigit()) {
var tokens = line.split(" ");
var cmd = tokens[1].strip();
var msg = line.split(" :")[1].strip();
switch(cmd) {
case "001":
onDoneConnecting();
break;
case "002":
case "003":
case "004":
case "375":
case "372":
break;
case "376":
//done logging in
break;
case "353":
//multiple?
var c = getChatByName(tokens[4]);
c.users = msg.split(" ");
c.justNamesed = true;
break;
case "366":
foreach (Chat c in chats) {
if (c.justNamesed) {
c.justNamesed = false;
onDoneNamesing(c); //c.users); //maybe fix it so it's copying the array
}
}
break;
default:
log("Unhandled numeric command: %s\n".printf(line));
break;
}
} else {
var csep = line.split(":");
var info = csep[1];
var msg = line.split(" :")[1];
var tokens = info.split(" ");
var cmd = tokens[1].strip();
var channel = tokens[2].strip();
var who = tokens[0].strip().split("!")[0];
switch (cmd.up()) {
case "PRIVMSG":
if (who == username) {
onMessageSelf(getChatByName(channel), who, msg, tags);
break;
}
if (msg.split(" ")[0] == "\001ACTION") {
msg = msg.replace("\001ACTION ", "");
onAction(getChatByName(channel), who, msg, tags);
} else onMessage(getChatByName(channel), who, msg, tags);
break;
case "QUIT":
onUserQuit(getChatByName(channel), who, msg);
break;
case "PART":
onUserPart(getChatByName(channel), who);
break;
case "JOIN":
if (who == username) {
chats.append(new Chat(channel));
onDoneJoining(getChatByName(channel));
} else onUserJoin(getChatByName(channel),who);
break;
case "USERNOTICE":
onUserSubbed(getChatByName(channel), who, msg, tags);
break;
case "NOTICE":
onTwitchNotice(getChatByName(channel), msg, tags);
break;
case "ROOMSTATE":
onRoomState(getChatByName(channel), tags);
break;
case "USERSTATE":
onUserState(getChatByName(channel), who, tags);
break;
case "CLEARCHAT":
break;
case "MODE":
break;
case "CAP":
break;
default:
log("Unhandled named command: %s\n".printf(line));
break;
}
}
}
private void sendRaw(string line) {
log("SEND %s\n".printf(line));
try {
connection.output_stream.write((line+"\r\n").data);
connection.output_stream.flush();
} catch (Error e) {
error("sendRaw: %s".printf(e.message));
}
}
public void send(string cmd) {
if (connected) sendRaw(cmd);
else toSend.push_tail(cmd);
}
public void join(string where) {
send("JOIN %s".printf(where));
}
public void part(string where) {
send("PART %s".printf(where));
}
public void quit() {
send("QUIT :bye");
connected = false;
}
public void privmsg(string where, string what) {
send("PRIVMSG %s :%s".printf(where, what));
}
public void names(string where) {
send("NAMES %s".printf(where));
}
public void cap_req(string what) {
send("CAP REQ :%s".printf(what));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment