Skip to content

Instantly share code, notes, and snippets.

@jgeboski
Last active August 29, 2015 14:08
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jgeboski/e5c5c7190c6c5ffb5a60 to your computer and use it in GitHub Desktop.
Yik-Yak to Twitter Gateway
#!/usr/bin/env python2
#
# Copyright 2014 James Geboski <jgeboski@gmail.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 3 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, see <http://www.gnu.org/licenses/>
#
# Yik-Yak Information:
# - https://github.com/djtech42/YikYakTerminal
import argparse
import base64
import hashlib
import hmac
import json
import logging
import os
import sys
import textwrap
import time
import uuid
from signal import signal, SIGINT
from threading import Event
from tweepy.error import TweepError
from urllib import quote_plus
from urllib2 import Request, urlopen
from urlparse import urljoin
try:
import tweepy
from tweepy import TweepError
except:
print "Tweepy is missing"
exit(1)
YIKYAK_URL = "https://us-east-api.yikyakapi.net/"
YIKYAK_AGENT = "Yik Yak/2.1.0.23 (iPhone; iOS 8.1; Scale/2.00)"
YIKYAK_KEY = "F7CAFA2F-FE67-4E03-A090-AC7FFF010729"
TWITTER_MAXLEN = 140
TIMEOUT = 60
class Yakker():
def __init__(self, log):
self.log = log
self.running = Event()
self.config = None
self.timeout = float()
self.yakkerid = str()
self.yaktime = str()
self.yakids = set()
self.latitude = float()
self.longitude = float()
self.twitconkey = str()
self.twitconsec = str()
self.twitacskey = str()
self.twitacssec = str()
self.running.set()
def __dir__(self):
return [
"latitude", "longitude", "timeout",
"twitconkey", "twitconsec", "twitacskey",
"twitacssec", "yakkerid", "yaktime",
"yakids"
]
def load(self, config):
if not os.path.exists(config) or os.path.getsize(config) < 1:
return
fp = open(config, "r")
cd = json.load(fp)
fp.close()
self.config = config
self.log.info("Loading config from %s", config)
for k, v in cd.iteritems():
if not hasattr(self, k):
continue
a = getattr(self, k)
setattr(self, k, type(a)(v))
self.log.info("Setting %s = %s", k, v)
def save(self, config = None):
if not config:
config = self.config
if not config:
self.log.warn("No config file specified, not saving")
return
path = os.path.dirname(config)
cd = dict()
if path and not os.path.exists(path):
os.makedirs(path)
self.log.info("Creating directory: %s", path)
for k in dir(self):
cd[k] = getattr(self, k)
if isinstance(cd[k], set):
cd[k] = list(cd[k])
self.log.info("Saving %s as %s", k, cd[k])
self.log.info("Saving config to %s", config)
fp = open(config, "w")
json.dump(cd, fp, indent = True, sort_keys = True)
fp.close()
def configure(self):
while True:
try:
self.timeout = raw_input("Timeout (seconds): ")
self.timeout = float(self.timeout)
break
except:
continue
while True:
try:
self.latitude = raw_input("Latitude (degrees): ")
self.latitude = float(self.latitude)
break
except:
continue
while True:
try:
self.longitude = raw_input("Longitude (degrees): ")
self.longitude = float(self.longitude)
break
except:
continue
while True:
self.twitconkey = raw_input("Twitter Consumer Key: ")
self.twitconsec = raw_input("Twitter Consumer Secret: ")
auth = tweepy.OAuthHandler(self.twitconkey, self.twitconsec)
print "Visit: %s" % (auth.get_authorization_url())
pin = raw_input("Verification PIN: ")
try:
auth.get_access_token(pin)
self.twitacskey = auth.access_token.key
self.twitacssec = auth.access_token.secret
break
except:
print "Failed to obtain access token"
continue
def start(self):
if self.running.isSet():
self.running.clear()
self.run()
def stop(self):
self.running.set()
def run(self):
auth = tweepy.OAuthHandler(self.twitconkey, self.twitconsec)
api = tweepy.API(auth, timeout = TIMEOUT)
auth.set_access_token(self.twitacskey, self.twitacssec)
while not self.running.isSet():
self.poll(api)
self.running.wait(self.timeout)
def poll(self, twitapi):
msgs = self.yakmsgs()
save = False
for msg in msgs:
if msg['time'] < self.yaktime or msg['messageID'] in self.yakids:
continue
save = True
self.log.info("Processing %s", msg)
lines = textwrap.wrap(
msg['message'],
width = TWITTER_MAXLEN,
break_on_hyphens = False
)
lat = msg['latitude']
lon = msg['longitude']
lmid = None
i = 0
while i < len(lines) and not self.running.isSet():
try:
self.log.info("Tweeting Yik-Yak message")
lmid = twitapi.update_status(lines[i], lmid, lat, lon).id
i += 1
except Exception as e:
if isinstance(e, TweepError):
try:
code = int(e.message[0]['code'])
except:
code = 0
if code == 185:
self.log.error("Tweet limit reached")
self.log.info("Retrying in 60 minutes...")
self.running.wait(3600)
continue
if code == 187:
self.log.warn("Duplicate tweet, skipping")
break
self.log.error("Failed to Tweet: %s", e)
self.log.info("Retrying in 10 seconds...")
self.running.wait(10)
if not self.running.isSet():
if msg['time'] != self.yaktime:
self.yaktime = msg['time']
self.yakids.clear()
self.yakids.add(msg['messageID'])
if save:
self.save()
def yakreq(self, subsys, params):
url = "/api/%s" % (subsys)
data = str()
if len(params) > 0:
data += "%s?%s" % (url, Yakker.psencode(params))
salt = str(int(time.time()))
data += salt
sha1 = hmac.new(YIKYAK_KEY.encode(), data.encode(), hashlib.sha1)
sha1 = base64.b64encode(sha1.digest())
params["hash"] = sha1
params["salt"] = salt
self.log.info("Sending Yik-Yak request (%s)", subsys)
try:
url = urljoin(YIKYAK_URL, "%s?%s" % (url, Yakker.psencode(params)))
req = Request(url, None, {"User-Agent": YIKYAK_AGENT})
res = urlopen(req, timeout = TIMEOUT)
self.log.info("Sent Yik-Yak request")
return res.getcode(), res.read()
except Exception as e:
self.log.error("Failed to send Yik-Yak request: %s", e)
return 0, dict()
def yakreg(self):
if not self.yakkerid:
self.yakkerid = str(uuid.uuid1()).upper()
self.log.info("Setting Yik-Yak UUID to %s", self.yakkerid)
self.log.info("Registering Yik-Yak UUID")
params = {
"userID": self.yakkerid,
"userLat": self.latitude,
"userLong": self.longitude
}
code, data = self.yakreq("registerUser", params)
if code != 200:
self.log.error("Failed to register Yik-Yak UUID")
return
self.log.info("Registered Yik-Yak UUID")
def yakmsgs(self):
if not self.yakkerid:
self.yakreg()
self.log.info("Getting peek messages")
params = {
"userID": self.yakkerid,
"userLat": self.latitude,
"userLong": self.longitude
}
code, data = self.yakreq("getMessages", params)
try:
rd = json.loads(data)
except:
rd = dict()
if not "messages" in rd:
self.log.error("Failed to get peek messages")
return list()
self.log.info("Processing messages")
return sorted(rd['messages'], key = lambda k: k['time'])
@staticmethod
def psencode(params):
keys = params.keys()
data = str()
keys.sort()
for k in keys:
k = quote_plus(str(k))
v = quote_plus(str(params[k]))
data += "%s=%s&" % (k, v)
if data.endswith("&"):
data = data[:-1]
return data
def parseargs():
parser = argparse.ArgumentParser(
description = "Yik-Yak to Twitter Gateway"
)
parser.add_argument(
"-d", "--daemon",
required = False,
action = "store_true",
dest = "daemon",
help = "enable daemon mode"
)
parser.add_argument(
"-v", "--verbose",
required = False,
action = "store_true",
dest = "verbose",
help = "enable debugging output"
)
parser.add_argument(
"-n", "--configure",
required = False,
action = "store_true",
dest = "configure",
help = "setup the initial configuration"
)
parser.add_argument(
"-c", "--config",
required = False,
type = str,
action = "store",
dest = "config",
metavar = "PATH",
default = "config.json",
help = "specify the configuration file path"
)
return parser.parse_args()
def main():
args = parseargs()
log = logging.getLogger("yakker")
yakker = Yakker(log)
logging.basicConfig(format = "[%(levelname)s] %(message)s")
log.setLevel(logging.INFO if args.verbose else logging.ERROR)
if args.daemon and not args.configure:
pid = os.fork()
if pid > 0:
sys.exit(0)
os.setsid()
os.umask(0)
sys.stdin = open("/dev/null", "r")
sys.stdout = open("/dev/null", "w")
sys.stderr = open("/dev/null", "w")
pid = os.fork()
if pid > 0:
sys.exit(0)
signal(SIGINT, lambda s, f: yakker.stop())
yakker.load(args.config)
if not args.configure:
yakker.start()
else:
yakker.configure()
yakker.save(args.config)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment