-
-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/game chat and cities #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
f258c24
5ccd9ee
0603c05
fa64c7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Один пост в день, каждый раз другой. Если материала нет — молчим: третий | ||
| # «⏱ 12 hours played» за неделю хуже пустого дня, в том числе для | ||
| # automated-labeling у модерации Bluesky. | ||
| class PostDailySocialPostJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform | ||
| content = Social::DailyPlanner.new.pick | ||
|
|
||
| if content.nil? | ||
| Rails.logger.info("[Social] daily post skipped: no fresh material") | ||
| return | ||
| end | ||
|
|
||
| Social.publish(content) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Одна джоба на все типы постов и все сети. Каждый ранний выход логируем с | ||
| # причиной: без этого отладка токенов превращается в гадание. | ||
| class PostSocialJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform(kind, dedup_key, network) | ||
| adapter = Social.adapter_for(network) | ||
| return log(kind, dedup_key, network, "unknown network") unless adapter | ||
| return log(kind, dedup_key, network, "adapter not configured") unless adapter.configured? | ||
| return log(kind, dedup_key, network, "already posted") if already_posted?(kind, dedup_key, network) | ||
|
|
||
| content = Social::Content.build(kind, dedup_key) | ||
| return log(kind, dedup_key, network, "no content") unless content | ||
| return log(kind, dedup_key, network, "material is gone") unless content.available? | ||
|
|
||
| post_id = adapter.new(content: content, locale: Social.locale_for(network)).call | ||
| return log(kind, dedup_key, network, "adapter returned nothing") unless post_id | ||
|
|
||
| record(kind, dedup_key, network, post_id) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def already_posted?(kind, dedup_key, network) | ||
| SocialPost.exists?(network: network, kind: kind, dedup_key: dedup_key) | ||
| end | ||
|
|
||
| def record(kind, dedup_key, network, post_id) | ||
| SocialPost.create!( | ||
| network: network, kind: kind, dedup_key: dedup_key, | ||
| external_post_id: post_id, posted_at: Time.current | ||
| ) | ||
| rescue ActiveRecord::RecordNotUnique | ||
| # Две джобы на один материал разошлись по времени — пост уже записан. | ||
| log(kind, dedup_key, network, "duplicate record") | ||
| end | ||
|
|
||
| def log(kind, dedup_key, network, reason) | ||
| Rails.logger.info("[Social] skip #{network} #{kind}/#{dedup_key}: #{reason}") | ||
| nil | ||
| end | ||
| end | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| module Telegram | ||
| # Доставка одного сообщения чата одному человеку. Отдельная джоба на | ||
| # получателя: Telegram ограничивает примерно одним сообщением в секунду на | ||
| # чат, и упершаяся в лимит доставка не должна задерживать остальных. | ||
| class DeliverChatMessageJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| MAX_RETRY_WAIT = 5.minutes | ||
|
|
||
| def perform(game_id, recipient_id, text) | ||
| game = Game.find_by(id: game_id) | ||
| recipient = User.find_by(id: recipient_id) | ||
| return unless game && recipient && recipient.telegram_chat_id.present? | ||
|
|
||
| # Проверяем участие именно перед отправкой: между постановкой в очередь и | ||
| # доставкой человека могли вывести из состава. | ||
| return unless game.chat_open? && game.team_member_ids.include?(recipient.id) | ||
|
|
||
| # Без parse_mode: это чужой текст, а не наш шаблон. С Markdown одиночный | ||
| # `_` или `[` либо исказит сообщение, либо уронит отправку четырёхсоткой. | ||
| response = Telegram::Api.post("sendMessage", { | ||
| "chat_id" => recipient.telegram_chat_id.to_s, | ||
| "link_preview_options" => Telegram::Api::LINK_PREVIEW_DISABLED, | ||
| "text" => text.to_s | ||
| }) | ||
|
|
||
| handle_rate_limit(response, game_id, recipient_id, text) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def handle_rate_limit(response, game_id, recipient_id, text) | ||
| return true if response.is_a?(Hash) && response["ok"] | ||
| return false unless response.is_a?(Hash) && response["error_code"].to_i == 429 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Telegram returns a parsed failure response such as Useful? React with 👍 / 👎. |
||
|
|
||
| wait = response.dig("parameters", "retry_after").to_i | ||
| wait = 1 if wait <= 0 | ||
| return false if wait > MAX_RETRY_WAIT.to_i | ||
|
|
||
| self.class.set(wait: wait.seconds).perform_later(game_id, recipient_id, text) | ||
| false | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| module Telegram | ||
| # Разбор одного сообщения чата: кто его увидит. Сама отправка — в | ||
| # DeliverChatMessageJob, по джобе на получателя. | ||
| class RelayChatMessageJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform(game_id, sender_id, body) | ||
| game = Game.find_by(id: game_id) | ||
| sender = User.find_by(id: sender_id) | ||
| return unless game && sender && body.present? | ||
|
|
||
| # Отправитель тоже мог выйти из состава, пока сообщение ждало очереди. | ||
| return unless game.chat_open? && game.team_member_ids.include?(sender.id) | ||
|
|
||
| text = Telegram::Chat::Message.render(game: game, sender: sender, body: body) | ||
|
|
||
| game.chat_members.where.not(id: sender.id).find_each do |recipient| | ||
| Telegram::DeliverChatMessageJob.perform_later(game.id, recipient.id, text) | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When two jobs for the same network/material overlap—for example after a rapid urgent-search off/on cycle—both can pass
already_posted?and call the external adapter. The unique index is only exercised afterward, so rescuingRecordNotUniqueprevents a duplicate database row but cannot undo the second public post. Claim or lock the dedup record before performing the external side effect.Useful? React with 👍 / 👎.