Basic command storage and parsing

A new class was added (ChatCommand) to represent commands, which are
all stored in the Server iself.

A command can be assigned any arbitrary BMessage, and can be given
arguments with some semantic meaning (so far just "string" or "room
participant," etc).

"invite" and moderation commands were added (ban/kick/mute etc).
This commit is contained in:
Jaidyn Ann 2021-06-15 00:19:52 -05:00
parent a4e2b1dc5a
commit 46d6d0a0b0
11 changed files with 339 additions and 12 deletions

View File

@ -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.
*/

View File

@ -12,6 +12,7 @@
#include <FindDirectory.h>
#include <IconUtils.h>
#include <Path.h>
#include <StringList.h>
#include <kernel/fs_attr.h>
@ -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()
{

View File

@ -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);

113
application/ChatCommand.cpp Normal file
View File

@ -0,0 +1,113 @@
/*
* Copyright 2021, Jaidyn Levesque <jadedctrl@teknik.io>
* All rights reserved. Distributed under the terms of the MIT license.
*/
#include "ChatCommand.h"
#include <StringList.h>
#include "Conversation.h"
#include "MainWindow.h"
#include "TheApp.h"
ChatCommand::ChatCommand(const char* name, BMessage msg, bool toProtocol,
List<int32> 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;
}

57
application/ChatCommand.h Normal file
View File

@ -0,0 +1,57 @@
/*
* Copyright 2021, Jaidyn Levesque <jadedctrl@teknik.io>
* All rights reserved. Distributed under the terms of the MIT license.
*/
#ifndef CHAT_COMMAND_H
#define CHAT_COMMAND_H
#include <Message.h>
#include <String.h>
#include <libsupport/KeyMap.h>
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<int32> 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<int32> fArgTypes;
};
typedef KeyMap<BString, ChatCommand*> CommandMap;
#endif // CHAT_COMMAND_H

View File

@ -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();
}

View File

@ -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;

View File

@ -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 \

View File

@ -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<int32> roomUser;
roomUser.AddItem(CMD_ROOM_PARTICIPANT);
List<int32> kickBody;
kickBody.AddItem(CMD_ROOM_PARTICIPANT);
kickBody.AddItem(CMD_BODY_STRING);
List<int32> knownUser;
knownUser.AddItem(CMD_KNOWN_USER);
List<int32> 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);
}

View File

@ -12,6 +12,7 @@
#include <libsupport/KeyMap.h>
#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;
};

View File

@ -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)