Last active
April 12, 2022 20:23
-
-
Save rbistolfi/bb7eb2185705044ca06adf1d16f84325 to your computer and use it in GitHub Desktop.
Mini IRC client for making the quaraintein bearable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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