786 lines
26 KiB
Ruby
786 lines
26 KiB
Ruby
require 'sinatra'
|
|
require 'xmlrpc/marshal'
|
|
require 'xmlrpc/create'
|
|
require 'discourse_api'
|
|
require 'json'
|
|
require 'pp'
|
|
require 'chronic'
|
|
require 'base64'
|
|
require 'net/https'
|
|
|
|
|
|
# TODO: monkey-patch XMLRPC requests to support tapatalk's invalid bools.
|
|
# The hack below works, ish.
|
|
|
|
# We need to monkey-patch our XMLRPC responses to have invalid(!) responses for
|
|
# dateTime.iso8601 objects
|
|
|
|
module Tapatalk
|
|
|
|
class Time < ::Time
|
|
|
|
end
|
|
|
|
end
|
|
|
|
module XMLRPC
|
|
class Create
|
|
|
|
private
|
|
|
|
alias_method :real_conv2value, :conv2value
|
|
|
|
def conv2value( param )
|
|
if param.is_a?(::Tapatalk::Time )
|
|
value = @writer.tag( "dateTime.iso8601", param.iso8601 ) # includes the timezone
|
|
@writer.ele("value", value)
|
|
else
|
|
real_conv2value( param )
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
|
|
PUSH_TYPES = "ann,conv,pm,sub,like,thank,quote,newtopic,tag"
|
|
|
|
CONFIG = YAML.load(
|
|
File.read(
|
|
File.join(
|
|
File.expand_path( File.join( File.dirname( __FILE__ ), ".." ) ),
|
|
"config.yml"
|
|
)
|
|
)
|
|
)
|
|
|
|
#use Rack::Session::Cookie, :key => "rack.session",
|
|
# :domain => DOMAIN,
|
|
# :path => "/",
|
|
# :expire_after => 86400 * 7, # login every 7 days...
|
|
# :secret => "change_me", # change these
|
|
# :old_secret => "change_me"
|
|
|
|
|
|
set :discourse_api, URI::parse( CONFIG.fetch( 'discourse_url' ) )
|
|
set :discourse_api_key, CONFIG.fetch( 'discourse_api_key' )
|
|
|
|
# For push notifications. TODO.
|
|
set :tapatalk_api, CONFIG.fetch( 'tapatalk_url', nil )
|
|
set :tapatalk_api_key, CONFIG.fetch( 'tapatalk_api_key', nil )
|
|
|
|
|
|
before do
|
|
# Pass through all cookies to the discourse instance
|
|
discourse( request.cookies )
|
|
|
|
# TODO: Override on auth failures, cookie timeouts, etc. to set this to false
|
|
mobiquo_login_header!( request.cookies.has_key?("_t") )
|
|
end
|
|
|
|
helpers do
|
|
|
|
def mobiquo_login_header!( val )
|
|
headers "Mobiquo_is_login" => val.to_s
|
|
end
|
|
|
|
# Use this as our entry point
|
|
def dispatch
|
|
xml = fixup_tapatalk_xml( @request.body.read )
|
|
|
|
fixup_tapatalk_xml( xml )
|
|
|
|
halt 400, "Empty XMLRPC request" if xml.empty?
|
|
|
|
# Parse xml
|
|
method, arguments = XMLRPC::Marshal.load_call(xml)
|
|
|
|
method = "xmlrpc_#{method.gsub(/([A-Z])/, '_ ').downcase}"
|
|
|
|
# FIXME: this is pretty evil
|
|
filtered_args = if method == "xmlrpc_login"
|
|
tmp = arguments.dup
|
|
tmp[1] = "[FILTERED]"
|
|
tmp
|
|
elsif method == "xmlrpc_update_push_status"
|
|
tmp = arguments.dup
|
|
tmp[2] = "[FILTERED]"
|
|
tmp
|
|
else
|
|
arguments
|
|
end
|
|
|
|
puts "method = #{method}, args = #{filtered_args.inspect}"
|
|
|
|
# Check if method exists
|
|
if(respond_to?(method))
|
|
content_type("text/xml", :charset => "utf-8")
|
|
send(method, *arguments)
|
|
else
|
|
puts "UNKNOWN METHOD: #{method}"
|
|
respond_xmlrpc( 'result' => false, 'result_text' => binary_xmlrpc( "Unknown method: #{method}" ) )
|
|
end
|
|
end
|
|
|
|
# Apparently, tapatalk formats booleans contrary to the XMLRPC standard.
|
|
# https://github.com/svdgraaf/django-tapatalk/blob/master/tapatalk/dispatcher.py
|
|
def fixup_tapatalk_xml( text )
|
|
text.gsub( '<boolean>true</boolean>', '<boolean>1</boolean>' ).
|
|
gsub( '<boolean>false</boolean>', '<boolean>0</boolean>' )
|
|
end
|
|
|
|
def make_xmlrpc_datetime( string )
|
|
t = Chronic.parse( string )
|
|
|
|
if request.env['HTTP_MOBIQUOID'] == "2" || request['HTTP_MOBIQUO_ID'] == "2" # apple
|
|
::Tapatalk::Time.new( t.year, t.month, t.day, t.hour, t.min, t.sec, t.utc_offset )
|
|
else
|
|
t
|
|
end
|
|
end
|
|
|
|
def binary_xmlrpc( string )
|
|
# assume it's utf-8 for now, wrap in base64 encoding
|
|
XMLRPC::Base64.new( string )
|
|
end
|
|
|
|
def make_avatar_url( template_txt, size = 120 )
|
|
template_txt.to_s.
|
|
gsub(/\A\/\//, "http://"). # srsly wat, discourse? Well, it's probably for selection between HTTP or HTTPS more easily. But still.
|
|
gsub( "{size}", size.to_s ) # url parameter
|
|
end
|
|
|
|
# Clean up the text, converting it to BBCode (TODO) unless use_html
|
|
|
|
def format_post_content_for_xmlrpc( html_text, use_html )
|
|
raise NotImplementedError.new( "Can't convert to BBCode yet" ) unless
|
|
use_html
|
|
|
|
html_text
|
|
end
|
|
|
|
def build_tapatalk_topic( discourse_topic, extra = {} )
|
|
result = {
|
|
'forum_id' => discourse_topic["category_id"].to_s,
|
|
'forum_name' => binary_xmlrpc( "" ), # TODO
|
|
'topic_id' => discourse_topic["id"].to_s,
|
|
'topic_title' => binary_xmlrpc( discourse_topic["title"].to_s ),
|
|
# 'prefix' => binary_xmlrpc( "" ), # optional
|
|
'post_author_name' => binary_xmlrpc( "" ),
|
|
'topic_author_name' => binary_xmlrpc( "" ), # override later
|
|
'topic_author_display_name' => binary_xmlrpc( "" ), # also
|
|
# 'post_author_id' => 0, # required, Int (apparently), level 4
|
|
'is_subscribed' => false, # TODO
|
|
'can_subscribe' => false, # TODO
|
|
'is_closed' => false, # TODO
|
|
'icon_url' => "", # optional, overriden later
|
|
# participated_uids # optional, level 4
|
|
'post_time' => make_xmlrpc_datetime( discourse_topic['last_posted_at'] ), # one of these is not like the other
|
|
'last_reply_time' => make_xmlrpc_datetime( discourse_topic['last_posted_at'] ),
|
|
'reply_number' => discourse_topic['posts_count'].to_i, # reply_count ?
|
|
'new_post' => !!discourse_topic['unseen'],
|
|
'view_number' => discourse_topic['views'].to_i,
|
|
'short_content' => binary_xmlrpc( "" ), # api level 4, not provided by discourse in some calls, TODO
|
|
'attachment' => "0",
|
|
'can_delete' => false,
|
|
'can_move' => false,
|
|
'can_close' => false,
|
|
'can_stick' => false,
|
|
'is_sticky' => false,
|
|
'can_approve' => false,
|
|
'is_approved' => true,
|
|
'can_rename' => false,
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
# Fill in some association-related data
|
|
if extra[:categories]
|
|
category = extra[:categories].find {|hsh| hsh["id"] == discourse_topic["category_id"] } || {}
|
|
result["forum_name"] = binary_xmlrpc( category.fetch( "name", "Uncategorised" ) )
|
|
end
|
|
|
|
if discourse_topic["posters"]
|
|
orig_poster = discourse_topic["posters"].find {|hsh| hsh['description'].to_s =~ /Original Poster/i }
|
|
|
|
result["post_author_id"] = orig_poster["user_id"] if orig_poster
|
|
|
|
if extra[:users] && result["post_author_id"]
|
|
user = extra[:users].find {|hsh| hsh["id"] == result["post_author_id"] }
|
|
if user
|
|
result["post_author_name"] = binary_xmlrpc( user["username"].to_s )
|
|
result["topic_author_name"] = binary_xmlrpc( user["username"].to_s )
|
|
result["topic_author_display_name"] = binary_xmlrpc( user["username"].to_s ) # TODO
|
|
result["icon_url"] = make_avatar_url( user["avatar_template"] )
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
# FIXME: how do these differ from post_author_name/id ? most recent / original?
|
|
result['topic_author_name'] = result['post_author_name']
|
|
result['topic_author_id'] = result['post_author_id'].to_s
|
|
result['timestamp'] = result['last_reply_time'].to_i.to_s
|
|
result
|
|
end
|
|
|
|
# Try to keep this instance around for as long as possible...
|
|
# TODO: To keep it around, push into Rack::Session, or cookies? Some sort of
|
|
# long-lived store. Can go away with process restarts, of course.
|
|
def discourse( cookies = {} )
|
|
@discourse ||= begin
|
|
uri = settings.discourse_api
|
|
uri = URI.parse(uri) unless uri.is_a?( URI::Generic )
|
|
client = DiscourseApi::Client.new( uri.host, uri.port, uri.scheme )
|
|
client.api_key = settings.discourse_api_key
|
|
client.cookies = cookies
|
|
client
|
|
end
|
|
end
|
|
|
|
def respond_xmlrpc( rsp )
|
|
data = XMLRPC::Marshal.dump_response( rsp )
|
|
|
|
data.gsub!( '<?xml version="1.0" ?>', '<?xml version="1.0" encoding="UTF-8"?>' )
|
|
data.tr!("\n", "")
|
|
|
|
data
|
|
end
|
|
|
|
## http://tapatalk.com/api/api_section.php ##
|
|
|
|
|
|
# First function called by tapatalk, apparently we should always return the
|
|
# whole hash.
|
|
def xmlrpc_get_config( name = nil )
|
|
mobiquo_login_header!( false )
|
|
result = {
|
|
|
|
# Uncontroversial
|
|
"sys_version" => "0.1", # For now
|
|
"version" => "dev", # For now
|
|
"api_level" => "3", # Maybe?
|
|
"is_open" => true, # Set to false to stop working
|
|
"guest_okay" => true,
|
|
|
|
# These are optional, and assume 1 if missing
|
|
"mark_read" => "0", # TODO
|
|
"subscribe_forum" => "0", # TODO
|
|
"announcement" => "0", # Probably?
|
|
|
|
# These are optional, and assume 0 if missing
|
|
# "delete_reason" => "0", # TODO
|
|
# "m_approve" => "0", # TODO
|
|
# "m_delete" => "0", # TODO
|
|
# "m_report" => "0", # TODO
|
|
# "guest_search" => "0", # TODO
|
|
# "guest_whosonline" => "0", # TODO
|
|
# "emoji" => "0", # Probably?
|
|
# "support_md5" => "0", # TODO
|
|
# "support_sha1" => "0", # TODO
|
|
# "conversation" => "0", # TODO
|
|
# "advanced_delete" => "0", # TODO
|
|
# "get_activity" => "0", # TODO
|
|
# "prefix_edit" => "0", # TODO
|
|
# "anonymous_login" => "0", # TODO
|
|
# "search_user" => "0", # TODO
|
|
# "user_recommend" => "0", # TODO
|
|
|
|
# Other optional params
|
|
"min_search_length" => 3, # Boring default
|
|
"alert" => "0", # TODO
|
|
"direct_unsubscribe" => "0", # TODO
|
|
"push_type" => PUSH_TYPES,
|
|
# "ban_delete_type" => "none", # TODO
|
|
|
|
"inappreg" => "0", # TODO
|
|
"inappsignin" => "1",
|
|
"ignore_user" => "0", # TODO
|
|
|
|
# These are API level 4 items. Ignore for now.
|
|
# "goto_post" => false, # TODO
|
|
# "goto_unread" => false, # TODO
|
|
# "mark_forum" => "0", # TODO
|
|
# "no_refresh_on_post" => "0", # TODO
|
|
# "get_latest_topic" => "0", # TODO
|
|
# "get_id_by_url" => "0", # TODO
|
|
# "anonymous" => "0", # TODO
|
|
# "searchid" => "0", # TODO
|
|
# "avatar" => "0", # TODO
|
|
# "pm_load" => "0", # TODO
|
|
# "subscribe_load" => "0", # TODO
|
|
# "inbox_stat" => "0", # TODO
|
|
# "multi_quote" => "0", # TODO
|
|
# "default_smilies" => "1", # Probably?
|
|
# "can_unread" => "0", # Probably
|
|
# "get_forum" => "1", # No sub-forums in discourse but it needs this anyway
|
|
# "get_topic_status" => "0", # TODO
|
|
# "get_participated_forum" => "0", # TODO
|
|
# "get_forum_status" => "1", # TODO
|
|
# "get_smilies" => "0", # TODO
|
|
# "advanced_online_users" => "0", # TODO
|
|
# "mark_pm_unread" => "0", # Probably?
|
|
# "advanced_search" => "0", # TODO
|
|
# "mass_subscribe" => "0", # TODO
|
|
# "user_id" => "0", # TODO
|
|
# "mark_topic_read" => "0", # TODO
|
|
# "first_unread" => "0", # TODO
|
|
|
|
# These are returned by some other forums but aren't documented
|
|
"key" => settings.tapatalk_api_key, # tapatalk API key, at a guess?
|
|
"push" => "1",
|
|
|
|
|
|
# "allow_moderate",
|
|
# "charset" => "ISO-8859-1",
|
|
"disable_bbcode" => "0",
|
|
# "disable_subscribe_forum" => "0",
|
|
# "forum_signature" => 1,
|
|
# "get_online_users" => "1",
|
|
|
|
# No idea what this does
|
|
# "hide_forum_id" => "",
|
|
|
|
# "reg_url" => "register.php",
|
|
"sign_in" => "0", # what does this do?
|
|
# "stats" => {"user"=>320812, "topic"=>233040, "post"=>2389071}
|
|
}
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
def xmlrpc_login( user = nil, pass = nil, anonymous = false, push = '1' )
|
|
mobiquo_login_header!( false )
|
|
return respond_xmlrpc(
|
|
'result' => false,
|
|
'result_text' => binary_xmlrpc( 'Anonymous login not supported' )
|
|
) if anonymous
|
|
|
|
# We need to send on the cookies we received from discourse, and ensure they're
|
|
# passed in for future API requests
|
|
|
|
# we need one of these to play with the session API
|
|
if discourse.csrf_token.nil?
|
|
discourse.csrf_token = discourse.session_csrf({}).fetch("csrf")
|
|
end
|
|
|
|
# We raise if session creation fails
|
|
user_info = begin
|
|
rsp = discourse.session_create( :login => user, :password => pass )
|
|
return respond_xmlrpc( 'result' => false, 'result_text' => binary_xmlrpc( rsp['error'] ) ) if rsp['error']
|
|
|
|
rsp
|
|
rescue => err
|
|
puts err.inspect
|
|
puts err.backtrace.join("\n")
|
|
return respond_xmlrpc( 'result' => false )
|
|
end
|
|
|
|
set_cookies = discourse.cookies.each.collect {|c| c.set_cookie_value }
|
|
headers( "Set-Cookie" => set_cookies ) if set_cookies.size > 0
|
|
|
|
user = user_info.fetch("user")
|
|
|
|
respond_xmlrpc(
|
|
'result' => true,
|
|
'user_id' => user["id"].to_s, # api level 4
|
|
'username' => binary_xmlrpc( user["username"].to_s ), # api level 4
|
|
# 'usergroup_id' => [], # api level 4
|
|
# 'email' => binary_xmlrpc( "" ) # api level 4 # not sent by discourse
|
|
# 'icon_url' => make_avatar_url( user["avatar_template"] ), # api level 4
|
|
'post_count' => 0, # api level 4 # discourse gives back stats with action_type and count members
|
|
'can_pm' => false, # TODO!
|
|
'can_send_pm' => false, # TODO! # can_send_private_message_to_user is always false...?
|
|
'can_moderate' => false, # TODO! user["moderator"]
|
|
# can_search => true, # api level 4
|
|
# can_whosonline => true, # api level 4
|
|
# can_profile => true, # api level 4
|
|
'can_upload_avatar' => false, # TODO
|
|
|
|
# phpBB3 returns the values as booleans
|
|
'push_type' => PUSH_TYPES.split(",").collect {|type|
|
|
{ 'name' => type, 'value' => ( push == "1" ) }
|
|
}
|
|
)
|
|
end
|
|
|
|
# Unsure what this does just yet, trying to get push notifications working.
|
|
# Pokes directory.tapatalk.com, as does update_push_status... glue?
|
|
def xmlrpc_sign_in( token, code, unknown, username, password )
|
|
pp [ "xmlrpc_sign_in args:", args ]
|
|
|
|
pp [ "rack environment:", request.env ]
|
|
|
|
respond_xmlrpc( 'result' => false )
|
|
end
|
|
|
|
def xmlrpc_logout_user
|
|
discourse.session_destroy({})
|
|
respond_xmlrpc( 'result' => true )
|
|
end
|
|
|
|
def xmlrpc_get_inbox_stat
|
|
# TODO: get real numbers instead...
|
|
respond_xmlrpc( 'inbox_read_count' => 0 )
|
|
end
|
|
|
|
# Ignore forum_id for now. Naughty naughty.
|
|
def xmlrpc_get_forum( return_description = nil, forum_id = nil)
|
|
|
|
forum_info = discourse.categories({})
|
|
category_list = forum_info.fetch( "category_list" )
|
|
categories = category_list.fetch( "categories" )
|
|
|
|
result = categories.collect {|hsh|
|
|
tmp = {
|
|
'forum_id' => hsh['id'].to_s,
|
|
'forum_name' => binary_xmlrpc( hsh['name'].to_s ), # FIXME: python encodes this to UTF8. Can't be nil
|
|
'parent_id' => "-1", # no such thing as sub-categories in Discourse
|
|
'sub_only' => false,
|
|
# 'child' => [], # Don't return this if we have no children
|
|
# 'is_protected' => false, # ?
|
|
'is_subscribed' => true, # TODO - we pretend all forums are subscribed for now
|
|
'can_subscribe' => false, # TODO
|
|
'can_post' => !!category_list["can_create_topic"], # FIXME: this may not be correct!
|
|
'new_post' => true, # TODO
|
|
"is_protected" => false, # Not required, apparently
|
|
'description' => binary_xmlrpc( hsh['description'].to_s ),
|
|
# "child" => [],
|
|
|
|
# These are returned by some others
|
|
'new_post' => false,
|
|
'url' => '',
|
|
'logo_url' => '',
|
|
'unread_count' => 0, # TODO
|
|
}
|
|
|
|
# FIXME: may not be right?
|
|
# if return_description
|
|
#
|
|
# end
|
|
|
|
tmp
|
|
}
|
|
result.select! {|cat| cat['parent_id'] == forum_id } if forum_id
|
|
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
def xmlrpc_get_subscribed_forum
|
|
xmlrpc_get_forum
|
|
end
|
|
|
|
def xmlrpc_get_subscribed_topic
|
|
xmlrpc_get_latest_topic
|
|
end
|
|
|
|
def xmlrpc_get_new_topic
|
|
xmlrpc_get_latest_topic
|
|
end
|
|
|
|
# In API level 4, we can say this doesn't exist and force tapatalk to use get_topic
|
|
def xmlrpc_get_unread_topic( start_num = 0, last_num = 0, search_id = nil, filters = nil )
|
|
|
|
result = { 'result' => true }
|
|
|
|
unread_topics = discourse.topics_unread({})
|
|
topic_list = unread_topics.fetch( "topic_list" )
|
|
topics = topic_list.fetch( "topics" )
|
|
|
|
# No idea what discourse actually returns, other than 0
|
|
result['topics'] = topics.select {|hsh| hsh["unread"] != 0 }.collect {|hsh|
|
|
build_tapatalk_topic( hsh, :users => unread_topics["users"], :categories => unread_topics["categories"] )
|
|
}
|
|
result['total_topic_num'] = result['topics'].count # api level 4
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
def xmlrpc_get_latest_topic( start_num = 0, last_num = 0, search_id = nil, filters = nil )
|
|
|
|
result = { 'result' => true }
|
|
|
|
latest_topics = discourse.topics_latest({})
|
|
topic_list = latest_topics.fetch( "topic_list" )
|
|
topics = topic_list.fetch( "topics" )
|
|
|
|
result['topics'] = topics.collect {|hsh|
|
|
build_tapatalk_topic( hsh, :users => latest_topics["users"], :categories => latest_topics["categories"] )
|
|
}
|
|
result['total_topic_num'] = result['topics'].count # api level 4
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
def xmlrpc_get_topic( forum_id, start_num = 0, last_num = 0, mode = "DATE" )
|
|
|
|
if forum_id == "0"
|
|
# In these cases, we're not interested in a particular forum
|
|
# TODO! This is for announcements (mode = "ANN")! FIXME!
|
|
# TODO: also for latest, like xmlrpc_get_latest_topic! FIXME!
|
|
|
|
return xmlrpc_get_latest_topic( start_num, last_num )
|
|
end
|
|
|
|
# TODO: Need to respect pagination, restrict to just one category
|
|
forum_info = discourse.category(:category_id => forum_id)
|
|
|
|
categories = forum_info.fetch( "categories" )
|
|
users = forum_info["users"]
|
|
|
|
categories.each {|hsh| hsh["can_create_topic"] = forum_info["topic_list"]["can_create_topic"] }
|
|
|
|
category = categories.find {|hsh| hsh["id"].to_s == forum_id }
|
|
|
|
# Null category to handle failure-to-find cases above
|
|
category ||= {
|
|
'topic_count' => 0,
|
|
'id' => forum_id.to_i,
|
|
"name" => "",
|
|
'can_create_topic' => false,
|
|
'topics' => []
|
|
}
|
|
|
|
# Return an empty list for weirdy ones
|
|
if mode == "ANN" || mode == "TOP"
|
|
return respond_xmlrpc(
|
|
'total_topic_num' => 0,
|
|
'unread_sticky_count' => 0,
|
|
'unread_announce_count' => 0,
|
|
'forum_id' => forum_id,
|
|
'forum_name' => binary_xmlrpc( category['name'].to_s ),
|
|
'can_post' => false,
|
|
'can_upload' => false,
|
|
'max_attachment' => 0,
|
|
'max_png_size' => 0,
|
|
'max_jpg_size' => 0,
|
|
'topics' => []
|
|
)
|
|
end
|
|
|
|
result = {
|
|
'forum_id' => category['id'].to_s,
|
|
'forum_name' => binary_xmlrpc( category['name'].to_s ),
|
|
# 'can_subscribe' => false, # TODO - api level 4
|
|
# 'is_subscribed' => false, # TODO - api level 4
|
|
# 'require_prefix' => false, # TODO - api level 4
|
|
'prefixes' => [],
|
|
'can_post' => !!category['can_create_topic'], # FIXME: may not be correct!
|
|
'can_upload' => false,
|
|
'max_attachment' => 0,
|
|
'max_png_size' => 0,
|
|
'max_jpg_size' => 0,
|
|
'unread_sticky_count' => 0, # FIXME
|
|
'unread_announce_count' => 0,
|
|
# 'can_subscribe' => false,
|
|
# 'is_subscribed' => false,
|
|
# 'require_prefix' => false, # api level 4
|
|
}
|
|
|
|
topic_list = forum_info.fetch( "topic_list" )
|
|
|
|
result['topics'] = topic_list.fetch( "topics" ).collect {|hsh|
|
|
build_tapatalk_topic( hsh, :users => users, :categories => categories )
|
|
}
|
|
|
|
=begin
|
|
good_keys = %w{
|
|
forum_id topic_id topic_title prefix topic_author_id topic_author_name
|
|
is_subscribed can_subscribe is_closed icon_url last_reply_time reply_number
|
|
new_post view_number short_content participated_uids
|
|
}
|
|
|
|
# Some of these aren't documented for get_topic
|
|
result['topics'].each do |topic|
|
|
topic.keys.each do |key|
|
|
if !good_keys.include?( key )
|
|
# puts "#{key} set to #{topic[key]}, deleting"
|
|
topic.delete( key )
|
|
end
|
|
end
|
|
|
|
end
|
|
=end
|
|
result['total_topic_num'] = result['topics'].count
|
|
|
|
# pp result
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
# def xmlrpc_get_id_by_url( url ) # TODO
|
|
# end
|
|
|
|
def xmlrpc_get_thread( topic_id, start_num = 0, end_num = 0, return_html = false )
|
|
|
|
topic_info = discourse.topic(:topic_id => topic_id.to_i)
|
|
post_stream = topic_info.fetch( "post_stream" )
|
|
posts = post_stream.fetch( "posts" )
|
|
|
|
result = {
|
|
"forum_id" => topic_info["category_id"].to_s,
|
|
"forum_name" => binary_xmlrpc( "Unknown" ), # FIXME: not returned in topic info, need to pull from cache with forum_id
|
|
"forum_title" => binary_xmlrpc( "Unknown" ), # django insists it's called this
|
|
"topic_id" => topic_info["id"].to_s,
|
|
"topic_title" => binary_xmlrpc( topic_info["title"].to_s ),
|
|
"prefix" => "",
|
|
"is_subscribed" => false,
|
|
"can_subscribe" => false,
|
|
"can_reply" => true, # FIXME: ensure this is accurate
|
|
"is_approved" => true, # FIXME: wat is das?
|
|
"can_upload" => false,
|
|
"is_poll" => false,
|
|
"is_closed" => !!topic_info["closed"],
|
|
"can_report" => false, # TODO
|
|
"breadcrumb" => [], # TODO ?
|
|
}
|
|
|
|
result["posts"] = posts.collect {|hsh|
|
|
{ "post_id" => hsh["id"].to_s,
|
|
"post_title" => binary_xmlrpc( hsh["user_title"].to_s ),
|
|
"post_content" => binary_xmlrpc( format_post_content_for_xmlrpc( hsh["cooked"], return_html ) ),
|
|
"forum_name" => result["forum_name"], # django says we need this
|
|
"forum_id" => result["forum_id"],
|
|
"post_author_id" => hsh["user_id"].to_s,
|
|
"post_author_name" => binary_xmlrpc( hsh["display_username"].to_s ), # FIXME: display username or real username?
|
|
"is_online" => !!hsh["yours"], # TODO: not included in discourse output?
|
|
"can_edit" => !!hsh["can_edit"], # TODO
|
|
"is_approved" => true, # django again
|
|
"icon_url" => make_avatar_url( hsh["avatar_template"] ), # TODO: this is a template, may need formatting
|
|
"post_time" => make_xmlrpc_datetime( hsh["updated_at"] ),
|
|
"allow_smilies" => true,
|
|
"reply_number" => hsh["post_number"].to_i,
|
|
"view_count" => hsh["reads"].to_i,
|
|
# "attachments" => [], # TODO!
|
|
# "thanks_info" => [], # TODO # Probably not needed anyway
|
|
# "likes_info" => [], # TODO!
|
|
}
|
|
}
|
|
result["total_post_num"] = result["posts"].count.to_i
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
def csrf_token!
|
|
# we need one of these to play with the session API, do POSTs, etc.
|
|
if discourse.csrf_token.nil?
|
|
discourse.csrf_token = discourse.session_csrf({}).fetch("csrf")
|
|
end
|
|
end
|
|
|
|
|
|
# attachments and group id are TODO with uploads in general
|
|
def xmlrpc_new_topic( forum_id, subject, text_body, prefix_id = nil, attachment_id_array = nil, group_id = nil )
|
|
csrf_token!
|
|
|
|
# To make it a topic instead of a post, specify a category instead of a topic_id
|
|
rsp = discourse.post_create( :category => forum_id, :title => subject, :raw => text_body )
|
|
|
|
respond_xmlrpc( 'result' => true, 'topic_id' => rsp["id"].to_s )
|
|
end
|
|
|
|
# attachments, return_html are api level 4
|
|
def xmlrpc_reply_post( forum_id, topic_id, subject, text_body, attachment_id_array = nil, return_html = false )
|
|
csrf_token!
|
|
rsp = discourse.post_create( :topic_id => topic_id, :title => subject, :raw => text_body )
|
|
|
|
result = {
|
|
'result' => true,
|
|
'post_id' => rsp["id"].to_s,
|
|
# 'state' => "1", # if post requires moderation
|
|
# post_content # api level 4
|
|
# 'can_edit' => !!rsp['can_edit'] # api level 4
|
|
# 'can_delete' => !!rsp['can_delete'] # api level 4
|
|
# 'post_time' => make_xmlrpc_datetime( rsp['updated_at'] ) # api level 4
|
|
# attachments # api level 4
|
|
}
|
|
|
|
respond_xmlrpc( result )
|
|
end
|
|
|
|
# def get_smilies # TODO - not a priority
|
|
# end
|
|
|
|
# Grr. Bad tapatalk, bad.
|
|
def php_serialize( hash )
|
|
# PHP's array serialize format is like this:
|
|
# a:<num-elems>:{s:<num-bytes>:"...";i:<val>;...;}
|
|
|
|
raise ArgumentError.new( "Not a hash" ) unless hash.is_a?(Hash )
|
|
|
|
elems = hash.collect {|k, v|
|
|
raise ArgumentError.new( "key #{k} not a string") unless k.is_a?( String )
|
|
raise ArgumentError.new( "value #{v} not Fixnum") unless v.is_a?( Fixnum )
|
|
"s:#{k.size}:\"#{k}\";i:#{v}"
|
|
}.join(";")
|
|
|
|
"a:#{hash.size}:{#{elems};}"
|
|
end
|
|
|
|
def xmlrpc_update_push_status( push_status, username, password )
|
|
user = discourse.user( :username => username ).fetch( "user" )
|
|
|
|
# FIXME: TODO: CHECK THE PASSWORD IS VALID!
|
|
if !username || !password || password == "" || username == "" ||
|
|
!user.has_key?("id") || !user.has_key?("username")
|
|
puts "Returning false - problems validating user"
|
|
pp user
|
|
return respond_xmlrpc( 'result' => false )
|
|
end
|
|
|
|
# push_status is a hash: {"pm"=>1, "conversation"=>1, "newtopic"=>1, "sub"=>1, "like"=>1, "tag"=>1, "quote"=>1}
|
|
# We have to do a php-style serialize() on it, base64-encode it and pass it
|
|
# on to tapatalk's push API. This is going to get nasty.
|
|
|
|
uri = URI.parse( settings.tapatalk_api )
|
|
uri.path = "/au_update_push_setting.php"
|
|
|
|
req_data = {
|
|
'url' => settings.discourse_api.to_s,
|
|
'key' => settings.tapatalk_api_key,
|
|
'uid' => user["id"], # FIXME: should we use id instead?
|
|
'data' => Base64::encode64( php_serialize( push_status ) ).tr!("\n", "")
|
|
}
|
|
|
|
# We have this strange call to a strange path - that seems to fail anyway -
|
|
# to do
|
|
tapatalk = Net::HTTP.new( "directory.tapatalk.com", 443 )
|
|
tapatalk.use_ssl = true
|
|
tapatalk.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME
|
|
|
|
rsp = tapatalk.start {|remote|
|
|
req = Net::HTTP::Post.new( uri.path )
|
|
req.form_data = req_data
|
|
remote.request( req )
|
|
}
|
|
raise "Unknown response: #{rsp.inspect} #{rsp.body}" unless rsp.is_a?( Net::HTTPOK )
|
|
|
|
rsp_data = JSON.parse( rsp.body, :create_extensions => false )
|
|
|
|
unless rsp_data['result']
|
|
puts "Warning: POSTing to directory failed: #{rsp_data.inspect}. Upstream ignores said failures, so we will too."
|
|
end
|
|
|
|
respond_xmlrpc( 'result' => true )
|
|
end
|
|
|
|
end
|
|
|
|
post "/" do
|
|
dispatch
|
|
end
|
|
|
|
post "/mobiquo/mobiquo.php" do
|
|
dispatch
|
|
end
|
|
|
|
get "/" do
|
|
halt 200, "Tapatalk endpoint"
|
|
end
|
|
|
|
get "/mobiquo/mobiquo.php" do
|
|
halt 200, "Tapatalk endpoint"
|
|
end
|