grim/purple-spasm
Clone
Summary
Browse
Changes
Graph
Move the parsing to regex and make real_send add the trailing \r\n
2020-04-19, Gary Kramlich
7388a4c9a1b3
Move the parsing to regex and make real_send add the trailing \r\n
/*
* Spasm - A Twitch Protocol Plugin
* Copyright (C) 2017-2019 Gary Kramlich <grim@reaperworld.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include
"spasm-chat.h"
#include
"spasm-const.h"
#include
<glib/gi18n-lib.h>
#include
<stdarg.h>
#include
<purple.h>
/******************************************************************************
* Structs
*****************************************************************************/
struct
_SpasmChatService
{
SpasmAccount
*
sa
;
GSocketClient
*
socket_client
;
GSocketConnection
*
socket_connection
;
GOutputStream
*
output_stream
;
GDataInputStream
*
input_stream
;
GHashTable
*
handlers
;
gint
id
;
/* used to track ids for this service */
GRegex
*
regex_message
;
GRegex
*
regex_target
;
};
typedef
void
(
*
SpasmChatMessageHandler
)(
SpasmChatService
*
sa
,
const
gchar
*
prefix
,
const
gchar
*
middle
,
const
gchar
*
trailing
);
/******************************************************************************
* Helpers
*****************************************************************************/
static
void
spasm_chat_service_regex_init
(
SpasmChatService
*
chat
)
{
chat
->
regex_message
=
g_regex_new
(
"(?::(?<prefix>[^ ]+) +)?"
"(?<command>[^ :]+)"
"(?:(?: +(?<middle>[^ :]+)))*"
"(?<coda> +:(?<trailing>.*)?)?"
,
0
,
0
,
NULL
);
g_assert
(
chat
->
regex_message
!=
NULL
);
chat
->
regex_target
=
g_regex_new
(
"(?:#(?<target>[^
\\
s]+))"
,
0
,
0
,
NULL
);
g_assert
(
chat
->
regex_target
!=
NULL
);
}
static
gchar
*
spasm_chat_service_nick_from_mask
(
const
gchar
*
mask
)
{
gchar
*
nick
=
NULL
,
*
bang
=
NULL
;
bang
=
strchr
(
mask
,
'!'
);
if
(
bang
==
NULL
)
{
nick
=
g_strdup
(
mask
);
}
else
{
nick
=
g_strndup
(
mask
,
bang
-
mask
);
/* eww pointer math... */
}
return
nick
;
}
/******************************************************************************
* Sending
*****************************************************************************/
static
void
spasm_chat_service_real_send
(
SpasmChatService
*
chat
,
const
gchar
*
format
,
...)
{
GCancellable
*
cancellable
=
NULL
;
GError
*
error
=
NULL
;
gchar
*
buffer
=
NULL
;
gboolean
success
;
va_list
vargs
;
cancellable
=
spasm_account_get_cancellable
(
chat
->
sa
);
va_start
(
vargs
,
format
);
buffer
=
g_strdup_vprintf
(
format
,
vargs
);
va_end
(
vargs
);
purple_debug_info
(
"spasm-chat"
,
"send buffer: %s
\n
"
,
buffer
);
success
=
g_output_stream_printf
(
chat
->
output_stream
,
NULL
,
cancellable
,
&
error
,
"%s
\r\n
"
,
buffer
);
g_free
(
buffer
);
if
(
!
success
)
{
PurpleConnection
*
purple_connection
=
spasm_account_get_connection
(
chat
->
sa
);
if
(
error
)
{
purple_connection_error
(
purple_connection
,
error
->
message
);
g_error_free
(
error
);
}
else
{
purple_connection_error
(
purple_connection
,
_
(
"unknown error"
));
}
return
;
}
g_output_stream_flush
(
chat
->
output_stream
,
NULL
,
NULL
);
}
/******************************************************************************
* Handlers
*****************************************************************************/
static
void
spasm_chat_service_handle_ping
(
SpasmChatService
*
chat
,
const
gchar
*
prefix
,
const
gchar
*
middle
,
const
gchar
*
trailing
)
{
spasm_chat_service_real_send
(
chat
,
"PONG :%s"
,
trailing
);
}
static
void
spasm_chat_service_handle_join
(
SpasmChatService
*
chat
,
const
gchar
*
prefix
,
const
gchar
*
middle
,
const
gchar
*
trailing
)
{
GMatchInfo
*
info
=
NULL
;
if
(
g_regex_match
(
chat
->
regex_target
,
middle
,
0
,
&
info
))
{
gchar
*
target
=
g_match_info_fetch_named
(
info
,
"target"
);
if
(
target
!=
NULL
)
{
PurpleConnection
*
connection
=
NULL
;
connection
=
spasm_account_get_connection
(
chat
->
sa
);
serv_got_joined_chat
(
connection
,
chat
->
id
++
,
target
);
}
g_free
(
target
);
}
g_match_info_unref
(
info
);
}
static
void
spasm_chat_service_handle_privmsg
(
SpasmChatService
*
chat
,
const
gchar
*
prefix
,
const
gchar
*
middle
,
const
gchar
*
trailing
)
{
GMatchInfo
*
info
=
NULL
;
if
(
g_regex_match
(
chat
->
regex_target
,
middle
,
0
,
&
info
))
{
PurpleAccount
*
account
=
NULL
;
PurpleConversation
*
conversation
=
NULL
;
gchar
*
target
=
NULL
,
*
nick
=
NULL
;
target
=
g_match_info_fetch_named
(
info
,
"target"
);
nick
=
spasm_chat_service_nick_from_mask
(
prefix
);
account
=
spasm_account_get_account
(
chat
->
sa
);
conversation
=
purple_find_conversation_with_account
(
PURPLE_CONV_TYPE_CHAT
,
target
,
account
);
if
(
conversation
!=
NULL
)
{
gint
id
=
purple_conv_chat_get_id
(
PURPLE_CONV_CHAT
(
conversation
));
serv_got_chat_in
(
spasm_account_get_connection
(
chat
->
sa
),
id
,
nick
,
0
,
trailing
,
time
(
NULL
));
}
g_free
(
target
);
g_free
(
nick
);
}
else
{
purple_debug_misc
(
"spasm-chat"
,
"failed to to find a target in
\"
%s
\"\n
"
,
middle
);
}
g_match_info_unref
(
info
);
}
static
GHashTable
*
spasm_chat_service_init_handlers
(
void
)
{
GHashTable
*
handlers
=
NULL
;
handlers
=
g_hash_table_new
(
g_str_hash
,
g_str_equal
);
g_hash_table_insert
(
handlers
,
"001"
,
NULL
);
/* Ignore RPL_WELCOME */
g_hash_table_insert
(
handlers
,
"002"
,
NULL
);
/* Ignore RPL_YOURHOST */
g_hash_table_insert
(
handlers
,
"003"
,
NULL
);
/* Ignore RPL_CREATED */
g_hash_table_insert
(
handlers
,
"004"
,
NULL
);
/* Ignore RPL_MYINFO */
g_hash_table_insert
(
handlers
,
"353"
,
NULL
);
/* Ignore RPL_NAMREPLY */
g_hash_table_insert
(
handlers
,
"366"
,
NULL
);
/* Ignore RPL_ENDOFNAMES */
g_hash_table_insert
(
handlers
,
"372"
,
NULL
);
/* Ignore RPL_MOTD */
g_hash_table_insert
(
handlers
,
"375"
,
NULL
);
/* Ignore RPL_MOTDSTART */
g_hash_table_insert
(
handlers
,
"376"
,
NULL
);
/* Ignore RPL_ENDOFMOTD */
g_hash_table_insert
(
handlers
,
"JOIN"
,
spasm_chat_service_handle_join
);
g_hash_table_insert
(
handlers
,
"PING"
,
spasm_chat_service_handle_ping
);
g_hash_table_insert
(
handlers
,
"PRIVMSG"
,
spasm_chat_service_handle_privmsg
);
return
handlers
;
}
/******************************************************************************
* Read Loop
*****************************************************************************/
static
void
spasm_chat_service_parse
(
SpasmChatService
*
chat
,
const
gchar
*
buffer
)
{
GMatchInfo
*
info
=
NULL
;
gchar
*
prefix
=
NULL
,
*
command
=
NULL
,
*
middle
=
NULL
,
*
trailing
=
NULL
;
gboolean
matches
=
FALSE
;
gpointer
value
;
g_return_if_fail
(
buffer
!=
NULL
);
matches
=
g_regex_match
(
chat
->
regex_message
,
buffer
,
0
,
&
info
);
if
(
!
matches
)
{
purple_debug_misc
(
"spasm-chat"
,
"failed to parse
\"
%s
\"\n
"
,
buffer
);
g_match_info_unref
(
info
);
return
;
}
prefix
=
g_match_info_fetch_named
(
info
,
"prefix"
);
command
=
g_match_info_fetch_named
(
info
,
"command"
);
middle
=
g_match_info_fetch_named
(
info
,
"middle"
);
trailing
=
g_match_info_fetch_named
(
info
,
"trailing"
);
g_match_info_unref
(
info
);
/* Look up the command handler. */
if
(
g_hash_table_lookup_extended
(
chat
->
handlers
,
command
,
NULL
,
&
value
))
{
/* Ignored replies have a NULL handler. */
if
(
value
!=
NULL
)
{
SpasmChatMessageHandler
handler
=
(
SpasmChatMessageHandler
)
value
;
handler
(
chat
,
prefix
,
middle
,
trailing
);
}
}
else
{
purple_debug_misc
(
"spasm-chat"
,
"no handler found for
\"
%s
\"\n
"
,
buffer
);
purple_debug_misc
(
"spasm-chat"
,
"prefix:
\"
%s
\"\n
"
,
prefix
);
purple_debug_misc
(
"spasm-chat"
,
"command:
\"
%s
\"\n
"
,
command
);
purple_debug_misc
(
"spasm-chat"
,
"middle:
\"
%s
\"\n
"
,
middle
);
purple_debug_misc
(
"spasm-chat"
,
"trailing:
\"
%s
\"\n
"
,
trailing
);
purple_debug_misc
(
"spasm-chat"
,
"----
\n
"
);
}
g_free
(
prefix
);
g_free
(
command
);
g_free
(
middle
);
g_free
(
trailing
);
}
static
void
spasm_chat_read
(
SpasmChatService
*
chat
);
static
void
spasm_chat_read_cb
(
GObject
*
obj
,
GAsyncResult
*
res
,
gpointer
data
)
{
GError
*
error
=
NULL
;
gchar
*
buffer
=
NULL
;
gsize
buffer_len
;
SpasmChatService
*
chat
=
(
SpasmChatService
*
)
data
;
buffer
=
g_data_input_stream_read_line_finish
(
G_DATA_INPUT_STREAM
(
obj
),
res
,
&
buffer_len
,
&
error
);
/* g_data_input_stream_read_line_finish, will return null with error set
* on connection error. It will also return null with error not set if
* there is no content to read.
*/
if
(
buffer
==
NULL
)
{
gchar
*
error_msg
=
NULL
;
if
(
error
!=
NULL
)
{
error_msg
=
g_strdup_printf
(
"spasm lost connection with server : %s"
,
error
->
message
);
g_error_free
(
error
);
}
else
{
error_msg
=
g_strdup_printf
(
"spasm server closed connection"
);
}
purple_connection_error
(
spasm_account_get_connection
(
chat
->
sa
),
error_msg
);
g_free
(
error_msg
);
return
;
}
spasm_chat_service_parse
(
chat
,
buffer
);
g_free
(
buffer
);
spasm_chat_read
(
chat
);
}
static
void
spasm_chat_read
(
SpasmChatService
*
chat
)
{
g_data_input_stream_read_line_async
(
chat
->
input_stream
,
G_PRIORITY_DEFAULT
,
spasm_account_get_cancellable
(
chat
->
sa
),
spasm_chat_read_cb
,
chat
);
}
/******************************************************************************
* chat login flow
*****************************************************************************/
static
void
spasm_chat_login_cb
(
GObject
*
obj
,
GAsyncResult
*
res
,
gpointer
data
)
{
GError
*
error
=
NULL
;
PurpleConnection
*
purple_connection
=
NULL
;
SpasmChatService
*
chat
=
(
SpasmChatService
*
)
data
;
purple_connection
=
spasm_account_get_connection
(
chat
->
sa
);
chat
->
socket_connection
=
g_socket_client_connect_to_host_finish
(
G_SOCKET_CLIENT
(
obj
),
res
,
&
error
);
if
(
chat
->
socket_connection
==
NULL
)
{
if
(
error
)
{
g_prefix_error
(
&
error
,
"failed to connect: "
);
purple_connection_error
(
purple_connection
,
error
->
message
);
g_error_free
(
error
);
}
else
{
purple_connection_error
(
purple_connection
,
_
(
"unknown error"
));
}
return
;
}
chat
->
output_stream
=
g_io_stream_get_output_stream
(
G_IO_STREAM
(
chat
->
socket_connection
));
/* now do the login */
spasm_chat_service_real_send
(
chat
,
"PASS oauth:%s"
,
spasm_account_get_access_token
(
chat
->
sa
)
);
/* now try to use our nick */
spasm_chat_service_real_send
(
chat
,
"NICK %s"
,
spasm_account_get_name
(
chat
->
sa
)
);
purple_connection_set_state
(
purple_connection
,
PURPLE_CONNECTED
);
chat
->
input_stream
=
g_data_input_stream_new
(
g_io_stream_get_input_stream
(
G_IO_STREAM
(
chat
->
socket_connection
))
);
spasm_chat_read
(
chat
);
}
SpasmChatService
*
spasm_chat_service_new
(
SpasmAccount
*
sa
)
{
SpasmChatService
*
chat
=
NULL
;
g_return_val_if_fail
(
sa
,
NULL
);
chat
=
g_slice_new0
(
SpasmChatService
);
chat
->
sa
=
sa
;
chat
->
handlers
=
spasm_chat_service_init_handlers
();
spasm_chat_service_regex_init
(
chat
);
return
chat
;
}
void
spasm_chat_service_free
(
SpasmChatService
*
chat
)
{
g_object_unref
(
chat
->
input_stream
);
g_object_unref
(
chat
->
output_stream
);
g_object_unref
(
chat
->
socket_client
);
g_object_unref
(
chat
->
socket_connection
);
g_regex_unref
(
chat
->
regex_message
);
g_regex_unref
(
chat
->
regex_target
);
g_slice_free
(
SpasmChatService
,
chat
);
}
void
spasm_chat_service_connect
(
SpasmChatService
*
chat
)
{
g_return_if_fail
(
chat
);
chat
->
socket_client
=
g_socket_client_new
();
g_socket_client_set_tls
(
chat
->
socket_client
,
TRUE
);
g_socket_client_connect_to_host_async
(
chat
->
socket_client
,
SPASM_CHAT_HOSTNAME
,
SPASM_CHAT_PORT
,
spasm_account_get_cancellable
(
chat
->
sa
),
spasm_chat_login_cb
,
chat
);
}
GList
*
spasm_chat_service_info
(
PurpleConnection
*
connection
)
{
GList
*
entries
=
NULL
;
struct
proto_chat_entry
*
pce
=
NULL
;
pce
=
g_new0
(
struct
proto_chat_entry
,
1
);
pce
->
label
=
_
(
"Channel"
);
pce
->
identifier
=
"channel"
;
entries
=
g_list_append
(
entries
,
pce
);
return
entries
;
}
GHashTable
*
spasm_chat_service_info_default
(
PurpleConnection
*
connection
,
const
gchar
*
name
)
{
GHashTable
*
defaults
=
g_hash_table_new_full
(
g_str_hash
,
g_str_equal
,
NULL
,
g_free
);
if
(
name
!=
NULL
)
{
g_hash_table_insert
(
defaults
,
"channel"
,
g_strdup
(
name
));
}
return
defaults
;
}
void
spasm_chat_service_join
(
PurpleConnection
*
connection
,
GHashTable
*
components
)
{
SpasmAccount
*
sa
=
NULL
;
SpasmChatService
*
chat
=
NULL
;
const
gchar
*
channel
=
NULL
;
channel
=
g_hash_table_lookup
(
components
,
"channel"
);
sa
=
purple_connection_get_protocol_data
(
connection
);
chat
=
spasm_account_get_chat_service
(
sa
);
spasm_chat_service_real_send
(
chat
,
"JOIN #%s"
,
channel
);
}
gchar
*
spasm_chat_service_name
(
GHashTable
*
components
)
{
return
g_strdup
(
g_hash_table_lookup
(
components
,
"channel"
));
}
void
spasm_chat_service_leave
(
PurpleConnection
*
connection
,
gint
id
)
{
}
gint
spasm_chat_service_send
(
PurpleConnection
*
connection
,
gint
id
,
const
gchar
*
message
,
PurpleMessageFlags
flags
)
{
SpasmAccount
*
sa
=
NULL
;
SpasmChatService
*
chat
=
NULL
;
PurpleConversation
*
conversation
=
purple_find_chat
(
connection
,
id
);
if
(
conversation
==
NULL
)
{
return
-1
;
}
sa
=
purple_connection_get_protocol_data
(
connection
);
chat
=
spasm_account_get_chat_service
(
sa
);
spasm_chat_service_real_send
(
chat
,
"PRIVMSG #%s :%s"
,
purple_conversation_get_name
(
conversation
),
message
);
serv_got_chat_in
(
connection
,
id
,
spasm_account_get_display_name
(
sa
),
flags
,
message
,
time
(
NULL
));
return
0
;
}
void
spasm_chat_service_set_topic
(
PurpleConnection
*
connection
,
gint
id
,
const
gchar
*
topic
)
{
}