PushBullet Class [AHK v2]

Post your working scripts, libraries and tools.
egocarib
Posts: 100
Joined: 21 May 2015, 18:21

PushBullet Class [AHK v2]

23 Jul 2018, 17:58

Image

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:
  1. Visit https://www.pushbullet.com/ and register.
  2. Install Pushbullet App (iPhone or Android).
  3. Visit https://www.pushbullet.com/account and get Access Token from your account Settings page (e.g. G8aldIDL93ldFADFwp9032ADF2klj3ld).
  4. Insert your access token in the demo script and try it out.
Demo script:

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)
	}
}
Pushbullet.ahk

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":"[email protected]","sender_email_normalized":"[email protected]","sender_name":"xxx","receiver_iden":"xxxxx","receiver_email":"[email protected]","receiver_email_normalized":"[email protected]","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
	}
}
Jxon.ahk

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
}
WebSocket.ahk

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 := ""
		}
	}
}
SpecialGuest
Posts: 26
Joined: 15 May 2016, 07:49

Re: PushBullet Class [AHK v2]

23 Jul 2018, 19:32

Nice job :+1: and thanks for sharing.

I previously had some fun with this somewhat similar yet more limited Telegram script:
https://autohotkey.com/boards/viewtopic.php?f=6&t=24919

The fact this Pushbullet setup is bidirectional in communication is a big plus, and it can handle quite a bit more.
I'm sure your V2 jxon and websocket upgrades will come in handy for all the V2 scripters as well.

Keep up the nice work :clap:
Last edited by SpecialGuest on 14 Nov 2018, 11:38, edited 1 time in total.
User avatar
Cerberus
Posts: 172
Joined: 12 Jan 2016, 15:46

Re: PushBullet Class [AHK v2]

19 Aug 2018, 17:26

Great! I've been looking for this for some time. I'll test it once I have my new computer. Bookmarking for now.
wsimon98
Posts: 7
Joined: 29 Oct 2018, 20:26

Re: PushBullet Class [AHK v2]

13 Nov 2018, 21:23

This seems cool, but I'm getting the following error:

Code: Select all

C:\Program Files\AutoHotkey\Willbilly\Miscellaneous\Jxon.ahk (114) : ==> Parameter #2 invalid.
     Specifically: "number"
>Exit code: 2    Time: 0.1998
Any suggestions?
User avatar
SALZKARTOFFEEEL
Posts: 89
Joined: 10 May 2017, 11:14
Location: Germany
Contact:

Re: PushBullet Class [AHK v2]

07 Feb 2019, 17:21

wsimon98 wrote:
13 Nov 2018, 21:23
This seems cool, but I'm getting the following error:

Code: Select all

C:\Program Files\AutoHotkey\Willbilly\Miscellaneous\Jxon.ahk (114) : ==> Parameter #2 invalid.
     Specifically: "number"
>Exit code: 2    Time: 0.1998
Any suggestions?
You are running it with AutoHotkey v1, it requires v2!

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: bqnwne937, Descolada, songdg and 24 guests