├── script ├── nrepl.sh ├── run.sh ├── test.sh ├── repl.sh └── package.sh ├── .gitignore ├── .claude ├── settings.json └── settings.local.json ├── dev └── user.clj ├── README.md ├── deps.edn ├── .github └── workflows │ └── jar.yml ├── CLAUDE.md └── src └── tgadmin └── core.clj /script/nrepl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -M:dev -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -M -m tgadmin.core -------------------------------------------------------------------------------- /script/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -A:dev -X user/-test -------------------------------------------------------------------------------- /script/repl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -X:dev clojure+.core.server/start-server -------------------------------------------------------------------------------- /script/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -M:uberdeps -m uberdeps.uberjar --target target/tgadmin.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target* 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | /.repl-port 11 | *.sublime-workspace 12 | *.sublime-project 13 | /.cpcache* 14 | .DS_Store 15 | config.edn 16 | known_users -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [], 4 | "deny": [ 5 | "Agent", 6 | "Bash", 7 | "Edit", 8 | "Glob", 9 | "Grep", 10 | "LS", 11 | "MultiEdit", 12 | "NotebookEdit", 13 | "NotebookRead", 14 | "Read", 15 | "Task", 16 | "TodoRead", 17 | "TodoWrite", 18 | "Write" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clj-reload.core :as clj-reload] 4 | [clojure+.hashp :as hashp] 5 | [clojure+.print :as print] 6 | [clojure+.error :as error] 7 | [clojure+.test :as test] 8 | [duti.core :as duti])) 9 | 10 | (hashp/install!) 11 | (print/install!) 12 | (error/install!) 13 | 14 | (clj-reload/init 15 | {:dirs ["src" "dev" "test"] 16 | :no-reload '#{user}}) 17 | 18 | (def reload 19 | clj-reload/reload) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram moderation bot 2 | 3 | Algorithm: 4 | 5 | 1. Check https://lols.bot database. If spam, ban 6 | 2. Is it the first message by this user in this group? If not, pass 7 | 3. Does the first message contain link, image, or stop words (работа/доход/etc). If not, pass 8 | 4. So now it’s the first message and it looks sus, we ask user to write a reply with the word “не бот” 9 | 5. User replied within one minute? Great, add them to whitelist 10 | 6. Otherwise, ban -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "mcp__clojure-mcp__glob_files", 5 | "mcp__clojure-mcp__grep", 6 | "mcp__clojure-mcp__read_file", 7 | "mcp__clojure-mcp__bash", 8 | "mcp__clojure-mcp__file_write", 9 | "mcp__clojure-mcp__LS", 10 | "mcp__clojure-mcp__clojure_eval", 11 | "mcp__clojure-mcp__file_edit", 12 | "mcp__clojure-mcp__clojure_edit", 13 | "mcp__clojure-mcp__clojure_edit_replace_sexp" 14 | ], 15 | "deny": [] 16 | } 17 | } -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {org.clojure/clojure {:mvn/version "1.12.1"} 3 | io.github.tonsky/clojure-plus {:mvn/version "1.6.1"} 4 | http-kit/http-kit {:mvn/version "2.8.0"} 5 | cheshire/cheshire {:mvn/version "6.0.0"}} 6 | 7 | :aliases 8 | {:dev 9 | {:extra-paths ["dev" "test"] 10 | :extra-deps 11 | {io.github.tonsky/clj-reload {:mvn/version "0.9.8"} 12 | io.github.tonsky/duti {:git/sha "99a1f6cb2e980608fa0a5638dd5f5b3614f46836"}} 13 | :jvm-opts ["-ea" 14 | "-Dclojure.main.report=stderr" 15 | "-Duser.language=en" 16 | "-Duser.country=US" 17 | "-Dfile.encoding=UTF-8" 18 | "-Djdk.attach.allowAttachSelf" 19 | "-XX:+UnlockDiagnosticVMOptions" 20 | "-XX:+DebugNonSafepoints" 21 | "-XX:+EnableDynamicAgentLoading"]} 22 | 23 | :nrepl 24 | {:extra-deps 25 | {nrepl/nrepl {:mvn/version "1.3.1"}} 26 | :main-opts ["-m" "nrepl.cmdline" "--port" "7888"]} 27 | 28 | :uberdeps 29 | {:replace-deps {uberdeps/uberdeps {:mvn/version "1.3.0"}} 30 | :replace-paths []}}} -------------------------------------------------------------------------------- /.github/workflows/jar.yml: -------------------------------------------------------------------------------- 1 | name: jar 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - 'src/**' 9 | - deps.edn 10 | - .github/workflows/jar.yml 11 | - script/package.sh 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - run: | 21 | echo "JAVA_HOME=$JAVA_HOME_21_X64" >> $GITHUB_ENV 22 | echo "$JAVA_HOME_21_X64/bin" >> $GITHUB_PATH 23 | 24 | - name: Setup Clojure 25 | uses: DeLaGuardo/setup-clojure@13.0 26 | with: 27 | cli: latest 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v4 31 | with: 32 | path: ~/.m2/repository 33 | key: ${{ runner.os }}-clojure-${{ hashFiles('deps.edn') }} 34 | restore-keys: | 35 | ${{ runner.os }}-clojure 36 | 37 | - name: Package 38 | run: ./script/package.sh 39 | 40 | - uses: actions/upload-artifact@v4 41 | with: 42 | name: jar 43 | path: 'target/*.jar' 44 | 45 | - name: Install SSH key 46 | uses: shimataro/ssh-key-action@v2 47 | with: 48 | key: ${{ secrets.SSH_KEY }} 49 | known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} 50 | 51 | - name: Deploy 52 | run: | 53 | scp target/tgadmin.jar tgadmin@tonsky.me: 54 | 55 | - name: Restart 56 | run: | 57 | ssh tgadmin@tonsky.me "kill \$(systemctl show --property MainPID --value tgadmin)" 58 | 59 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is a Telegram moderation bot written in Clojure that automatically detects and bans spam accounts in Telegram groups. The bot uses a multi-step algorithm to identify suspicious first messages and gives legitimate users a chance to prove they are not bots. 8 | 9 | ## Key Commands 10 | 11 | - **Run the bot**: `./script/run.sh` or `clojure -M -m tgadmin.core` 12 | - **Start REPL**: `./script/repl.sh` or `clojure -M:dev -m user` 13 | - **Run tests**: `./script/test.sh` or `clojure -A:dev -X user/-test` 14 | - **Build JAR**: `./script/package.sh` (creates `target/tgadmin.jar`) 15 | - **Reload code in REPL**: `(reload)` (via clj-reload) 16 | 17 | ## Architecture 18 | 19 | ### Core Components 20 | 21 | 1. **Main namespace**: `tgadmin.core` - Contains all bot logic including: 22 | - Message filtering logic with multiple checks (external links, media, stop words, etc.) 23 | - User management (whitelisting, banning) 24 | - Telegram API integration via http-kit 25 | - Challenge/response system for suspicious users 26 | 27 | 2. **Configuration**: 28 | - `config.edn` - Contains bot token and other settings 29 | - `known_users` - Persistent storage of whitelisted users 30 | 31 | 3. **State Management**: 32 | - `*known-users` - Atom containing whitelisted user IDs 33 | - `*pending-warnings` - Atom tracking users who need to respond to challenges 34 | 35 | ### Bot Algorithm 36 | 37 | The bot implements a sophisticated spam detection algorithm: 38 | 39 | 1. Checks https://lols.bot database for known spammers 40 | 2. Allows messages from previously seen users 41 | 3. For first-time users, checks for suspicious content: 42 | - External links or media 43 | - Mixed language characters (Latin + Cyrillic) 44 | - Special Unicode symbols 45 | - Stop words (работа, доход, курсы, etc.) 46 | 4. Suspicious users receive a challenge to reply with "не бот" within 1 minute 47 | 5. Users who pass are whitelisted; others are banned 48 | 49 | ### Key Functions 50 | 51 | - `handle-message` - Main entry point for processing Telegram updates 52 | - `check-message` - Runs all spam detection checks 53 | - `warn` - Issues challenge to suspicious users 54 | - `ack` - Processes challenge responses 55 | - `ban-user` - Bans user and deletes their messages 56 | - `whitelist-user` - Adds user to known_users file 57 | 58 | ### External Dependencies 59 | 60 | - `http-kit` - HTTP client for Telegram API 61 | - `cheshire` - JSON parsing 62 | - Development tools: `clj-reload`, `nrepl`, `duti` -------------------------------------------------------------------------------- /src/tgadmin/core.clj: -------------------------------------------------------------------------------- 1 | (ns tgadmin.core 2 | (:require 3 | [cheshire.core :as json] 4 | [clojure.edn :as edn] 5 | [clojure.java.io :as io] 6 | [clojure.string :as str] 7 | [clojure+.core :as clojure+ :refer [if+ when+ cond+]] 8 | [org.httpkit.client :as http]) 9 | (:import 10 | [java.io File FileWriter] 11 | [java.util Timer TimerTask])) 12 | 13 | ;; utils 14 | 15 | (defonce ^Timer timer 16 | (Timer. true)) 17 | 18 | (defn- timer-task ^TimerTask [f] 19 | (proxy [TimerTask] [] 20 | (run [] 21 | (try 22 | (f) 23 | (catch Throwable t 24 | (.printStackTrace t)))))) 25 | 26 | (defn schedule-impl [^long delay f] 27 | (let [t (timer-task f)] 28 | (.schedule timer t delay) 29 | #(.cancel t))) 30 | 31 | (defmacro schedule [delay & body] 32 | `(schedule-impl ~delay 33 | (fn [] 34 | ~@body))) 35 | 36 | (defn swap-dissoc! [*atom key] 37 | (let [[before after] (swap-vals! *atom dissoc key)] 38 | (get before key))) 39 | 40 | (defn quote-strings [ss] 41 | (str "'" (str/join "', '" (distinct ss)) "'")) 42 | 43 | (defn trim [s] 44 | (if (<= (count s) 80) 45 | s 46 | (str (subs s 0 80) "..."))) 47 | 48 | ;; config 49 | 50 | (def config 51 | (edn/read-string (slurp "config.edn"))) 52 | 53 | (def token 54 | (:token config)) 55 | 56 | (def react-period-ms 57 | (:react-period-ms config 60000)) 58 | 59 | ;; Telegram API 60 | 61 | (defn post! 62 | ([method] 63 | (post! method {})) 64 | ([method opts] 65 | (try 66 | (let [opts (cond-> opts 67 | ; https://core.telegram.org/bots/api#markdownv2-style 68 | (= "MarkdownV2" (:parse_mode opts)) 69 | (update :text str/replace #"[_*~`>#\+\-|\{\}\.!]" #(str "\\" %))) 70 | req {:url (str "https://api.telegram.org/bot" token method) 71 | :method :post 72 | :body (json/generate-string opts) 73 | :headers {"Content-Type" "application/json"}} 74 | resp @(http/request req) 75 | body (json/parse-string (:body resp) true)] 76 | (if (:ok body) 77 | (:result body) 78 | (do 79 | (println "[ ERROR ]" body) 80 | nil))) 81 | (catch InterruptedException e 82 | (throw e)) 83 | (catch Exception e 84 | (.printStackTrace e) 85 | nil)))) 86 | 87 | ;; state 88 | 89 | ;; #{user-id ...} 90 | (def *known-users 91 | (atom 92 | (->> (slurp "known_users") 93 | (re-seq #"(?m)^-?\d+") 94 | (map parse-long) 95 | set))) 96 | 97 | ;; {user-id {:message message 98 | ;; :warning warning}} 99 | (def *pending-warnings 100 | (atom {})) 101 | 102 | ;; Time to first clown monitoring 103 | (def reaction-channel-id 104 | #_-1002729833355 ;; nikitonsky_pub_test 105 | -1001339432494) ;; nikitonsky_pub 106 | 107 | (def reaction-group-id 108 | #_-1002762672757 ;; nikitonsky_chat_test 109 | -1001436433940) ;; nikitonsky_chat 110 | 111 | (def *reaction-channel-posts 112 | "{message_id {:date timestamp}}" 113 | (atom {})) 114 | 115 | ;; app 116 | 117 | (defn check-external [message] 118 | (try 119 | (let [resp @(http/request 120 | {:url (str "https://lols.bot/?a=" (:id (:from message))) 121 | :method :get 122 | :connect-timeout 5000})] 123 | (when (= 200 (:status resp)) 124 | (let [body (json/parse-string (:body resp) true)] 125 | (when (:banned body) 126 | (str "banned at lols.bot"))))) 127 | (catch Exception e 128 | (.printStackTrace e)))) 129 | 130 | (defn check-media [message] 131 | (when-some [types (not-empty 132 | (concat 133 | (when (:photo message) 134 | ["photo"]) 135 | (cond 136 | (:video message) ["video"] 137 | (:animation message) ["animation"] 138 | (:document message) ["document"]) 139 | (when (some #(= "url" (:type %)) (:entities message)) 140 | ["url"]) 141 | (when (some #(= "text_link" (:type %)) (:entities message)) 142 | ["text_link"]) 143 | (when (some #(= "mention" (:type %)) (:entities message)) 144 | ["mention"])))] 145 | (str "containing: " (str/join ", " types)))) 146 | 147 | (defn check-mixed-lang [message] 148 | (when-some [text (:text message)] 149 | (when-some [words (not-empty 150 | (re-seq #"(?uUi)\b\w*(?:\p{IsLatin}\p{IsCyrillic}+\p{IsLatin}+\p{IsCyrillic}|\p{IsCyrillic}\p{IsLatin}+\p{IsCyrillic}+\p{IsLatin})\w*\b" text))] 151 | (str "mixing cyrillic with latin: " (quote-strings words))))) 152 | 153 | (defn check-symbols [message] 154 | (when-some [text (:text message)] 155 | (when (or (str/index-of text "⁨")) 156 | (str "suspicious characters")))) 157 | 158 | (defn check-stop-words [message] 159 | (when-some [s (:text message)] 160 | (when-some [words (not-empty 161 | (concat 162 | (re-seq #"(?uUi)\b(?:сотрудничеств|сфер|выплат|направлени|заработ|доход|доллар|средств|деньг|личк|русло|актив|работ|команд|обучени|юмор|улыб|мудак|говн|курс)[а-я]*\b" s) 163 | (re-seq #"(?uUi)\b(?:лс)\b" s) 164 | (re-seq #"(?uUi)\b[а-я]*(?:менеджмент)[а-я]*\b" s) 165 | (re-seq #"(?uUi)\b(?:[0-9\.]+ ?р(?:уб)?\.?)\b" s) 166 | (re-seq #"(?uUi)\b(?:usdt|usd|https|http|binance|bitcoin|web|18|p2p|trading)\b" s) 167 | (re-seq #"(?uUi)(?:\$|💸|❇️|🚀|❗️)" s)))] 168 | (str "stop-words: " (quote-strings words))))) 169 | 170 | (defn check-message [message] 171 | ((some-fn check-media check-mixed-lang check-symbols check-stop-words) 172 | message)) 173 | 174 | (defn message-str [message] 175 | (str 176 | (:username (:chat message)) "/" (:message_id message) 177 | " by " (:id (:from message)) 178 | (when-some [username (:username (:from message))] 179 | (str " (" username ")")))) 180 | 181 | (defn user-str ^String [user] 182 | (let [{:keys [id username first_name last_name]} user] 183 | (str id 184 | (when username (str " @" username)) 185 | (when first_name (str " " first_name)) 186 | (when last_name (str " " last_name))))) 187 | 188 | (defn whitelist-user [user] 189 | (swap! *known-users conj (:id user)) 190 | (println "[ WHITELIST ]" (user-str user)) 191 | (with-open [w (FileWriter. (io/file "known_users") true)] 192 | (.write w (user-str user)) 193 | (.write w "\n"))) 194 | 195 | (defn ban-user [user reason message & messages] 196 | (let [chat-id (:id (:chat message)) 197 | user (:from message) 198 | user-id (:id user)] 199 | (doseq [message (cons message messages)] 200 | (println "[ DELETING ]" (message-str message) "for" reason) 201 | (post! "/deleteMessage" {:chat_id chat-id, :message_id (:message_id message)})) 202 | (println "[ BAN ]" (user-str user) "for" reason) 203 | (post! "/banChatMember" {:chat_id chat-id, :user_id user-id}))) 204 | 205 | (defn warn [message reason] 206 | (let [chat-id (:id (:chat message)) 207 | message-id (:message_id message) 208 | _ (println "[ WARNING ]" (message-str message) "for" reason) 209 | user (:from message) 210 | user-id (:id user)] 211 | (let [mention (if (:username user) 212 | (str "@" (:username user)) 213 | (str "[" (or (:first_name user) (:last_name user) "%username%") "](tg://user?id=" (:id user) ")")) 214 | warning (post! "/sendMessage" 215 | {:chat_id chat-id 216 | :reply_parameters {:message_id message-id} 217 | ; :message_thread_id (:message_thread_id message) 218 | :parse_mode "MarkdownV2" 219 | :text (str "Привет " mention ", это антиспам. Напиши сообщение со словом «небот» и я отстану. Ну а если бот, хана тебе")}) 220 | warning-id (:message_id warning)] 221 | (swap! *pending-warnings assoc user-id {:message message 222 | :warning warning}) 223 | (schedule react-period-ms 224 | (when (swap-dissoc! *pending-warnings user-id) 225 | (ban-user user reason message warning)))))) 226 | 227 | (defn ack [ack-message] 228 | (let [user-id (:id (:from ack-message)) 229 | chat-id (:id (:chat ack-message)) 230 | {warning :warning} (swap-dissoc! *pending-warnings user-id)] 231 | (whitelist-user (:from ack-message)) 232 | (post! "/deleteMessage" {:chat_id chat-id, :message_id (:message_id warning)}) 233 | (post! "/deleteMessage" {:chat_id chat-id, :message_id (:message_id ack-message)}))) 234 | 235 | (defn deny [message] 236 | (let [user (:from message) 237 | user-id (:id user)] 238 | (when-some [{first-message :message 239 | warning :warning} (swap-dissoc! *pending-warnings user-id)] 240 | (ban-user user "repeated message" first-message warning message)))) 241 | 242 | (defn handle-message [message] 243 | (let [user (:from message) 244 | user-id (:id user) 245 | chat-id (:id (:chat message))] 246 | (cond+ 247 | ;; known 248 | (and 249 | (@*known-users user-id) 250 | #_(not= "nikitonsky" (:username user))) 251 | :nop 252 | 253 | ;; pending -- ack 254 | (and 255 | (contains? @*pending-warnings user-id) 256 | (some->> (:text message) (re-find #"(?uUi)\bне\s?(?:ро)?бо[тм]\b"))) 257 | (ack message) 258 | 259 | ;; pending -- repeated message 260 | (contains? @*pending-warnings user-id) 261 | (deny message) 262 | 263 | ;; unknown -- banned by lols 264 | :let [reason (check-external message)] 265 | reason 266 | (ban-user user reason message) 267 | 268 | ;; unknown -- sus 269 | :let [reason (check-message message)] 270 | reason 271 | (warn message reason) 272 | 273 | ;; unknown -- okay 274 | (:text message) 275 | (whitelist-user user)))) 276 | 277 | (defn handle-reaction-post [message] 278 | (when (= reaction-channel-id (-> message :forward_from_chat :id)) 279 | (let [message-id (:forward_from_message_id message) 280 | date (:forward_date message)] 281 | (swap! *reaction-channel-posts assoc message-id {:date date}) 282 | (println (str "[ TRACKING REACTIONS ] " (-> message :forward_from_chat :title) ", post #" message-id ": “" (trim (:text message)) "”"))))) 283 | 284 | (defn handle-reaction-count [reaction-count] 285 | (let [{message-id :message_id 286 | reactions :reactions 287 | reaction-date :date 288 | {chat-id :id 289 | chat-title :title} :chat} reaction-count] 290 | (when+ (and 291 | (= chat-id reaction-channel-id) 292 | :let [[reaction & _] (filter #(= "🤡" (-> % :type :emoji)) reactions)] 293 | reaction 294 | :let [{post-date :date} (@*reaction-channel-posts message-id)] 295 | post-date) 296 | (let [minutes (-> (- reaction-date post-date) (quot 60)) 297 | declension (cond 298 | (#{11 12 13 14} (mod minutes 100)) "минут" 299 | (= 1 (mod minutes 10)) "минута" 300 | (#{2 3 4} (mod minutes 10)) "минуты" 301 | :else "минут")] 302 | (println (str "[ FIRST REACTION ] Channel " chat-title ", post #" message-id ", reaction " reaction ", delta t " minutes " minutes")) 303 | (post! "/sendMessage" 304 | {:chat_id reaction-group-id 305 | :text (str "Время до первого 🤡 — " minutes " " declension)}) 306 | (swap! *reaction-channel-posts dissoc message-id))))) 307 | 308 | (defn log-update [u] 309 | (cond-> u 310 | (-> u :message :reply_to_message :text) 311 | (update :message update :reply_to_message update :text trim) 312 | 313 | true 314 | prn)) 315 | 316 | (defn -main [& args] 317 | (println "[ STARTED ]") 318 | (loop [offset 0] 319 | (if-some [updates (post! "/getUpdates" 320 | {:offset offset 321 | :allowed_updates ["message" "message_reaction_count"]})] 322 | (do 323 | (doseq [update updates 324 | :let [_ (log-update update)]] 325 | (try 326 | (cond 327 | (:message update) 328 | (do 329 | (handle-reaction-post (:message update)) 330 | (handle-message (:message update))) 331 | 332 | (:message_reaction_count update) 333 | (handle-reaction-count (:message_reaction_count update))) 334 | (catch Exception e 335 | (.printStackTrace e)))) 336 | 337 | (if (empty? updates) 338 | (recur offset) 339 | (recur (-> updates last :update_id inc long)))) 340 | (recur offset)))) 341 | 342 | (comment 343 | (-main) 344 | 345 | ;; post in channel 346 | {:update_id 558985903 347 | :message 348 | {:date 1753738345 349 | 350 | :forward_from_chat 351 | {:id -1002729833355 352 | :title "Channel Test" 353 | :username "nikitonsky_pub_test" 354 | :type "channel"} 355 | 356 | :chat 357 | {:id -1002762672757 358 | :title "Channel Test Chat" 359 | :username "nikitonsky_chat_test" 360 | :type "supergroup"} 361 | 362 | :is_automatic_forward true 363 | :message_id 15 364 | 365 | :forward_origin 366 | {:type "channel" 367 | :chat 368 | {:id -1002729833355 369 | :title "Channel Test" 370 | :username "nikitonsky_pub_test" 371 | :type "channel"} 372 | :message_id 7 373 | :author_signature "Nikita Prokopov" 374 | :date 1753738342} 375 | 376 | :from 377 | {:id 777000 378 | :is_bot false 379 | :first_name "Telegram"} 380 | 381 | :forward_signature "Nikita Prokopov" 382 | :forward_from_message_id 7 383 | :forward_date 1753738342 384 | :sender_chat 385 | {:id -1002729833355 386 | :title "Channel Test" 387 | :username "nikitonsky_pub_test" 388 | :type "channel"} 389 | :text "channel test post 5"}} 390 | 391 | ;; reactions 392 | {:update_id 558985904 393 | :message_reaction_count 394 | {:chat 395 | {:id -1002729833355 396 | :title "Channel Test" 397 | :username "nikitonsky_pub_test" 398 | :type "channel"} 399 | :message_id 7 400 | :date 1753738503 401 | :reactions 402 | [{:type 403 | {:type "emoji" 404 | :emoji "🤡"} 405 | :total_count 1}]}} 406 | 407 | (json/parse-string 408 | (:body @(http/get "https://lols.bot/?a=232806939")) true) 409 | 410 | (json/parse-string 411 | (:body @(http/get "https://lols.bot/?a=2069820207")) true) 412 | 413 | (:content-type (:headers @(http/get "https://lols.bot/?a=2069820207"))) 414 | (json/parse-string (:body @(http/get "https://lols.bot/asdas")) true) 415 | 416 | (post! "/sendMessage" 417 | {:chat_id -1001436433940 418 | :reply_parameters {:message_id 95692} 419 | ; :message_thread_id 95594 420 | :parse_mode "MarkdownV2" 421 | :text "test"}) 422 | 423 | 424 | (post! "/getMe") 425 | (post! "/getUpdates" {:offset 558841683}) 426 | 427 | (post! "/getChat" {:chat_id chat-id}) 428 | 429 | (post! "/getChatMember" {:chat_id chat-id 430 | :user_id 232806939}) 431 | 432 | ;; TEXT 433 | {:update_id 558841686, :message {:message_id 6, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933169, :text "test"}} 434 | 435 | ;; LINK 436 | {:update_id 558841688, :message {:message_id 8, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933181, :text "link https://core.telegram.org/bots/api#available-methods", :entities [{:offset 5, :length 52, :type "url"}]}} 437 | 438 | ;; MENTION 439 | {:update_id 558841689, :message {:message_id 9, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933195, :text "mention @nikitonksy", :entities [{:offset 8, :length 11, :type "mention"}]}} 440 | 441 | ;; REPLY 442 | {:update_id 558841693, :message {:message_id 13, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933272, :message_thread_id 8, :reply_to_message {:message_id 8, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933181, :text "link https://core.telegram.org/bots/api#available-methods", :entities [{:offset 5, :length 52, :type "url"}]}, :text "reply"}} 443 | 444 | ;; REPLY WITH LINK 445 | {:update_id 558841694, :message {:message_id 14, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933343, :message_thread_id 8, :reply_to_message {:message_id 8, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933181, :text "link https://core.telegram.org/bots/api#available-methods", :entities [{:offset 5, :length 52, :type "url"}]}, :text "reply with link https://tonsky.me", :entities [{:offset 16, :length 17, :type "url"}]}} 446 | 447 | ;; IMAGE 448 | {:update_id 558841695, :message {:message_id 15, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933391, :photo [{:file_id "AgACAgIAAx0Cf56CYQADD2VDqo-bgyhW7BV397vVP8F9VXWKAAKH0jEbY1cgSvnVzKahmq-PAQADAgADcwADMwQ", :file_unique_id "AQADh9IxG2NXIEp4", :file_size 1591, :width 67, :height 90} {:file_id "AgACAgIAAx0Cf56CYQADD2VDqo-bgyhW7BV397vVP8F9VXWKAAKH0jEbY1cgSvnVzKahmq-PAQADAgADbQADMwQ", :file_unique_id "AQADh9IxG2NXIEpy", :file_size 25163, :width 240, :height 320} {:file_id "AgACAgIAAx0Cf56CYQADD2VDqo-bgyhW7BV397vVP8F9VXWKAAKH0jEbY1cgSvnVzKahmq-PAQADAgADeAADMwQ", :file_unique_id "AQADh9IxG2NXIEp9", :file_size 116710, :width 600, :height 800} {:file_id "AgACAgIAAx0Cf56CYQADD2VDqo-bgyhW7BV397vVP8F9VXWKAAKH0jEbY1cgSvnVzKahmq-PAQADAgADeQADMwQ", :file_unique_id "AQADh9IxG2NXIEp-", :file_size 185228, :width 960, :height 1280}], :caption "image"}} 449 | 450 | ;; FILE 451 | {:update_id 558841696, :message {:message_id 16, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933395, :document {:file_name "signal-2023-11-02-091621_002.jpeg", :mime_type "image/jpeg", :thumbnail {:file_id "AAMCAgADHQJ_noJhAAMQZUOqkyF6iAMPsCLxUYJ90jUDB00AAtQ6AAJjVyBKdBF0SoE7F_MBAAdtAAMzBA", :file_unique_id "AQAD1DoAAmNXIEpy", :file_size 22448, :width 240, :height 320}, :thumb {:file_id "AAMCAgADHQJ_noJhAAMQZUOqkyF6iAMPsCLxUYJ90jUDB00AAtQ6AAJjVyBKdBF0SoE7F_MBAAdtAAMzBA", :file_unique_id "AQAD1DoAAmNXIEpy", :file_size 22448, :width 240, :height 320}, :file_id "BQACAgIAAx0Cf56CYQADEGVDqpMheogDD7Ai8VGCfdI1AwdNAALUOgACY1cgSnQRdEqBOxfzMwQ", :file_unique_id "AgAD1DoAAmNXIEo", :file_size 423638}, :caption "files"}} 452 | 453 | ;; VIDEO 454 | {:update_id 558841697, :message {:message_id 17, :from {:id 232806939, :is_bot false, :first_name "Nikita", :last_name "Prokopov", :username "nikitonsky"}, :chat {:id -1002141094497, :title "Grumpy Queue", :username "grumpy_queue", :type "supergroup"}, :date 1698933447, :video {:thumb {:file_id "AAMCAgADHQJ_noJhAAMRZUOqxwNtG2hUFElRIzYbsbZdMDIAAtw6AAJjVyBKSdcY4T_zjQ4BAAdtAAMzBA", :file_unique_id "AQAD3DoAAmNXIEpy", :file_size 15524, :width 257, :height 320}, :file_name "TBPInvictus-1719397053468492105.mp4", :mime_type "video/mp4", :width 360, :duration 24, :file_size 1114419, :file_unique_id "AgAD3DoAAmNXIEo", :thumbnail {:file_id "AAMCAgADHQJ_noJhAAMRZUOqxwNtG2hUFElRIzYbsbZdMDIAAtw6AAJjVyBKSdcY4T_zjQ4BAAdtAAMzBA", :file_unique_id "AQAD3DoAAmNXIEpy", :file_size 15524, :width 257, :height 320}, :file_id "BAACAgIAAx0Cf56CYQADEWVDqscDbRtoVBRJUSM2G7G2XTAyAALcOgACY1cgSknXGOE_840OMwQ", :height 448}, :caption "video"}} 455 | 456 | ;; EDIT 457 | {:update_id 558841862, :edited_message {:message_id 85324, :from {:id 1329861181, :is_bot false, :first_name "Алиса", :last_name "Королёва", :username "caralice"}, :chat {:id -1001436433940, :title "Стоящие под стрелой", :username "nikitonsky_chat", :type "supergroup"}, :date 1698937255, :edit_date 1698946666, :message_thread_id 85299, :reply_to_message {:date 1698936933, :forward_from_chat {:id -1001339432494, :title "Стой под стрелой", :username "nikitonsky_pub", :type "channel"}, :edit_date 1698936970, :chat {:id -1001436433940, :title "Стоящие под стрелой", :username "nikitonsky_chat", :type "supergroup"}, :is_automatic_forward true, :message_id 85299, :from {:id 777000, :is_bot false, :first_name "Telegram"}, :forward_signature "Nikita Prokopov", :forward_from_message_id 551, :forward_date 1698936930, :sender_chat {:id -1001339432494, :title "Стой под стрелой", :username "nikitonsky_pub", :type "channel"}, :text "Если говорить об идеях, то одна, которая меня никак не отпускает — это объединить будильник с календарем. Почему это два разных приложения?\n\nСейчас будильник сделан как будто для людей, которые встают каждый день в одно и то же время и у них в жизни ничего не меняется. У меня, к сожалению, жизнь устроена по-другому и поэтому в приложении стопитсот будильников, которые когда-то были актуальны (скорее всего один раз) и с тех пор просто занимают место.\n\nНедавно я сделал себе пару регулярных будильников, чтобы вставать на занятия. Все бы хорошо, но случаются исключения (отпуск, например) и переносы. И приходится опять во всей это толпе будильников ходить и включать-выключать туда-сюда (а потом не забыть включить обратно).\n\nНо самый странный интеракшн — это включить будильник на 10:30 так, чтобы он не прозвенел — оказывается, он когда-то создавался с фильтром «только по четвергам и субботам», но в часах такую несущественную деталь, конечно, не показывают. Получается, ты его вроде включил, а он решил утром не звенеть. Надежно, ничего не скажешь.\n\nВ общем, мой поинт. Это все давно решено в календаре: и повротяющиеся события, и переносы, и исчезания старых неактуальных отметок, и визуализация. Плюс, будильник напрямую завязан на события (кроме случаев, когда ты решил «а чего бы просто по приколу не встать в пять утра», конечно).\n\nНу и нафига тогда отдельное приложение?"}, :text "есть ещё приложение \"напоминания\", которое пытается и в календарь, и в заметки одновременно"}} 458 | 459 | 460 | 461 | [{:update_id 558841683 462 | :message {:message_id 3 463 | :from {:id 232806939 464 | :is_bot false 465 | :first_name "Nikita" 466 | :last_name "Prokopov" 467 | :username "nikitonsky"} 468 | :chat {:id -1002141094497 469 | :title "Grumpy Queue" 470 | :username "grumpy_queue" 471 | :type "supergroup"} 472 | :date 1698930820 473 | :text "test"}}] 474 | 475 | [{:update_id 558841680 476 | :channel_post {:message_id 63 477 | :sender_chat {:id -1001150152488 478 | :title "Grumpy Website Test" 479 | :username "grumpy_test" 480 | :type "channel"} 481 | :chat {:id -1001150152488 482 | :title "Grumpy Website Test" 483 | :username "grumpy_test" 484 | :type "channel"} 485 | :date 1698930640 486 | :text "test"}}] 487 | [{:update_id 558841679 488 | :my_chat_member {:chat {:id -1001150152488 489 | :title "Grumpy Website Test" 490 | :username "grumpy_test" 491 | :type "channel"} 492 | :from {:id 232806939 493 | :is_bot false 494 | :first_name "Nikita" 495 | :last_name "Prokopov" 496 | :username "nikitonsky"} 497 | :date 1698930384 498 | :old_chat_member {:user {:id 6750399431 499 | :is_bot true 500 | :first_name "nikitonsky_admin" 501 | :username "nikitonsky_admin_bot"} 502 | :status "left"} 503 | :new_chat_member {:can_post_messages true 504 | :can_manage_video_chats false 505 | :can_post_stories true 506 | :can_manage_voice_chats false 507 | :can_invite_users false 508 | :can_delete_messages true 509 | :can_be_edited false 510 | :can_edit_messages true 511 | :is_anonymous false 512 | :can_change_info false 513 | :can_restrict_members true 514 | :status "administrator" 515 | :can_edit_stories true 516 | :can_promote_members false 517 | :can_manage_chat true 518 | :user {:id 6750399431 519 | :is_bot true 520 | :first_name "nikitonsky_admin" 521 | :username "nikitonsky_admin_bot"} 522 | :can_delete_stories true}}}]) --------------------------------------------------------------------------------