Chat-O-Matic/application/Server.cpp

946 lines
21 KiB
C++
Raw Normal View History

/*
2011-12-03 16:38:03 -06:00
* Copyright 2009-2011, Andrea Anzani. All rights reserved.
* Copyright 2009-2011, Pier Luigi Fiorini. All rights reserved.
* Copyright 2021, Jaidyn Levesque. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Andrea Anzani, andrea.anzani@gmail.com
* Pier Luigi Fiorini, pierluigi.fiorini@gmail.com
* Jaidyn Levesque, jadedctrl@teknik.io
*
* Contributors:
* Dario Casalinuovo
*/
#include "Server.h"
#include <Application.h>
#include <Debug.h>
#include <Entry.h>
2010-05-30 07:17:32 -05:00
#include <Notification.h>
#include <Path.h>
#include <StringList.h>
#include <TranslationUtils.h>
#include "Account.h"
#include "AccountManager.h"
#include "CayaMessages.h"
#include "CayaProtocol.h"
#include "CayaPreferences.h"
#include "CayaProtocolMessages.h"
#include "CayaUtils.h"
#include "DefaultItems.h"
#include "ImageCache.h"
#include "InviteDialogue.h"
#include "ProtocolLooper.h"
#include "ProtocolManager.h"
#include "RoomFlags.h"
#include "RosterItem.h"
#include "UserInfoWindow.h"
Server::Server()
2010-05-19 17:28:26 -05:00
:
BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE)
{
}
2010-05-19 17:28:26 -05:00
void
Server::Quit()
{
for (int i = 0; i < fLoopers.CountItems(); i++)
RemoveProtocolLooper(fLoopers.KeyAt(i));
}
void
Server::LoginAll()
{
for (uint32 i = 0; i < fLoopers.CountItems(); i++) {
ProtocolLooper* looper = fLoopers.ValueAt(i);
BMessage* msg = new BMessage(IM_MESSAGE);
msg->AddInt32("im_what", IM_SET_OWN_STATUS);
msg->AddInt32("status", CAYA_ONLINE);
looper->PostMessage(msg);
}
}
filter_result
Server::Filter(BMessage* message, BHandler **target)
{
filter_result result = B_DISPATCH_MESSAGE;
2010-05-19 17:28:26 -05:00
switch (message->what) {
case CAYA_CLOSE_CHAT_WINDOW:
{
BString id = message->FindString("chat_id");
if (id.Length() > 0) {
bool found = false;
// Conversation* item = fChatMap.ValueFor(id, &found);
}
result = B_SKIP_MESSAGE;
break;
}
case IM_MESSAGE:
result = ImMessage(message);
break;
case CAYA_REPLICANT_MESSENGER:
{
BMessenger* messenger = new BMessenger();
status_t ret = message->FindMessenger(
"messenger", messenger);
if (ret != B_OK || !messenger->IsValid()) {
printf("err %s\n", strerror(ret));
break;
}
AccountManager* accountManager = AccountManager::Get();
accountManager->SetReplicantMessenger(messenger);
accountManager->ReplicantStatusNotify(accountManager->Status());
break;
}
case CAYA_DISABLE_ACCOUNT:
{
int64 instance = 0;
if (message->FindInt64("instance", &instance) != B_OK) {
result = B_SKIP_MESSAGE;
break;
}
RemoveProtocolLooper(instance);
break;
}
case CAYA_USER_INFO:
{
User* user = _EnsureUser(message);
if (user != NULL) {
UserInfoWindow* win = new UserInfoWindow(user);
win->Show();
}
break;
}
2021-06-15 00:45:51 -05:00
case CAYA_REQUEST_HELP:
{
BString body;
BString cmd_name = message->FindString("misc_str");
int64 instance = message->FindInt64("instance");
2021-06-15 00:45:51 -05:00
Conversation* chat = _EnsureConversation(message);
if (chat == NULL)
break;
if (cmd_name.IsEmpty() == false) {
ChatCommand* cmd = CommandById(cmd_name, instance);
2021-06-15 00:45:51 -05:00
if (cmd == NULL)
body = "-- That command doesn't exist. Try '/help' for a "
"list.\n";
else {
body = "** ";
body << cmd->GetName() << "" << cmd->GetDesc() << "\n";
}
}
else {
CommandMap combinedCmds;
combinedCmds.AddList(fCommands);
combinedCmds.AddList(chat->GetProtocolLooper()->Commands());
2021-06-15 00:45:51 -05:00
body << "** Commands: ";
for (int i = 0; i < combinedCmds.CountItems(); i++) {
ChatCommand* cmd = combinedCmds.ValueAt(i);
2021-06-15 00:45:51 -05:00
if (i > 0) body << ", ";
body << cmd->GetName();
}
body << "\n";
}
BMessage* help = new BMessage(IM_MESSAGE);
help->AddInt32("im_what", IM_MESSAGE_RECEIVED);
help->AddString("body", body);
help->AddInt64("when", 0);
chat->ImMessage(help);
break;
}
default:
// Dispatch not handled messages to main window
break;
}
return result;
}
filter_result
Server::ImMessage(BMessage* msg)
{
filter_result result = B_DISPATCH_MESSAGE;
int32 im_what = msg->FindInt32("im_what");
switch (im_what) {
case IM_CONTACT_LIST:
{
int i = 0;
BString id;
while (msg->FindString("user_id", i++, &id) == B_OK)
_EnsureContact(msg);
result = B_SKIP_MESSAGE;
break;
}
case IM_OWN_STATUS_SET:
{
int32 status;
const char* protocol;
if (msg->FindInt32("status", &status) != B_OK)
return B_SKIP_MESSAGE;
if (msg->FindString("protocol", &protocol) != B_OK)
return B_SKIP_MESSAGE;
AccountManager* accountManager = AccountManager::Get();
accountManager->SetStatus((CayaStatus)status);
break;
}
case IM_STATUS_SET:
{
int32 status;
if (msg->FindInt32("status", &status) != B_OK)
return B_SKIP_MESSAGE;
User* user = _EnsureUser(msg);
if (!user)
break;
user->SetNotifyStatus((CayaStatus)status);
BString statusMsg;
if (msg->FindString("message", &statusMsg) == B_OK) {
user->SetNotifyPersonalStatus(statusMsg);
}
break;
}
case IM_OWN_CONTACT_INFO:
{
Contact* contact = _EnsureContact(msg);
if (contact != NULL) {
contact->GetProtocolLooper()->SetOwnId(contact->GetId());
}
break;
}
case IM_CONTACT_INFO:
{
Contact* contact = _EnsureContact(msg);
if (!contact)
break;
2010-05-19 12:11:22 -05:00
const char* name = NULL;
2021-06-07 11:45:30 -05:00
if ((msg->FindString("user_name", &name) == B_OK)
&& (strcmp(name, "") != 0))
contact->SetNotifyName(name);
BString status;
if (msg->FindString("message", &status) == B_OK)
contact->SetNotifyPersonalStatus(status);
break;
}
case IM_EXTENDED_CONTACT_INFO:
{
Contact* contact = _EnsureContact(msg);
if (!contact)
break;
if (contact->GetName().Length() > 0)
break;
const char* name = NULL;
2021-06-07 11:45:30 -05:00
if ((msg->FindString("full_name", &name) == B_OK)
&& (strcmp(name, "") != 0))
contact->SetNotifyName(name);
2021-06-07 11:45:30 -05:00
else if ((msg->FindString("user_name", &name) == B_OK)
&& (strcmp(name, "") != 0))
contact->SetNotifyName(name);
break;
}
case IM_AVATAR_SET:
{
User* user = _EnsureUser(msg);
if (!user)
break;
entry_ref ref;
if (msg->FindRef("ref", &ref) == B_OK) {
BBitmap* bitmap = BTranslationUtils::GetBitmap(&ref);
user->SetNotifyAvatarBitmap(bitmap);
} else
user->SetNotifyAvatarBitmap(NULL);
break;
}
case IM_CREATE_CHAT:
{
BString user_id = msg->FindString("user_id");
if (user_id.IsEmpty() == false) {
User* user = ContactById(user_id, msg->FindInt64("instance"));
user->GetProtocolLooper()->PostMessage(msg);
}
break;
}
case IM_CHAT_CREATED:
{
Conversation* chat = _EnsureConversation(msg);
User* user = _EnsureUser(msg);
if (chat != NULL && user != NULL) {
chat->AddUser(user);
chat->ShowView(false, true);
}
break;
}
case IM_JOIN_ROOM:
{
SendProtocolMessage(msg);
break;
}
case IM_ROOM_JOINED:
{
_EnsureConversation(msg);
break;
}
case IM_ROOM_PARTICIPANTS:
{
Conversation* chat = _EnsureConversation(msg);
BStringList ids;
BStringList name;
msg->FindStrings("user_name", &name);
if (msg->FindStrings("user_id", &ids) != B_OK)
break;
ProtocolLooper* protoLooper = _LooperFromMessage(msg);
for (int i = 0; i < ids.CountStrings(); i++) {
User* user = _EnsureUser(ids.StringAt(i), protoLooper);
if (name.CountStrings() >= i) {
user->SetNotifyName(name.StringAt(i));
}
chat->AddUser(user);
}
break;
}
case IM_ROOM_PARTICIPANT_JOINED:
case IM_ROOM_PARTICIPANT_LEFT:
case IM_ROOM_PARTICIPANT_BANNED:
case IM_ROOM_PARTICIPANT_KICKED:
{
Conversation* chat = _EnsureConversation(msg);
if (chat == NULL)
break;
chat->ImMessage(msg);
break;
}
case IM_ROOM_METADATA:
{
Conversation* chat = _EnsureConversation(msg);
if (chat != NULL)
chat->ImMessage(msg);
break;
}
2021-06-07 11:45:30 -05:00
case IM_ROOM_ROLECHANGED:
{
Conversation* chat = _EnsureConversation(msg);
BString user_id;
Role* role = _GetRole(msg);
if (chat == NULL || msg->FindString("user_id", &user_id) != B_OK
|| role == NULL)
break;
chat->SetRole(user_id, role);
break;
}
2021-06-07 11:45:30 -05:00
case IM_ROOM_NAME_SET:
{
BString name;
Conversation* chat = _EnsureConversation(msg);
if (msg->FindString("chat_name", &name) != B_OK || chat == NULL)
break;
chat->SetNotifyName(name.String());
break;
}
2021-06-07 11:45:30 -05:00
case IM_ROOM_SUBJECT_SET:
{
BString subject;
Conversation* chat = _EnsureConversation(msg);
if (msg->FindString("subject", &subject) != B_OK || chat == NULL)
break;
chat->SetNotifySubject(subject.String());
break;
}
case IM_SEND_MESSAGE:
{
// Route this message through the appropriate ProtocolLooper
Conversation* conversation = _EnsureConversation(msg);
if (conversation->GetProtocolLooper())
conversation->GetProtocolLooper()->PostMessage(msg);
break;
}
case IM_MESSAGE_SENT:
case IM_MESSAGE_RECEIVED:
{
Conversation* item = _EnsureConversation(msg);
item->ImMessage(msg);
break;
}
case IM_ROOM_INVITE_RECEIVED:
{
BString chat_id;
User* user = _EnsureUser(msg);
BString user_id = msg->FindString("user_id");
BString user_name = user_id;
BString chat_name = msg->FindString("chat_name");
BString body = msg->FindString("body");
ProtocolLooper* looper = _LooperFromMessage(msg);
if (msg->FindString("chat_id", &chat_id) != B_OK || looper == NULL)
{
result = B_SKIP_MESSAGE;
break;
}
if (chat_name.IsEmpty() == true)
chat_name = chat_id;
if (user != NULL)
user_name = user->GetName();
BString alertBody("You've been invited to %room%.");
if (user_id.IsEmpty() == false)
alertBody = "%user% has invited you to %room%.";
if (body.IsEmpty() == false)
alertBody << "\n\n\"%body%\"";
alertBody.ReplaceAll("%user%", user_name);
alertBody.ReplaceAll("%room%", chat_name);
alertBody.ReplaceAll("%body%", body);
BMessage* accept = new BMessage(IM_MESSAGE);
accept->AddInt32("im_what", IM_ROOM_INVITE_ACCEPT);
accept->AddString("chat_id", chat_id);
BMessage* reject = new BMessage(IM_MESSAGE);
accept->AddInt32("im_what", IM_ROOM_INVITE_REFUSE);
reject->AddString("chat_id", chat_id);
InviteDialogue* invite = new InviteDialogue(BMessenger(looper),
"Invitation received",
alertBody.String(), accept, reject);
invite->Go();
break;
}
case IM_USER_STARTED_TYPING:
case IM_USER_STOPPED_TYPING:
{
// User* user = _EnsureUser();
// Conversation* chat = _EnsureConversation();
result = B_SKIP_MESSAGE;
break;
}
2010-05-30 07:17:32 -05:00
case IM_PROGRESS:
{
const char* protocol = NULL;
const char* title = NULL;
const char* message = NULL;
float progress = 0.0f;
if (msg->FindString("protocol", &protocol) != B_OK)
return result;
if (msg->FindString("title", &title) != B_OK)
return result;
if (msg->FindString("message", &message) != B_OK)
return result;
if (msg->FindFloat("progress", &progress) != B_OK)
return result;
if (!CayaPreferences::Item()->NotifyProtocolStatus)
break;
2010-05-30 07:17:32 -05:00
CayaProtocolAddOn* addOn
= ProtocolManager::Get()->ProtocolAddOn(protocol);
BNotification notification(B_PROGRESS_NOTIFICATION);
notification.SetGroup(BString("Caya"));
2010-05-30 07:17:32 -05:00
notification.SetTitle(title);
notification.SetIcon(addOn->ProtoIcon());
2010-05-30 07:17:32 -05:00
notification.SetContent(message);
notification.SetProgress(progress);
notification.Send();
2010-05-30 07:17:32 -05:00
break;
}
case IM_NOTIFICATION:
{
int32 type = (int32)B_INFORMATION_NOTIFICATION;
const char* protocol = NULL;
const char* title = NULL;
const char* message = NULL;
if (msg->FindString("protocol", &protocol) != B_OK)
return result;
if (msg->FindInt32("type", &type) != B_OK)
return result;
if (msg->FindString("title", &title) != B_OK)
return result;
if (msg->FindString("message", &message) != B_OK)
return result;
if (!CayaPreferences::Item()->NotifyProtocolStatus)
break;
CayaProtocolAddOn* addOn
= ProtocolManager::Get()->ProtocolAddOn(protocol);
BNotification notification((notification_type)type);
notification.SetGroup(BString("Caya"));
notification.SetTitle(title);
notification.SetIcon(addOn->ProtoIcon());
notification.SetContent(message);
notification.Send();
break;
}
case IM_REGISTER_COMMAND:
{
ChatCommand* cmd = new ChatCommand(msg);
if (cmd == NULL) break;
ProtocolLooper* looper = _LooperFromMessage(msg);
if (looper == NULL)
fCommands.AddItem(cmd->GetName(), cmd);
else
looper->AddCommand(cmd);
break;
}
2021-06-16 04:33:06 -05:00
case IM_REGISTER_USERLIST_ITEM:
{
ProtocolLooper* looper = _LooperFromMessage(msg);
if (looper == NULL)
fUserItems.AddItem(new BMessage(*msg));
else
looper->AddUserPopUpItem(new BMessage(*msg));
break;
}
2021-06-16 04:33:06 -05:00
case IM_REGISTER_CHATLIST_ITEM:
{
ProtocolLooper* looper = _LooperFromMessage(msg);
if (looper == NULL)
fChatItems.AddItem(new BMessage(*msg));
else
looper->AddChatPopUpItem(new BMessage(*msg));
break;
}
2021-06-16 05:03:10 -05:00
case IM_REGISTER_MENUBAR_ITEM:
{
ProtocolLooper* looper = _LooperFromMessage(msg);
if (looper != NULL)
looper->AddMenuBarItem(new BMessage(*msg));
break;
}
case IM_PROTOCOL_READY:
{
// Ready notification
ProtocolLooper* looper = _LooperFromMessage(msg);
if (looper == NULL)
break;
CayaProtocol* proto = looper->Protocol();
BString content("%user% has connected!");
content.ReplaceAll("%user%", looper->Protocol()->GetName());
BNotification notification(B_INFORMATION_NOTIFICATION);
notification.SetGroup(BString("Caya"));
notification.SetTitle("Connected");
notification.SetContent(content);
notification.SetIcon(proto->Icon());
notification.Send();
// Join cached rooms
BEntry entry;
char fileName[B_FILE_NAME_LENGTH] = {'\0'};
BDirectory dir(CayaRoomsCachePath(proto->GetName()));
while (dir.GetNextEntry(&entry, true) == B_OK)
if (entry.GetName(fileName) == B_OK) {
int32 flags;
BFile file(&entry, B_READ_ONLY);
if (file.InitCheck() != B_OK)
continue;
if (file.ReadAttr("Caya:flags", B_INT32_TYPE, 0, &flags,
sizeof(int32)) < 0)
continue;
if (!(flags & ROOM_AUTOJOIN) && !(flags & ROOM_AUTOCREATE))
continue;
BMessage join(IM_MESSAGE);
int32 im_what = IM_JOIN_ROOM;
if (flags & ROOM_AUTOCREATE) {
im_what = IM_CREATE_CHAT;
join.AddString("user_id", fileName);
}
join.AddInt32("im_what", im_what);
join.AddString("chat_id", fileName);
looper->PostMessage(&join);
}
break;
}
default:
break;
}
return result;
}
void
Server::AddProtocolLooper(bigtime_t instanceId, CayaProtocol* cayap)
{
ProtocolLooper* looper = new ProtocolLooper(cayap, instanceId);
fLoopers.AddItem(instanceId, looper);
fAccounts.AddItem(cayap->GetName(), instanceId);
}
void
Server::RemoveProtocolLooper(bigtime_t instanceId)
{
ProtocolLooper* looper = GetProtocolLooper(instanceId);
if (looper == NULL)
return;
ChatMap chats = looper->Conversations();
for (int i = 0; i < chats.CountItems(); i++)
delete chats.ValueAt(i);
UserMap users = looper->Users();
for (int i = 0; i < users.CountItems(); i++)
delete users.ValueAt(i);
fLoopers.RemoveItemFor(instanceId);
fAccounts.RemoveItemFor(looper->Protocol()->GetName());
looper->Lock();
looper->Quit();
}
ProtocolLooper*
Server::GetProtocolLooper(bigtime_t instanceId)
{
bool found = false;
return fLoopers.ValueFor(instanceId, &found);
}
AccountInstances
Server::GetAccounts()
{
return fAccounts;
}
void
Server::SendProtocolMessage(BMessage* msg)
{
// Skip null messages
if (!msg)
return;
// Check if message contains the instance field
bigtime_t id;
if (msg->FindInt64("instance", &id) == B_OK) {
bool found = false;
ProtocolLooper* looper
= fLoopers.ValueFor(id, &found);
if (found)
looper->PostMessage(msg);
}
}
void
Server::SendAllProtocolMessage(BMessage* msg)
{
// Skip null messages
if (!msg)
return;
// Send message to all protocols
for (uint32 i = 0; i < fLoopers.CountItems(); i++) {
ProtocolLooper* looper = fLoopers.ValueAt(i);
looper->PostMessage(msg);
}
}
RosterMap
Server::Contacts() const
{
RosterMap contacts;
for (int i = 0; i < fAccounts.CountItems(); i++) {
ProtocolLooper* fruitLoop = fLoopers.ValueFor(fAccounts.ValueAt(i));
if (fruitLoop == NULL) continue;
contacts.AddList(fruitLoop->Contacts());
}
return contacts;
}
2021-05-23 14:39:07 -05:00
Contact*
Server::ContactById(BString id, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
Contact* result = NULL;
if (looper != NULL)
result = looper->ContactById(id);
return result;
}
void
Server::AddContact(Contact* contact, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
if (looper != NULL)
looper->AddContact(contact);
}
UserMap
Server::Users() const
{
UserMap users;
for (int i = 0; i < fAccounts.CountItems(); i++) {
ProtocolLooper* fruitLoop = fLoopers.ValueFor(fAccounts.ValueAt(i));
if (fruitLoop == NULL) continue;
users.AddList(fruitLoop->Users());
}
return users;
}
User*
Server::UserById(BString id, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
User* result = NULL;
if (looper != NULL)
result = looper->UserById(id);
return result;
}
void
Server::AddUser(User* user, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
if (looper != NULL)
looper->AddUser(user);
}
ChatMap
Server::Conversations() const
{
ChatMap chats;
for (int i = 0; i < fAccounts.CountItems(); i++) {
ProtocolLooper* fruitLoop = fLoopers.ValueFor(fAccounts.ValueAt(i));
if (fruitLoop == NULL) continue;
chats.AddList(fruitLoop->Conversations());
}
return chats;
}
Conversation*
Server::ConversationById(BString id, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
Conversation* result = NULL;
if (looper != NULL)
result = looper->ConversationById(id);
return result;
}
void
Server::AddConversation(Conversation* chat, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
if (looper != NULL)
looper->AddConversation(chat);
}
ChatCommand*
Server::CommandById(BString id, int64 instance)
{
ProtocolLooper* looper = fLoopers.ValueFor(instance);
ChatCommand* result = NULL;
if (looper != NULL)
result = looper->CommandById(id);
if (result == NULL)
result = fCommands.ValueFor(id);
return result;
}
BObjectList<BMessage>
2021-06-16 04:33:06 -05:00
Server::ChatPopUpItems()
{
return fChatItems;
}
BObjectList<BMessage>
Server::UserPopUpItems()
{
return fUserItems;
}
ProtocolLooper*
Server::_LooperFromMessage(BMessage* message)
{
if (!message)
return NULL;
bigtime_t identifier;
if (message->FindInt64("instance", &identifier) == B_OK) {
bool found = false;
ProtocolLooper* looper = fLoopers.ValueFor(identifier, &found);
if (found)
return looper;
}
return NULL;
}
2021-05-23 14:39:07 -05:00
Contact*
Server::_EnsureContact(BMessage* message)
{
BString id = message->FindString("user_id");
ProtocolLooper* looper = _LooperFromMessage(message);
if (looper == NULL) return NULL;
Contact* contact = looper->ContactById(id);
if (contact == NULL && id.IsEmpty() == false && looper != NULL) {
contact = new Contact(id, Looper());
contact->SetProtocolLooper(looper);
looper->AddContact(contact);
}
return contact;
}
User*
Server::_EnsureUser(BMessage* message)
{
BString id = message->FindString("user_id");
return _EnsureUser(id, _LooperFromMessage(message));
}
User*
Server::_EnsureUser(BString id, ProtocolLooper* protoLooper)
{
User* user = protoLooper->UserById(id);
if (user == NULL && id.IsEmpty() == false) {
user = new User(id, Looper());
user->SetProtocolLooper(protoLooper);
protoLooper->AddUser(user);
}
return user;
}
Conversation*
Server::_EnsureConversation(BMessage* message)
{
ProtocolLooper* looper;
if (!message || (looper = _LooperFromMessage(message)) == NULL)
return NULL;
BString chat_id = message->FindString("chat_id");
Conversation* item = NULL;
if (chat_id.IsEmpty() == false) {
item = looper->ConversationById(chat_id);
if (item == NULL) {
item = new Conversation(chat_id, Looper());
item->SetProtocolLooper(looper);
item->AddUser(looper->ContactById(looper->GetOwnId()));
looper->AddConversation(item);
BMessage meta(IM_MESSAGE);
meta.AddInt32("im_what", IM_GET_ROOM_METADATA);
meta.AddString("chat_id", chat_id);
BMessage users(IM_MESSAGE);
users.AddInt32("im_what", IM_GET_ROOM_PARTICIPANTS);
users.AddString("chat_id", chat_id);
looper->MessageReceived(&meta);
looper->MessageReceived(&users);
}
}
return item;
}
2021-05-23 14:39:07 -05:00
Role*
Server::_GetRole(BMessage* msg)
{
if (!msg)
return NULL;
BString title;
int32 perms;
int32 priority;
if (msg->FindString("role_title", &title) != B_OK
|| msg->FindInt32("role_perms", &perms) != B_OK
|| msg->FindInt32("role_priority", &priority) != B_OK)
return NULL;
return new Role(title, perms, priority);
}