Use Asterisk as an SRST (Survivable Remote Site Telephony) Gateway with Cisco CallManager
What is this and why do I need it? Here’s the scenario. You have a central office with five remote offices. You run Cisco CallManager at the central office and you simply deploy phones out at the remotes to use the central Callmanager. This saves you money by using a single call server. Now imagine the link that connects your Dallas office back to the central site, fails. No one in your Dallas office can receive or place calls! Now, imagine that the link from the central office connecting you to all of your remotes fails, and then none of the five remote sites can make calls! This could be devastating. What does one do?
You install some SRST (survivable remote site telephony). This is basically a fall-back phone system for your remote sites. Going back to the Dallas office, if the link fails with an SRST gateway present, your phones simply register with the local gateway and continue to function. They will often have limited functionality at this point, but you can still make and receive calls!
Now, imagine you have 64 phones at each remote site. Cisco will tell you that you require Unified Communications Manager Express running on a 2851 ($~5K) and a VWIC (voice WIC ~$300) for each site. This at five locations will be somewhere around ~$27K. Now imagine you could get the same functionality for easily less than $1K per site! Seems like a no brainer, right? What if I also said, you could have it automatically update the SRST site with the phones that belong at the site. How you may ask…if you are asking, you should have read the title of the article a little closer…use Asterisk!
A few easy steps and some scripts I’ve written will do all the hard work for you.
Click the link below for the full article!
What do I need?
- A server for each site. This is what we will install Asterisk on. I say server, but it could be a machine that is worth a few hundred dollars.
- Voice interface card. This will be a PRI card, an FXO card(some way to connect to a telephony network), or could be a SIP provider. You will generally have a pots line for 911.
- A CallManager server running 4.X
- A copy of Trixbox CE (Community Edition – as in free)
- Newest version of chan-sccp
Installing Trixbox:
The great thing about trixbox is that it is a snap to install. You put the CD in, boot up, hit enter and it formats and installs the OS and gets the phone system running without intervention.
Once trixbox is running, you should go ahead and update it to the newest revision and install all your desired packages. I’m not going to go through this with you since it is beyond the scope of this article. If you are new to trixbox, I would recommend trixbox without tears.
Once you have trixbox installed, we will install the newest chan-sccp module. Cisco phones use a control protocol called skinny. They can be flashed to run SIP, but if you have a callmanager 4.X install, you are using skinny, so we will configure our srst gateways to use skinny also.
***How to install chan-sccp and webmin on your trixbox***
Next, we need to install FTP on this guy. There are a million FTP servers for linux, so pick your favorite. All we really need is to allow our ftp user access to the temporary folder where we want to upload the files.
Now we need to put on our shell script and create the cronjob for it.
***cronjob instructions***
This shell script will take the files you FTP over and copy them to the correct location. It will then restart your asterisk server to accept the new settings. I’ve named mine srst.sh
1 2 3 4 5 6 7 8 9 10 11 12 | #!/bin/sh #remove old sep files, then copy in new ones rm -f /tftp-root/*.xml cp /tmp/srst/*.xml /tftp-root/ #remove old sccp file and then copy new one rm -f /etc/asterisk/sccp.conf cp /tmp/srst/sccp.conf /etc/asterisk/sccp.conf #restart asterisk to accept new settings service asterisk restart |
***This is configuring the cronjob via webmin***
CallManager Configuration:
Now we are to the callmanager. First add the SRST gateway to your callmanager:
***Add SRST gateway to our callmanager***
We now need to create a read-only account on our callmanager database.
***Create read-only account on our callmanager database***
We now copy our asterisk srst program into a folder on the callmanager and configure two files. After that we schedule it to run at whatever specific interval we want. One caveat is that the script only graps your front line. This will give you the ability to still send and receive calls.
All files in a single zip compiled( Asterisk SRST (1226 downloads) ), or grab the source below.
The config file srst.txt is where you list the different srsts you want to use as well as your database connection information.
srst.txt config file with our example srst
1 2 3 4 5 6 7 8 9 | #This is where you put your DB name, username and password. DBU-PW needs to be at the beginning. It indicates #that the DD username and password are on this line. #DBU-PW,DB Name,DB Username,DBPassword #DBU-PW,CCM0302,srsttest,test DBU-PW,CCM0302,srsttest,test #This is where you list the SRSTs and their connection info. #SRST IP,FTP-User,FTP-Password,Temp FTP Folder on srst server #1.1.1.1,asterisk,password,/tmp/srst 192.168.222.103,asterisk,password,/tmp/srst |
The sccp.txt config file lists some settings for the srst server. The script uses these when it build the sccp.conf file. You can accept the default file with no modifications if you like.
sccp.txt config file. You can change these settings if you like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [general] keepalive = 30 context = default dateFormat = D.M.Y ; date format port = 2000 ; port to bind to (2000 = skinny) debug = 4 accountcode=skinny ; recordname appearing in cdr callwaiting_tone = 0x2d ; turn off callwaiting tone == 0 language=en echocancel = on silencesuppression = off cfwdall = on ; turn on call forward button cfwdbusy = on ; turn on call forward when busy button dnd = on ; turn on "do not disturb" digittimeoutchat = # ; type hash to stop dialing timeout disallow = all allow = alaw allow = ulaw ; |
Here is the code for the autoit script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | #include <Array.au3> #include <file.au3> ;available recordset commands ;rs.MoveFirst() moves to the start of a recordset ;rs.EOF tells if it is eof or not ;rs.BOF tells if it is bof or not ;rs.MoveNext() moves to the next set of records if not FileExists(@ScriptDir & "\srst.txt") Then MsgBox(0,"create srst.txt in script folder", "Missing srst.txt file") Exit EndIf ;clear the ftp log file ;FileDelete(@ScriptDir & "ftpLog.txt") ;read in srst.txt file Dim $aSRSTFile If Not _FileReadToArray(@ScriptDir & "\srst.txt",$aSRSTFile) Then MsgBox(4096,"Error", " Error reading " & @ScriptDir & "\srst.txt to Array error:" & @error) Exit EndIf ;loop through the srst text file For $x = 1 to $aSRSTFile[0] ;make sure entry isn't null if $aSRSTFile[$x] <> "" Then ;check if it is our DB info if StringLeft($aSRSTFile[$x], 7) == "DBU-PW," Then ;this is the dbconfig stuff, set your variables. ;setup our connection $dbName = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",") + 1, StringInStr($aSRSTFile[$x], ",", 0, 2) - 1 - StringInStr($aSRSTFile[$x], ",")) $dbUser = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",", 0, 2) + 1, StringInStr($aSRSTFile[$x], ",", 0, 3) - 1 - StringInStr($aSRSTFile[$x], ",", 0, 2)) $dbPass = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",", 0, -1) + 1) $conn = ObjCreate("ADODB.Connection") $DSN = "DRIVER={SQL Server};SERVER=127.0.0.1;DATABASE=" & $dbName & ";UID=" & $dbUser & ";PWD=" & $dbPass & ";" ;MsgBox(0,0,$DSN) $conn.Open($DSN) ;define our recordsets $rsTypeProduct = ObjCreate("ADODB.RecordSet") $rsSRST = ObjCreate("ADODB.RecordSet") $rsDevicePool = ObjCreate("ADODB.RecordSet") $rsDevice = ObjCreate("ADODB.RecordSet") $rsDeviceNumPlanMap = ObjCreate("ADODB.RecordSet") $rsNumPlan = ObjCreate("ADODB.RecordSet") ;pull all of our records into the recordset $queryTypeProduct = "select Name, tkModel from TypeProduct" $rsTypeProduct.Open($queryTypeProduct, $conn) $querySRST = "select pkid, Name, IPAddr1 from SRST" $rsSRST.Open($querySRST, $conn) $queryDevicePool = "select pkid, fkSRST from DevicePool" $rsDevicePool.Open($queryDevicePool, $conn) $queryDevice = "select pkid, Name, Description, tkModel, fkDevicePool from Device" $rsDevice.Open($queryDevice, $conn) $queryDeviceNumPlanMap = "select pkid, fkDevice, fkNumPlan from DeviceNumPlanMap" $rsDeviceNumPlanMap.Open($queryDeviceNumPlanMap, $conn) $queryNumPlan = "select pkid, DNOrPattern from NumPlan" $rsNumPlan.Open($queryNumPlan, $conn) ;make sure it isn't a comment Elseif StringLeft($aSRSTFile[$x], 1) <> "#" Then ;we got a hot one, dog ;setup the ftp info $srstIP = StringLeft($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",") - 1) $ftpUser = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",") + 1, StringInStr($aSRSTFile[$x], ",", 0, 2) - 1 - StringInStr($aSRSTFile[$x], ",")) $ftpPass = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",", 0, 2) + 1, StringInStr($aSRSTFile[$x], ",", 0, 3) - 1 - StringInStr($aSRSTFile[$x], ",", 0, 2)) $ftpTempFolder = StringMid($aSRSTFile[$x], StringInStr($aSRSTFile[$x], ",", 0, -1) + 1) ;**************************files********************************* ;create the temp directory for the sep files DirCreate(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SEP-Files") ;create the temp directory for the sccp files DirCreate(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files") ;clean out the sccp-files folder FileDelete(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\devices.txt") FileDelete(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\lines.txt") ;clean out the sep-files folder $cleanSEP = _FileListToArray(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SEP-Files") if @error == 0 Then for $q = 1 to $cleanSEP[0] FileDelete(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SEP-Files\" & $cleanSEP[$q]) Next Else MsgBox(0,"folder empty", "clean sep function folder empty", 1.5) EndIf ;open our device file, used to build the sccp.conf file $fileDevices = FileOpen(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\devices.txt", 1) ; Check if file opened for writing OK If $fileDevices = -1 Then MsgBox(0, "Error", "Unable to open file.", 1) ;Exit EndIf ;open our lines file, used to build the sccp.conf file $fileLines = FileOpen(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\lines.txt", 1) ; Check if file opened for writing OK If $fileLines = -1 Then MsgBox(0, "Error", "Unable to open file.", 1) ;Exit EndIf ;**************************end-files********************************* ;find srst ID $srstPkid = "" $rsSRST.MoveFirst() while not $rsSRST.EOF if $rsSRST.Fields("IPAddr1").Value == $srstIP Then ;found it, record the pkid $srstPkid = $rsSRST.Fields("pkid").Value EndIf $rsSRST.MoveNext() WEnd if $srstPkid <> "" Then ;we found our match ;lets find our device pool IDs This is an array, because we could have multiples dim $devicePoolPkid[1] = [0] $rsDevicePool.MoveFirst() while not $rsDevicePool.EOF ;loop through looking for matches. We could potentially have multiple matches, so we need to ;account for this. if $rsDevicePool.Fields("fkSRST").value == $srstPkid Then ;we've found a match _ArrayAdd($devicePoolPkid, $rsDevicePool.Fields("pkid").value) $devicePoolPkid[0] = $devicePoolPkid[0] + 1 EndIf $rsDevicePool.MoveNext WEnd ;This is where we build the files ;start sifting through the devices for $z = 1 to $devicePoolPkid[0] $rsDevice.MoveFirst() while not $rsDevice.EOF if $rsDevice.Fields("fkDevicePool").value == $devicePoolPkid[$z] Then ;we found a matching device. ;no search in devicenumplan match to find the ID for the Directory Num $rsDeviceNumPlanMap.MoveFirst() while not $rsDeviceNumPlanMap.EOF if $rsDevice.Fields("pkid").Value == $rsDeviceNumPlanMap.Fields("fkDevice").value Then ;we found our match $rsNumPlan.MoveFirst() while not $rsNumPlan.EOF ;look for the matching directory number if $rsNumPlan.Fields("pkid").Value == $rsDeviceNumPlanMap.Fields("fkNumPlan").Value Then ;got the match for the directory number ToolTip("running functs-" & $x & "-" & $z, 0, 0) ;now lets start creating our files, call functions _CreateSCCP() _CreateSEP() EndIf $rsNumPlan.MoveNext() WEnd EndIf $rsDeviceNumPlanMap.MoveNext() WEnd EndIf $rsDevice.MoveNext() WEnd Next EndIf ;close the devices file FileClose($fileDevices) ;close the lines file FileClose($fileLines) ;**********write master sccp.conf file*************** ;now combine the two files with the sccp.txt file. ;copy tepmplate file FileCopy(@ScriptDir & "\sccp.txt", @ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sccp.conf", 1) ;now open the file to do a little writing $fileSccpConf = FileOpen(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sccp.conf", 1) ;set variable if no files exist bypass $noFiles = 0 Dim $aDevicesC If Not _FileReadToArray(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\devices.txt",$aDevicesC) Then MsgBox(4096,"Error", " Error reading log to Array error:" & @error, 2) ;set no files flag $noFiles = 1 EndIf Dim $aLinesC If Not _FileReadToArray(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\SCCP-Files\lines.txt",$aLinesC) Then MsgBox(4096,"Error", " Error reading log to Array error:" & @error, 2) ;set no files flag $noFiles = 1 EndIf ; Check if file opened for writing OK If $fileSccpConf = -1 Then MsgBox(0, "Error", "Unable to open file.", 1) ;Exit EndIf ;add the srst ip to the sccp file FileWriteLine($fileSccpConf, @CRLF & "bindaddr = " & $srstIP & @CRLF) FileWriteLine($fileSccpConf, @CRLF) ;start the devices section FileWriteLine($fileSccpConf, "[devices]" & @CRLF) ;check to make sure we had the files in existance if $noFiles == 0 Then ;loop through the devices file writing all the entries For $p = 1 to $aDevicesC[0] FileWriteLine($fileSccpConf, $aDevicesC[$p] & @CRLF) Next ;start the lines section FileWriteLine($fileSccpConf, @CRLF) FileWriteLine($fileSccpConf, "[lines]" & @CRLF) ;loop through the lines section writing all the entries For $p = 1 to $aLinesC[0] FileWriteLine($fileSccpConf, $aLinesC[$p] & @CRLF) Next EndIf ;close the file FileClose($fileSccpConf) ;**********END write master sccp.conf file*************** ;ftp the files to the srst server if $noFiles == 0 Then _FtpFiles() EndIf EndIf EndIf Next ;close the DB connection $conn.close ;END of prog Func _FtpFiles() FileDelete(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\ftpFiles.txt") $fileFtp = FileOpen(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\ftpFiles.txt", 1) ; Check if file opened for writing OK If $fileFtp = -1 Then MsgBox(0, "Error", "Unable to open file.") Exit EndIf ;list contents of our sep folder $tempFileList = _FileListToArray(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sep-files\", "*.cnf",1) ;if we have files, lets ftp them if $tempFileList[0] > 0 Then FileWrite($fileFtp, "OPEN " & $srstIP & @CRLF) FileWrite($fileFtp, $ftpUser & @CRLF) FileWrite($fileFtp, $ftpPass & @CRLF) FileWrite($fileFtp, "binary" & @CRLF) FileWrite($fileFtp, "cd " & $ftpTempFolder & @CRLF) FileWrite($fileFtp, "lcd " & FileGetShortName(@ScriptDir & "\" & stringreplace($srstIP, ".", "-")) & @CRLF) FileWrite($fileFtp, "MPUT " & FileGetShortName(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sccp.conf") & @CRLF) FileWrite($fileFtp, "lcd " & FileGetShortName(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sep-files") & @CRLF) ;loop though files adding them for $e = 1 to $tempFileList[0] FileWrite($fileFtp, "MPUT " & $tempFileList[$e] & @CRLF) Next FileWrite($fileFtp, "BYE" & @CRLF) ;close ftp file FileClose($fileFtp) ;do the ftp ;run("c:\WINNT\system32\FTP.EXE -i -s:" & FileGetShortName(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\ftpFiles.txt") & " >> " & FileGetShortName(@ScriptDir) & "\ftpLog.txt") run("c:\WINNT\system32\FTP.EXE -i -s:" & FileGetShortName(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\ftpFiles.txt")) EndIf EndFunc ;create sep file fuction Func _CreateSEP() ;this builds the sep files that will dump into the tftp folder $file = FileOpen(@ScriptDir & "\" & stringreplace($srstIP, ".", "-") & "\sep-files\" & $rsDevice.Fields("Name").Value & ".cnf", 1) ; Check if file opened for writing OK If $file = -1 Then MsgBox(0, "Error", "Unable to open file.", 1) ;Exit EndIf FileWrite($file, "<device>" & @CRLF) FileWrite($file, " <devicePool>" & @CRLF) FileWrite($file, " <callManagerGroup>" & @CRLF) FileWrite($file, " <members>" & @CRLF) FileWrite($file, " <member priority=""0"">" & @CRLF) FileWrite($file, " <callManager>" & @CRLF) FileWrite($file, " <ports>" & @CRLF) FileWrite($file, " <ethernetPhonePort>2000</ethernetPhonePort>" & @CRLF) FileWrite($file, " </ports>" & @CRLF) FileWrite($file, " <processNodeName>" & $srstIP & "</processNodeName>" & @CRLF) FileWrite($file, " </callManager>" & @CRLF) FileWrite($file, " </member>" & @CRLF) FileWrite($file, " </members>" & @CRLF) FileWrite($file, " </callManagerGroup>" & @CRLF) FileWrite($file, " </devicePool>" & @CRLF) FileWrite($file, " <versionStamp>{Jan 28 2008 19:01:00}</versionStamp>" & @CRLF) FileWrite($file, " <loadInformation></loadInformation>" & @CRLF) FileWrite($file, " <userLocale>" & @CRLF) FileWrite($file, " <name>United_Kingdom</name>" & @CRLF) FileWrite($file, " <langCode>de</langCode>" & @CRLF) FileWrite($file, " </userLocale>" & @CRLF) FileWrite($file, " <networkLocale>United_Kingdom</networkLocale>" & @CRLF) FileWrite($file, " <idleTimeout>0</idleTimeout>" & @CRLF) FileWrite($file, " <authenticationURL></authenticationURL>" & @CRLF) FileWrite($file, " <directoryURL></directoryURL>" & @CRLF) FileWrite($file, " <idleURL></idleURL>" & @CRLF) FileWrite($file, " <informationURL></informationURL>" & @CRLF) FileWrite($file, " <messagesURL></messagesURL>" & @CRLF) FileWrite($file, " <proxyServerURL></proxyServerURL>" & @CRLF) FileWrite($file, " <servicesURL></servicesURL>" & @CRLF) FileWrite($file, "</device>" & @CRLF) FileClose($file) EndFunc Func _CreateSCCP() ; this builds the temporary devices and lines files that will be combined to create the sccp.conf file FileWrite($fileDevices, "type = " & _DeviceType() & @CRLF) FileWrite($fileDevices, "description = " & $rsDevice.Fields("Description").Value & @CRLF) FileWrite($fileDevices, "tzoffset = 0" & @CRLF) FileWrite($fileDevices, "autologin = *" & $rsNumPlan.Fields("DNOrPattern").Value & @CRLF) FileWrite($fileDevices, "device => " & $rsDevice.Fields("Name").Value & @CRLF) FileWrite($fileDevices, @CRLF) FileWrite($fileLines, "id = " & $rsNumPlan.Fields("DNOrPattern").Value & @CRLF) FileWrite($fileLines, "label = " & $rsNumPlan.Fields("DNOrPattern").Value & @CRLF) FileWrite($fileLines, "description = " & $rsDevice.Fields("Description").Value & @CRLF) FileWrite($fileLines, "context = default" & @CRLF) FileWrite($fileLines, "callwaiting = 1" & @CRLF) FileWrite($fileLines, "incominglimit = 3" & @CRLF) FileWrite($fileLines, "cid_name = " & $rsDevice.Fields("Description").Value & @CRLF) FileWrite($fileLines, "cid_num = " & $rsNumPlan.Fields("DNOrPattern").Value & @CRLF) FileWrite($fileLines, "line => *" & $rsNumPlan.Fields("DNOrPattern").Value & @CRLF) FileWrite($fileLines, @CRLF) EndFunc Func _DeviceType() ;do the phone typelookup ;loop through the models to find out the type $rsTypeProduct.MoveFirst() while not $rsTypeProduct.EOF if $rsDevice.Fields("tkModel").Value == $rsTypeProduct.Fields("tkModel").Value Then ;we have our models matched if $rsTypeProduct.Fields("tkModel").Value == 30016 Then ;is softphone return "7980" Else Return StringRight($rsTypeProduct.Fields("Name").Value, 4) EndIf EndIf $rsTypeProduct.MoveNext() WEnd EndFunc |
The last thing to do is schedule the backup program to run via windows scheduler. You will want this to run at least a few minutes before the shell script runs on the srst server. We are done, time for you to test!
You will want to route incoming calls to the receptionist, or a call handler. You could also change the code in the script so that it adds a second line to the phones that incoming lines point to. You will also need to setup outgoing routing so that your users can make calls.
I’m hoping to get access to 6.1 soon. If I can, I’ll attempt to port my work to the new version. From what I understand it will be something of an undertaking.
I spent about 5 hours of solid coding on this one, so if you like it, or use it, drop me a line or leave a comment. I like nothing more than getting feedback…other than money…you can give me money if you like…heh