Skip to content

Instantly share code, notes, and snippets.

@rbistolfi
Last active April 12, 2022 20:23
Show Gist options
  • Select an option

  • Save rbistolfi/bb7eb2185705044ca06adf1d16f84325 to your computer and use it in GitHub Desktop.

Select an option

Save rbistolfi/bb7eb2185705044ca06adf1d16f84325 to your computer and use it in GitHub Desktop.
Mini IRC client for making the quaraintein bearable
module irc
go 1.18
require (
github.com/charmbracelet/bubbletea v0.20.0
github.com/jiyeyuran/go-eventemitter v1.4.0
)
require (
github.com/containerd/console v1.0.3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
)
package main
import (
"os"
"fmt"
"log"
"net"
"bufio"
"time"
s "strings"
)
import "github.com/jiyeyuran/go-eventemitter"
import tea "github.com/charmbracelet/bubbletea"
// IRC client data structure
type T800 struct {
host string
port int
nick string
connection net.Conn
connected bool
emitter eventemitter.IEventEmitter
}
// Connect to an IRC network
func (bot *T800) connect() {
host := fmt.Sprintf("%s:%d", bot.host, bot.port)
con, err := net.Dial("tcp", host)
checkErr(err)
bot.connection = con
bot.connected = true
bot.connectionMade()
}
// Open and maintain a TCP connection
func (bot *T800) listen() {
if bot.connection == nil {
bot.connect()
bot.emitter.Emit("connection-made", bot)
}
reader := bufio.NewReader(bot.connection)
for bot.connected {
reply, err := reader.ReadBytes('\n')
checkErr(err)
message := NewIRCMessage(string(reply))
bot.emitter.Emit("message-received", bot, message)
bot.emitter.Emit(message.command, bot, message)
}
bot.emitter.Emit("connection-lost", bot)
}
// Send a command to the IRC server
func (bot *T800) sendCommand(command string) {
message := fmt.Sprintf("%s\r\n", command)
_, err := bot.connection.Write([]byte(message))
checkErr(err)
}
// Say something. A helper for PRIVMSG commands
func (bot *T800) say(who string, what string) {
message := fmt.Sprintf(
":%s PRIVMSG %s :%s",
bot.nick,
who,
what,
)
bot.sendCommand(message)
}
// Connection made callback for identifying against the net
func (bot *T800) connectionMade() {
bot.sendCommand(fmt.Sprintf("NICK %s", bot.nick))
bot.sendCommand(fmt.Sprintf("USER %s 8 * :T800", bot.nick))
}
// IRC client constructor
func NewT800(nick string) T800 {
bot := T800{
host: "irc.libera.chat",
port: 6667,
nick: nick,
}
bot.emitter = eventemitter.NewEventEmitter()
return bot
}
// An incomming IRC message
type IRCMessage struct {
raw string
prefix string
command string
arguments []string
}
// Parse IRC message and build an IRCMessage struct
func NewIRCMessage(raw string) IRCMessage {
msg := IRCMessage{}
msg.raw = raw
trailing := ""
if raw[0:1] == ":" {
result := s.SplitN(raw, " ", 2)
msg.prefix = result[0]
raw = result[1]
}
if s.Index(raw, " :") > -1 {
result := s.SplitN(raw, " :", 2)
raw = result[0]
trailing = result[1]
msg.arguments = s.Fields(raw)
msg.arguments = append(msg.arguments, trailing)
} else {
msg.arguments = s.Fields(raw)
}
msg.command, msg.arguments = msg.arguments[0], msg.arguments[1:]
return msg
}
// Application state
type model struct {
peer string
client *T800
messages chan string
buffer []string
input string
}
// A string type for dispatching
type IRCText string
// Init a model struct for bootstrapping the app
func NewModel(nick string, peer string) model {
bot := NewT800(nick)
m := model{
peer: peer,
}
m.messages = make(chan string)
m.client = &bot
go m.client.listen()
m.client.emitter.On("PING", func (c *T800, msg IRCMessage) {
c.sendCommand("PONG")
})
m.client.emitter.On("PRIVMSG", func (c *T800, msg IRCMessage) {
select {
case m.messages <- s.TrimSuffix(msg.arguments[1], "\n"):
// msg sent
default:
}
})
m.client.emitter.On("message-received", func (c *T800, msg IRCMessage) {
//fmt.Println(msg)
})
m.client.emitter.On("connection-made", func (c *T800) {
select {
case m.messages <- "Connection made":
// msg sent
default:
}
m.client.say(m.peer, "Connection made!")
})
m.client.emitter.On("connection-lost", func (c *T800) {
logMessage("Connection lost!")
})
return m
}
// Init state and app. Registers a func for periodically checking incomming msgs
func (m model) Init() tea.Cmd {
return tea.Batch(
tea.EnterAltScreen,
tick(m.messages),
tick(m.messages),
tick(m.messages),
)
}
// State update function. Called after user input
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "enter":
m.client.say(m.peer, m.input)
my_msg := formatMessage(m.client.nick, m.input)
m.buffer = append(m.buffer, my_msg)
m.input = ""
return m, nil
case "backspace":
if len(m.input) > 0 {
m.input = m.input[:len(m.input) - 1]
}
return m, nil
default:
m.input = fmt.Sprintf("%s%s", m.input, msg.String())
}
case IRCText:
msg_s := formatMessage(m.peer, string(msg))
m.buffer = append(m.buffer, msg_s)
}
// Return the updated model to the Bubble Tea runtime for processing.
// Note that we're not returning a command.
return m, nil
}
// Gather messages from the chan
func tick(sub chan string) tea.Cmd {
return func() tea.Msg {
return IRCText(<-sub)
}
}
// Render the UI
func (m model) View() string {
ss := ""
if m.client.connected {
ss = fmt.Sprintf("\nConnected to %s. Talking to %s\n", m.client.host, m.peer)
} else {
ss = fmt.Sprintf("\nConnecting to %s. Talking to %s\n", m.client.host, m.peer)
}
ss += fmt.Sprintf("%s\n", s.Repeat("-", 80))
for _, msg := range m.buffer {
ss += msg
}
ss += fmt.Sprintf("%s\n", s.Repeat("-", 80))
ss += fmt.Sprintf("> %s", m.input)
return ss
}
// DOIT
func main() {
nick := os.Args[1]
peer := os.Args[2]
m := NewModel(nick, peer)
p := tea.NewProgram(m, tea.WithAltScreen())
if err := p.Start(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
func logMessage(m string) {
fmt.Println(m)
}
func formatMessage(author string, msg string) string {
now := time.Now().Local()
now_s := now.Format(time.ANSIC)
return fmt.Sprintf("%s [%s] %s\n", now_s, author, msg)
}
Connection := Object clone
Connection connect := method(
write("Starting operations")
socket := Socket clone
socket setHost("irc.freenode.net")
socket setPort(6667)
socket connect()
self socket := socket
socket read
write(socket readBuffer)
self sendCommand("NICK rbistolfi2")
self sendCommand("USER rbistolfi2 0 * :Rodrigo Bistolfi")
socket read
write(socket readBuffer)
while(socket isOpen,
if (socket read,
self messageReceived
)
socket readBuffer empty
)
)
Connection sendCommand := method(command,
write("Writting #{command}" interpolate)
self socket write("#{command}\r\n" interpolate)
)
Connection messageReceived := method(
write(message)
message := self socket readBuffer asString
if(message containsSeq("PING"), self sendCommand("PONG"))
)
conn := Connection clone
conn connect()
import re
import random
import asyncio
class T800:
def __init__(self, host, port=6667, nick=None, channel=None):
self.host = host
self.port = port
self.handlers = []
self.nick = nick
self.channel = channel
async def connect(self):
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, loop=self.loop)
while True:
data = await self.reader.readline()
await self.message_received(data)
async def write(self, message):
self.writer.write(message.encode())
await self.writer.drain()
async def send_command(self, command):
await self.write('{}\r\n'.format(command))
async def message_received(self, data):
data = data.decode().strip()
#print(data)
print(repr(data))
# Ok server is talking to us
if 'NOTICE' in data:
await self.send_command('NICK %s' % self.nick)
await self.send_command('USER %s 8 * :T800' % self.nick)
await self.join(self.channel)
# Server is testing us
if 'PING' in data:
await self.send_command(':%s PONG' % self.nick)
for rx, handler in self.handlers:
# Do not talk to strangers
if 'PRIVMSG' in data and rx.match(data):
await handler(self, data)
async def join(self, channel):
await self.send_command('JOIN %s' % channel)
async def say(self, what, channel=None):
channel = channel or self.channel
await self.send_command(':%s PRIVMSG %s :%s' % (self.nick, channel, what))
def handle(self, rx: str):
def wrapper(func):
nonlocal rx
rx = re.compile(rx, re.I)
self.handlers.append((rx, func))
return func
return wrapper
def run(self):
self.loop = asyncio.get_event_loop()
self.loop.run_until_complete(self.connect())
self.loop.close()
c = T800('chat.freenode.net', nick='C_3PO', channel='#gus-challenge')
@c.handle(r'.*C[-_]?3PO.*')
async def quote_c3po(client, message):
quotes = [
"I suggest a new strategy, Artoo: let the Wookie win.",
"Sir, it's very possible this asteroid is not stable.",
"Don't you call me a mindless philosopher you overweight glob of grease!",
"R2 says the chances of survival are 725... to one",
"Wait. Oh My ! What have you done. I'm backwards you filthy furball",
"We're doomed",
"R2-D2, it is you, it Is You!",
"Excuse me sir, but that R2-D2 is in prime condition, a real bargain",
"If I told you half the things I've heard about this Jabba the Hutt, you'd probably short circuit",
"Die Jedi Dogs! Oh, what did I say?",
"R2D2! You know better than to trust a strange computer!",
"Goodness! Han Solo! It is I, C-3PO! You probably do not recognize me because of the red arm...",
"Taking one last look Sir, at my friends",
"I am C-3PO, human/cyborg relations. And you are?",
]
quote = random.choice(quotes)
await client.say(quote, channel='#gus-challenge')
c.run()
"Toy IRC client"
Object subclass: #IRCClient
instanceVariableNames: 'announcer stream connected nick channel'
classVariableNames: ''
package: 'IRC'
IRCClient >> announceLineReceived: aString
"Broadcast the message we just received. Useful for reacting to a message through the Announcements API"
| announcement |
announcement := LineReceivedAnnouncement new.
announcement data: aString.
announcer announce: announcement.
IRCClient >> onLineReceived: aBlock
"Register a message handler with our announcer. Registered blocks will receive a LineReceivedAnnouncement"
announcer when: LineReceivedAnnouncement do: aBlock.
IRCClient >> lineReceived: aString
"Our default message handler. Joins a channel, answers PINGs and talks like R2D2"
| quote |
((aString findString: 'hostname') > 0) ifTrue: [
Transcript cr; show: 'Connecting'.
stream sendCommand: 'NICK ', nick.
stream sendCommand: 'USER ', nick, ' 0 * :Rodrigo Bistolfi'.
^ self.
].
((aString findString: 'PING') > 0) ifTrue: [
Transcript cr; show: 'Answering PING'.
stream sendCommand: 'PONG'.
^ self.
].
(((aString findString: 'R2') > 0) and: ((aString findString: 'PRIVMSG') > 0)) ifTrue: [
Transcript cr; show: 'Answering R2D2'.
quote := #(
'bip bip biripipi',
'biri pipi bip bipbip',
'pi biri biri pibiri pipi',
'biripipipi bip biri bipip'
) atRandom.
self say: quote to: channel.
^ self.
]
IRCClient >> connect: aString
"Create a connection. I am not sure about the fork call, I need to learn more Pharo Smalltalk"
stream := SocketStream openConnectionToHostNamed: aString port: 6667.
stream timeout: 0.
connected := true.
[
[ connected ] whileTrue: [
self announceLineReceived: (stream nextLine).
(Delay forMilliseconds: 100) wait.
]
] fork.
IRCClient >> sendCommand: aString
"Forward command to our socket"
stream sendCommand: aString.
IRCClient >> disconnect
"Exit the main loop and close our socket"
connected := false.
stream close.
IRCClient >> say: aString to: aChannel
"Say something"
stream sendCommand: ':T800 PRIVMSG ', aChannel, ' :', aString.
IRCClient >> join: aChannel
"Join an IRC channel"
stream sendCommand: ':', nick, ' JOIN ', aChannel.
IRCCLient >> quit
"Disconnect from the IRC server and close connections"
self sendCommand: ':', nick, ' quit'.
self disconnect.
IRCClient >> initialize
"Create our announcer and connect the default message handler"
announcer := Announcer new.
self onLineReceived: [ :ann |
Transcript cr; show: (ann data).
self lineReceived: (ann data).
].
nick := 'R2D2'.
channel := '#gus-challenge'.
^ self.
Announcement subclass: #LineReceivedAnnouncement
"This is our message event. Has a data accessor for retrieving the IRC message"
instanceVariableNames: 'data'
classVariableNames: ''
package: 'IRC'!
LineReceivedAnnouncement >> data
"Expose or received message"
^ data.
LineReceivedAnnouncement >> data: aString
"Expose or received message"
data := aString.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment