Yik-Yak to Twitter Gateway
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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