commit fb57a410218eecaf9d9cc44ead33b6fe74031436 Author: Eugen Wissner Date: Tue Sep 30 18:26:11 2025 +0200 Initial commit diff --git a/.project b/.project new file mode 100644 index 0000000..81083cc --- /dev/null +++ b/.project @@ -0,0 +1,3 @@ +{ + 'srcDirectory' : 'src' +} \ No newline at end of file diff --git a/src/.properties b/src/.properties new file mode 100644 index 0000000..ad0471d --- /dev/null +++ b/src/.properties @@ -0,0 +1,3 @@ +{ + #format : #tonel +} \ No newline at end of file diff --git a/src/TinyChat-client/TCConsole.class.st b/src/TinyChat-client/TCConsole.class.st new file mode 100644 index 0000000..3f7055e --- /dev/null +++ b/src/TinyChat-client/TCConsole.class.st @@ -0,0 +1,62 @@ +Class { + #name : 'TCConsole', + #superclass : 'SpPresenter', + #instVars : [ + 'chat', + 'list', + 'input' + ], + #category : 'TinyChat-client', + #package : 'TinyChat-client' +} + +{ #category : 'as yet unclassified' } +TCConsole class >> attach: aTinyChat [ + + | window | + window := self new chat: aTinyChat. + window open whenClosedDo: [ aTinyChat disconnect ]. + ^ window +] + +{ #category : 'as yet unclassified' } +TCConsole class >> defaultLayout [ + ^ SpBoxLayout newTopToBottom + add: #list; add: #input; yourself +] + +{ #category : 'accessing' } +TCConsole >> chat: anObject [ + chat := anObject +] + +{ #category : 'accessing' } +TCConsole >> initializeWidgets [ + + list := SpListPresenter new. + input := SpTextInputFieldPresenter new + placeholder: 'Type your message here...'; + enabled: true; + whenSubmitDo: [ :string | chat send: string. input text: '' ]. + self focusOrder add: input +] + +{ #category : 'accessing' } +TCConsole >> input [ + ^ input +] + +{ #category : 'accessing' } +TCConsole >> list [ + ^ list +] + +{ #category : 'accessing' } +TCConsole >> print: aCollectionOfMessages [ + list items: (aCollectionOfMessages collect: [ :m | m printString ]) +] + +{ #category : 'accessing' } +TCConsole >> title [ + ^ 'TinyChat' +] diff --git a/src/TinyChat-client/TinyChat.class.st b/src/TinyChat-client/TinyChat.class.st new file mode 100644 index 0000000..f19e3a4 --- /dev/null +++ b/src/TinyChat-client/TinyChat.class.st @@ -0,0 +1,130 @@ +Class { + #name : 'TinyChat', + #superclass : 'Object', + #instVars : [ + 'url', + 'login', + 'exit', + 'messages', + 'console', + 'lastMessageIndex' + ], + #category : 'TinyChat-client', + #package : 'TinyChat-client' +} + +{ #category : 'as yet unclassified' } +TinyChat class >> connect: aHost port: aPort login: aLogin [ + + ^ self new + host: aHost port: aPort login: aLogin; + start +] + +{ #category : 'initialization' } +TinyChat >> cmdLastMessageID [ + ^ self command: '/messages/count' +] + +{ #category : 'initialization' } +TinyChat >> cmdMessagesFromLastIndexToEnd [ + "Returns the server messages from my current last index to the last on the server." + ^ self command: '/messages' argument: lastMessageIndex +] + +{ #category : 'initialization' } +TinyChat >> cmdNewMessage [ + ^self command: '/messages/add' +] + +{ #category : 'initialization' } +TinyChat >> command: aPath [ + ^'{1}{2}' format: { url . aPath } +] + +{ #category : 'initialization' } +TinyChat >> command: aPath argument: anArgument [ + ^'{1}{2}/{3}' format: { url . aPath . anArgument asString } +] + +{ #category : 'initialization' } +TinyChat >> disconnect [ + self sendNewMessage: (TCMessage from: login text: 'I exited from the chat room.'). + exit := true +] + +{ #category : 'as yet unclassified' } +TinyChat >> host: aHost port: aPort login: aLogin [ + url := 'http://' , aHost , ':' , aPort asString. + login := aLogin +] + +{ #category : 'initialization' } +TinyChat >> initialize [ + super initialize. + exit := false. + lastMessageIndex := 0. + messages := OrderedCollection new +] + +{ #category : 'initialization' } +TinyChat >> readLastMessageID [ + | id | + id := (ZnClient new url: self cmdLastMessageID; get) asInteger. + id = 0 ifTrue: [ id := 1 ]. + ^ id +] + +{ #category : 'initialization' } +TinyChat >> readMissingMessages [ + "Gets the new messages that have been posted since the last request." + | response receivedMessages | + response := (ZnClient new url: self cmdMessagesFromLastIndexToEnd; get). + ^ response + ifNil: [ 0 ] + ifNotNil: [ + receivedMessages := response substrings: (String crlf). + receivedMessages do: [ :msg | messages add: (TCMessage fromString: msg) ]. + receivedMessages size + ] +] + +{ #category : 'initialization' } +TinyChat >> refreshMessages [ + [ + [ exit ] whileFalse: [ + (Delay forSeconds: 2) wait. + lastMessageIndex := lastMessageIndex + (self readMissingMessages). + console print: messages + ] + ] fork +] + +{ #category : 'initialization' } +TinyChat >> send: aString [ + "When we send a message, we push it to the server and in addition + we update the local list of posted messages" + + | msg | + msg := TCMessage from: login text: aString. + self sendNewMessage: msg. + lastMessageIndex := lastMessageIndex + (self readMissingMessages). + console print: messages +] + +{ #category : 'initialization' } +TinyChat >> sendNewMessage: aMessage [ + ^ ZnClient new + url: self cmdNewMessage; + formAt: 'sender' put: (aMessage sender); + formAt: 'text' put: (aMessage text); + post +] + +{ #category : 'initialization' } +TinyChat >> start [ + console := TCConsole attach: self. + self sendNewMessage: (TCMessage from: login text: 'I joined the chat room'). + lastMessageIndex := self readLastMessageID. + self refreshMessages +] diff --git a/src/TinyChat-client/package.st b/src/TinyChat-client/package.st new file mode 100644 index 0000000..acbdb12 --- /dev/null +++ b/src/TinyChat-client/package.st @@ -0,0 +1 @@ +Package { #name : 'TinyChat-client' } diff --git a/src/TinyChat-server/TCMessageQueue.class.st b/src/TinyChat-server/TCMessageQueue.class.st new file mode 100644 index 0000000..859c77d --- /dev/null +++ b/src/TinyChat-server/TCMessageQueue.class.st @@ -0,0 +1,46 @@ +Class { + #name : 'TCMessageQueue', + #superclass : 'Object', + #instVars : [ + 'messages' + ], + #category : 'TinyChat-server', + #package : 'TinyChat-server' +} + +{ #category : 'adding' } +TCMessageQueue >> add: aMessage [ + messages add: aMessage +] + +{ #category : 'adding' } +TCMessageQueue >> formattedMessagesFrom: aMessageNumber [ + + ^ String streamContents: [ :formattedMessagesStream | + (self listFrom: aMessageNumber) + do: [ :m | formattedMessagesStream << m printString ] + ] +] + +{ #category : 'initialization' } +TCMessageQueue >> initialize [ + super initialize. + messages := OrderedCollection new +] + +{ #category : 'adding' } +TCMessageQueue >> listFrom: aIndex [ + ^ (aIndex > 0 and: [ aIndex <= messages size ]) + ifTrue: [ messages copyFrom: aIndex to: messages size ] + ifFalse: [ #() ] +] + +{ #category : 'adding' } +TCMessageQueue >> reset [ + messages removeAll +] + +{ #category : 'adding' } +TCMessageQueue >> size [ + ^ messages size +] diff --git a/src/TinyChat-server/TCServer.class.st b/src/TinyChat-server/TCServer.class.st new file mode 100644 index 0000000..cda06a0 --- /dev/null +++ b/src/TinyChat-server/TCServer.class.st @@ -0,0 +1,75 @@ +Class { + #name : 'TCServer', + #superclass : 'Object', + #instVars : [ + 'teapotServer', + 'messagesQueue' + ], + #category : 'TinyChat-server', + #package : 'TinyChat-server' +} + +{ #category : 'initialization' } +TCServer class >> startOn: aPortNumber [ + ^self new + initializePort: aPortNumber; + registerRoutes; + registerErrorHandlers; + yourself +] + +{ #category : 'initialization' } +TCServer class >> stopAll [ + self allInstancesDo: #stop +] + +{ #category : 'initialization' } +TCServer >> addMessage: aRequest [ + messagesQueue add: (TCMessage from: (aRequest at: #sender) text: (aRequest at: #text)). +] + +{ #category : 'initialization' } +TCServer >> initialize [ + super initialize. + messagesQueue := TCMessageQueue new +] + +{ #category : 'initialization' } +TCServer >> initializePort: anInteger [ + teapotServer := Teapot configure: { + #defaultOutput -> #text. + #port -> anInteger. + #debugMode -> true + }. + teapotServer start +] + +{ #category : 'initialization' } +TCServer >> messageCount [ + ^ messagesQueue size +] + +{ #category : 'initialization' } +TCServer >> messagesFrom: request [ + ^ messagesQueue formattedMessagesFrom: (request at: #id) +] + +{ #category : 'initialization' } +TCServer >> registerErrorHandlers [ + teapotServer + exception: KeyNotFound -> (TeaResponse notFound body: 'No such message') +] + +{ #category : 'initialization' } +TCServer >> registerRoutes [ + teapotServer + GET: '/messages/count' -> (Send message: #messageCount to: self); + GET: '/messages/' -> (Send message: #messagesFrom: to: self); + POST: '/messages/add' -> (Send message: #addMessage: to: self) +] + +{ #category : 'initialization' } +TCServer >> stop [ + teapotServer stop. + messagesQueue reset +] diff --git a/src/TinyChat-server/package.st b/src/TinyChat-server/package.st new file mode 100644 index 0000000..9fa4170 --- /dev/null +++ b/src/TinyChat-server/package.st @@ -0,0 +1 @@ +Package { #name : 'TinyChat-server' } diff --git a/src/TinyChat/TCMessage.class.st b/src/TinyChat/TCMessage.class.st new file mode 100644 index 0000000..64962a9 --- /dev/null +++ b/src/TinyChat/TCMessage.class.st @@ -0,0 +1,73 @@ +Class { + #name : 'TCMessage', + #superclass : 'Object', + #instVars : [ + 'sender', + 'text', + 'separator' + ], + #category : 'TinyChat', + #package : 'TinyChat' +} + +{ #category : 'instance creation' } +TCMessage class >> from: aSender text: aText [ + + ^ self new sender: aSender; text: aText; yourself +] + +{ #category : 'instance creation' } +TCMessage class >> fromString: aString [ + ^ self new + fromString: aString; + yourself +] + +{ #category : 'initialization' } +TCMessage >> fromString: aString [ + "Compose a message from a string of this form 'sender>message'." + + | items | + items := aString substrings: separator. + self sender: items first. + self text: items second. +] + +{ #category : 'initialization' } +TCMessage >> initialize [ + super initialize. + separator := '>' +] + +{ #category : 'printing' } +TCMessage >> printOn: aStream [ + "Generate a string representation of the receiver based on its instance variables." + + aStream + << self sender; << separator; + << self text; << String crlf +] + +{ #category : 'accessing' } +TCMessage >> sender [ + + ^ sender +] + +{ #category : 'accessing' } +TCMessage >> sender: anObject [ + + sender := anObject +] + +{ #category : 'accessing' } +TCMessage >> text [ + + ^ text +] + +{ #category : 'accessing' } +TCMessage >> text: anObject [ + + text := anObject +] diff --git a/src/TinyChat/package.st b/src/TinyChat/package.st new file mode 100644 index 0000000..fe8e725 --- /dev/null +++ b/src/TinyChat/package.st @@ -0,0 +1 @@ +Package { #name : 'TinyChat' }