diff --git a/application/CayaProtocolMessages.h b/application/CayaProtocolMessages.h index b82b034..8f5b665 100644 --- a/application/CayaProtocolMessages.h +++ b/application/CayaProtocolMessages.h @@ -54,7 +54,9 @@ enum im_what_code { IM_SEND_MESSAGE = 20, //! Chat message has been sent →Caya - // Requires: String "chat_id", String "user_id", String "body" + // If no user_id is specified, it's treated as a system message + // Requires: String "chat_id", String "body" + // Allows: String "user_id" IM_MESSAGE_SENT = 21, //! Chat message received →Caya @@ -76,7 +78,6 @@ enum im_what_code { IM_USER_STOPPED_TYPING = 25, - /* * Messages related to contact changes. */ diff --git a/application/CayaUtils.cpp b/application/CayaUtils.cpp index 09b5506..2ad63fa 100644 --- a/application/CayaUtils.cpp +++ b/application/CayaUtils.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -40,6 +41,31 @@ CayaStatusToString(CayaStatus status) } +bool +IsCommand(BString line) +{ + return line.StartsWith("/"); +} + + +BString +CommandName(BString line) +{ + BStringList words; + line.Split(" ", true, words); + return words.StringAt(0).RemoveFirst("/"); +} + + +BString +CommandArgs(BString line) +{ + BString remove("/"); + remove << CommandName(line) << " "; + return line.RemoveFirst(remove); +} + + BResources* CayaResources() { diff --git a/application/CayaUtils.h b/application/CayaUtils.h index bb17f51..a93b854 100644 --- a/application/CayaUtils.h +++ b/application/CayaUtils.h @@ -17,6 +17,10 @@ const char* CayaStatusToString(CayaStatus status); +bool IsCommand(BString line); +BString CommandName(BString line); +BString CommandArgs(BString line); + BResources* CayaResources(); const char* CayaAccountsPath(); @@ -33,7 +37,7 @@ const char* CayaContactCachePath(const char* accountName, const char* userIdenti rgb_color CayaTintColor(rgb_color color, int severity); rgb_color CayaForegroundColor(rgb_color background); -// Borrowed from BePodder's own libfunky +// Borrowed from BePodder's own libfunky. Groovy B) status_t ReadAttributeData(BNode* node, const char* name, char** buffer, int32 *size); status_t WriteAttributeMessage(BNode* node, const char* name, BMessage* data); status_t ReadAttributeMessage(BNode* node, const char* name, BMessage* data); diff --git a/application/ChatCommand.cpp b/application/ChatCommand.cpp new file mode 100644 index 0000000..117e346 --- /dev/null +++ b/application/ChatCommand.cpp @@ -0,0 +1,113 @@ +/* + * Copyright 2021, Jaidyn Levesque + * All rights reserved. Distributed under the terms of the MIT license. + */ + +#include "ChatCommand.h" + +#include + +#include "Conversation.h" +#include "MainWindow.h" +#include "TheApp.h" + + +ChatCommand::ChatCommand(const char* name, BMessage msg, bool toProtocol, + List argTypes) + : + fName(name), + fMessage(msg), + fToProto(toProtocol), + fArgTypes(argTypes) +{ +} + + +void +ChatCommand::SetDesc(const char* description) +{ + fDescription = description; +} + + +bool +ChatCommand::Parse(BString args, BString* errorMsg, Conversation* chat) +{ + BMessage* msg = new BMessage(fMessage); + msg->AddString("chat_id", chat->GetId()); + msg->AddInt64("instance", chat->GetProtocolLooper()->GetInstance()); + + if (fArgTypes.CountItems() == 0) { + msg->AddString("misc_str", args); + return _Send(msg, chat); + } + + if (_ProcessArgs(args, msg, errorMsg, chat) == true) + return _Send(msg, chat); + return false; +} + + +bool +ChatCommand::_ProcessArgs(BString args, BMessage* msg, BString* errorMsg, + Conversation* chat) +{ + int32 argCount = fArgTypes.CountItems(); + BStringList argList; + args.Split(" ", false, argList); + + for (int i = 0; i < argCount; i++) { + BString arg = argList.StringAt(i); + const char* strName = "misc_str"; + + switch (fArgTypes.ItemAt(i)) + { + case CMD_ROOM_PARTICIPANT: + { + if (chat->UserById(arg) == NULL) { + errorMsg->SetTo("%user% isn't a member of this room."); + errorMsg->ReplaceAll("%user%", arg); + return false; + } + msg->AddString("user_id", arg); + break; + } + case CMD_KNOWN_USER: + { + if (chat->GetProtocolLooper()->UserById(arg) == NULL) { + errorMsg->SetTo("You aren't contacts with and have no chats " + "in common with %user%. Shame."); + errorMsg->ReplaceAll("%user%", arg); + return false; + } + msg->AddString("user_id", arg); + break; + } + case CMD_ANY_USER: + msg->AddString("user_id", arg); + break; + case CMD_BODY_STRING: + strName = "body"; + default: + // If string's the last argument, it can be longer than one word + if (i == (argCount - 1) && argList.CountStrings() > argCount) + for (int j = i + 1; j < argList.CountStrings(); j++) + arg << " " << argList.StringAt(j); + msg->AddString(strName, arg); + } + } + return true; +} + + +bool +ChatCommand::_Send(BMessage* msg, Conversation* chat) +{ + if (fToProto == true) + chat->GetProtocolLooper()->PostMessage(msg); + else + ((TheApp*)be_app)->GetMainWindow()->PostMessage(msg); + return true; +} + + diff --git a/application/ChatCommand.h b/application/ChatCommand.h new file mode 100644 index 0000000..b1e9b58 --- /dev/null +++ b/application/ChatCommand.h @@ -0,0 +1,57 @@ +/* + * Copyright 2021, Jaidyn Levesque + * All rights reserved. Distributed under the terms of the MIT license. + */ +#ifndef CHAT_COMMAND_H +#define CHAT_COMMAND_H + +#include +#include + +#include + +class Conversation; + + +enum cmd_arg_type +{ + CMD_ROOM_PARTICIPANT = 'CArp', // "user_id" in message + CMD_KNOWN_USER = 'CAku', // "user_id" in message + CMD_ANY_USER = 'CAau', // "user_id" in message + CMD_BODY_STRING = 'CAbs', // "body" in message + CMD_MISC_STRING = 'CAms' // "misc_str" in message +}; + + +class ChatCommand { +public: + ChatCommand(const char* name, BMessage msg, bool toProtocol, + List argTypes); + + const char* GetName() { return fName.String(); } + + void SetDesc(const char* description); + const char* GetDesc() { return fDescription.String(); } + + bool Parse(BString args, BString* errorMsg, Conversation* chat); + +private: + bool _ProcessArgs(BString args, BMessage* msg, BString* errorMsg, + Conversation* chat); + + bool _Send(BMessage* msg, Conversation* chat); + + BString fName; + BString fDescription; + BMessage fMessage; + + bool fToProto; + List fArgTypes; +}; + + +typedef KeyMap CommandMap; + + +#endif // CHAT_COMMAND_H + diff --git a/application/Conversation.cpp b/application/Conversation.cpp index 4b73b53..0272c3b 100644 --- a/application/Conversation.cpp +++ b/application/Conversation.cpp @@ -13,6 +13,7 @@ #include "CayaProtocolMessages.h" #include "CayaRenderView.h" #include "CayaUtils.h" +#include "ChatCommand.h" #include "ConversationItem.h" #include "ConversationView.h" #include "MainWindow.h" @@ -82,7 +83,27 @@ Conversation::ImMessage(BMessage* msg) } case IM_SEND_MESSAGE: { - fMessenger.SendMessage(msg); + BString body; + if (msg->FindString("body", &body) != B_OK) + break; + + if (IsCommand(body.String()) == false) { + fMessenger.SendMessage(msg); + break; + } + + BString name = CommandName(body); + BString args = CommandArgs(body); + ChatCommand* cmd = _GetServer()->CommandById(name); + + if (cmd == NULL) { + _WarnUser(BString("That isn't a valid command.")); + break; + } + + BString error(""); + if (cmd->Parse(args, &error, this) == false) + _WarnUser(error); break; } case IM_ROOM_METADATA: @@ -330,6 +351,16 @@ Conversation::GetRole(BString id) } +void +Conversation::_WarnUser(BString message) +{ + BMessage* warning = new BMessage(IM_MESSAGE); + warning->AddInt32("im_what", IM_MESSAGE_RECEIVED); + warning->AddString("body", message.Append('\n', 1).InsertChars("-- ", 0)); + GetView()->MessageReceived(warning); +} + + void Conversation::_LogChatMessage(BMessage* msg) { @@ -449,8 +480,7 @@ Conversation::_EnsureUser(BMessage* msg) } // Not anywhere; create user else if (user == NULL) { - user = new User(id, - ((TheApp*)be_app)->GetMainWindow()->GetServer()->Looper()); + user = new User(id, _GetServer()->Looper()); user->SetProtocolLooper(fLooper); fLooper->AddUser(user); @@ -466,3 +496,10 @@ Conversation::_EnsureUser(BMessage* msg) } +Server* +Conversation::_GetServer() +{ + return ((TheApp*)be_app)->GetMainWindow()->GetServer(); +} + + diff --git a/application/Conversation.h b/application/Conversation.h index 0b8394a..7018bba 100644 --- a/application/Conversation.h +++ b/application/Conversation.h @@ -71,6 +71,8 @@ public: int32 GetFlags(int32 flags) { return fRoomFlags; } private: + void _WarnUser(BString message); + void _LogChatMessage(BMessage* msg); status_t _GetChatLogs(BMessage* msg); @@ -78,9 +80,10 @@ private: void _LoadRoomFlags(); void _EnsureCachePath(); - User* _EnsureUser(BMessage* msg); + Server* _GetServer(); + BMessenger fMessenger; ProtocolLooper* fLooper; ConversationView* fChatView; diff --git a/application/Makefile b/application/Makefile index ead102a..f4bafa3 100644 --- a/application/Makefile +++ b/application/Makefile @@ -36,6 +36,7 @@ SRCS = \ application/AccountManager.cpp \ application/CayaProtocolAddOn.cpp \ application/CayaUtils.cpp \ + application/ChatCommand.cpp \ application/Contact.cpp \ application/Conversation.cpp \ application/EditingFilter.cpp \ diff --git a/application/Server.cpp b/application/Server.cpp index 4ca3bd8..2b3b127 100644 --- a/application/Server.cpp +++ b/application/Server.cpp @@ -39,6 +39,7 @@ Server::Server() : BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE) { + _InitDefaultCommands(); } @@ -718,6 +719,20 @@ Server::AddConversation(Conversation* chat, int64 instance) } +CommandMap +Server::Commands() +{ + return fCommands; +} + + +ChatCommand* +Server::CommandById(BString id) +{ + return fCommands.ValueFor(id); +} + + ProtocolLooper* Server::_LooperFromMessage(BMessage* message) { @@ -833,4 +848,69 @@ Server::_GetRole(BMessage* msg) return new Role(title, perms, priority); } +void +Server::_InitDefaultCommands() +{ + List roomUser; + roomUser.AddItem(CMD_ROOM_PARTICIPANT); + List kickBody; + kickBody.AddItem(CMD_ROOM_PARTICIPANT); + kickBody.AddItem(CMD_BODY_STRING); + List knownUser; + knownUser.AddItem(CMD_KNOWN_USER); + List anyUser; + anyUser.AddItem(CMD_ANY_USER); + + BMessage kickMsg(IM_MESSAGE); + kickMsg.AddInt32("im_what", IM_ROOM_KICK_PARTICIPANT); + ChatCommand* kick = new ChatCommand("kick", kickMsg, true, kickBody); + kick->SetDesc("Force a user to temporarily leave the room, assuming your " + "power level's high enough."); + fCommands.AddItem("kick", kick); + + BMessage banMsg(IM_MESSAGE); + banMsg.AddInt32("im_what", IM_ROOM_BAN_PARTICIPANT); + ChatCommand* ban = new ChatCommand("ban", banMsg, true, kickBody); + ban->SetDesc("Kick a user out of the room and slam the door behind them― " + "locking it while you're at it."); + fCommands.AddItem("ban", ban); + + BMessage unbanMsg(IM_MESSAGE); + unbanMsg.AddInt32("im_what", IM_ROOM_UNBAN_PARTICIPANT); + ChatCommand* unban = new ChatCommand("unban", unbanMsg, true, anyUser); + unban->SetDesc("Undo a previous ban, allowing the user to rejoin (if they " + "still want to)."); + fCommands.AddItem("unban", unban); + + BMessage muteMsg(IM_MESSAGE); + muteMsg.AddInt32("im_what", IM_ROOM_MUTE_PARTICIPANT); + ChatCommand* mute = new ChatCommand("mute", muteMsg, true, roomUser); + mute->SetDesc("Disallow a user from sending visible messages."); + fCommands.AddItem("mute", mute); + + BMessage unmuteMsg(IM_MESSAGE); + unmuteMsg.AddInt32("im_what", IM_ROOM_UNMUTE_PARTICIPANT); + ChatCommand* unmute = new ChatCommand("unmute", unmuteMsg, true, roomUser); + unmute->SetDesc("Restore a user's ability to send messages."); + fCommands.AddItem("unmute", unmute); + + BMessage deafenMsg(IM_MESSAGE); + deafenMsg.AddInt32("im_what", IM_ROOM_DEAFEN_PARTICIPANT); + ChatCommand* deafen = new ChatCommand("deafen", deafenMsg, true, roomUser); + deafen->SetDesc("Disallow a user from reading messages sent in the room."); + fCommands.AddItem("deafen", deafen); + + BMessage undeafenMsg(IM_MESSAGE); + undeafenMsg.AddInt32("im_what", IM_ROOM_UNDEAFEN_PARTICIPANT); + ChatCommand* undeafen = new ChatCommand("undeafen", undeafenMsg, true, roomUser); + undeafen->SetDesc("Restore a user's ability to receive messages."); + fCommands.AddItem("undeafen", undeafen); + + BMessage inviteMsg(IM_MESSAGE); + inviteMsg.AddInt32("im_what", IM_ROOM_SEND_INVITE); + ChatCommand* invite = new ChatCommand("invite", inviteMsg, true, knownUser); + invite->SetDesc("Invite a user to the current room."); + fCommands.AddItem("invite", invite); +} + diff --git a/application/Server.h b/application/Server.h index ef0a772..55cccb2 100644 --- a/application/Server.h +++ b/application/Server.h @@ -12,6 +12,7 @@ #include #include "CayaConstants.h" +#include "ChatCommand.h" #include "Contact.h" #include "Conversation.h" #include "ProtocolLooper.h" @@ -58,6 +59,9 @@ public: Conversation* ConversationById(BString id, int64 instance); void AddConversation(Conversation* chat, int64 instance); + CommandMap Commands(); + ChatCommand* CommandById(BString id); + private: ProtocolLooper* _LooperFromMessage(BMessage* message); @@ -68,11 +72,13 @@ private: Role* _GetRole(BMessage* msg); + void _InitDefaultCommands(); void _ReplicantStatusNotify(CayaStatus status); ProtocolLoopers fLoopers; AccountInstances fAccounts; + CommandMap fCommands; BString fMySelf; }; diff --git a/application/views/ConversationView.cpp b/application/views/ConversationView.cpp index c737358..b3a6d5c 100644 --- a/application/views/ConversationView.cpp +++ b/application/views/ConversationView.cpp @@ -111,14 +111,13 @@ ConversationView::ImMessage(BMessage* msg) } case IM_MESSAGE_RECEIVED: { - BString message = msg->FindString("body"); BString id = msg->FindString("user_id"); User* sender = fConversation->UserById(id); - BString uname = sender->GetName(); // Send a notification, if it's appropriate if ((Window() == NULL || Window()->IsActive() == false) - && (!CayaPreferences::Item()->NotifyNewMessage)) + && (!CayaPreferences::Item()->NotifyNewMessage) + && sender != NULL) { fMessageCount++; BString notify_message; @@ -128,14 +127,14 @@ ConversationView::ImMessage(BMessage* msg) notify_message << " new message from "; else notify_message << " new messages from "; - notify_message << uname; + notify_message << sender->GetName(); BNotification notification(B_INFORMATION_NOTIFICATION); notification.SetGroup(BString("Caya")); notification.SetTitle(BString("New message")); notification.SetIcon(sender->AvatarBitmap()); notification.SetContent(notify_message); - notification.SetMessageID(uname); + notification.SetMessageID(sender->GetName()); notification.Send(); // Check if the user want the notification if (!CayaPreferences::Item()->NotifyNewMessage)