PushBullet is an app that lets you "push" messages, files, and other data between devices. It has a nice API for doing things programmatically. Pushbullet can be used for free but there is a limit to the number of pushes you can send programmatically through the API for free each month (I think it's 500).
PushBullet Class
Send and receive PushBullet notifications across devices, and set up event handlers to process PushBullet messages in AHK. For example, you can send a message from your phone to trigger an AHK function.
Requires: AHK v2 (tested with 2.0-a097, probably works with 2.0-a078 or later)
Limitations: A few features of the PushBullet API are not yet implemented, such as pushing files (I have not been able to figure out how to create the required multipart/form-data in AHK v2)
Credits: jNizM (basic PushBullet function), cocobelgica (Jxon), G33kDude (WebSocket)
Initial set up:
Demo script:
Limitations: A few features of the PushBullet API are not yet implemented, such as pushing files (I have not been able to figure out how to create the required multipart/form-data in AHK v2)
Credits: jNizM (basic PushBullet function), cocobelgica (Jxon), G33kDude (WebSocket)
Initial set up:
- Visit https://www.pushbullet.com/ and register.
- Install Pushbullet App (iPhone or Android).
- Visit https://www.pushbullet.com/account and get Access Token from your account Settings page (e.g. G8aldIDL93ldFADFwp9032ADF2klj3ld).
- Insert your access token in the demo script and try it out.
Code: Select all
#Persistent
#SingleInstance
#Include PushBullet.ahk
user_access_token := "ENTER_YOUR_ACCESS_TOKEN" ;example: o.RFMsmkl5FsklsjfmGxgVVK2rhMlsmd
pb := new PushBullet(user_access_token)
;Send a PushBullet message. You'll receive a push notification on your device.
pb.PushNote("", "This is a simple PushBullet message test.")
;Set up some event listeners - these functions will be called whenever you send a PushBullet message (either through AHK or by manually sending a message on your device)
uID1 := pb.AddListener(Func("TestRelayPush"))
uID2 := pb.AddListener(Func("TestRelayEphemeral"))
;SEND YOURSELF A MESSAGE FROM THE PUSHBULLET APP TO TEST THE EVENTS, OR UNCOMMENT THE FOLLOWING LINE:
; pb.PushNote("Test Title", "The quick brown fox jumped over the lazy dog.")
Return
TestRelayPush(payload)
{
if payload.type == "pushes"
{
pushMsgs := ""
for each, push in payload.data
pushMsgs .= " Message[" A_Index "] = '" push.body "'`r`n"
MsgBox("Received " . payload.data.Length() . " pushes!`r`n`r`n" . pushMsgs)
;push the details of the first message back as an ephemeral, to test the other event
global pb
pb.PushEphemeral(payload.data[1])
}
}
TestRelayEphemeral(payload)
{
if payload.type == "ephemeral"
{
txt := "Received ephemeral! It has these keys:`n"
for key, val in payload.data
txt .= "`t" key "`n"
MsgBox(txt)
}
}
Code: Select all
;PushBullet Class
; by egocarib
;initial inspiration from jNizM (https://autohotkey.com/boards/viewtopic.php?f=7&t=4842)
#Include WebSocket.ahk
#Include Jxon.ahk
Class PushBullet
{
__New(pbAccessToken)
{
this.AccessToken := pbAccessToken
}
__Delete()
{
if IsObject(this.PushEventStream)
{
this.PushEventStream.Disconnect()
this.PushEventStream := ""
}
}
;PUSH FUNCTIONS
; each function returns 1 on success
; if return value is 0, call GetPushResponse for response & error details
PushNote(title, message)
{
Return this._Push( { "type" : "note"
, "title" : title
, "body" : message } )
}
PushLink(title, message, url)
{
Return this._Push( { "type": "link"
, "title" : title
, "body" : message
, "url" : url } )
}
PushFile(message, filepath)
{
;not implemented
;need to figure out how to create multipart/form-data in AHKv2
; haven't been able to get any version of CreateFormData to work in v2
}
PushEphemeral(ephemeral)
{
;an ephemeral is any arbitrary JSON object
if !IsObject(ephemeral)
{
this.PushResult := this.PushStatus := ""
Return 0
}
Return this._Push( { "type" : "push"
, "push" : ephemeral }
, "ephemerals" )
}
;get response/error details from the most recent push attempt
GetPushResponse(ByRef result, ByRef status := "")
{
try
result := Jxon_Load(this.PushResult)
catch ;error on intermediate server might return non-JSON response
result := { "response" : this.PushResult }
status := this.PushStatus
if status != 200
{
;make sure there's always an error object in the result for non-200 status
if !result.HasKey("error")
result.error := { "type" : "unknown"
, "message" : "Unknown error occurred."
, "cat" : "~(=^.^)" }
}
}
;register a callback function (Func/BoundFunc object) to receive push events
AddListener(callback)
{
static uniqueID := 0
callbackType := Type(callback)
if (callbackType != "Func") && (callbackType != "BoundFunc")
Return 0
if !IsObject(this.Listeners)
this.Listeners := {}
uniqueID++
this.Listeners[uniqueID] := callback
if !IsObject(this.PushEventStream)
{
this.PushEventStream := new PushBulletEventStream(this.AccessToken, ObjBindMethod(this, "_OnPushEvent"))
this._GetLatestPushes(lastMod, 1) ;retrieves the latest push and automatically updates this.LastPushTime
}
Return uniqueID
}
;remove Func/boundFunc callback
RemoveListener(uniqueID)
{
if !this.Listeners.HasKey(uniqueID)
Return 0
this.Listeners.Delete(uniqueID)
moreCallbacks := 0 ;check if any callback functions remain
for each, callback in this.Listeners
{
moreCallbacks := 1
Break
}
if !moreCallbacks && IsObject(this.PushEventStream)
{
this.PushEventStream.Disconnect()
this.PushEventStream := ""
}
Return 1
}
ConnectionActive()
{
Return IsObject(this.PushEventStream) ? this.PushEventStream.ConnectionActive() : 0
}
_Push(pbObj, postType := "pushes")
{
pbJson := Jxon_Dump(pbObj) ;assumes valid object and doesn't currently catch exceptions
OutputDebug("PB: Json=" pbJson)
WinHTTP := ComObjCreate("WinHTTP.WinHttpRequest.5.1")
WinHTTP.SetProxy(0)
WinHTTP.Open("POST", "https://api.pushbullet.com/v2/" . postType, 0)
WinHTTP.SetCredentials(this.AccessToken, "", 0)
WinHTTP.SetRequestHeader("Content-Type", "application/json")
WinHTTP.Send(pbJson)
this.PushResult := WinHTTP.ResponseText
this.PushStatus := WinHTTP.Status
WinHTTP := ""
if (this.PushStatus != 200)
OutputDebug("PB: Push failed with status " this.PushStatus)
Return (this.PushStatus == 200) ;any other status code is an error
}
_OnPushEvent(eventData)
{
eventObj := ""
;tickle (push)
if eventData.type == "tickle"
{
if eventData.subtype == "push"
{
pushArray := this._GetLatestPushes(this.LastPushTime)
if !IsObject(pushArray)
Return
eventObj := { "type" : "pushes"
, "data" : pushArray }
}
else if eventData.subtype == "device"
{
;not implemented
}
}
;ephemeral
else if eventData.type == "push"
{
eventObj := { "type" : "ephemeral"
, "data" : eventData.push }
}
if IsObject(eventObj)
{
for uID, listener in this.Listeners
{
OutputDebug("PB: calling listener " uID " with " eventObj.type "")
listener.Call(eventObj)
}
}
}
_GetLatestPushes(ByRef lastModified, maxToRetrieve := 500)
{
OutputDebug("PB: called _GetLatestPushes [after=" lastModified "]")
lastModified := StrReplace(lastModified, "e+", "e`%2B") ;uri-encode the "e+" (consider replacing this with a real URI encode function)
limit := (maxToRetrieve < 500) ? maxToRetrieve : "" ;currently doesn't support PushBullet pagination (getting over 500 results)
params := "?active=true"
. (lastModified ? ("&modified_after=" lastModified) : "")
. (limit ? ("&limit=" limit) : "")
OutputDebug("PB: params=[" params "]")
WinHTTP := ComObjCreate("WinHTTP.WinHttpRequest.5.1")
WinHTTP.SetProxy(0)
WinHTTP.Open("GET", "https://api.pushbullet.com/v2/pushes" . params, 0)
WinHTTP.SetCredentials(this.AccessToken, "", 0)
WinHTTP.SetRequestHeader("Content-Type", "application/json")
WinHTTP.Send()
if WinHTTP.Status != 200
{
OutputDebug("PB: Failed with status " WinHTTP.Status)
WinHTTP := ""
Return 0
}
OutputDebug("PB: latestPush => " WinHTTP.ResponseText)
;SAMPLE RESPONSE:
;{"accounts":[],"blocks":[],"channels":[],"chats":[],"clients":[],"contacts":[],"devices":[],"grants":[],"pushes":[{"active":true,"iden":"zjlfdjlsjflwejr1234jlsf","created":1.50802825607309e+09,"modified":1.508028266117232e+09,"type":"note","dismissed":true,"direction":"self","sender_iden":"xxxxx","sender_email":"xxx@gmail.com","sender_email_normalized":"xxx@gmail.com","sender_name":"xxx","receiver_iden":"xxxxx","receiver_email":"xxx@gmail.com","receiver_email_normalized":"xxx@gmail.com","title":"Example Title","body":"Example body text."}],"profiles":[],"subscriptions":[],"texts":[],"cursor":"eyJWZXJzaW9uIjoxLCJNb2RpZmllZEFmdGVyIjoiMDAwMS0wMS0wMVQwMDowMDowMFoiLCJNb2RpZmllZEJlZm9yZSI6IjIwMTctMTAtMTVUMDA6NDQ6MjYuMTE3MjMyWiJ9"}
responseObj := Jxon_Load(WinHTTP.ResponseText)
WinHTTP := ""
OutputDebug("PB: latestPush ct=" responseObj.pushes.Length())
OutputDebug("PB: latestPush modTime=" responseObj.pushes[1].modified)
OutputDebug("PB: latestPush title='" responseObj.pushes[1].title "', body='" responseObj.pushes[1].body "'")
;should probably validate that responseObj.<subtype> is an object and throw error if not
this.LastPushTime := responseObj.pushes[1].modified
Return responseObj.pushes ;only return the pushes array
}
}
class PushBulletEventStream extends WebSocket
{
__New(pbAccessToken, pushCallbackFunc)
{
base.__New("wss://stream.pushbullet.com/websocket/" . pbAccessToken)
this.pushCallbackFunc := pushCallbackFunc
this.LastNopTick := A_TickCount ;used to determine whether the connection is still active
}
OnOpen(Event)
{
;do nothing
}
OnMessage(Event)
{
if IsObject(this.pushCallbackFunc)
{
eventData := Jxon_Load(Event.data)
if eventData.type == "nop"
this.LastNopTick := A_TickCount
else
this.pushCallbackFunc.Call(eventData)
}
}
ConnectionActive()
{
;should receive "nop" every 30 seconds. If it's been 60 sec without one, assume connection isn't active
Return ((A_TickCount - this.LastNopTick) < 60000)
}
OnClose(Event)
{
this.Disconnect()
}
OnError(Event)
{
;not implemented
}
}
Code: Select all
;Jxon
;original code by cocobelgica:
; https://github.com/cocobelgica/AutoHotkey-JSON/blob/master/Jxon.ahk
;updated for AHKv2 by egocarib
Jxon_Load(ByRef src, args*)
{
static q := Chr(34)
key := "", is_key := false
stack := [ tree := [] ]
is_arr := { (tree): 1 }
next := q . "{[01234567890-tfn"
pos := 0
while ( (ch := SubStr(src, ++pos, 1)) != "" )
{
if InStr(" `t`n`r", ch)
continue
if !InStr(next, ch, true)
{
ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
msg := Format("{}: line {} col {} (char {})"
, (next == "") ? ["Extra data", ch := SubStr(src, pos)][1]
: (next == "'") ? "Unterminated string starting at"
: (next == "\") ? "Invalid \escape"
: (next == ":") ? "Expecting ':' delimiter"
: (next == q) ? "Expecting object key enclosed in double quotes"
: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
: (next == ",}") ? "Expecting ',' delimiter or object closing '}'"
: (next == ",]") ? "Expecting ',' delimiter or array closing ']'"
: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
, ln, col, pos)
throw Exception(msg, -1, ch)
}
is_array := is_arr[obj := stack[1]]
if i := InStr("{[", ch)
{
val := (proto := args[i]) ? new proto : {}
is_array? ObjPush(obj, val) : obj[key] := val
ObjInsertAt(stack, 1, val)
is_arr[val] := !(is_key := ch == "{")
next := q . (is_key ? "}" : "{[]0123456789-tfn")
}
else if InStr("}]", ch)
{
ObjRemoveAt(stack, 1)
next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
}
else if InStr(",:", ch)
{
is_key := (!is_array && ch == ",")
next := is_key ? q : q . "{[0123456789-tfn"
}
else ; string | number | true | false | null
{
if (ch == q) ; string
{
i := pos
while i := InStr(src, q,, i+1)
{
val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
static end := A_AhkVersion<"2" ? 0 : -1
if (SubStr(val, end) != "\")
break
}
if !i ? (pos--, next := "'") : 0
continue
pos := i ; update pos
val := StrReplace(val, "\/", "/")
, val := StrReplace(val, "\" . q, q)
, val := StrReplace(val, "\b", "`b")
, val := StrReplace(val, "\f", "`f")
, val := StrReplace(val, "\n", "`n")
, val := StrReplace(val, "\r", "`r")
, val := StrReplace(val, "\t", "`t")
i := 0
while i := InStr(val, "\",, i+1)
{
if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
continue 2
; \uXXXX - JSON unicode escape sequence
xxxx := Abs("0x" . SubStr(val, i+2, 4))
if (A_IsUnicode || xxxx < 0x100)
val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
}
if is_key
{
key := val, next := ":"
continue
}
}
else ; number | true | false | null
{
val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
; For numerical values, numerify integers and keep floats as is.
; I'm not yet sure if I should numerify floats in v2.0-a ...
if val is "number"
{
if val is "integer"
val += 0
}
; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
; SOMETIMES return strings due to certain optimizations. Since it
; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
else if (val == "true" || val == "false")
val := %val% + 0
; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
; as it would raise an exception in AHK_H(overriding built-in var)
else if (val == "null")
val := ""
; any other values are invalid, continue to trigger error
else if (pos--, next := "#")
continue
pos += i-1
}
is_array? ObjPush(obj, val) : obj[key] := val
next := obj==tree ? "" : is_array ? ",]" : ",}"
}
}
return tree[1]
}
Jxon_Dump(obj, indent:="", lvl:=1)
{
static q := Chr(34)
if IsObject(obj)
{
static Type := Func("Type")
if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
is_array := 0
for k in obj
is_array := k == A_Index
until !is_array
if indent is "integer"
{
if (indent < 0)
throw Exception("Indent parameter must be a postive integer.", -1, indent)
spaces := indent, indent := ""
Loop spaces
indent .= " "
}
indt := ""
Loop (indent ? lvl : 0)
indt .= indent
lvl += 1, out := "" ; Make #Warn happy
for k, v in obj
{
if IsObject(k) || (k == "")
throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
if !is_array
out .= ( ObjGetCapacity([k], 1) ? Jxon_Dump(k) : q . k . q ) ;// key
. ( indent ? ": " : ":" ) ; token + padding
out .= Jxon_Dump(v, indent, lvl) ; value
. ( indent ? ",`n" . indt : "," ) ; token + indent
}
if (out != "")
{
out := Trim(out, ",`n" . indent)
if (indent != "")
out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
}
return is_array ? "[" . out . "]" : "{" . out . "}"
}
; Number
else if (ObjGetCapacity([obj], 1) == "")
return obj
; String (null -> not supported by AHK)
if (obj != "")
{
obj := StrReplace(obj, "\", "\\")
, obj := StrReplace(obj, "/", "\/")
, obj := StrReplace(obj, q, "\" . q)
, obj := StrReplace(obj, "`b", "\b")
, obj := StrReplace(obj, "`f", "\f")
, obj := StrReplace(obj, "`n", "\n")
, obj := StrReplace(obj, "`r", "\r")
, obj := StrReplace(obj, "`t", "\t")
static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
while RegExMatch(obj, needle, m)
obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
}
return q . obj . q
}
Code: Select all
;WebSocket
;original code by G33kDude:
; https://github.com/G33kDude/WebSocket.ahk/blob/master/WebSocket.ahk
;updated for AHKv2 by egocarib
class WebSocket
{
__New(WS_URL)
{
static wb
; Create an IE instance
this.Gui := GuiCreate()
WB := this.Gui.Add("ActiveX", "", "Shell.Explorer").Value
; Write an appropriate document
WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
. "content='IE=edge'><body></body>")
while (WB.ReadyState < 4)
Sleep(50)
Doc := WB.document
; Add our handlers to the JavaScript namespace
Doc.parentWindow.ahk_savews := this._SaveWS.Bind(this)
Doc.parentWindow.ahk_event := this._Event.Bind(this)
Doc.parentWindow.ahk_ws_url := WS_URL
; Add some JavaScript to the page to open a socket
Script := doc.createElement("script")
Script.text := "ws = new WebSocket(ahk_ws_url); ahk_savews(ws);`n"
. "ws.onopen = function(event){ ahk_event('Open', event); };`n"
. "ws.onclose = function(event){ ahk_event('Close', event); };`n"
. "ws.onerror = function(event){ ahk_event('Error', event); };`n"
. "ws.onmessage = function(event){ ahk_event('Message', event); };"
Doc.body.appendChild(Script)
}
; Called by the JS to save the WS object to the host
_SaveWS(WebSock)
{
this.WebSock := WebSock
}
; Called by the JS in response to WS events
_Event(EventName, Event)
{
this["On" . EventName](Event)
}
; Sends data through the WebSocket
Send(Data)
{
this.WebSock.send(Data)
}
; Closes the WebSocket connection
Close(Code:=1000, Reason:="")
{
this.WebSock.close(Code, Reason)
}
; Closes and deletes the WebSocket, removing
; references so the class can be garbage collected
Disconnect()
{
if IsObject(this.Gui)
{
this.Close()
this.Gui.Destroy()
this.Gui := ""
}
}
}