- Use the name instead of the key in AF_initDataChunkQueue. (#107)
- Fix email and self_entity changing ids server-side. And initial conv list (#104)
- update versions of modules
- Adds ability to modify OTR status
- Add client delivery medium type
- Update node version
- Adds additional fields to ENTITY schema
- Updates the subscribe method to only
babel
andbabel_presence_last_seen
Add support for conversation metadata fetching
Fixes a breaking change of the google apis.
This is a minor release. It does not solve login problems that are related to recent Google API changes. They have been solved in yakyak/yakyak client because auth method there is different. That solution involves user interaction therefore it can't be implemented hangupsjs library.
We are still looking for a solution.
It seems the entities information that previously was available in the init data is no longer there. Relying on these entities would now break.
tdryer pointed out that hangups have stopped doing this init data request, since it's not necessary. hangupsjs should follow (soon) and remove everything around pvt/init. this will be a major release.
Client library for Google Hangouts in nodejs.
This library is in no way affiliated with or endorsed by Google. Use at your own risk.
Port of https://github.com/tdryer/hangups to node js.
I take no credit for the excellent work of Tom Dryer putting together the original python client library for Google Hangouts. This port is simply taking his work and porting it to coffeescript step by step.
The library is rather new and needs more tests, error handling etc.
$ npm install hangupsjs
The client is started with connect()
passing callback function for a
promise for a login object containing the credentials.
Example usage (javascript below):
Client = require 'hangupsjs'
Q = require 'q'
# callback to get promise for creds using stdin. this in turn
# means the user must fire up their browser and get the
# requested token.
creds = -> auth:Client.authStdin
client = new Client()
# set more verbose logging
client.loglevel 'debug'
# receive chat message events
client.on 'chat_message', (ev) ->
console.log ev
# connect and post a message.
# the id is a conversation id.
client.connect(creds).then ->
client.sendchatmessage('UgzJilj2Tg_oqkAaABAQ', [
[0, 'Hello World']
])
.done()
The same example code in javascript:
var Client = require('hangupsjs');
var Q = require('q');
// callback to get promise for creds using stdin. this in turn
// means the user must fire up their browser and get the
// requested token.
var creds = function() {
return {
auth: Client.authStdin
};
};
var client = new Client();
// set more verbose logging
client.loglevel('debug');
// receive chat message events
client.on('chat_message', function(ev) {
return console.log(ev);
});
// connect and post a message.
// the id is a conversation id.
client.connect(creds).then(function() {
return client.sendchatmessage('UgzJilj2Tg_oqkAaABAQ',
[[0, 'Hello World']]);
}).done();
hangupsjs will not try to keep the connection open endlessly. the push
channel has some reconnect logic, but it will eventually back off with
a connect_failed
event.
additionally the client also monitors activity. the push channel
receives events at least every 20-30 seconds, if there are no chat
events, we get a noop
.
after a successful connect()
, the client monitors the channel to
ensure we receive any event at least every 45 seconds. if 45 seconds
passes and the push channel got nothing, the client stops with a
connect_failed
event.
To construct a client that just doesn't give up we do:
var reconnect = function() {
client.connect(creds).then(function() {
// we are now connected. a `connected`
// event was emitted.
});
};
// whenever it fails, we try again
client.on('connect_failed', function() {
Q.Promise(function(rs) {
// backoff for 3 seconds
setTimeout(rs,3000);
}).then(reconnect);
});
// start connection
reconnect();
High level API calls that are not doing direct hangouts calls.
Client(opts)
opts.jarstore
(optional) instance of
Store
to use
instead of default file persistence for cookies.
opts.cookiespath
(optional) path to file in which to store cached
login cookies. Defaults to cookies.json
in module dir. not used
if opts.jarstore
is passed.
opts.rtokenpath
(optional) path to file in which to store the
oauth refresh token. Defaults to refreshtoken.txt
in module dir.
opts.proxy
(optional) proxy URL that gets passed to
request. Documentation is
here
connect: (creds) ->
Attempts to connect the client to hangouts. See
isInited
for the steps that connects the client.
Returns a promise for connection. The promise only resolves when init
is completed. On the connected
event.
creds
: is callback that returns a promise for login creds. The creds
are either {creds:-><promise for token>}
or
{cookies:<array of strings or tough-cookie-jar>}
To login using an email/password combo, you need to login using OAuth and provide the access token to the API. Furthermore it uses a google white listed OAuth CLIENT_ID and CLIENT_SECRET that shows up as "iOS Device" in your accounts page.
This is the login URL, also available as Client.OAUTH2_LOGIN_URL
.
The library provides a stdin-method that requests the token.
creds = -> auth:Client.authStdin
client.connect(creds).then -> # and so on...
The other way to log in is to provide a string array of cookies for
the google.com
domain that are set up as part of a successful login.
Typically these cookies are called: NID
, SID
, HSID
, SSID
,
APISID
, SAPISID
Example:
creds = -> Q {cookies:[
'NID=67=QI6go9WM<redacted>WDFxv; Expires=Wed, 04 Nov 2015 06:10:24 GMT; Domain=google.com; Path=/; HttpOnly'
'SID=DASDPgAAA<redacted>AKJASKJD; Expires=Thu, 04 May 2017 06:10:24 GMT; Domain=google.com; Path=/'
'HSID=AR<redacted>QX_; Expires=Thu, 04 May 2017 06:10:24 GMT; Domain=google.com; Path=/; HttpOnly; Priority=HIGH'
'SSID=Ak<redacted>D; Expires=Thu, 04 May 2017 06:10:24 GMT; Domain=google.com; Path=/; Secure; HttpOnly; Priority=HIGH'
'APISID=kM<redacted>seXb; Expires=Thu, 04 May 2017 06:10:24 GMT; Domain=google.com; Path=/; Priority=HIGH'
'SAPISID=cl<redacted>Od; Expires=Thu, 04 May 2017 06:10:24 GMT; Domain=google.com; Path=/; Secure; Priority=HIGH'
]}
client.connect(creds).then -> # and so on...
disconnect: ->
Disconnects the client.
isInited
For Client to be fully inited the following must happen on
connect
-
Get login cookies against https://accounts.google.com/ServiceLogin or reuse cached cookies.
-
Using the cookies, fetch a PVT token (whatever that is) against https://talkgadget.google.com/talkgadget/_/extension-start
-
Load the chat widget HTML + javascript using the PVT token from https://talkgadget.google.com/u/0/talkgadget/_/chat
-
From the returned javascript get an
apikey
and some other headers used in each api call later. -
Fetch channel
sid
/gsid
from https://0.client-channel.google.com/client-channel/channel-bind -
Using the
sid
/gsid
open a long poll request against the same URL as in 5. This is the push data channel. -
From first data coming through the push data channel, extract a
clientid
which also is used in each api call later. -
Post a subscribe request against same URL as in 5 to make push data channel receive chat events.
Only after all these steps are completed will isInited
return true.
loglevel: (level) ->
Sets the log level one of debug
, info
, warn
or error
.
logout: () ->
Logs the current client out by removing refresh token and cached cookies.
Example:
# force cleared state
client.logout().then ->
# will now require new credentials
client.connect(creds)
.then ->
...
Helper to compose message segments
that goes into
sendchatmessage
. The builder has these methods.
Example:
bld = new Client.MessageBuilder()
segments = bld.text('Hello ').bold('World').text('!!!').toSegments()
client.sendchatmessage('UgzfaJwj2Tg_oqk5EhEp5faABAQ', segments)
(txt, bold=false, italic=false, strikethrough=false, underline=false, href=null) ->
Adds a text segment.
builder.text('Hello')
Adds a text segment in bold.
Adds a text segment in italic.
Adds a text segment strikethroughed.
Adds an underlined text segment.
Adds a new line.
Adds a text that is a link.
Turns the builder into an array of segments usable for sendchatmessage
.
Each API call does a direct operation against hangouts. Each call returns a promise for the result.
sendchatmessage: (conversation_id, segments, image_id = None, otr_status = OffTheRecordStatus.ON_THE_RECORD, client_generated_id = null, delivery_medium = [ClientDeliveryMediumType.BABEL], message_action_type = [[MessageActionType.NONE, ""]]) ->
Send a chat message to a conversation.
conversation_id
: the conversation to send a message to.
segments
: array of segments to send. See
messagebuilder
for help.
image_id
: is an optional ID of an image retrieved from
uploadimage
. If provided, the image will be
attached to the # message.
otr_status
: determines whether the message will be saved in the
server's chat history. Note that the OTR status of the conversation is
irrelevant, clients may send messages with whatever OTR status they
like. One of Client.OffTheRecordStatus.OFF_THE_RECORD
or
Client.OffTheRecordStatus.ON_THE_RECORD
.
client_generated_id
is an identifier that is kept in the event both
in the result of this call and the following chat_event. it can be
used to tie together a client send with the update from the
server. The default is null
which makes the client generate a random
id.
delivery_medium
: determines via which medium the message will be
delivered. If caller does not specify value we pick the value BABEL to
ensure the message is delivered via default medium. In fact the caller
should retrieve current conversation's default delivery medium from
self_conversation_state.delivery_medium_option when calling to ensure
the message is delivered back to the conversation on same medium always.
message_action_type
: determines if the message is a simple text message
or if the message is an action like /me
. One of Client.MessageActionType.NONE
or Client.MessageActionType.ME_ACTION
setactiveclient: (active, timeoutsecs) ->
The active client receives notifications. This marks the client as active.
active
: boolean indicating active state
timeoutsecs
: the length of active in seconds.
syncallnewevents: (timestamp) ->
List all events occuring at or after timestamp. Timestamp can be a date or long millis.
timestamp
: date instance specifying the time after which to return
all events occuring in.
getselfinfo: ->
Return information about your account.
setconversationnotificationlevel: (conversation_id, level) ->
Set the notification level of a conversation.
Pass Client.NotificationLevel.QUIET
to disable notifications, or
Client.NotificationLevel.RING
to enable them.
setfocus: (conversation_id, focus=FocusStatus.FOCUSED, timeoutsecs=20) ->
Set focus (occurs whenever you give focus to a client).
conversation_id
: the conversation you are focusing.
typing
: constant indicating focus status. One of
Client.FocusStatus.FOCUSED
or Client.FocusStatus.UNFOCUSED
timeoutsecs
: the length of focus in seconds.
settyping: (conversation_id, typing=TypingStatus.TYPING) ->
Send typing notification.
conversation_id
: the conversation you want to send typing
notification for.
typing
: constant indicating typing status. One of
Client.TypingStatus.TYPING
, Client.TypingStatus.PAUSED
or
Client.TypingStatus.STOPPED
setpresence: (online, mood=None) ->
Set the presence or mood of this client.
online
: boolean indicating whether client is online.
mood
: emoticon UTF-8 smiley like 0x1f603
querypresence: (chat_id) ->
Check someone's presence status.
chat_id
: the identifer of the user to check.
removeuser: (conversation_id) ->
Remove self from chat.
conversation_id
: the conversation to remove self from.
deleteconversation: (conversation_id) ->
Delete one-to-one conversation.
conversation_id
: the conversation to delete.
updatewatermark: (conversation_id, timestamp) ->
Update the watermark (read timestamp) for a conversation.
conversation_id
: the conversation to update the read timestamp for.
timestamp
: the date or long millis to set as read timestamp.
adduser: (conversation_id, chat_ids) ->
Add user(s) to existing conversation.
conversation_id
: the conversation to add user(s) to.
chat_ids
: array of user chat_ids to add.
renameconversation: (conversation_id, name) ->
Set the name of a conversation.
conversation_id
: the conversation to change.
name
: the name to change to.
createconversation: (chat_ids, force_group=false) ->
Create a new conversation.
chat_ids
: is an array of chat_id which should be invited to
conversation (except yourself).
force_group
: set to true if you invite just one chat_id, but still
want a group.
The new conversation ID is returned as res.conversation.id.id
getconversation: (conversation_id, timestamp, max_events=50) ->
Return conversation events.
This is mainly used for retrieving conversation scrollback. Events occurring before timestamp are returned, in order from oldest to newest.
conversation_id
: the conversation to get events in.
timestamp
: the timestamp as long millis or date to get events
before.
max_events
: number of events to retrieve.
syncrecentconversations: (timestamp_since=null) ->
List the contents of recent conversations, including messages. Similar to syncallnewevents, but returns a limited number of conversations (20) rather than all conversations in a given date range.
To get older conversations, use the timestamp_since parameter.
searchentities: (search_string, max_results=10) ->
Search for people.
search_string
: string to look for.
max_results
: number of results to return.
getentitybyid: (chat_ids) ->
Return information about a list of chat_ids.
chat_ids
: array of user chat ids to get information for.
sendeasteregg: (conversation_id, easteregg) ->
Send an easteregg to a conversation.
conversation_id
: conversation to bother.
easteregg
: may not be empty. could be one of 'ponies', 'pitchforks',
'bikeshed', 'shydino'
uploadimage: (path, filename=null, timeout=30000) ->
Uploads an image that can be later attached to a chat message.
imagefile
is a string path
filename
can optionally be provided otherwise the path name is used.
timeout
can be used to upload larger images, that may need more than 30 sec to be sent
returns an image_id
that can be used in sendchatmessage
.
The following events are available on the Client
object. Example:
client.on 'chat_message', (msg) ->
# ... do something
When someone calls client.connect()
and it indicates we are trying
to connect the client.
When the client is fully inited and connected.
Indicates that the client connection either didn't start or was
interrupted. Either way, the client will not try to connect again by
itself. Another client.connect
is required.
Emitted in three cases.
-
After
connecting
(inclient.connect()
) indicating that the client could not connect at all. -
After
connected
when running the polling (server push channel) successfully, but is interrupted (such as lost network connection). -
If the server push channel receives no events after 45 seconds (server emits at least
noop
every 20-30 seconds).
On a received chat message.
Whenever an update about the conversation itself is needed. Like when a new conversation is created, this event comes first with the metadata about it.
The conversation state is stored in self_conversation_state of the event. The self_conversation_state.delivery_medium_option contains an array of the delivery medium options which indicate all possible medium. The array element with current_default == true should be the one used to send message via by default. Currently there are 3 types of known medium, BABEL, Google Voice and SMS. BABEL is the Google Hangouts codename BTW.
Member joining/leaving conversation.
On a renamed conversation.
When a user focuses a conversation.
On changes to video/audio calls. A "hangout" is in google API talk
strictly a video/audio event. START_HANGOUT
and END_HANGOUT
would
indicate attempts to start/end audio/video events.
When a user is typing.
When a user updates their read timestamp.
When user changes the notification level of his own conversation. I.e. setconversationnotificationlevel.
When anyone in the conversation triggers an easter egg.
When a conversation is deleted by the user. As a response
to deleteconversation
.
The following events are possible and not investigated. Please tell me in an issue if you figure one out.
conversation_notification
reply_to_invite
settings
self_presence
See #10presence
See #10block
invitation_watermark
Copyright © 2015 Martin Algesten
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.