diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..a3463c4 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,16 @@ +PATH + remote: . + specs: + skyper (0.1.0) + rb-appscript (>= 0.3.0) + +GEM + remote: https://rubygems.org/ + specs: + rb-appscript (0.6.1) + +PLATFORMS + ruby + +DEPENDENCIES + skyper! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..45e4b29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Andriy Dmytrenko + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index f508b4e..be8ccc9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,34 @@ -skyper -====== +# Skyper -Ruby Skype API \ No newline at end of file +A ruby gem, implementing Skype public API. +https://developer.skype.com/public-api-reference + +Currently works only on MacOS X, using AppleScript to connect to Skype. +Inspired by rb-skypemac. + + +## Installation + +Add this line to your application's Gemfile: + + gem 'skyper' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install skyper + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f57ae68 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/lib/skyper.rb b/lib/skyper.rb new file mode 100644 index 0000000..0b31c9b --- /dev/null +++ b/lib/skyper.rb @@ -0,0 +1,8 @@ +$:.unshift File.expand_path('../',__FILE__) +module Skyper + autoload :SkypeObject, 'skyper/skype_object' + autoload :User, 'skyper/user' + autoload :Chat, 'skyper/chat' + autoload :Group, 'skyper/group' +end +Dir[File.join(File.dirname(__FILE__), 'skyper/**/*.rb')].sort.each { |lib| require lib } diff --git a/lib/skyper/call.rb b/lib/skyper/call.rb new file mode 100644 index 0000000..99e916a --- /dev/null +++ b/lib/skyper/call.rb @@ -0,0 +1,50 @@ +module Skyper + # Represents a Skype call. Developer is responsible for calling Call#hangup at the end of each call, whether it was placed + # by the caller or is an answered call. + class Call < SkypeObject + property_reader :TIMESTAMP => Time, :PARTNER_HANDLE => Skyper::User, :RATE => Integer + property_reader *%w[PARTNER_DISPNAME TARGET_IDENTITY CONF_ID TYPE STATUS VIDEO_STATUS VIDEO_SEND_STATUS FAILUREREASON SUBJECT + PSTN_NUMBER DURATION PSTN_STATUS CONF_PARTICIPANTS_COUNT VM_DURATION VM_ALLOWED_DURATION RATE_CURRENCY RATE_PRECISION INPUT OUTPUT CAPTURE_MIC VAA_INPUT_STATUS + FORWARDED_BY TRANSFER_ACTIVE TRANSFER_STATUS TRANSFERRED_BY TRANSFERRED_TO] + self.object_name = "CALL" + class << self + def active_call_ids + r = Skype.send_command "SEARCH ACTIVECALLS" + r.gsub(/CALLS /, "").split(", ") + end + + def active_calls + Call.active_call_ids.map { |id| Call.new id unless id == "COMMAND_PENDING"} + end + + def create(*users) + user_handles = users.map { |u| (u.is_a? User) ? u.handle : u } + status = Skype.send_command "CALL #{user_handles.join(', ')}" + if status =~ /CALL (\d+) STATUS/ + call = Call.new($1) + else + raise RuntimeError.new("Call failed. Skype returned '#{status}'") + end + end + end + + # Attempts to hang up a call. Note: If Skype hangs while placing the call, this method could hang indefinitely. + # Use Skype#Call instead of this method unless you like memory leaks. + # Raises SkypeError if an error is reported from the Skype API + def hangup + self.status = "FINISHED" + end + + def send_video(toggle_flag) + alter("#{bool_to_flag(toggle_flag)}_VIDEO_SEND") + end + + def rcv_video(toggle_flag) + alter("#{bool_to_flag(toggle_flag)}_VIDEO_RECEIVE") + end + protected + def bool_to_flag(bool) + bool ? "START" : "STOP" + end + end +end diff --git a/lib/skyper/chat.rb b/lib/skyper/chat.rb new file mode 100644 index 0000000..0f09844 --- /dev/null +++ b/lib/skyper/chat.rb @@ -0,0 +1,42 @@ +module Skyper + class Chat < SkypeObject + property_accessor *%w[NAME TIMESTAMP ADDER POSTERS TOPIC TOPICXML ACTIVEMEMBERS FRIENDLYNAME BOOKMARKED MEMBEROBJECTS PASSWORDHINT GUIDELINES + OPTIONS DESCRIPTION DIALOG_PARTNER ACTIVITY_TIMESTAMP TYPE MYSTATUS MYROLE BLOB APPLICANTS] + property_reader :RECENTCHATMESSAGES => Array + property_reader :STATUS, :MEMBERS, :CHATMESSAGES + self.object_name = "CHAT" + attr_reader :id + + class << self + # create by user_handle + def create(user_handle) + r = Skype.send_command "CHAT CREATE #{user_handle}" + Chat.parse_from r + end + + # Returns an array of your Skype recent chats + def recent_chats + r = Skype.send_command "SEARCH RECENTCHATS" + chat_ids = parse_type(r.sub(/^CHATS\s+/, ""), Array) + chat_ids.map do |id| + Chat.new(id) + end + end + end + + # chat message to chat + def chat_message(message) + r = Skype.send_command "CHATMESSAGE #{id} #{message}" + ChatMessage.parse_from(r) + end + + def recent_chat_messages + self.recentchatmessages.map {|id| ChatMessage.new(id)} + end + + def set_topic(topic) + alter('SETTOPIC', topic) + end + + end +end diff --git a/lib/skyper/chat_message.rb b/lib/skyper/chat_message.rb new file mode 100644 index 0000000..604e8b7 --- /dev/null +++ b/lib/skyper/chat_message.rb @@ -0,0 +1,7 @@ +module Skyper + class ChatMessage < Skyper::SkypeObject + property_accessor :BODY, :FROM_DISPNAME, :TYPE, :STATUS, :LEAVEREASON, :CHATNAME, :IS_EDITABLE, :EDITED_BY, :EDITED_TIMESTAMP, :OPTIONS, :ROLE + property_reader :TIMESTAMP => Time, :FROM_HANDLE => Skyper::User, :USERS => Array + self.object_name = "CHATMESSAGE" + end +end diff --git a/lib/skyper/group.rb b/lib/skyper/group.rb new file mode 100644 index 0000000..88a4ffa --- /dev/null +++ b/lib/skyper/group.rb @@ -0,0 +1,31 @@ +module Skyper + class Group < SkypeObject + #TYPE: {ALL | CUSTOM | HARDWIRED | SHARED_GROUP | PROPOSED_SHARED_GROUP} + property_reader :TYPE, :CUSTOM_GROUP_ID, :NROFUSERS, :NROFUSERS_ONLINE, :USERS + property_accessor :DISPLAYNAME + self.object_name = "GROUP" + + class << self + # Returns hash of symols (group types) => Group objects + def groups + r = Skype.send_command "SEARCH GROUPS ALL" + r.gsub!(/^\D+/, "") + group_ids = r.split /,\s*/ + group_ids.map do |id| + Group.new(id) + end + end + end + + # Returns array of skype names of users in this group + def member_user_names + r = users + r && r.sub(/^.*USERS /, "").split(", ") or [] + end + + # Returns array of Users in this Group + def user_objects + member_user_names.map { |h| User.new h } + end + end +end \ No newline at end of file diff --git a/lib/skyper/iam.rb b/lib/skyper/iam.rb new file mode 100644 index 0000000..4819f2a --- /dev/null +++ b/lib/skyper/iam.rb @@ -0,0 +1,18 @@ +module Skyper + + # Singleton for managing Skype user status + class Iam + @@STATUSES = [:ONLINE, :OFFLINE, :SKYPEME, :AWAY, :NA, :DND, :INVISIBLE] + + def Iam.set_user_status(status) + raise NoMethodError.new("#{status} in #{Iam.to_s}") if not @@STATUSES.index status.upcase.to_sym + Skype.send_command "SET USERSTATUS #{status}" + end + + # Handles all of the user status permutations accepted by Skype otherwise Errors. + # For example, Iam.away is legal. + def Iam.method_missing(id) + Iam.set_user_status(id.id2name) + end + end +end \ No newline at end of file diff --git a/lib/skyper/platform/mac.rb b/lib/skyper/platform/mac.rb new file mode 100644 index 0000000..578efed --- /dev/null +++ b/lib/skyper/platform/mac.rb @@ -0,0 +1,26 @@ +module Skyper + module Mac + class Connection + attr_accessor :logger + attr_reader :options + + def initialize(options={}) + @options = options + @logger ||= Logger.new(STDOUT).tap do |logger| + logger.level = Logger::ERROR + end + end + + # @param[String] command Skype command + def send_command(command) + params = {:script_name => options[:script_name].to_s, :command => command} + logger.debug "#{self.class}##{__method__} params: #{params.inspect}" + response = Appscript::app('Skype').send_ params + response.tap do + logger.debug "#{self.class}##{__method__} response: #{response}" + end + end + + end + end +end diff --git a/lib/skyper/profile.rb b/lib/skyper/profile.rb new file mode 100644 index 0000000..06a572f --- /dev/null +++ b/lib/skyper/profile.rb @@ -0,0 +1,9 @@ +module Skyper + class Profile < SkypeObject + property_reader *%w[PSTN_BALANCE PSTN_BALANCE_CURRENCY SMS_VALIDATED_NUMBERS] + property_accessor *%w[FULLNAME SEX LANGUAGES COUNTRY IPCOUNTRY PROVINCE CITY PHONE_HOME PHONE_OFFICE PHONE_MOBILE HOMEPAGE ABOUT MOOD_TEXT RICH_MOOD_TEXT TIMEZONE + CALL_APPLY_CF CALL_NOANSWER_TIMEOUT CALL_FORWARD_RULES CALL_SEND_TO_VM] + property_accessor :BIRTHDAY => Time + self.object_name = "PROFILE" + end +end diff --git a/lib/skyper/skype.rb b/lib/skyper/skype.rb new file mode 100644 index 0000000..0992f19 --- /dev/null +++ b/lib/skyper/skype.rb @@ -0,0 +1,95 @@ +require 'appscript' +require 'logger' +require 'singleton' +require 'forwardable' + +module Skyper + + # Singleton for interfacing with Skype + class Skype + include Singleton + attr_accessor :script_name + attr_reader :connection + + def initialize + @connection = Skyper::Mac::Connection.new + send_command("PROTOCOL 8") + end + + # @param[String] command Skype command + def send_command(command) + @connection.send_command(command) + end + + def profile + @profile ||= Profile.new(nil) + end + + # Initiates a Skype call + def call(*users) + Call.create(*users) + end + + # Returns an Array of Call IDs if there is an incoming Skype call otherwise nil + def incoming_calls + Call.active_calls + end + + # Answers a call given a skype Call ID. Returns an Array of Call objects. + def answer(call) + cmd = "ALTER CALL #{call.call_id} ANSWER" + r = Skype.send_command cmd + raise RuntimeError("Failed to answer call. Skype returned '#{r}'") unless r == cmd + end + + # Returns an Array of Group + def groups + @groups ||= Group.groups + end + + # Returns Array of all User in a particular Group type. Accepts types as defined by Group.types + def find_users_of_type(group_type) + groups.find { |g| g.type == group_type}.users + end + + # Returns an array of users online friends as User objects + def online_friends + find_users_of_type "ONLINE_FRIENDS" + end + + # Array of all User that are friends of the current user + def all_friends + find_users_of_type "ALL_FRIENDS" + end + + # Array of all User defined as Skype Out users + def skypeout_friends + find_users_of_type "SKYPEOUT_FRIENDS" + end + + # Array of all User that the user knows + def all_users + find_users_of_type "ALL_USERS" + end + + # Array of User recently contacted by the user, friends or not + def recently_contacted_users + find_users_of_type "RECENTLY_CONTACTED_USERS" + end + + # Array of User waiting for authorization + def users_waiting_for_authorization + find_users_of_type "USERS_WAITING_MY_AUTHORIZATION" + end + + # Array of User blocked + def blocked_users + find_users_of_type "USERS_BLOCKED_BY_ME" + end + class << self + extend Forwardable + def_delegators :instance, *Skyper::Skype.instance_methods(false) + end + end + +end diff --git a/lib/skyper/skype_error.rb b/lib/skyper/skype_error.rb new file mode 100644 index 0000000..ea8b9de --- /dev/null +++ b/lib/skyper/skype_error.rb @@ -0,0 +1,4 @@ +module Skyper + class SkypeError Time, :PRICE => Integer, :PRICE_PERICISION => Integer + self.object_name = "SMS" + + class << self + # @params [Symbol] type one of following: OUTGOING CONFIRMATION_CODE_REQUEST CONFIRMATION_CODE_SUBMIT + def create(target, type=:OUTGOING) + r = Skyper::Skype.send_command("CREATE SMS #{type.to_s.upcase} #{target}") #"SMS 354924 STATUS COMPOSING" + self.parse_from(r) + end + end + + def send_sms + alter("SEND") + end + + end +end \ No newline at end of file diff --git a/lib/skyper/user.rb b/lib/skyper/user.rb new file mode 100644 index 0000000..a2e1d54 --- /dev/null +++ b/lib/skyper/user.rb @@ -0,0 +1,15 @@ +module Skyper + class User < SkypeObject + property_reader :IS_VIDEO_CAPABLE => :bool, :IS_VOICEMAIL_CAPABLE => :bool, :IS_CF_ACTIVE => :bool, :HASCALLEQUIPMENT => :bool, :LASTONLINETIMESTAMP => Time + property_reader *%w[HANDLE FULLNAME BIRTHDAY SEX LANGUAGE COUNTRY PROVINCE CITY PHONE_HOME PHONE_OFFICE PHONE_MOBILE HOMEPAGE ABOUT + ONLINESTATUS SkypeOut SKYPEME CAN_LEAVE_VM RECEIVEDAUTHREQUEST MOOD_TEXT + RICH_MOOD_TEXT ALIASES TIMEZONE NROF_AUTHED_BUDDIES] + property_accessor *%w[BUDDYSTATUS ISBLOCKED ISAUTHORIZED SPEEDDIAL DISPLAYNAME] + self.object_name = "USER" + + def chat + Chat.create id + end + + end +end diff --git a/lib/skyper/version.rb b/lib/skyper/version.rb new file mode 100644 index 0000000..d38dc64 --- /dev/null +++ b/lib/skyper/version.rb @@ -0,0 +1,3 @@ +module Skyper + VERSION = "0.1.0" +end diff --git a/skyper.gemspec b/skyper.gemspec new file mode 100644 index 0000000..c203fb8 --- /dev/null +++ b/skyper.gemspec @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/skyper/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Andriy Dmytrenko"] + gem.email = ["refresh.xss@gmail.com"] + gem.description = %q{Ruby interface to Skype on Mac OS X} + gem.summary = %q{Ruby interface to Skype on Mac OS X} + gem.homepage = "" + + gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.name = "skyper" + gem.require_paths = ["lib"] + gem.version = Skyper::VERSION + gem.add_runtime_dependency 'rb-appscript', '>=0.3.0' +end