20 Commits

Author SHA1 Message Date
e7d2598186 Bump version
Signed-off-by: Danila Fedorin <danila.fedorin@gmail.com>
2025-12-25 18:34:13 -08:00
a48699c7c3 [Claude Haiki] Update to new, working Crystal version 2025-12-25 17:37:39 -08:00
c4a74152d6 Bump the version 2020-04-25 19:00:55 -07:00
0f5b17fd2c Start working on fixing Crystal incompatibility 2020-04-25 18:54:59 -07:00
87d1ff8086 Bump the version, I guess. 2019-04-14 22:39:57 -07:00
8c8f7beb5c Rewrite bot to avoid using two fibers and to handle errors in network. 2019-04-14 22:39:15 -07:00
259bed9823 Move some of the sending code outside of the bot. 2018-04-16 19:47:35 -07:00
a94176f65c Fix typo causing compilation error. 2018-01-24 13:20:13 -08:00
bfda135c19 Add hooks that get called when the polling mechanism starts or ends. 2018-01-19 12:15:10 -08:00
806d8db44c Fix seemingly deadlocking loop scenario. 2018-01-16 16:49:01 -08:00
60835022fb Add cast to fix sending File URLs. 2018-01-13 23:11:45 -08:00
cebc91d13a Add a method to send a photo. 2018-01-13 22:41:07 -08:00
81eee54695 Properly handle the @ part of a command. 2018-01-12 22:28:13 -08:00
e4381e7daf Only send nil to channel if worker was just spawned. 2018-01-12 22:18:20 -08:00
d020c0c9bf Move the spawn code out of the poll method. 2018-01-12 22:07:32 -08:00
caca0b1398 Add a way to send messages. 2018-01-12 22:07:20 -08:00
4eaaaa28b7 Add poll and end_poll methods the bot. 2018-01-12 21:54:22 -08:00
1183ee7bc7 Add a timeout to the update polling. 2018-01-12 20:58:51 -08:00
d02b25d026 Implement method to process updates. 2018-01-12 20:56:04 -08:00
cb1c4f2e54 Add a way to register routes that should be run by the bot on update. 2018-01-12 20:28:05 -08:00
20 changed files with 523 additions and 169 deletions

View File

@@ -1,9 +1,9 @@
name: telepathy
version: 0.1.0
version: 0.2.0
authors:
- Danila Fedorin <danila.fedorin@gmail.com>
crystal: 0.23.1
crystal: 1.0.0
license: MIT

View File

@@ -1,9 +1,7 @@
require "./spec_helper"
describe Telepathy do
# TODO: Write tests
it "works" do
false.should eq(true)
it "loads the library" do
Telepathy.should be_truthy
end
end

View File

@@ -1,30 +1,115 @@
require "http"
require "./utils.cr"
module Telepathy
class Bot
enum Control
Done
end
def initialize(@api_token : String)
@request_base = "https://api.telegram.org/bot#{@api_token}"
@this_user = uninitialized User?
@this_user = get_me
@last_update_id = uninitialized Int64?
@last_update_id = nil
@command_hooks = {} of String => Update, Array(String) -> Void
@message_hooks = [] of Update -> Void
@poll_start_hooks = [] of -> Void
@poll_end_hooks = [] of -> Void
@poll_channel = Channel(Int64?|Control).new
@update_channel = Channel(Array(Update)|Control).new
@poll_running = false
end
def get_me
response = HTTP::Client.get(@request_base + "/getMe",
headers: HTTP::Headers{"User-agent" => "Telepathy"})
return Response(User).from_json(response.body).result
Utils.get_me(@api_token)
end
def get_updates
update_data = {} of String => Int64 | String
@last_update_id.try { |id| update_data["offset"] = id }
response = HTTP::Client.get(@request_base + "/getUpdates",
headers: HTTP::Headers{"User-agent" => "Telepathy", "Content-type" => "application/json" },
body: update_data.to_json)
updates = Response(Array(Update)).from_json(response.body).result
updates.last?.try { |update| @last_update_id = update.update_id + 1 }
return updates
def get_updates(timeout = 0)
Utils.get_updates(@api_token, timeout, @last_update_id)
end
def send_message(chat_id : String | Int64, text : String,
parse_mode : Utils::ParseMode = Utils::ParseMode::Normal,
disable_web_preview : Bool = false,
disable_notification : Bool = false,
reply_to_message_id : Int64? = nil)
Utils.send_message(@api_token, chat_id, text,
parse_mode, disable_web_preview,
disable_notification, reply_to_message_id)
end
def send_photo(chat_id : String | Int64, photo : String | File, caption : String? = nil,
disable_notification : Bool = false,
reply_to_message_id : Int64? = nil)
Utils.send_photo(@api_token, chat_id, photo, caption, disable_notification, reply_to_message_id)
end
def command(command_name, &block : Update, Array(String) -> Void)
@command_hooks[command_name] = block
end
def message(&block : Update -> Void)
@message_hooks.push(block)
end
def poll_start(&block : -> Void)
@poll_start_hooks.push(block);
end
def poll_end(&block : -> Void)
@poll_end_hooks.push(block);
end
private def process_updates(updates)
updates.each do |update|
if message = update.message
@message_hooks.each &.call(update)
if entity = message.entities.try { |it| it.first? }
text = message.text.as String
if entity.offset == 0 && entity.type == "bot_command"
divider_index = (text.index /\s|@/) || text.size
first_space_index = (text.index /\s/) || text.size
command = text[1...divider_index]
remaining = text[first_space_index..text.size]
params = remaining.empty? ? ([] of String) : (remaining[1...remaining.size].split ' ')
@command_hooks[command]?.try { |command| command.call(update, params) }
end
end
end
end
end
private def spawn_worker
spawn do
while @poll_running
begin
updates = get_updates 10
process_updates updates
@last_update_id = updates.last?.try &.update_id.+(1) || @last_update_id
rescue
end
end
@poll_end_hooks.each do |hook|
hook.call
end
end
end
def poll
if !@poll_running
@poll_running = true
@poll_start_hooks.each do |hook|
hook.call
end
spawn_worker
end
end
def end_poll
@poll_running = false
end
end
end

View File

@@ -2,13 +2,24 @@ require "json"
module Telepathy
class Audio
JSON.mapping(
file_id: String,
duration: Int64,
performer: String?,
title: String?,
mime_type: String?,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String
@[JSON::Field(key: "duration")]
property duration : Int64
@[JSON::Field(key: "performer")]
property performer : String?
@[JSON::Field(key: "title")]
property title : String?
@[JSON::Field(key: "mime_type")]
property mime_type : String?
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

View File

@@ -3,20 +3,42 @@ require "./message.cr"
module Telepathy
class Chat
JSON.mapping(
id: Int64,
type: String,
title: String?,
username: String?,
first_name: String?,
last_name: String?,
all_members_are_administrators: Bool?,
# TODO photo
description: String?,
invite_link: String?,
pinned_message: Message?,
sticker_set_name: String?,
can_set_sticker_set: Bool?
)
include JSON::Serializable
@[JSON::Field(key: "id")]
property id : Int64
@[JSON::Field(key: "type")]
property type : String
@[JSON::Field(key: "title")]
property title : String?
@[JSON::Field(key: "username")]
property username : String?
@[JSON::Field(key: "first_name")]
property first_name : String?
@[JSON::Field(key: "last_name")]
property last_name : String?
@[JSON::Field(key: "all_members_are_administrators")]
property all_members_are_administrators : Bool?
@[JSON::Field(key: "description")]
property description : String?
@[JSON::Field(key: "invite_link")]
property invite_link : String?
@[JSON::Field(key: "pinned_message")]
property pinned_message : Message?
@[JSON::Field(key: "sticker_set_name")]
property sticker_set_name : String?
@[JSON::Field(key: "can_set_sticker_set")]
property can_set_sticker_set : Bool?
end
end

View File

@@ -2,11 +2,18 @@ require "json"
module Telepathy
class Contact
JSON.mapping(
phone_number: String,
first_name: String,
last_name: String?,
user_id: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "phone_number")]
property phone_number : String
@[JSON::Field(key: "first_name")]
property first_name : String
@[JSON::Field(key: "last_name")]
property last_name : String?
@[JSON::Field(key: "user_id")]
property user_id : Int64?
end
end

View File

@@ -3,12 +3,21 @@ require "./photo_size.cr"
module Telepathy
class Document
JSON.mapping(
file_id: String,
thumb: PhotoSize?,
file_name: String?,
mime_type: String?,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String
@[JSON::Field(key: "thumb")]
property thumb : PhotoSize?
@[JSON::Field(key: "file_name")]
property file_name : String?
@[JSON::Field(key: "mime_type")]
property mime_type : String?
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

View File

@@ -2,9 +2,12 @@ require "json"
module Telepathy
class Location
JSON.mapping(
longitude: Float32,
latitude: Float32
)
include JSON::Serializable
@[JSON::Field(key: "longitude")]
property longitude : Float32
@[JSON::Field(key: "latitude")]
property latitude : Float32
end
end

View File

@@ -1,6 +1,6 @@
require "json"
require "./user.cr"
require "./char.cr"
require "./chat.cr"
require "./message_entity.cr"
require "./audio.cr"
require "./document.cr"
@@ -14,48 +14,114 @@ require "./venue.cr"
module Telepathy
class Message
JSON.mapping(
message_id: Int64,
from: User?,
date: Int64,
chat: Chat,
forward_from: User?,
forward_from_chat: Chat?,
forward_from_message_id: Int64?,
forward_signature: String?,
forward_date: Int64?,
reply_to_message: Message?,
edit_date: Int64?,
media_group_id: String?,
author_signature: String?,
text: String?,
entities: Array(MessageEntity)?,
caption_entities: Array(MessageEntity)?,
audio: Audio?,
document: Document?,
photo: Array(PhotoSize)?,
video: Video?,
voice: Voice?,
video_note: VideoNote?,
# TODO game
# TODO sticker
caption: String?,
contact: Contact?,
location: Location?,
venue: Venue?,
new_chat_members: Array(User)?,
left_chat_member: User?,
new_chat_title: String?,
# new_chat_photo
delete_chat_photo: Bool?,
group_chat_created: Bool?,
supergroup_chat_created: Bool?,
channel_chat_created: Bool?,
migrate_to_chat_id: Int64?,
migrate_from_chat_id: Int64?,
pinned_message: Message?
# invoice
# successful_payment
)
include JSON::Serializable
@[JSON::Field(key: "message_id")]
property message_id : Int64
@[JSON::Field(key: "from")]
property from : User?
@[JSON::Field(key: "date")]
property date : Int64
@[JSON::Field(key: "chat")]
property chat : Chat
@[JSON::Field(key: "forward_from")]
property forward_from : User?
@[JSON::Field(key: "forward_from_chat")]
property forward_from_chat : Chat?
@[JSON::Field(key: "forward_from_message_id")]
property forward_from_message_id : Int64?
@[JSON::Field(key: "forward_signature")]
property forward_signature : String?
@[JSON::Field(key: "forward_date")]
property forward_date : Int64?
@[JSON::Field(key: "reply_to_message")]
property reply_to_message : Message?
@[JSON::Field(key: "edit_date")]
property edit_date : Int64?
@[JSON::Field(key: "media_group_id")]
property media_group_id : String?
@[JSON::Field(key: "author_signature")]
property author_signature : String?
@[JSON::Field(key: "text")]
property text : String?
@[JSON::Field(key: "entities")]
property entities : Array(MessageEntity)?
@[JSON::Field(key: "caption_entities")]
property caption_entities : Array(MessageEntity)?
@[JSON::Field(key: "audio")]
property audio : Audio?
@[JSON::Field(key: "document")]
property document : Document?
@[JSON::Field(key: "photo")]
property photo : Array(PhotoSize)?
@[JSON::Field(key: "video")]
property video : Video?
@[JSON::Field(key: "voice")]
property voice : Voice?
@[JSON::Field(key: "video_note")]
property video_note : VideoNote?
@[JSON::Field(key: "caption")]
property caption : String?
@[JSON::Field(key: "contact")]
property contact : Contact?
@[JSON::Field(key: "location")]
property location : Location?
@[JSON::Field(key: "venue")]
property venue : Venue?
@[JSON::Field(key: "new_chat_members")]
property new_chat_members : Array(User)?
@[JSON::Field(key: "left_chat_member")]
property left_chat_member : User?
@[JSON::Field(key: "new_chat_title")]
property new_chat_title : String?
@[JSON::Field(key: "delete_chat_photo")]
property delete_chat_photo : Bool?
@[JSON::Field(key: "group_chat_created")]
property group_chat_created : Bool?
@[JSON::Field(key: "supergroup_chat_created")]
property supergroup_chat_created : Bool?
@[JSON::Field(key: "channel_chat_created")]
property channel_chat_created : Bool?
@[JSON::Field(key: "migrate_to_chat_id")]
property migrate_to_chat_id : Int64?
@[JSON::Field(key: "migrate_from_chat_id")]
property migrate_from_chat_id : Int64?
@[JSON::Field(key: "pinned_message")]
property pinned_message : Message?
end
end

View File

@@ -3,12 +3,21 @@ require "./user.cr"
module Telepathy
class MessageEntity
JSON.mapping(
type: String,
offset: Int64,
length: Int64,
url: String?,
user: User?
)
include JSON::Serializable
@[JSON::Field(key: "type")]
property type : String
@[JSON::Field(key: "offset")]
property offset : Int64
@[JSON::Field(key: "length")]
property length : Int64
@[JSON::Field(key: "url")]
property url : String?
@[JSON::Field(key: "user")]
property user : User?
end
end

View File

@@ -2,11 +2,18 @@ require "json"
module Telepathy
class PhotoSize
JSON.mapping(
file_id: String?,
width: Int64,
height: Int64,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String?
@[JSON::Field(key: "width")]
property width : Int64
@[JSON::Field(key: "height")]
property height : Int64
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

View File

@@ -2,9 +2,12 @@ require "json"
module Telepathy
class Response(T)
JSON.mapping(
ok: Bool,
result: T
)
include JSON::Serializable
@[JSON::Field(key: "ok")]
property ok : Bool
@[JSON::Field(key: "result")]
property result : T
end
end

View File

@@ -2,17 +2,21 @@ require "json"
module Telepathy
class Update
JSON.mapping(
update_id: Int64,
message: Message?,
edited_message: Message?,
channel_post: Message?,
edited_channel_post: Message?
# TODO inline_query
# TODO chosen_inline_result
# TODO callback_query
# shipping_query
# pre_checkout_query
)
include JSON::Serializable
@[JSON::Field(key: "update_id")]
property update_id : Int64
@[JSON::Field(key: "message")]
property message : Message?
@[JSON::Field(key: "edited_message")]
property edited_message : Message?
@[JSON::Field(key: "channel_post")]
property channel_post : Message?
@[JSON::Field(key: "edited_channel_post")]
property edited_channel_post : Message?
end
end

View File

@@ -2,13 +2,24 @@ require "json"
module Telepathy
class User
JSON.mapping(
id: Int64,
is_bot: Bool,
first_name: String,
last_name: String?,
username: String?,
language_code: String?
)
include JSON::Serializable
@[JSON::Field(key: "id")]
property id : Int64
@[JSON::Field(key: "is_bot")]
property is_bot : Bool
@[JSON::Field(key: "first_name")]
property first_name : String
@[JSON::Field(key: "last_name")]
property last_name : String?
@[JSON::Field(key: "username")]
property username : String?
@[JSON::Field(key: "language_code")]
property language_code : String?
end
end

View File

@@ -3,11 +3,18 @@ require "./location.cr"
module Telepathy
class Venue
JSON.mapping(
location: Location,
title: String,
address: String,
foursquare_id: String?
)
include JSON::Serializable
@[JSON::Field(key: "location")]
property location : Location
@[JSON::Field(key: "title")]
property title : String
@[JSON::Field(key: "address")]
property address : String
@[JSON::Field(key: "foursquare_id")]
property foursquare_id : String?
end
end

View File

@@ -3,14 +3,27 @@ require "./photo_size.cr"
module Telepathy
class Video
JSON.mapping(
file_id: String,
width: Int64,
height: Int64,
duration: Int64,
thumb: PhotoSize?,
mime_type: String?,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String
@[JSON::Field(key: "width")]
property width : Int64
@[JSON::Field(key: "height")]
property height : Int64
@[JSON::Field(key: "duration")]
property duration : Int64
@[JSON::Field(key: "thumb")]
property thumb : PhotoSize?
@[JSON::Field(key: "mime_type")]
property mime_type : String?
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

View File

@@ -3,12 +3,21 @@ require "./photo_size.cr"
module Telepathy
class VideoNote
JSON.mapping(
file_id: String,
length: Int64,
duration: Int64,
thumb: PhotoSize?,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String
@[JSON::Field(key: "length")]
property length : Int64
@[JSON::Field(key: "duration")]
property duration : Int64
@[JSON::Field(key: "thumb")]
property thumb : PhotoSize?
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

View File

@@ -3,11 +3,18 @@ require "./photo_size.cr"
module Telepathy
class Voice
JSON.mapping(
file_id: String,
duration: Int64,
mime_type: String?,
file_size: Int64?
)
include JSON::Serializable
@[JSON::Field(key: "file_id")]
property file_id : String
@[JSON::Field(key: "duration")]
property duration : Int64
@[JSON::Field(key: "mime_type")]
property mime_type : String?
@[JSON::Field(key: "file_size")]
property file_size : Int64?
end
end

83
src/telepathy/utils.cr Normal file
View File

@@ -0,0 +1,83 @@
module Telepathy::Utils
extend self
API_URL = "https://api.telegram.org/bot"
enum ParseMode
Normal
Markdown
HTML
end
def get_me(api_key : String)
response = HTTP::Client.get(API_URL + api_key + "/getMe",
headers: HTTP::Headers{"User-agent" => "Telepathy"})
return Response(User).from_json(response.body).result
end
def get_updates(api_key : String, timeout = 0, last_update_id : Int? = nil)
update_data = {} of String => Int64 | Int32 | String
update_data["timeout"] = timeout
last_update_id.try { |id| update_data["offset"] = id }
response = HTTP::Client.get(API_URL + api_key + "/getUpdates",
headers: HTTP::Headers{"User-agent" => "Telepathy",
"Content-type" => "application/json" },
body: update_data.to_json)
return Response(Array(Update)).from_json(response.body).result
end
def send_message(api_key : String, chat_id : String | Int64, text : String,
parse_mode : ParseMode = ParseMode::Normal,
disable_web_preview : Bool = false,
disable_notification : Bool = false,
reply_to_message_id : Int64? = nil)
message_data = { "chat_id" => chat_id, "text" => text } of String => Int64 | String | Bool
message_data["disable_web_preview"] = true if disable_web_preview
message_data["disable_notification"] = true if disable_notification
if parse_mode == ParseMode::Markdown
message_data["parse_mode"] = "Markdown"
elsif parse_mode == ParseMode::HTML
message_data["parse_mode"] = "HTML"
end
reply_to_message_id.try { |id| message_data["reply_to_message_id"] = id }
HTTP::Client.get(API_URL + api_key + "/sendMessage",
headers: HTTP::Headers{"User-agent" => "Telepathy", "Content-type" => "application/json" },
body: message_data.to_json)
end
def send_photo(api_key : String, chat_id : String | Int64, photo : String | File, caption : String? = nil,
disable_notification : Bool = false,
reply_to_message_id : Int64? = nil)
IO.pipe do |reader, writer|
channel = Channel(String).new(1)
spawn do
HTTP::FormData.build(writer) do |formdata|
channel.send(formdata.content_type)
case chat_id
when String
formdata.field("chat_id", chat_id.as(String))
when Int64
formdata.field("chat_id", chat_id.as(Int64).to_s)
end
formdata.field("disable_notification", disable_notification.to_s)
caption.try { |caption| formdata.field("caption", caption) }
reply_to_message_id.try { |id| formdata.field("reply_to_message_id", id) }
case photo
when String
formdata.field("photo", photo.as(String))
when File
photo_file = photo.as(File)
formdata.file("photo", photo_file,
HTTP::FormData::FileMetadata.new(filename: File.basename(photo_file.path)))
end
end
writer.close
end
response = HTTP::Client.get(API_URL + api_key + "/sendPhoto",
headers: HTTP::Headers{"User-agent" => "Telepathy",
"Content-type" => channel.receive },
body: reader)
end
end
end

View File

@@ -1,3 +1,3 @@
module Telepathy
VERSION = "0.1.0"
VERSION = "0.1.1"
end